feat: implement collapsable sidebar UI, API blueprints, and core map/sidebar interaction scripts
This commit is contained in:
parent
62e8d38fbc
commit
2e3694d2f7
6 changed files with 181 additions and 31 deletions
|
|
@ -188,6 +188,34 @@ def get_my_all_notes():
|
||||||
results.append(n_dict)
|
results.append(n_dict)
|
||||||
return jsonify(results)
|
return jsonify(results)
|
||||||
|
|
||||||
|
@api_bp.route('/notes/default', methods=['GET'])
|
||||||
|
def get_default_notes():
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
# Get owned notepads + favorite notepads
|
||||||
|
owned_pad_ids = [p.id for p in Notepad.query.filter_by(user_id=user_id).all()]
|
||||||
|
fav_pad_ids = [f.notepad_id for f in Favorite.query.filter_by(user_id=user_id).all()]
|
||||||
|
all_visible_pad_ids = list(set(owned_pad_ids + fav_pad_ids))
|
||||||
|
|
||||||
|
notes = Note.query.filter(Note.notepad_id.in_(all_visible_pad_ids), Note.is_hidden == False).all()
|
||||||
|
else:
|
||||||
|
# Logged out: All public notes
|
||||||
|
notes = Note.query.join(Notepad).filter(Notepad.is_public == True, Note.is_hidden == False).all()
|
||||||
|
|
||||||
|
# Eagerly load note counts to avoid N+1 queries when calling pad.to_dict()
|
||||||
|
note_counts = dict(db.session.query(Note.notepad_id, func.count(Note.id)).filter(Note.is_hidden == False).group_by(Note.notepad_id).all())
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for note in notes:
|
||||||
|
n_dict = note.to_dict()
|
||||||
|
n_dict['notepad'] = note.notepad.to_dict(note_count=note_counts.get(note.notepad_id, 0))
|
||||||
|
n_dict['owner_id'] = note.notepad.user_id
|
||||||
|
n_dict['is_public'] = note.notepad.is_public
|
||||||
|
if user_id and note.notepad.user_id == user_id:
|
||||||
|
n_dict['share_token'] = get_share_token('note', note.id)
|
||||||
|
results.append(n_dict)
|
||||||
|
return jsonify(results)
|
||||||
|
|
||||||
@api_bp.route('/notes', methods=['POST'])
|
@api_bp.route('/notes', methods=['POST'])
|
||||||
def create_note():
|
def create_note():
|
||||||
user_id = session.get('user_id')
|
user_id = session.get('user_id')
|
||||||
|
|
|
||||||
|
|
@ -181,4 +181,30 @@ input.admin-search:focus {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
box-shadow: 0 0 0 3px var(--primary-light);
|
box-shadow: 0 0 0 3px var(--primary-light);
|
||||||
background-color: var(--bg-card);
|
background-color: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Forms & Input Elements styling adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.btn {
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="email"],
|
||||||
|
.form-group input[type="password"],
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea,
|
||||||
|
input.admin-search {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,13 +29,11 @@
|
||||||
|
|
||||||
/* Map Note Card (Zoomed In) */
|
/* Map Note Card (Zoomed In) */
|
||||||
.map-note-card {
|
.map-note-card {
|
||||||
background: white;
|
background: transparent;
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
box-shadow: var(--shadow-md);
|
||||||
width: 200px;
|
width: 200px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
border: 1px solid #ddd;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
@ -101,4 +99,38 @@
|
||||||
.custom-div-icon {
|
.custom-div-icon {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Leaflet Popup Overrides */
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 240px !important;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-note-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.popup-note-header {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.popup-note-body {
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
}
|
||||||
|
.popup-note-body .note-card-link {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
@ -383,4 +383,43 @@
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%) rotate(90deg) scale(1.05);
|
transform: translateX(-50%) rotate(90deg) scale(1.05);
|
||||||
}
|
}
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.sidebar-header h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.notepad-card {
|
||||||
|
padding: 10px 16px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.notepad-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.notepad-card-footer {
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
.note-card {
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin: 6px 10px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.note-card-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.note-card-body {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.note-card-meta {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
.note-card-actions {
|
||||||
|
margin-top: 4px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.note-card-link {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,27 +205,16 @@ window.SpotNotes.loadNotepads = async function(isInitialLoad = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial Load Logic (Check for URL params first, else default behavior based on auth)
|
// Initial / Default Load Logic: when no notepad is selected, show default notes on map
|
||||||
if (isInitialLoad === true) {
|
const params = new URLSearchParams(window.location.search);
|
||||||
const params = new URLSearchParams(window.location.search);
|
if (isInitialLoad === true && params.has('pad')) {
|
||||||
if (params.has('pad')) {
|
const token = params.get('token');
|
||||||
const token = params.get('token');
|
window.SpotNotes.loadNotesForPad(params.get('pad'), undefined, undefined, undefined, true, token);
|
||||||
window.SpotNotes.loadNotesForPad(params.get('pad'), undefined, undefined, undefined, true, token);
|
} else if (isInitialLoad === true && params.has('note')) {
|
||||||
} else if (params.has('note')) {
|
const token = params.get('token');
|
||||||
const token = params.get('token');
|
window.SpotNotes.loadSingleNote(params.get('note'), token);
|
||||||
window.SpotNotes.loadSingleNote(params.get('note'), token);
|
} else {
|
||||||
} else if (window.SpotNotes.isLoggedIn) {
|
await window.SpotNotes.loadDefaultNotes();
|
||||||
window.SpotNotes.loadAllMyNotes(); // Default action for logged in users
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/notepads?type=public');
|
|
||||||
const pads = await res.json();
|
|
||||||
const globalPad = pads.find(p => p.name === "Global Public Notes");
|
|
||||||
if (globalPad) {
|
|
||||||
window.SpotNotes.loadNotesForPad(globalPad.id, globalPad.name, true, globalPad.owner_id);
|
|
||||||
}
|
|
||||||
} catch (err) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -822,3 +811,22 @@ window.SpotNotes.renderActiveNotesList = function(filterText = '') {
|
||||||
// Usually a sidebar filter only filters the list if it says "Add a simple, fast text input filter floating above the note list in the sidebar... filtering the existing activeNotes array before re-rendering the sidebar list".
|
// Usually a sidebar filter only filters the list if it says "Add a simple, fast text input filter floating above the note list in the sidebar... filtering the existing activeNotes array before re-rendering the sidebar list".
|
||||||
// We will leave the map markers as they are to show all active notes. Or wait, if we only render the list, the map shows everything. Let's just leave the map markers as is (calling renderMapMarkers once on load).
|
// We will leave the map markers as they are to show all active notes. Or wait, if we only render the list, the map shows everything. Let's just leave the map markers as is (calling renderMapMarkers once on load).
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.SpotNotes.loadDefaultNotes = async function() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/notes/default');
|
||||||
|
if (!res.ok) throw new Error("Failed to load");
|
||||||
|
const notes = await res.json();
|
||||||
|
window.SpotNotes.activeNotes = notes;
|
||||||
|
window.SpotNotes.renderMapMarkers();
|
||||||
|
|
||||||
|
// Also auto-center map bounds cleanly if we have notes and no pad/note query params
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (notes.length > 0 && !params.has('pad') && !params.has('note')) {
|
||||||
|
const bounds = L.latLngBounds(notes.map(n => [n.lat, n.lng]));
|
||||||
|
window.SpotNotes.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading default notes:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -121,11 +121,15 @@ window.SpotNotes.renderMapMarkers = function() {
|
||||||
if (showFull) {
|
if (showFull) {
|
||||||
const icon = L.divIcon({
|
const icon = L.divIcon({
|
||||||
className: '',
|
className: '',
|
||||||
html: `<div class="map-note-card" style="border-top: 4px solid ${padColor};">
|
html: `<div class="map-note-card">
|
||||||
<div style="font-weight:bold; margin-bottom:4px;">${window.SpotNotes.escapeHtml(note.title)}</div>
|
<div class="popup-note-header" style="background-color: ${padColor}; color: white; padding: 6px 10px; font-weight: bold; border-radius: 4px 4px 0 0; text-shadow: 0 1px 2px rgba(0,0,0,0.15); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||||
<div style="white-space: pre-wrap;">${window.SpotNotes.escapeHtml(note.content)}</div>
|
${window.SpotNotes.escapeHtml(note.title)}
|
||||||
<div style="font-size:0.7rem; color:#888; margin-top:4px;">${dateStr}</div>
|
</div>
|
||||||
${directionsBtn} ${shareBtn} ${actionBtns}
|
<div style="padding: 10px; background: white; border-radius: 0 0 4px 4px; border: 1px solid #ddd; border-top: none;">
|
||||||
|
<div style="white-space: pre-wrap; margin-bottom: 6px;">${window.SpotNotes.escapeHtml(note.content)}</div>
|
||||||
|
<div style="font-size:0.7rem; color:#888; margin-top:4px;">${dateStr}</div>
|
||||||
|
<div style="margin-top: 6px;">${directionsBtn} ${shareBtn} ${actionBtns}</div>
|
||||||
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
iconSize: [200, 'auto'],
|
iconSize: [200, 'auto'],
|
||||||
iconAnchor: [100, 0]
|
iconAnchor: [100, 0]
|
||||||
|
|
@ -142,7 +146,20 @@ window.SpotNotes.renderMapMarkers = function() {
|
||||||
popupAnchor: [0, -36]
|
popupAnchor: [0, -36]
|
||||||
});
|
});
|
||||||
marker = L.marker([note.lat, note.lng], { icon: svgIcon });
|
marker = L.marker([note.lat, note.lng], { icon: svgIcon });
|
||||||
marker.bindPopup(`<b>${window.SpotNotes.escapeHtml(note.title)}</b><br><div style="white-space: pre-wrap;">${window.SpotNotes.escapeHtml(note.content)}</div><span style="font-size:0.7rem; color:#888;">${dateStr}</span><br>${directionsBtn} ${shareBtn} ${actionBtns}`);
|
|
||||||
|
const popupHtml = `
|
||||||
|
<div class="popup-note-card">
|
||||||
|
<div class="popup-note-header" style="background-color: ${padColor}; color: white; padding: 8px 12px; font-weight: bold; text-shadow: 0 1px 2px rgba(0,0,0,0.15); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||||
|
${window.SpotNotes.escapeHtml(note.title)}
|
||||||
|
</div>
|
||||||
|
<div class="popup-note-body">
|
||||||
|
<div style="white-space: pre-wrap; margin-bottom: 6px;">${window.SpotNotes.escapeHtml(note.content)}</div>
|
||||||
|
<div style="font-size:0.7rem; color:#888; margin-bottom: 6px;">${dateStr}</div>
|
||||||
|
<div style="margin-top: 6px;">${directionsBtn} ${shareBtn} ${actionBtns}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
marker.bindPopup(popupHtml, { padding: [0, 0] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const bumpZIndex = function() {
|
const bumpZIndex = function() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue