diff --git a/blueprints/api.py b/blueprints/api.py index 30c4080..b327446 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -6,6 +6,7 @@ from models.note import Note from models.collaborator import Collaborator from models.user import User from models.favorite import Favorite +from sqlalchemy import func import csv from io import StringIO @@ -68,13 +69,23 @@ def get_notepads(): query = query.filter_by(is_public=True) pads = query.order_by(Notepad.name).all() + + # Optimize note count query (O(1) database hit instead of O(N)) + note_counts = dict(db.session.query(Note.notepad_id, func.count(Note.id)).filter(Note.is_hidden == False).group_by(Note.notepad_id).all()) + + # Optimize write permissions check + collab_write_pad_ids = set() + if user_id: + collab_write_pad_ids = {c.notepad_id for c in Collaborator.query.filter_by(user_id=user_id, permission='write').all()} + results = [] for pad in pads: - p_dict = pad.to_dict() + note_count = note_counts.get(pad.id, 0) + p_dict = pad.to_dict(note_count=note_count) if user_id: if pad.user_id == user_id: p_dict['share_token'] = get_share_token('notepad', pad.id) - p_dict['can_write'] = check_pad_write_permission(pad, user_id) + p_dict['can_write'] = (pad.user_id == user_id) or (pad.id in collab_write_pad_ids) else: p_dict['can_write'] = False results.append(p_dict) @@ -156,7 +167,6 @@ def get_note(note_id): n_dict['share_token'] = get_share_token('note', note_id) return jsonify(n_dict) - @api_bp.route('/notes/mine', methods=['GET']) def get_my_all_notes(): user_id = session.get('user_id') @@ -164,10 +174,14 @@ def get_my_all_notes(): return jsonify({'error': 'Unauthorized'}), 401 notes = Note.query.join(Notepad).filter(Notepad.user_id == user_id, 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() + n_dict['notepad'] = note.notepad.to_dict(note_count=note_counts.get(note.notepad_id, 0)) n_dict['owner_id'] = user_id n_dict['is_public'] = note.notepad.is_public n_dict['share_token'] = get_share_token('note', note.id) diff --git a/blueprints/search.py b/blueprints/search.py index f4f54cc..19e261c 100644 --- a/blueprints/search.py +++ b/blueprints/search.py @@ -1,9 +1,10 @@ from flask import Blueprint, request, jsonify, session -from sqlalchemy import or_ +from sqlalchemy import or_, func from extensions import db from models.note import Note from models.notepad import Notepad from models.favorite import Favorite +from models.collaborator import Collaborator from .api import check_pad_write_permission import urllib.request import urllib.parse @@ -45,13 +46,19 @@ def search_notes(): favs = Favorite.query.filter_by(user_id=user_id).all() fav_ids = {f.notepad_id for f in favs} + # Eagerly fetch note counts and permissions to prevent N+1 queries + note_counts = dict(db.session.query(Note.notepad_id, func.count(Note.id)).filter(Note.is_hidden == False).group_by(Note.notepad_id).all()) + collab_write_pad_ids = set() + if user_id: + collab_write_pad_ids = {c.notepad_id for c in Collaborator.query.filter_by(user_id=user_id, permission='write').all()} + # Construct response with extra info for frontend (owner_id, is_public) output = [] for note, pad in results: n_dict = note.to_dict() - p_dict = pad.to_dict() + p_dict = pad.to_dict(note_count=note_counts.get(pad.id, 0)) p_dict['is_favorite'] = pad.id in fav_ids - p_dict['can_write'] = check_pad_write_permission(pad, user_id) + p_dict['can_write'] = (pad.user_id == user_id) or (pad.id in collab_write_pad_ids) n_dict['notepad'] = p_dict n_dict['owner_id'] = pad.user_id # Keep for map marker compatibility diff --git a/models/notepad.py b/models/notepad.py index b7d14b3..a0d1fff 100644 --- a/models/notepad.py +++ b/models/notepad.py @@ -15,12 +15,12 @@ class Notepad(db.Model): def __init__(self, **kwargs): super().__init__(**kwargs) - def to_dict(self): + def to_dict(self, note_count=None): return { 'id': self.id, 'name': self.name, 'is_public': self.is_public, 'owner_id': self.user_id, 'color': self.color, - 'note_count': self.notes.filter_by(is_hidden=False).count() + 'note_count': note_count if note_count is not None else self.notes.filter_by(is_hidden=False).count() } \ No newline at end of file diff --git a/refactor.py b/refactor.py deleted file mode 100644 index a04b328..0000000 --- a/refactor.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import re - -base_dir = r'z:\source\forgejo\spotnotes.app\static\assets\js' -index_path = os.path.join(base_dir, 'index.js') - -with open(index_path, 'r', encoding='utf-8') as f: - content = f.read() - -# Remove the blocks that were moved to map.js and sidebar.js -# initMap -content = re.sub(r'function initMap\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) -# initSidebar -content = re.sub(r'function initSidebar\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) -# renderMapMarkers -content = re.sub(r'function renderMapMarkers\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) -# stopTracking -content = re.sub(r'function stopTracking\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) -# updateTrackingUI -content = re.sub(r'function updateTrackingUI\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) -# renderNotepadList -content = re.sub(r'function renderNotepadList\(pads, containerId, isFavorite\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) -# setupPublicToggle -content = re.sub(r'function setupPublicToggle\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) -# toggleSection -content = re.sub(r'function toggleSection\(event\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL) - - -# Now, wrap the whole thing or replace globals -# It's easier to rewrite index.js using a structured output or targeted regexes. -# Let's replace the variable declarations: -globals_to_replace = [ - 'map', 'activeNotes', 'userLocation', 'activePadName', 'activePadId', - 'activePadIsPublic', 'activePadOwnerId', 'activePadCanWrite', 'currentUser', - 'isTracking', 'userMarker', 'notifiedNoteIds', 'isLoggedIn', 'currentMarkers', 'topZIndex' -] - -# Remove the var declarations -for var in globals_to_replace: - content = re.sub(r'let\s+' + var + r'\s*=[^;]+;\n?', '', content) - -# Prepend window.SpotNotes. to usages -for var in globals_to_replace: - # Use negative lookbehind/lookahead to avoid matching substring or properties if any - content = re.sub(r'(? - -

${window.SpotNotes.escapeHtml(currentName)}

- - ${manageShareBtn} - ${exportBtn} - -
- -
- ${addNoteBtn} -""" -content = content.replace(""" -
- -

${window.SpotNotes.escapeHtml(currentName)}

- - ${manageShareBtn} - ${exportBtn} -
- ${addNoteBtn} -""", header_replace) - -# Now, extract the rendering logic into a new function `window.SpotNotes.renderActiveNotesList` -old_render_block = """ - // Render Sidebar List - sidebarBody.innerHTML = ''; - if (notes.length === 0) { - sidebarBody.innerHTML = '

No notes yet.

'; - } - - notes.forEach(note => { - const item = document.createElement('div'); - item.className = 'sidebar-note-item'; - - let actionBtns = ''; - if (window.SpotNotes.activePadCanWrite) { - const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\\\'").replace(/"/g, '"'); - const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\\\'").replace(/"/g, '"').replace(/\\n/g, '\\\\n'); - actionBtns = ` - - - `; - } else if (window.SpotNotes.activePadIsPublic) { - actionBtns = ``; - } - - const dateStr = new Date(note.created_at).toLocaleString(); - - item.innerHTML = ` -
${window.SpotNotes.escapeHtml(note.title)}
-
${window.SpotNotes.escapeHtml(note.content)}
-
- ${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)} -
-
- Directions - ${actionBtns} -
- `; - item.addEventListener('click', () => { - window.SpotNotes.stopTracking(); - window.SpotNotes.map.setView([note.lat, note.lng], 16); - }); - sidebarBody.appendChild(item); - }); - - window.SpotNotes.renderMapMarkers(); -""" - -new_render_call = """ - // Render Sidebar List - window.SpotNotes.renderActiveNotesList(); -""" - -content = content.replace(old_render_block, new_render_call) - -# Add event listener for the filter input inside loadNotesForPad -filter_listener = """ - const filterInput = document.getElementById('filter-active-notes'); - if (filterInput) { - filterInput.addEventListener('keyup', (e) => { - window.SpotNotes.renderActiveNotesList(e.target.value); - }); - } -""" -content = content.replace(new_render_call, new_render_call + filter_listener) - - -# Now append the function window.SpotNotes.renderActiveNotesList to the end of the file -render_function = """ -window.SpotNotes.renderActiveNotesList = function(filterText = '') { - const sidebarBody = document.getElementById('sidebar-body'); - if (!sidebarBody) return; - - sidebarBody.innerHTML = ''; - - const filterLower = filterText.toLowerCase(); - const filteredNotes = window.SpotNotes.activeNotes.filter(note => { - return note.title.toLowerCase().includes(filterLower) || note.content.toLowerCase().includes(filterLower); - }); - - if (filteredNotes.length === 0) { - sidebarBody.innerHTML = '

No notes match.

'; - } - - filteredNotes.forEach(note => { - const item = document.createElement('div'); - item.className = 'sidebar-note-item'; - - let actionBtns = ''; - if (window.SpotNotes.activePadCanWrite) { - const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\\\'").replace(/"/g, '"'); - const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\\\'").replace(/"/g, '"').replace(/\\n/g, '\\\\n'); - actionBtns = ` - - - `; - } else if (window.SpotNotes.activePadIsPublic) { - actionBtns = ``; - } - - const dateStr = new Date(note.created_at).toLocaleString(); - - item.innerHTML = ` -
${window.SpotNotes.escapeHtml(note.title)}
-
${window.SpotNotes.escapeHtml(note.content)}
-
- ${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)} -
-
- Directions - ${actionBtns} -
- `; - item.addEventListener('click', () => { - window.SpotNotes.stopTracking(); - window.SpotNotes.map.setView([note.lat, note.lng], 16); - }); - sidebarBody.appendChild(item); - }); - - // We do NOT call window.SpotNotes.renderMapMarkers() here, because we only want to filter the sidebar, not the map itself. - // Wait, the task says: "filtering the existing activeNotes array before re-rendering the sidebar list". - // It doesn't strictly say to filter the map markers, but maybe we should? - // 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). -}; -""" - -content += "\n" + render_function - -with open(index_path, 'w', encoding='utf-8') as f: - f.write(content) - -print('Filtered active notes logic applied') diff --git a/static/assets/css/auth.css b/static/assets/css/auth.css index e41b6f3..ee8928d 100644 --- a/static/assets/css/auth.css +++ b/static/assets/css/auth.css @@ -1,46 +1,54 @@ -/* Auth & Content Containers */ +/* Auth Containers */ .auth-container, .content-container { - background: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); + background: var(--bg-card); + padding: 2.5rem; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + border: 1px solid var(--border); width: 100%; - margin: 20px; + margin: 40px auto; + transition: var(--transition); } -.auth-container { max-width: 400px; } -.content-container { max-width: 800px; margin-top: 20px; } - -/* Forms */ -.form-group { margin-bottom: 1rem; } - -.form-group label { - display: block; - margin-bottom: 0.5rem; - color: #333; - font-weight: 500; +.auth-container { + max-width: 440px; +} +.content-container { + max-width: 800px; } -.form-group input { - width: 100%; - padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 16px; /* Mobile friendly */ - min-height: 44px; +/* Auth Header */ +.auth-container h2 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 24px; + text-align: center; + color: var(--text-main); } -.links { margin-top: 1rem; text-align: center; font-size: 0.9rem; } -.links a { color: #007bff; text-decoration: none; display: inline-block; padding: 10px; min-height: 44px; } -.links a:hover { text-decoration: underline; } +/* Links */ +.links { + margin-top: 1.5rem; + text-align: center; + font-size: 0.9rem; + color: var(--text-muted); +} +.links a { + color: var(--primary); + text-decoration: none; + font-weight: 600; + transition: var(--transition); + display: inline-block; +} +.links a:hover { + color: var(--primary-hover); + text-decoration: underline; +} -/* Ensure buttons on content pages are white */ -.content-container .btn { color: white; } - -@media (max-width: 600px) { +@media (max-width: 768px) { .auth-container, .content-container { - padding: 1.5rem; - margin: 10px; - width: calc(100% - 20px); + padding: 2rem 1.5rem; + margin: 20px 12px; + width: calc(100% - 24px); } } \ No newline at end of file diff --git a/static/assets/css/base.css b/static/assets/css/base.css index d1ed1ba..eb06dc7 100644 --- a/static/assets/css/base.css +++ b/static/assets/css/base.css @@ -1,3 +1,47 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +:root { + /* Color Palette */ + --primary: #4f46e5; /* Modern Indigo */ + --primary-hover: #4338ca; + --primary-light: #e0e7ff; + --secondary: #0ea5e9; /* Ocean sky blue */ + --secondary-hover: #0284c7; + + /* Backgrounds */ + --bg-page: #f8fafc; + --bg-card: #ffffff; + --bg-glass: rgba(255, 255, 255, 0.85); + + /* Borders */ + --border: #e2e8f0; + --border-glass: rgba(255, 255, 255, 0.4); + --border-focus: #a5b4fc; + + /* Text */ + --text-main: #0f172a; /* Deep slate */ + --text-muted: #64748b; /* Light slate */ + --text-on-primary: #ffffff; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-glass: 0 8px 32px 0 rgba(31, 38, 135, 0.08); + + /* Font */ + --font-sans: 'Inter', system-ui, -apple-system, sans-serif; + + /* Radii */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-full: 9999px; + + /* Transitions */ + --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + /* Reset and Base Styles */ * { box-sizing: border-box; @@ -8,7 +52,9 @@ body, html { height: 100%; width: 100%; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-family: var(--font-sans); + color: var(--text-main); + background-color: var(--bg-page); overflow: hidden; } @@ -17,28 +63,122 @@ body.page-body { flex-direction: column; justify-content: flex-start; align-items: center; - background: #f4f4f4; - overflow: auto; + background: radial-gradient(circle at 50% 50%, #f1f5f9 0%, #e2e8f0 100%); + overflow-y: auto; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: var(--radius-full); + opacity: 0.6; +} +::-webkit-scrollbar-thumb:hover { + background: var(--text-main); } /* Generic Components */ .btn { width: 100%; - padding: 0.75rem; - color: white; - border: none; - border-radius: 4px; + padding: 0.75rem 1.5rem; + color: var(--text-on-primary); + border: 1px solid transparent; + border-radius: var(--radius-md); cursor: pointer; - font-size: 1rem; - transition: opacity 0.2s; + font-family: var(--font-sans); + font-weight: 500; + font-size: 0.95rem; + transition: var(--transition); min-height: 44px; - display: flex; + display: inline-flex; justify-content: center; align-items: center; + gap: 8px; + box-shadow: var(--shadow-sm); } -.btn:hover { opacity: 0.9; } -.btn-primary { background: #007bff; } -.btn-success { background: #28a745; } -.btn-secondary { background: #6c757d; margin-top: 10px; } -.hidden { display: none !important; } \ No newline at end of file +.btn:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn:active { + transform: translateY(0); +} + +.btn-primary { + background: var(--primary); +} +.btn-primary:hover { + background: var(--primary-hover); +} + +.btn-success { + background: #10b981; /* Emerald green */ +} +.btn-success:hover { + background: #059669; +} + +.btn-secondary { + background: var(--bg-card); + color: var(--text-main); + border-color: var(--border); + margin-top: 10px; +} +.btn-secondary:hover { + background: var(--border); +} + +.hidden { display: none !important; } + +/* Global Forms & Input Elements styling */ +.form-group { + margin-bottom: 1.25rem; + width: 100%; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); +} + +.form-group input[type="text"], +.form-group input[type="email"], +.form-group input[type="password"], +.form-group select, +.form-group textarea, +input.admin-search { + width: 100%; + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + outline: none; + font-family: var(--font-sans); + font-size: 0.95rem; + color: var(--text-main); + background-color: var(--bg-page); + transition: var(--transition); + min-height: 42px; +} + +.form-group input[type="text"]:focus, +.form-group input[type="email"]:focus, +.form-group input[type="password"]:focus, +.form-group select:focus, +.form-group textarea:focus, +input.admin-search:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px var(--primary-light); + background-color: var(--bg-card); +} \ No newline at end of file diff --git a/static/assets/css/modals.css b/static/assets/css/modals.css index 7a6a98a..ec9ad34 100644 --- a/static/assets/css/modals.css +++ b/static/assets/css/modals.css @@ -1,49 +1,110 @@ -/* Modals */ +/* Modals overlay */ .modal { position: fixed; - top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.5); + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(15, 23, 42, 0.4); display: flex; justify-content: center; align-items: center; z-index: 2000; - backdrop-filter: blur(2px); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + animation: modalFadeIn 0.2s ease-out forwards; } -.modal.hidden { display: none; } +.modal.hidden { display: none !important; } +/* Modal Content Card */ .modal-content { - background: white; - padding: 25px; - border-radius: 8px; - width: 90%; - max-width: 400px; + background: var(--bg-card); + padding: 30px; + border-radius: var(--radius-lg); + width: 92%; + max-width: 440px; position: relative; - box-shadow: 0 4px 15px rgba(0,0,0,0.2); - max-height: 90vh; + box-shadow: var(--shadow-lg); + max-height: 85vh; overflow-y: auto; + border: 1px solid var(--border); + animation: modalScaleUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } -@media (max-width: 600px) { +/* Close Button */ +.close-modal { + position: absolute; + top: 16px; + right: 18px; + font-size: 1.6rem; + cursor: pointer; + color: var(--text-muted); + transition: var(--transition); + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-full); +} +.close-modal:hover { + color: var(--text-main); + background-color: var(--border); +} + +.modal-content h3 { + font-size: 1.3rem; + font-weight: 700; + margin-bottom: 8px; + color: var(--text-main); +} + +.modal-content h4 { + font-size: 1rem; + font-weight: 600; + margin: 16px 0 8px 0; + color: var(--text-main); +} + + +/* Color picker specific */ +.form-group input[type="color"] { + width: 50px; + height: 40px; + padding: 0; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + background: none; +} + +/* Animations */ +@keyframes modalFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes modalScaleUp { + from { transform: scale(0.96); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +/* Mobile Modals */ +@media (max-width: 768px) { .modal { - align-items: flex-end; /* Align to bottom for better mobile reach or use full height */ + align-items: flex-end; /* Anchor at the bottom on mobile */ } .modal-content { width: 100%; - height: 100%; max-width: none; - max-height: 100%; - border-radius: 0; - padding: 20px; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + padding: 24px; + max-height: 80vh; + animation: modalSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; } -} - -.close-modal { - position: absolute; - top: 10px; right: 15px; - font-size: 1.5rem; - cursor: pointer; - color: #aaa; - transition: color 0.2s; -} -.close-modal:hover { color: #333; } \ No newline at end of file + @keyframes modalSlideUp { + from { transform: translateY(100%); } + to { transform: translateY(0); } + } +} \ No newline at end of file diff --git a/static/assets/css/navbar.css b/static/assets/css/navbar.css index d09e335..2a01d43 100644 --- a/static/assets/css/navbar.css +++ b/static/assets/css/navbar.css @@ -4,26 +4,37 @@ top: 0; left: 0; width: 100%; - height: 50px; - background-color: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(5px); + height: 60px; + background-color: var(--bg-glass); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); display: flex; justify-content: space-between; align-items: center; - padding: 0 15px; + padding: 0 24px; z-index: 1000; - box-shadow: 0 2px 5px rgba(0,0,0,0.1); - flex-shrink: 0 + box-shadow: var(--shadow-sm); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + transition: var(--transition); } .brand { - font-weight: bold; - font-size: 1.2rem; - color: #333; + font-weight: 700; + font-size: 1.25rem; + color: var(--text-main); text-decoration: none; flex: 1; display: flex; justify-content: flex-start; + align-items: center; + transition: var(--transition); +} +.brand img { + transition: var(--transition); +} +.brand:hover img { + transform: scale(1.03); } .nav-links { @@ -31,45 +42,94 @@ display: flex; justify-content: flex-end; align-items: center; - gap: 15px; + gap: 20px; } .nav-links a { text-decoration: none; - color: #555; + color: var(--text-muted); font-size: 0.9rem; - transition: color 0.2s; + font-weight: 500; + transition: var(--transition); white-space: nowrap; + position: relative; + padding: 6px 0; } -.nav-links a:hover { color: #000; } +.nav-links a:hover { + color: var(--primary); +} + +.nav-links a::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--primary); + transform: scaleX(0); + transform-origin: right; + transition: transform 0.25s ease; +} +.nav-links a:hover::after { + transform: scaleX(1); + transform-origin: left; +} /* Search Bar in Navbar */ .nav-search { - flex: 0 1 400px; - margin: 0 15px; + flex: 0 1 420px; + margin: 0 20px; display: flex; justify-content: center; } +.nav-search form { + display: flex; + width: 100%; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: var(--transition); +} +.nav-search form:focus-within { + border-color: var(--primary); + box-shadow: 0 0 0 3px var(--primary-light); +} + .nav-search input { width: 100%; - padding: 8px 10px; - border: 1px solid #ccc; - border-radius: 4px 0 0 4px; + padding: 10px 16px; + border: none; outline: none; - font-size: 16px; /* Prevents zoom on iOS */ - min-height: 44px; + font-family: var(--font-sans); + font-size: 0.95rem; + background: transparent; + color: var(--text-main); + min-height: 40px; } +.nav-search input::placeholder { + color: var(--text-muted); +} + .nav-search button { - padding: 5px 12px; - border: 1px solid #ccc; - border-left: none; - background: #f8f9fa; - border-radius: 0 4px 4px 0; + padding: 0 16px; + border: none; + background: transparent; cursor: pointer; - min-height: 44px; + font-size: 1.1rem; + color: var(--text-muted); + transition: var(--transition); + display: flex; + align-items: center; + justify-content: center; +} +.nav-search button:hover { + color: var(--primary); + background-color: var(--bg-page); } -.nav-search button:hover { background: #e2e6ea; } .nav-toggle { display: none; @@ -77,37 +137,27 @@ border: none; font-size: 1.5rem; cursor: pointer; - color: #333; + color: var(--text-main); padding: 5px; } /* Mobile Navbar */ -@media (max-width: 600px) { +@media (max-width: 768px) { .navbar { flex-wrap: wrap; height: auto; - padding: 10px; - gap: 10px; + padding: 12px 16px; + gap: 12px; } .brand { flex: 1; order: 1; } - .nav-toggle { - display: none !important; - } .nav-links { - display: flex; flex: 1; order: 2; justify-content: flex-end; - align-items: center; - gap: 10px; - background: transparent; - padding: 0; - } - .nav-links a { - font-size: 0.9rem; + gap: 12px; } .nav-search { flex: 1 1 100%; diff --git a/static/assets/css/sidebar.css b/static/assets/css/sidebar.css index 051e784..3d6830b 100644 --- a/static/assets/css/sidebar.css +++ b/static/assets/css/sidebar.css @@ -1,40 +1,54 @@ -/* Right Collapsible Sidebar */ +/* Right Collapsible Floating Sidebar */ .sidebar { position: absolute; - top: 50px; /* Below navbar */ - right: 0; - bottom: 0; - width: 320px; - background-color: white; + top: 75px; /* Below navbar (60px) + margin */ + right: 15px; + bottom: 15px; + width: 340px; + background-color: var(--bg-glass); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); z-index: 1000; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); - transition: transform 0.3s ease-in-out; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border-glass); + border-radius: var(--radius-lg); + transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); display: flex; flex-direction: column; + overflow: visible; /* Let toggle stick out */ } .sidebar.collapsed { - transform: translateX(320px); + transform: translateX(calc(100% + 20px)); } /* Toggle Button attached to sidebar */ .sidebar-toggle { position: absolute; left: -40px; - top: 10px; + top: 20px; width: 40px; - height: 40px; - background-color: white; - border: none; - border-radius: 5px 0 0 5px; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); + height: 48px; + background-color: var(--bg-glass); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--border-glass); + border-right: none; + border-radius: var(--radius-md) 0 0 var(--radius-md); + box-shadow: -4px 4px 12px rgba(0,0,0,0.08); cursor: pointer; - font-weight: bold; - color: #555; + font-size: 1.1rem; + color: var(--text-muted); display: flex; align-items: center; justify-content: center; - font-size: 1.2rem; + transition: var(--transition); +} +.sidebar-toggle:hover { + color: var(--primary); + background-color: var(--bg-card); + width: 42px; + left: -42px; } .sidebar-content { @@ -47,138 +61,326 @@ .sidebar-header { display: flex; - justify-content: space-between; - align-items: center; - padding: 15px; - border-bottom: 1px solid #eee; - background: #fff; + flex-direction: column; + align-items: stretch; + padding: 16px 20px; + border-bottom: 1px solid var(--border); flex-shrink: 0; + gap: 8px; +} +.sidebar-header h2 { + font-size: 1.2rem; + font-weight: 700; + color: var(--text-main); } #sidebar-body { flex: 1; overflow-y: auto; min-height: 0; + padding: 10px 0; } .btn-new-pad { - background-color: #007bff; + background-color: var(--primary); color: white; border: none; - width: 30px; - height: 30px; - border-radius: 50%; + width: 32px; + height: 32px; + border-radius: var(--radius-full); cursor: pointer; font-size: 1.2rem; - line-height: 1; display: flex; align-items: center; justify-content: center; + transition: var(--transition); + box-shadow: var(--shadow-sm); +} +.btn-new-pad:hover { + background-color: var(--primary-hover); + transform: scale(1.05); } .btn-center-location { width: calc(100% - 30px); - margin: 15px; + margin: 10px 15px; padding: 10px; - background-color: #17a2b8; + background-color: var(--secondary); color: white; border: none; - border-radius: 4px; + border-radius: var(--radius-md); cursor: pointer; - font-weight: bold; + font-weight: 600; + font-size: 0.9rem; flex-shrink: 0; + transition: var(--transition); + box-shadow: var(--shadow-sm); +} +.btn-center-location:hover { + background-color: var(--secondary-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); } -.btn-center-location:hover { background-color: #138496; } .btn-add-note { - background-color: #28a745; + background-color: var(--primary); color: white; border: none; - padding: 5px 10px; - border-radius: 4px; + padding: 8px 14px; + border-radius: var(--radius-md); font-size: 0.8rem; + font-weight: 600; cursor: pointer; + transition: var(--transition); + align-self: flex-start; +} +.btn-add-note:hover { + background-color: var(--primary-hover); + transform: translateY(-1px); } -.btn-add-note:hover { background-color: #218838; } /* Sidebar Lists & Items */ -.list-section { border-bottom: 1px solid #f0f0f0; } +.list-section { + border-bottom: 1px solid var(--border); +} +.list-section:last-child { + border-bottom: none; +} .list-section-header, .btn-toggle-section { width: 100%; text-align: left; - background: #fff; + background: transparent; border: none; - padding: 12px 15px; + padding: 14px 20px; cursor: pointer; - font-weight: 600; - font-size: 0.85rem; - color: #555; + font-weight: 700; + font-size: 0.8rem; + color: var(--text-muted); text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 1px; margin: 0; - transition: background-color 0.2s; -} -.list-section-header:hover, -.btn-toggle-section:hover { - background-color: #f8f9fa; - color: #333; -} - -.notepad-item, .sidebar-note-item { - padding: 10px 15px; - border-bottom: 1px solid #eee; - cursor: pointer; -} -.notepad-item:hover, .sidebar-note-item:hover { - background-color: #fafafa; -} - -/* Collaborator List in Share Modal */ -.collaborator-item { display: flex; justify-content: space-between; align-items: center; - padding: 8px 0; - border-bottom: 1px solid #f0f0f0; + transition: var(--transition); } -.collaborator-item:last-child { - border-bottom: none; -} -.collaborator-item button { - background: none; - border: none; - color: #dc3545; - cursor: pointer; - font-size: 0.8rem; +.list-section-header:hover, +.btn-toggle-section:hover { + background-color: rgba(0, 0, 0, 0.02); + color: var(--text-main); } -/* Mobile Sidebar */ -@media (max-width: 600px) { +/* Notepad Cards inside list */ +.notepad-card { + padding: 12px 20px; + border-bottom: 1px solid rgba(0, 0, 0, 0.03); + cursor: pointer; + transition: var(--transition); + display: flex; + flex-direction: column; + gap: 6px; +} +.notepad-card:hover { + background-color: rgba(255, 255, 255, 0.5); +} + +.notepad-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.notepad-info { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 0.95rem; + color: var(--text-main); +} + +.notepad-color-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: var(--radius-full); + border: 1px solid rgba(0, 0, 0, 0.1); +} + +.notepad-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.btn-action-icon { + border: none; + background: transparent; + cursor: pointer; + font-size: 0.9rem; + padding: 4px; + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + transition: var(--transition); + color: var(--text-muted); +} +.btn-action-icon:hover { + background-color: rgba(0, 0, 0, 0.05); + color: var(--text-main); + transform: scale(1.1); +} + +.btn-action-delete:hover { + background-color: #fee2e2; + color: #ef4444; +} +.btn-action-fav { + font-size: 1rem; +} +.btn-action-fav:hover { + color: #ec4899; +} + +.notepad-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + color: var(--text-muted); + padding-left: 18px; +} + +.badge-count { + background-color: var(--primary-light); + color: var(--primary); + padding: 2px 8px; + border-radius: var(--radius-full); + font-weight: 600; + font-size: 0.75rem; +} +/* Note Cards in active pad view */ +.note-card { + padding: 14px 20px; + margin: 8px 12px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition); + box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; + gap: 6px; +} +.note-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: var(--primary-hover); +} + +.note-card-title { + font-weight: 700; + font-size: 0.95rem; + color: var(--text-main); +} + +.note-card-body { + font-size: 0.85rem; + color: var(--text-muted); + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.note-card-meta { + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.3; +} + +.note-card-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + margin-top: 6px; +} + +.note-card-link { + font-size: 0.75rem; + font-weight: 500; + color: #000000 !important; + text-decoration: none; + transition: var(--transition); + padding: 4px 8px; + border-radius: 4px; + border: none; + cursor: pointer; + display: inline-block; + background-color: #e2e8f0; +} +.note-card-link:hover { + opacity: 0.85; + text-decoration: none; +} + +.note-card-link.directions { + background-color: #d1fae5; + color: #065f46 !important; +} +.note-card-link.directions:hover { + background-color: #a7f3d0; +} + +.note-card-link.share { + background-color: #6bbbf7; +} + +.note-card-link.edit { + background-color: #cce5ff; +} + +.note-card-link.delete, +.note-card-link.report { + background-color: #ffcccc; +} + +/* Mobile Sidebar Adaptation */ +@media (max-width: 768px) { .sidebar { - width: 100%; - max-width: none; + width: calc(100% - 24px); + margin: 12px; top: auto; - bottom: 0; + bottom: 12px; left: 0; right: 0; - height: 60vh; - border-radius: 20px 20px 0 0; - box-shadow: 0 -4px 15px rgba(0,0,0,0.15); + height: 45vh; + border-radius: var(--radius-lg); transform: translateY(0); - transition: transform 0.3s ease-in-out; } .sidebar.collapsed { - transform: translateY(100%); + transform: translateY(calc(100% + 20px)); } .sidebar-toggle { left: 50%; - top: -30px; + right: auto; + top: -40px; + width: 48px; + height: 40px; + border: 1px solid var(--border-glass); + border-bottom: none; + border-radius: var(--radius-md) var(--radius-md) 0 0; transform: translateX(-50%) rotate(90deg); - width: 60px; - height: 30px; - border-radius: 5px 0 0 5px; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); + box-shadow: 0 -4px 10px rgba(0,0,0,0.05); + } + .sidebar-toggle:hover { + width: 48px; + left: 50%; + transform: translateX(-50%) rotate(90deg) scale(1.05); } } \ No newline at end of file diff --git a/static/assets/css/styles.css b/static/assets/css/styles.css deleted file mode 100644 index 6896780..0000000 --- a/static/assets/css/styles.css +++ /dev/null @@ -1,339 +0,0 @@ -/* Reset and Base Styles */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body, html { - height: 100%; - width: 100%; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - overflow: hidden; /* Prevent scrollbars, map handles panning */ -} - -/* Map Container */ -#map { - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - z-index: 1; -} - -/* Map Crosshair */ -#map-crosshair { - position: absolute; - top: 50%; - left: 50%; - width: 20px; - height: 20px; - transform: translate(-50%, -50%); - pointer-events: none; - z-index: 900; -} -#map-crosshair::before, #map-crosshair::after { - content: ''; - position: absolute; - background: rgba(0,0,0,0.6); -} -#map-crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; } -#map-crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; } - -/* Top Navigation Bar */ -.navbar { - position: relative; - top: 0; - left: 0; - width: 100%; - height: 40px; - background-color: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(5px); - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 20px; - z-index: 1000; /* Above map */ - box-shadow: 0 2px 5px rgba(0,0,0,0.1); -} - -.brand { - font-weight: bold; - font-size: 1.2rem; - color: #333; - text-decoration: none; -} - -.nav-links a { - text-decoration: none; - color: #555; - margin-left: 20px; - font-size: 0.9rem; - transition: color 0.2s; -} - -.nav-links a:hover { - color: #000; -} - -/* Search Bar in Navbar */ -.nav-search { - flex: 1; - margin: 0 20px; - display: flex; - max-width: 400px; -} -.nav-search input { - width: 100%; - padding: 5px 10px; - border: 1px solid #ccc; - border-radius: 4px 0 0 4px; - outline: none; -} -.nav-search button { - padding: 5px 10px; - border: 1px solid #ccc; - border-left: none; - background: #f8f9fa; - border-radius: 0 4px 4px 0; - cursor: pointer; -} -.nav-search button:hover { - background: #e2e6ea; -} - -/* Right Collapsible Sidebar */ -.sidebar { - position: absolute; - top: 60px; /* Below navbar */ - right: 0; - bottom: 0; - width: 300px; - background-color: white; - z-index: 1000; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); - transition: transform 0.3s ease-in-out; - display: flex; - flex-direction: column; -} - -.sidebar.collapsed { - transform: translateX(300px); -} - -/* Toggle Button attached to sidebar */ -.sidebar-toggle { - position: absolute; - left: -40px; - top: 10px; - width: 40px; - height: 40px; - background-color: white; - border: none; - border-radius: 5px 0 0 5px; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); - cursor: pointer; - font-weight: bold; - color: #555; -} - -.sidebar-content { - padding: 20px; - overflow-y: auto; - flex: 1; -} - -.sidebar-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; -} - -.btn-new-pad { - background-color: #007bff; - color: white; - border: none; - width: 30px; - height: 30px; - border-radius: 50%; - cursor: pointer; - font-size: 1.2rem; - line-height: 1; -} - -/* Center Location Button */ -.btn-center-location { - width: 100%; - padding: 10px; - margin-bottom: 15px; - background-color: #17a2b8; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: bold; -} -.btn-center-location:hover { background-color: #138496; } - -/* Add Note Button (Themed) */ -.btn-add-note { - background-color: #28a745; - color: white; - border: none; - padding: 5px 10px; - border-radius: 4px; - font-size: 0.8rem; - cursor: pointer; -} -.btn-add-note:hover { background-color: #218838; } - -/* --- Auth & Content Page Styles --- */ - -/* Use this class on body for login/register/about pages */ -body.page-body { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - background: #f4f4f4; - overflow: auto; /* Allow scrolling */ -} - -.auth-container, .content-container { - background: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - width: 100%; -} - -.auth-container { - max-width: 400px; -} - -.content-container { - max-width: 800px; - margin: 20px auto; -} - -.form-group { - margin-bottom: 1rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - color: #333; -} - -.form-group input { - width: 100%; - padding: 0.5rem; - border: 1px solid #ddd; - border-radius: 4px; -} - -.btn { - width: 100%; - padding: 0.75rem; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; -} -.btn:hover { opacity: 0.9; } -.btn-primary { background: #007bff; } -.btn-success { background: #28a745; } - -.links { margin-top: 1rem; text-align: center; font-size: 0.9rem; } - -/* Ensure buttons on content pages are white */ -.content-container .btn { color: white; } - -/* --- Modals --- */ -.modal { - position: fixed; - top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 2000; -} - -.modal.hidden { display: none; } - -.modal-content { - background: white; - padding: 20px; - border-radius: 8px; - width: 90%; - max-width: 400px; - position: relative; -} - -.close-modal { - position: absolute; - top: 10px; right: 15px; - font-size: 1.5rem; - cursor: pointer; - color: #aaa; -} - -.btn-secondary { background: #6c757d; margin-top: 10px; } - -/* Sidebar Note Items */ -.sidebar-note-item { - padding: 10px; - border-bottom: 1px solid #eee; - cursor: pointer; -} -.sidebar-note-item:hover { - background-color: #f9f9f9; -} - -/* Map Note Card (Zoomed In) */ -.map-note-card { - background: white; - padding: 8px; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - width: 200px; - font-size: 0.85rem; - border: 1px solid #ddd; - white-space: normal; -} - -/* Sidebar Sections */ -.list-section { - border-bottom: 1px solid #f0f0f0; -} - -.list-section-header, -.btn-toggle-section { - width: 100%; - text-align: left; - background: #fff; - border: none; - padding: 12px 15px; - cursor: pointer; - font-weight: 600; - font-size: 0.85rem; - color: #555; - text-transform: uppercase; - letter-spacing: 0.5px; - margin: 0; - transition: background-color 0.2s; -} -.list-section-header:hover, -.btn-toggle-section:hover { - background-color: #f8f9fa; - color: #333; -} - -.notepad-item:hover { - background-color: #fafafa; -} diff --git a/static/assets/js/index.js b/static/assets/js/index.js index d396cfe..a8f0597 100644 --- a/static/assets/js/index.js +++ b/static/assets/js/index.js @@ -133,10 +133,12 @@ window.SpotNotes.loadNotepads = async function(isInitialLoad = false) { // Reset Sidebar Header const header = document.querySelector('.sidebar-header'); if (header) { - let headerHTML = '

Notepads

'; + let headerHTML = `
+

Notepads

`; if (window.SpotNotes.isLoggedIn) { headerHTML += ''; } + headerHTML += '
'; header.innerHTML = headerHTML; if (window.SpotNotes.isLoggedIn) { @@ -295,20 +297,28 @@ window.SpotNotes.loadNotesForPad = async function(padId, padName, isPublic, owne const header = document.querySelector('.sidebar-header'); if (header) { const addNoteBtn = window.SpotNotes.activePadCanWrite ? `` : ''; - const manageShareBtn = isOwner ? `` : ''; - const exportBtn = ``; + const manageShareBtn = isOwner ? `` : ''; + const exportBtn = ``; + const shareBtn = ``; + header.innerHTML = ` -
- -

${window.SpotNotes.escapeHtml(currentName)}

- - ${manageShareBtn} - ${exportBtn} + -
- +
+ +
+
+ ${addNoteBtn}
- ${addNoteBtn} `; document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads); } @@ -526,25 +536,32 @@ window.SpotNotes.displaySearchResults = function(notes, location, query) { entry.notes.forEach(note => { const item = document.createElement('div'); - item.className = 'sidebar-note-item'; + item.className = 'note-card'; const dateStr = new Date(note.created_at).toLocaleString(); - let actionBtn = ''; // Renaming to actionBtns for consistency + let actionBtn = ''; if (note.notepad && note.notepad.can_write) { const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '"'); const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); - actionBtn = ` `; + actionBtn = ` `; } else if (note.is_public) { - actionBtn = ``; + actionBtn = ``; } + const shareToken = note.share_token || ''; + const shareBtn = ``; + item.innerHTML = ` -
${window.SpotNotes.escapeHtml(note.title)}
-
${window.SpotNotes.escapeHtml(note.content)}
-
+
${window.SpotNotes.escapeHtml(note.title)}
+
${window.SpotNotes.escapeHtml(note.content)}
+
${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
-
${actionBtn}
+
+ Directions + ${shareBtn} + ${actionBtn} +
`; item.addEventListener('click', () => { window.SpotNotes.stopTracking(); @@ -761,32 +778,36 @@ window.SpotNotes.renderActiveNotesList = function(filterText = '') { filteredNotes.forEach(note => { const item = document.createElement('div'); - item.className = 'sidebar-note-item'; + item.className = 'note-card'; let actionBtns = ''; if (window.SpotNotes.activePadCanWrite) { const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '"'); const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); actionBtns = ` - - + + `; } else if (window.SpotNotes.activePadIsPublic) { - actionBtns = ``; + actionBtns = ``; } + const shareToken = note.share_token || ''; + const shareBtn = ``; + const dateStr = new Date(note.created_at).toLocaleString(); item.innerHTML = ` -
${window.SpotNotes.escapeHtml(note.title)}
-
${window.SpotNotes.escapeHtml(note.content)}
-
+
${window.SpotNotes.escapeHtml(note.title)}
+
${window.SpotNotes.escapeHtml(note.content)}
+
${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
-
- Directions - ${actionBtns} -
+
+ Directions + ${shareBtn} + ${actionBtns} +
`; item.addEventListener('click', () => { window.SpotNotes.stopTracking(); diff --git a/static/assets/js/index.js.bak b/static/assets/js/index.js.bak deleted file mode 100644 index d60b096..0000000 --- a/static/assets/js/index.js.bak +++ /dev/null @@ -1,1024 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - initMap(); - initSidebar(); - loadNotepads(true); - - const form = document.getElementById('form-add-collaborator'); - if (form) { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - const padId = document.getElementById('share-pad-id').value; - const email = document.getElementById('collaborator-email').value; - const permission = document.getElementById('collaborator-permission').value; - - try { - const res = await fetch(`/api/notepads/${padId}/collaborators`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, permission }) - }); - - if (res.ok) { - form.reset(); - const padName = document.getElementById('share-pad-name').textContent; - openShareSettingsModal(padId, padName); - } else { - const err = await res.json(); - alert(`Failed to add collaborator: ${err.error}`); - } - } catch (err) { - console.error(err); - alert('An error occurred.'); - } - }); - } - - const formEditPad = document.getElementById('form-edit-pad'); - if (formEditPad) { - formEditPad.addEventListener('submit', async (e) => { - e.preventDefault(); - const padId = document.getElementById('pad-id-edit').value; - const name = document.getElementById('pad-name-edit').value; - const isPublic = document.getElementById('pad-public-edit').checked; - - try { - const res = await fetch(`/api/notepads/${padId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, is_public: isPublic }) - }); - if (res.ok) { - document.getElementById('modal-edit-pad').classList.add('hidden'); - loadNotepads(); // Refresh everything - } else { - alert('Failed to update notepad.'); - } - } catch (err) { console.error(err); } - }); - } - - const formEditNote = document.getElementById('form-edit-note'); - if (formEditNote) { - formEditNote.addEventListener('submit', async (e) => { - e.preventDefault(); - const noteId = document.getElementById('note-id-edit').value; - const title = document.getElementById('note-title-edit').value; - const content = document.getElementById('note-content-edit').value; - - try { - const res = await fetch(`/api/notes/${noteId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title, content }) - }); - if (res.ok) { - document.getElementById('modal-edit-note').classList.add('hidden'); - // Refresh current view - if (activePadId) { - loadNotesForPad(activePadId); - } else { - // This was likely from a search result, which is harder to refresh. - // A full reload of notepads is a safe fallback. - alert('Note updated. Refreshing list.'); - loadNotepads(); - } - } else { - alert('Failed to update note.'); - } - } catch (err) { console.error(err); } - }); - } -}); - -let map; -let activeNotes = []; -let userLocation = null; -let activePadName = ""; -let activePadId = null; -let activePadIsPublic = false; -let activePadOwnerId = null; -let activePadCanWrite = false; -let currentUser = null; -let isTracking = false; -let userMarker = null; -let notifiedNoteIds = new Set(); - -let isLoggedIn = false; -function initMap() { - // Default view (e.g., Oklahoma area if geolocation fails) - map = L.map('map').setView([35.6528, -97.4781], 10); - - // Ask for location - if (navigator.geolocation) { - navigator.geolocation.watchPosition((position) => { - const { latitude, longitude } = position.coords; - userLocation = { lat: latitude, lng: longitude }; - - const centerBtn = document.getElementById('btn-center-user'); - if (centerBtn) centerBtn.style.display = 'block'; - - if (isTracking) { - map.setView([latitude, longitude]); - } else if (!userMarker && !new URLSearchParams(window.location.search).has('pad') && !new URLSearchParams(window.location.search).has('note')) { - // Only auto-center if we aren't loading a shared item - // First fix - map.setView([latitude, longitude], 13); - console.log("Map centered on user location:", latitude, longitude); - } - - // Add User Location Dot - if (userMarker) { - userMarker.setLatLng([latitude, longitude]); - } else { - userMarker = L.circleMarker([latitude, longitude], { - radius: 8, - fillColor: "#3388ff", - color: "#fff", - weight: 2, - opacity: 1, - fillOpacity: 0.8 - }).addTo(map).bindPopup("You are here"); - } - - // Location-Based Browser Alerts - const userLatLng = L.latLng(latitude, longitude); - if (activeNotes && activeNotes.length > 0) { - activeNotes.forEach(note => { - const noteLatLng = L.latLng(note.lat, note.lng); - if (userLatLng.distanceTo(noteLatLng) <= 50) { - if (!notifiedNoteIds.has(note.id)) { - notifiedNoteIds.add(note.id); - alert(`You are near a note!\n\n${note.title}\n${note.content}`); - } - } - }); - } - }, (err) => { - console.warn("Geolocation error:", err); - }, { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0 - }); - } - - // Add OpenStreetMap tiles - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap' - }).addTo(map); - - // Listen for zoom changes to toggle note display - map.on('zoomend', () => { - if (activeNotes.length > 0) { - renderMapMarkers(); - } - }); - - map.on('dragstart', stopTracking); - map.on('popupopen', stopTracking); -} - -function initSidebar() { - const sidebar = document.getElementById('sidebar'); - const toggleBtn = document.getElementById('sidebar-toggle'); - - toggleBtn.addEventListener('click', () => { - sidebar.classList.toggle('collapsed'); - // Change arrow direction based on state - if (sidebar.classList.contains('collapsed')) { - const sections = document.querySelectorAll('.list-section'); - sections.forEach(section => { - section.querySelector('.list-section-content').style.display = 'none'; - }); - toggleBtn.innerHTML = '<'; // Point left (open) - } else { - toggleBtn.innerHTML = '>'; // Point right (close) - } - }); - - const centerBtn = document.getElementById('btn-center-user'); - if (centerBtn) { - centerBtn.addEventListener('click', () => { - isTracking = true; - updateTrackingUI(); - if (userLocation) { - map.setView([userLocation.lat, userLocation.lng], 16); - } - }); - } -} - -async function loadNotepads(isInitialLoad = false) { - // Reset state - activeNotes = []; - currentMarkers.forEach(marker => map.removeLayer(marker)); - currentMarkers = []; - - // Check auth first - try { - const authRes = await fetch('/auth/me'); - const authData = await authRes.json(); - isLoggedIn = authData.authenticated; - currentUser = authData.user || null; - } catch (e) { console.error(e); } - - // Reset Sidebar Header - const header = document.querySelector('.sidebar-header'); - if (header) { - let headerHTML = '

Notepads

'; - if (isLoggedIn) { - headerHTML += ''; - } - header.innerHTML = headerHTML; - - if (isLoggedIn) { - document.getElementById('btn-create-pad').addEventListener('click', () => { - document.getElementById('modal-create-pad').classList.remove('hidden'); - }); - } - } - - const sidebarBody = document.getElementById('sidebar-body'); - - if (isLoggedIn) { - sidebarBody.innerHTML = ` -
- -
-

Loading...

-
-
-
- -

Loading...

-
-
- -

Loading...

-
-
- - -
- `; - const listSection = document.querySelectorAll('.list-section-header'); - listSection.forEach(header => { - header.addEventListener('click',toggleSection) - }) - setupPublicToggle(); - - try { - await loadNotepadsOfType('favorites', 'favorite-notepads-list'); - await loadNotepadsOfType('shared', 'shared-notepads-list'); - const res = await fetch('/api/notepads?type=mine'); - const notepads = await res.json(); - renderNotepadList(notepads, 'my-notepads-list'); - } catch (err) { - console.error("Failed to load notepads", err); - document.getElementById('my-notepads-list').innerHTML = '

Error loading data.

'; - } - } else { - // Logged out: Only Public Notepads - sidebarBody.innerHTML = ` -
-

Public Notepads

-

Loading...

-
- `; - - try { - const res = await fetch('/api/notepads?type=public'); - const pads = await res.json(); - renderNotepadList(pads, 'public-notepads-list'); - } catch (err) { - console.error(err); - } - } - - // Initial Load Logic (Check for URL params first, else default behavior based on auth) - if (isInitialLoad === true) { - const params = new URLSearchParams(window.location.search); - if (params.has('pad')) { - const token = params.get('token'); - loadNotesForPad(params.get('pad'), undefined, undefined, undefined, true, token); - } else if (params.has('note')) { - const token = params.get('token'); - loadSingleNote(params.get('note'), token); - } else if (isLoggedIn) { - 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) { - loadNotesForPad(globalPad.id, globalPad.name, true, globalPad.owner_id); - } - } catch (err) {} - } - } -} - -function setupPublicToggle() { - const btn = document.getElementById('btn-toggle-public'); - const list = document.getElementById('public-notepads-list'); - - btn.addEventListener('click', async () => { - if (list.style.display === 'none') { - list.style.display = 'block'; - btn.innerHTML = 'Public Notepads ▾'; - - // Lazy load if empty - if (list.innerHTML === '') { - list.innerHTML = '

Loading...

'; - try { - const res = await fetch('/api/notepads?type=public'); - const pads = await res.json(); - renderNotepadList(pads, 'public-notepads-list'); - } catch (err) { - list.innerHTML = '

Error loading public pads.

'; - } - } - } else { - list.style.display = 'none'; - btn.innerHTML = 'Public Notepads ▸'; - } - }); -} - -function renderNotepadList(pads, containerId, isFavorite) { - const container = document.getElementById(containerId); - container.innerHTML = ''; - - if (pads.length === 0) { - container.innerHTML = '

No notepads found.

'; - return; - } - - pads.forEach(pad => { - const item = document.createElement('div'); - item.className = 'notepad-item'; - item.style.padding = '10px'; - item.style.borderBottom = '1px solid #eee'; - item.style.cursor = 'pointer'; - - let icon = pad.is_public ? '🌍' : '🔒'; - - let favButton = ''; - const editBtn = (currentUser && pad.owner_id === currentUser.id) ? - `` - : ''; - - const isOwner = currentUser && pad.owner_id === currentUser.id; - const manageShareBtn = isOwner ? - `` - : ''; - - if(isLoggedIn){ - const action = isFavorite ? 'unfavorite' : 'favorite'; - const label = isFavorite ? '♥' : '♡'; - favButton = ``; - } - - const deleteBtn = isOwner ? - `` - : ''; - - const shareToken = pad.share_token || ''; - const shareBtn = ``; - - item.innerHTML = ` -
-
${icon} ${escapeHtml(pad.name)}
-
- ${editBtn} ${manageShareBtn} ${shareBtn} ${favButton} - ${deleteBtn} -
-
${pad.note_count || 0} notes
-
- `; - - item.addEventListener('click', () => loadNotesForPad(pad.id, pad.name, pad.is_public, pad.owner_id, false, null, pad.can_write)); - container.appendChild(item); - }); -} - -let currentMarkers = []; -let topZIndex = 1000000; - -async function toggleFavorite(padId, action) { - try { - const res = await fetch(`/api/notepads/${padId}/${action}`, { method: 'POST' }); - if (res.ok) { - // Refresh the lists - loadNotepadsOfType('favorites', 'favorite-notepads-list'); - loadNotepadsOfType('mine', 'my-notepads-list'); - loadNotepadsOfType('public', 'public-notepads-list'); - - } else { - alert(`Failed to ${action} notepad`); - } - } catch (err) { - console.error(err); - } -} - -async function loadNotepadsOfType(type, containerId) { - try { - const res = await fetch(`/api/notepads?type=${type}`); - const notepads = await res.json(); - const isFavorite = type === 'favorites'; - renderNotepadList(notepads, containerId, isFavorite); - } catch (err) { - console.error(`Failed to load ${type} notepads`, err); - document.getElementById(containerId).innerHTML = '

Error loading data.

'; - } -} -async function loadNotesForPad(padId, padName, isPublic, ownerId, centerOnFirst = false, token = null, canWrite = undefined) { - activePadId = padId; - - // If metadata is missing (e.g. from URL share), fetch it - if (padName === undefined) { - try { - const res = await fetch(`/api/notepads/${padId}?token=${token || ''}`); - const pad = await res.json(); - padName = pad.name; isPublic = pad.is_public; ownerId = pad.owner_id; activePadCanWrite = pad.can_write; - } catch(e) { console.error(e); activePadCanWrite = false; } - } - - if (padName !== undefined) activePadName = padName; - if (isPublic !== undefined) activePadIsPublic = isPublic; - if (ownerId !== undefined) activePadOwnerId = ownerId; - - if (canWrite !== undefined) { - activePadCanWrite = canWrite; - } else if (padName !== undefined) { - // Fallback if not passed: assume true only if owner - activePadCanWrite = (currentUser && ownerId === currentUser.id); - } - - const currentName = activePadName; - const isOwner = currentUser && activePadOwnerId === currentUser.id; - - // Clear existing markers - currentMarkers.forEach(marker => map.removeLayer(marker)); - currentMarkers = []; - activeNotes = []; - - // Update Sidebar Header - const header = document.querySelector('.sidebar-header'); - if (header) { - const addNoteBtn = activePadCanWrite ? `` : ''; - const manageShareBtn = isOwner ? `` : ''; - const exportBtn = ``; - header.innerHTML = ` -
- -

${escapeHtml(currentName)}

- - ${manageShareBtn} - ${exportBtn} -
- ${addNoteBtn} - `; - document.getElementById('btn-back-pads').addEventListener('click', loadNotepads); - } - - const sidebarBody = document.getElementById('sidebar-body'); - sidebarBody.innerHTML = '

Loading notes...

'; - - try { - const res = await fetch(`/api/notepads/${padId}/notes?token=${token || ''}`); - const notes = await res.json(); - activeNotes = notes; - - // Render Sidebar List - sidebarBody.innerHTML = ''; - if (notes.length === 0) { - sidebarBody.innerHTML = '

No notes yet.

'; - } - - notes.forEach(note => { - const item = document.createElement('div'); - item.className = 'sidebar-note-item'; - - let actionBtns = ''; - if (activePadCanWrite) { - const escapedTitle = escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '"'); - const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); - actionBtns = ` - - - `; - } else if (activePadIsPublic) { - actionBtns = ``; - } - - const dateStr = new Date(note.created_at).toLocaleString(); - - item.innerHTML = ` -
${escapeHtml(note.title)}
-
${escapeHtml(note.content)}
-
- ${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)} -
-
${actionBtns}
- `; - item.addEventListener('click', () => { - stopTracking(); - map.setView([note.lat, note.lng], 16); - }); - sidebarBody.appendChild(item); - }); - - renderMapMarkers(); - - if (centerOnFirst && notes.length > 0) { - map.setView([notes[0].lat, notes[0].lng], 13); - } - - } catch (err) { - console.error(err); - sidebarBody.innerHTML = '

Error loading notes.

'; - } -} - -async function loadSingleNote(noteId, token = null) { - // Reset state - activeNotes = []; - currentMarkers.forEach(marker => map.removeLayer(marker)); - currentMarkers = []; - activePadId = null; - - const sidebarBody = document.getElementById('sidebar-body'); - sidebarBody.innerHTML = '

Loading note...

'; - - try { - const res = await fetch(`/api/notes/${noteId}?token=${token || ''}`); - if (!res.ok) throw new Error("Note not found or private"); - const note = await res.json(); - - // Set active notes to just this one so renderMapMarkers works - activeNotes = [note]; - - // Update Header - const header = document.querySelector('.sidebar-header'); - if (header) { - header.innerHTML = ` -
- -

Shared Note

-
- `; - document.getElementById('btn-back-pads').addEventListener('click', loadNotepads); - } - - sidebarBody.innerHTML = ''; - const noteInfoDiv = document.createElement('div'); - noteInfoDiv.style.padding = '10px'; - - const p = document.createElement('p'); - p.appendChild(document.createTextNode('Viewing single note from ')); - - if (note.notepad.is_public) { - const link = document.createElement('a'); - link.href = "#"; - link.textContent = note.notepad.name; - link.style.fontWeight = 'bold'; - link.onclick = (e) => { - e.preventDefault(); - loadNotesForPad(note.notepad.id, note.notepad.name, true, note.owner_id, false, null, note.notepad.can_write); - }; - p.appendChild(link); - } else { - const strong = document.createElement('strong'); - strong.textContent = note.notepad.name; - p.appendChild(strong); - } - p.appendChild(document.createTextNode('.')); - noteInfoDiv.appendChild(p); - sidebarBody.appendChild(noteInfoDiv); - - renderMapMarkers(); - map.setView([note.lat, note.lng], 16); - - if (window.innerWidth <= 600) { - const sidebar = document.getElementById('sidebar'); - const toggleBtn = document.getElementById('sidebar-toggle'); - if (sidebar && !sidebar.classList.contains('collapsed')) { - sidebar.classList.add('collapsed'); - if (toggleBtn) toggleBtn.innerHTML = '<'; - } - } - - } catch (err) { - sidebarBody.innerHTML = '

Failed to load note. It may be private or deleted.

'; - } -} - -function renderMapMarkers() { - // Clear existing - currentMarkers.forEach(marker => map.removeLayer(marker)); - currentMarkers = []; - - const zoom = map.getZoom(); - const showFull = zoom >= 14; // Zoom threshold - - activeNotes.forEach(note => { - let marker; - let actionBtns = ''; - - // Determine permissions: Use note-specific info if available (search results), else fall back to pad globals - const isOwner = currentUser && (note.owner_id ? note.owner_id === currentUser.id : activePadOwnerId === currentUser.id); - const isPublic = note.is_public !== undefined ? note.is_public : activePadIsPublic; - const canWrite = (note.notepad && note.notepad.can_write !== undefined) ? note.notepad.can_write : activePadCanWrite; - - if (canWrite) { - const escapedTitle = escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '"'); - const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); - actionBtns = `
`; - } else if (isPublic) { - actionBtns = `
`; - } - - const shareToken = note.share_token || ''; - const shareBtn = ``; - - const dateStr = new Date(note.created_at).toLocaleString(); - - - - if (showFull) { - const icon = L.divIcon({ - className: '', - html: `
-
${escapeHtml(note.title)}
-
${escapeHtml(note.content)}
-
${dateStr}
- ${shareBtn} ${actionBtns} -
`, - iconSize: [200, 'auto'], - iconAnchor: [100, 0] - }); - marker = L.marker([note.lat, note.lng], { icon: icon }).addTo(map); - } else { - marker = L.marker([note.lat, note.lng]).addTo(map); - marker.bindPopup(`${escapeHtml(note.title)}
${escapeHtml(note.content)}
${dateStr}
${shareBtn} ${actionBtns}`); - } - const bumpZIndex = function() { - this.setZIndexOffset(topZIndex++); - }; - marker.on('click', bumpZIndex); - marker.on('mousedown', bumpZIndex); - - currentMarkers.push(marker); - }); -} - -window.displaySearchResults = function(notes, location, query) { - // Reset Pad State to "Search Mode" - activePadId = null; - activePadName = `Search: ${query}`; - activePadIsPublic = false; // Mixed results - activePadOwnerId = null; // Mixed results - activeNotes = notes; - - // Update Sidebar Header - const header = document.querySelector('.sidebar-header'); - if (header) { - header.innerHTML = ` -
- -

Results: ${escapeHtml(query)}

-
- `; - document.getElementById('btn-back-pads').addEventListener('click', loadNotepads); - } - - const sidebarBody = document.getElementById('sidebar-body'); - sidebarBody.innerHTML = ''; - - if (notes.length === 0 && !location) { - sidebarBody.innerHTML = '

No results found.

'; - renderMapMarkers(); - return; - } else if (notes.length === 0 && location) { - sidebarBody.innerHTML = '

Location found on map.

'; - renderMapMarkers(); - map.setView([location.lat, location.lng], 13); - return; - } - - // Group notes by section and notepad - const sections = { - favorites: {}, // Map - mine: {}, - public: {} - }; - - notes.forEach(note => { - const pad = note.notepad; - - // Helper to add to a section - const addToSection = (secKey) => { - if (!sections[secKey][pad.id]) { - sections[secKey][pad.id] = { pad: pad, notes: [] }; - } - sections[secKey][pad.id].notes.push(note); - }; - - if (pad.is_favorite) addToSection('favorites'); - if (currentUser && pad.owner_id === currentUser.id) addToSection('mine'); - if (pad.is_public) addToSection('public'); - }); - - // Helper function to render a collapsible section - const renderSection = (title, dataMap, isOpen) => { - const sectionDiv = document.createElement('div'); - sectionDiv.className = 'list-section'; - - const arrow = isOpen ? '▾' : '▸'; - const display = isOpen ? 'block' : 'none'; - - const headerBtn = document.createElement('button'); - headerBtn.className = 'list-section-header'; - headerBtn.innerHTML = `${title} ${arrow}`; - headerBtn.onclick = toggleSection; - - const contentDiv = document.createElement('div'); - contentDiv.className = 'list-section-content'; - contentDiv.style.display = display; - - const padIds = Object.keys(dataMap); - if (padIds.length === 0) { - contentDiv.innerHTML = '

No matching notes.

'; - } else { - padIds.forEach(padId => { - const entry = dataMap[padId]; - const pad = entry.pad; - - // Notepad Header, styled like the default view - const padItem = document.createElement('div'); - padItem.className = 'notepad-item'; - padItem.style.backgroundColor = '#e9ecef'; // Differentiate as a sub-header - padItem.style.cursor = 'default'; - - let icon = pad.is_public ? '🌍' : '🔒'; - let favButton = ''; - if(isLoggedIn){ - const isFavorite = pad.is_favorite; - const action = isFavorite ? 'unfavorite' : 'favorite'; - const label = isFavorite ? '♥' : '♡'; - favButton = ``; - } - - padItem.innerHTML = ` -
-
${icon} ${escapeHtml(pad.name)}
-
- ${favButton} -
-
- `; - contentDiv.appendChild(padItem); - - entry.notes.forEach(note => { - const item = document.createElement('div'); - item.className = 'sidebar-note-item'; - const dateStr = new Date(note.created_at).toLocaleString(); - - let actionBtn = ''; // Renaming to actionBtns for consistency - if (note.notepad && note.notepad.can_write) { - const escapedTitle = escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '"'); - const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); - actionBtn = ` `; - } else if (note.is_public) { - actionBtn = ``; - } - - item.innerHTML = ` -
${escapeHtml(note.title)}
-
${escapeHtml(note.content)}
-
- ${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)} -
-
${actionBtn}
- `; - item.addEventListener('click', () => { - stopTracking(); - map.setView([note.lat, note.lng], 16); - }); - contentDiv.appendChild(item); - }); - }); - } - - sectionDiv.appendChild(headerBtn); - sectionDiv.appendChild(contentDiv); - sidebarBody.appendChild(sectionDiv); - }; - - if (isLoggedIn) { - if (Object.keys(sections.favorites).length > 0) renderSection('Favorite Notepads', sections.favorites, true); - if (Object.keys(sections.mine).length > 0) renderSection('My Notepads', sections.mine, true); - if (Object.keys(sections.public).length > 0) renderSection('Public Notepads', sections.public, true); - } else { - if (Object.keys(sections.public).length > 0) renderSection('Public Notepads', sections.public, true); - } - - renderMapMarkers(); - - if (notes.length > 0) { - map.setView([notes[0].lat, notes[0].lng], 15); - } else if (location) { - map.setView([location.lat, location.lng], 13); - } -}; - -window.reportNote = async function(noteId) { - if (!confirm("Report this note? It will be hidden from public view.")) return; - - try { - const res = await fetch(`/api/notes/${noteId}/report`, { method: 'POST' }); - if (res.ok) { - alert("Note reported and hidden."); - // Refresh current pad - loadNotesForPad(activePadId, activePadName, activePadIsPublic); - } - } catch (err) { - console.error("Error reporting note:", err); - } -}; - -window.deleteNotepad = async function(padId) { - if (!confirm("Are you sure you want to delete this notepad? All notes inside will be lost.")) return; - - try { - const res = await fetch(`/api/notepads/${padId}`, { method: 'DELETE' }); - if (res.ok) { - loadNotepads(); // Refresh list - } else { - alert("Failed to delete notepad."); - } - } catch (err) { console.error(err); } -}; - -window.openShareSettingsModal = async function(padId, padName) { - const modal = document.getElementById('modal-share-settings'); - document.getElementById('share-pad-name').textContent = padName; - document.getElementById('share-pad-id').value = padId; - - const listDiv = document.getElementById('collaborators-list'); - listDiv.innerHTML = '

Loading...

'; - modal.classList.remove('hidden'); - - try { - const res = await fetch(`/api/notepads/${padId}/collaborators`); - if (!res.ok) { - const err = await res.json(); - throw new Error(err.error || 'Failed to load collaborators'); - } - const collaborators = await res.json(); - - listDiv.innerHTML = ''; - if (collaborators.length === 0) { - listDiv.innerHTML = '

No collaborators yet.

'; - } else { - collaborators.forEach(collab => { - const item = document.createElement('div'); - item.className = 'collaborator-item'; - item.innerHTML = ` - ${escapeHtml(collab.email)} (${collab.permission}) - - `; - listDiv.appendChild(item); - }); - } - } catch (err) { - listDiv.innerHTML = `

${err.message}

`; - } -} - -window.removeCollaborator = async function(padId, userId) { - if (!confirm("Are you sure you want to remove this collaborator?")) return; - - try { - const res = await fetch(`/api/notepads/${padId}/collaborators/${userId}`, { method: 'DELETE' }); - if (res.ok) { - const padName = document.getElementById('share-pad-name').textContent; - openShareSettingsModal(padId, padName); - } else { - const err = await res.json(); - alert(`Failed to remove collaborator: ${err.error}`); - } - } catch (err) { - console.error(err); - alert('An error occurred.'); - } -} - -async function loadAllMyNotes() { - activePadId = null; - activePadName = "All My Notes"; - activePadIsPublic = false; - activePadCanWrite = true; - if (currentUser) { - activePadOwnerId = currentUser.id; - } - - // Clear existing markers - currentMarkers.forEach(marker => map.removeLayer(marker)); - currentMarkers = []; - activeNotes = []; - - try { - const res = await fetch('/api/notes/mine'); - if (!res.ok) throw new Error("Failed to load"); - const notes = await res.json(); - activeNotes = notes; - - renderMapMarkers(); - - if (notes.length > 0) { - // Auto-center map bounds cleanly around all the user's notes - const bounds = L.latLngBounds(notes.map(n => [n.lat, n.lng])); - map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 }); - } - - } catch (err) { - console.error("Error loading all my notes:", err); - } -} - -window.shareNotepad = function(padId, token) { - let url = `${window.location.origin}/?pad=${padId}`; - if (token) url += `&token=${token}`; - navigator.clipboard.writeText(url).then(() => { - alert("Notepad link copied to clipboard!"); - }); -}; - -window.shareNote = function(noteId, token) { - let url = `${window.location.origin}/?note=${noteId}`; - if (token) url += `&token=${token}`; - navigator.clipboard.writeText(url).then(() => { - alert("Note link copied to clipboard!"); - }); -}; - -function stopTracking() { - isTracking = false; - updateTrackingUI(); -} - -function updateTrackingUI() { - const btn = document.getElementById('btn-center-user'); - if (btn) { - if (isTracking) { - btn.textContent = "Tracking Location..."; - btn.style.backgroundColor = "#28a745"; - } else { - btn.textContent = "Target My Location"; - btn.style.backgroundColor = ""; - } - } -} - -function escapeHtml(text) { - if (text === null || text === undefined) return ''; - return String(text) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -function toggleSection(event) { - const section = event.target.closest('.list-section'); - const content = section.querySelector('.list-section-content'); - const isCollapsed = content.style.display === 'none'; - content.style.display = isCollapsed ? 'block' : 'none'; - event.target.innerHTML = isCollapsed ? event.target.innerHTML.replace('▸', '▾') : event.target.innerHTML.replace('▾', '▸'); -} - -window.deleteNote = async function(noteId) { - if (!confirm("Delete this note?")) return; - - try { - const res = await fetch(`/api/notes/${noteId}`, { method: 'DELETE' }); - if (res.ok) { - // If we are in a pad view, reload the pad. - if (activePadId) { - loadNotesForPad(activePadId); - } else { - // Otherwise (e.g., search results), the safest refresh is to return to the main list. - alert("Note deleted. Returning to notepad list."); - loadNotepads(); - } - } else { - alert("Failed to delete note."); - } - } catch (err) { console.error(err); } -}; \ No newline at end of file diff --git a/static/assets/js/sidebar.js b/static/assets/js/sidebar.js index dfabeed..40900e0 100644 --- a/static/assets/js/sidebar.js +++ b/static/assets/js/sidebar.js @@ -66,50 +66,51 @@ window.SpotNotes.renderNotepadList = function(pads, containerId, isFavorite) { container.innerHTML = ''; if (pads.length === 0) { - container.innerHTML = '

No notepads found.

'; + container.innerHTML = '

No notepads found.

'; return; } pads.forEach(pad => { const item = document.createElement('div'); - item.className = 'notepad-item'; - item.style.padding = '10px'; - item.style.borderBottom = '1px solid #eee'; - item.style.cursor = 'pointer'; + item.className = 'notepad-card'; let icon = pad.is_public ? '🌍' : '🔒'; let favButton = ''; const editBtn = (window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id) ? - `` + `` : ''; const isOwner = window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id; const manageShareBtn = isOwner ? - `` + `` : ''; if(window.SpotNotes.isLoggedIn){ const action = isFavorite ? 'unfavorite' : 'favorite'; const label = isFavorite ? '♥' : '♡'; - favButton = ``; + favButton = ``; } const deleteBtn = isOwner ? - `` + `` : ''; const shareToken = pad.share_token || ''; - const shareBtn = ``; + const shareBtn = ``; item.innerHTML = ` -
-
${icon} ${window.SpotNotes.escapeHtml(pad.name)}
-
- ${editBtn} ${manageShareBtn} ${shareBtn} ${favButton} - ${deleteBtn} +
+
+ + ${icon} ${window.SpotNotes.escapeHtml(pad.name)}
-
${pad.note_count || 0} notes
+
+ ${editBtn} ${manageShareBtn} ${shareBtn} ${favButton} ${deleteBtn} +
+
+ `;