diff --git a/GEMINI.md b/GEMINI.md
index f827b5f..4e53793 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -1,40 +1,10 @@
# SpotNotes Future Implementation & Refactoring Plan
-
-## 1. Refactoring `index.js` (Reducing Complexity)
+## 1. Feature: Mobile UX & Responsive Overhaul
- **Status:** **[To Do]**
- **Action Plan:**
- - `index.js` is currently a monolithic file (~1000 lines) handling global state, Leaflet map initialization, sidebar DOM manipulation, and raw API calls simultaneously.
- - **Extract Map Logic:** Create `static/assets/js/map.js` to encapsulate `initMap`, `renderMapMarkers`, geolocation tracking, and all Leaflet-specific code.
- - **Extract UI/Sidebar Logic:** Create `static/assets/js/sidebar.js` to handle `initSidebar`, `renderNotepadList`, HTML generation for note list items, and DOM toggling.
- - **Extract API & State Logic:** Keep the core data fetching (`loadNotepads`, `loadNotesForPad`, `loadAllMyNotes`) in `index.js` (or a dedicated `data.js`) to act as the central controller.
- - **Namespace Globals:** Group global variables (`activeNotes`, `activePadId`, `currentUser`, `map`, etc.) into a single `window.SpotNotes` state object to prevent scope pollution and make dependencies explicitly clear across the new script files.
-
-## 2. Feature: "Get Directions" Button
-- **Status:** **[To Do]**
-- **Action Plan:**
- - **Low-Hanging Fruit:** Add a quick "Directions" or "Navigate" link/button to the map marker popups and sidebar note items.
- - Clicking the button will easily open a new tab mapping directly to the note's coordinates: `https://www.google.com/maps/dir/?api=1&destination={lat},{lng}`.
- - Extremely useful UX for a location-based app, requiring absolutely zero backend changes.
-
-## 3. Feature: Map Marker Clustering
-- **Status:** **[To Do]**
-- **Action Plan:**
- - **Low-Hanging Fruit:** Integrate the official `Leaflet.markercluster` plugin.
- - Currently, viewing an "All My Notes" view with hundreds of notes in the same city creates a messy, overlapping visual experience and impacts browser performance.
- - Clustering groups nearby markers into a single circle with a count, automatically expanding when the user clicks or zooms in.
- - Implementation requires just adding the CDN links to `index.html` and wrapping the markers in a `L.markerClusterGroup()` inside the `renderMapMarkers` loop.
-
-## 4. Feature: Sort & Filter Active Notes
-- **Status:** **[To Do]**
-- **Action Plan:**
- - **Low-Hanging Fruit:** Add a simple, fast text input filter floating above the note list in the sidebar whenever a notepad is actively viewed.
- - Allows users to instantly search for a specific note's title or content within a massive notepad *without* triggering a slow, global backend search.
- - Implementation is 100% frontend logic: listening to the input's `keyup` event and filtering the existing `activeNotes` array before re-rendering the sidebar list.
-
-## 5. Feature: Custom Marker Colors per Notepad
-- **Status:** **[To Do]**
-- **Action Plan:**
- - Update the `Notepad` database model to include an optional `color` field (defaulting to the standard Leaflet blue).
- - Add a simple HTML ` ` selector to the Create/Edit Notepad modals.
- - Update the Leaflet marker icon logic to apply a CSS tint/filter (or use customized SVG icons) matching the notepad's selected color.
- - This allows users to visually distinguish between different notes (e.g., "Restaurants" vs "Birding Spots") instantly when looking at the map in mixed views like "All My Notes".
+ - **Audit & Consistency:** Review every application state (Login, Register, Admin Dashboard, Map View) to ensure visual parity with the desktop experience while optimizing for vertical screen constraints.
+ - **Compact Layout & Spacing:** Re-evaluate the spacing and layering of icons, buttons, and UI elements. Aim for a "small and tight" aesthetic that maximizes map visibility without cramping everything into an unreadable jumble.
+ - **Sidebar to Bottom-Sheet:** On mobile devices, explore transitioning the right-side drawer into a "Bottom-Sheet" component (sliding up from the bottom) to provide a more ergonomic, thumb-reachable interface for note lists.
+ - **Enhanced Touch Targets:** Standardize all interactive elements (buttons, icons, notepad list items) to a minimum 44x44px touch target to prevent accidental clicks and improve accessibility.
+ - **Responsive Modals:** Refactor modal windows to occupy 100% of the viewport height/width on small screens, ensuring that form inputs and the "Add Note" textarea remain fully visible above the virtual keyboard.
+ - **Mobile Navigation:** Consolidate the desktop navbar into a streamlined mobile header, utilizing a hamburger menu or action bar to keep the search functionality and account links accessible but unobtrusive.
diff --git a/blueprints/api.py b/blueprints/api.py
index f1c6421..9a30aab 100644
--- a/blueprints/api.py
+++ b/blueprints/api.py
@@ -230,6 +230,7 @@ def update_notepad(pad_id):
data = request.get_json()
notepad.name = data.get('name', notepad.name)
notepad.is_public = data.get('is_public', notepad.is_public)
+ notepad.color = data.get('color', notepad.color)
db.session.commit()
return jsonify(notepad.to_dict())
diff --git a/models/notepad.py b/models/notepad.py
index 44f3562..0e44f3d 100644
--- a/models/notepad.py
+++ b/models/notepad.py
@@ -6,6 +6,7 @@ class Notepad(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
is_public = db.Column(db.Boolean, default=False)
+ color = db.Column(db.String(7), default="#3388ff")
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
diff --git a/refactor.py b/refactor.py
new file mode 100644
index 0000000..a04b328
--- /dev/null
+++ b/refactor.py
@@ -0,0 +1,98 @@
+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'(?
+ ←
+
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 = `
+ 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 = `
+
@@ -264,16 +171,16 @@ async function loadNotepads(isInitialLoad = false) {
`;
const listSection = document.querySelectorAll('.list-section-header');
listSection.forEach(header => {
- header.addEventListener('click',toggleSection)
+ header.addEventListener('click',window.SpotNotes.toggleSection)
})
- setupPublicToggle();
+ window.SpotNotes.setupPublicToggle();
try {
- await loadNotepadsOfType('favorites', 'favorite-notepads-list');
- await loadNotepadsOfType('shared', 'shared-notepads-list');
+ await window.SpotNotes.loadNotepadsOfType('favorites', 'favorite-notepads-list');
+ await window.SpotNotes.loadNotepadsOfType('shared', 'shared-notepads-list');
const res = await fetch('/api/notepads?type=mine');
const notepads = await res.json();
- renderNotepadList(notepads, 'my-notepads-list');
+ window.SpotNotes.renderNotepadList(notepads, 'my-notepads-list');
} catch (err) {
console.error("Failed to load notepads", err);
document.getElementById('my-notepads-list').innerHTML = '
Error loading data.
';
@@ -290,7 +197,7 @@ async function loadNotepads(isInitialLoad = false) {
try {
const res = await fetch('/api/notepads?type=public');
const pads = await res.json();
- renderNotepadList(pads, 'public-notepads-list');
+ window.SpotNotes.renderNotepadList(pads, 'public-notepads-list');
} catch (err) {
console.error(err);
}
@@ -301,120 +208,38 @@ async function loadNotepads(isInitialLoad = false) {
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);
+ window.SpotNotes.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
+ window.SpotNotes.loadSingleNote(params.get('note'), token);
+ } else if (window.SpotNotes.isLoggedIn) {
+ 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) {
- loadNotesForPad(globalPad.id, globalPad.name, true, globalPad.owner_id);
+ window.SpotNotes.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 = `
${label} `;
- }
-
- 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) {
+window.SpotNotes.toggleFavorite = async function(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');
+ window.SpotNotes.loadNotepadsOfType('favorites', 'favorite-notepads-list');
+ window.SpotNotes.loadNotepadsOfType('mine', 'my-notepads-list');
+ window.SpotNotes.loadNotepadsOfType('public', 'public-notepads-list');
} else {
alert(`Failed to ${action} notepad`);
@@ -424,65 +249,68 @@ async function toggleFavorite(padId, action) {
}
}
-async function loadNotepadsOfType(type, containerId) {
+window.SpotNotes.loadNotepadsOfType = async function(type, containerId) {
try {
const res = await fetch(`/api/notepads?type=${type}`);
const notepads = await res.json();
const isFavorite = type === 'favorites';
- renderNotepadList(notepads, containerId, isFavorite);
+ window.SpotNotes.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;
+window.SpotNotes.loadNotesForPad = async function(padId, padName, isPublic, ownerId, centerOnFirst = false, token = null, canWrite = undefined) {
+ window.SpotNotes.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; }
+ padName = pad.name; isPublic = pad.is_public; ownerId = pad.owner_id; window.SpotNotes.activePadCanWrite = pad.can_write;
+ } catch(e) { console.error(e); window.SpotNotes.activePadCanWrite = false; }
}
- if (padName !== undefined) activePadName = padName;
- if (isPublic !== undefined) activePadIsPublic = isPublic;
- if (ownerId !== undefined) activePadOwnerId = ownerId;
+ if (padName !== undefined) window.SpotNotes.activePadName = padName;
+ if (isPublic !== undefined) window.SpotNotes.activePadIsPublic = isPublic;
+ if (ownerId !== undefined) window.SpotNotes.activePadOwnerId = ownerId;
if (canWrite !== undefined) {
- activePadCanWrite = canWrite;
+ window.SpotNotes.activePadCanWrite = canWrite;
} else if (padName !== undefined) {
// Fallback if not passed: assume true only if owner
- activePadCanWrite = (currentUser && ownerId === currentUser.id);
+ window.SpotNotes.activePadCanWrite = (window.SpotNotes.currentUser && ownerId === window.SpotNotes.currentUser.id);
}
- const currentName = activePadName;
- const isOwner = currentUser && activePadOwnerId === currentUser.id;
+ const currentName = window.SpotNotes.activePadName;
+ const isOwner = window.SpotNotes.currentUser && window.SpotNotes.activePadOwnerId === window.SpotNotes.currentUser.id;
// Clear existing markers
- currentMarkers.forEach(marker => map.removeLayer(marker));
- currentMarkers = [];
- activeNotes = [];
+ window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
+ window.SpotNotes.currentMarkers = [];
+ window.SpotNotes.activeNotes = [];
// Update Sidebar Header
const header = document.querySelector('.sidebar-header');
if (header) {
- const addNoteBtn = activePadCanWrite ? `
+ Add Note ` : '';
- const manageShareBtn = isOwner ? `
βοΈ ` : '';
+ const addNoteBtn = window.SpotNotes.activePadCanWrite ? `
+ Add Note ` : '';
+ const manageShareBtn = isOwner ? `
βοΈ ` : '';
const exportBtn = `
β¬οΈ `;
header.innerHTML = `
←
-
${escapeHtml(currentName)}
- π
+ ${window.SpotNotes.escapeHtml(currentName)}
+ π
${manageShareBtn}
${exportBtn}
+
+
+
${addNoteBtn}
`;
- document.getElementById('btn-back-pads').addEventListener('click', loadNotepads);
+ document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads);
}
const sidebarBody = document.getElementById('sidebar-body');
@@ -491,51 +319,21 @@ async function loadNotesForPad(padId, padName, isPublic, ownerId, centerOnFirst
try {
const res = await fetch(`/api/notepads/${padId}/notes?token=${token || ''}`);
const notes = await res.json();
- activeNotes = notes;
+ window.SpotNotes.activeNotes = notes;
// Render Sidebar List
- sidebarBody.innerHTML = '';
- if (notes.length === 0) {
- sidebarBody.innerHTML = '
No notes yet.
';
+ window.SpotNotes.renderActiveNotesList();
+ window.SpotNotes.renderMapMarkers();
+
+ const filterInput = document.getElementById('filter-active-notes');
+ if (filterInput) {
+ filterInput.addEventListener('keyup', (e) => {
+ window.SpotNotes.renderActiveNotesList(e.target.value);
+ });
}
- 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 = `
-
Edit
-
Delete
- `;
- } else if (activePadIsPublic) {
- actionBtns = `
Report `;
- }
-
- 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);
+ window.SpotNotes.map.setView([notes[0].lat, notes[0].lng], 13);
}
} catch (err) {
@@ -544,12 +342,12 @@ async function loadNotesForPad(padId, padName, isPublic, ownerId, centerOnFirst
}
}
-async function loadSingleNote(noteId, token = null) {
+window.SpotNotes.loadSingleNote = async function(noteId, token = null) {
// Reset state
- activeNotes = [];
- currentMarkers.forEach(marker => map.removeLayer(marker));
- currentMarkers = [];
- activePadId = null;
+ window.SpotNotes.activeNotes = [];
+ window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
+ window.SpotNotes.currentMarkers = [];
+ window.SpotNotes.activePadId = null;
const sidebarBody = document.getElementById('sidebar-body');
sidebarBody.innerHTML = '
Loading note...
';
@@ -559,8 +357,8 @@ async function loadSingleNote(noteId, token = null) {
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];
+ // Set active notes to just this one so window.SpotNotes.renderMapMarkers works
+ window.SpotNotes.activeNotes = [note];
// Update Header
const header = document.querySelector('.sidebar-header');
@@ -571,7 +369,7 @@ async function loadSingleNote(noteId, token = null) {
Shared Note
`;
- document.getElementById('btn-back-pads').addEventListener('click', loadNotepads);
+ document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads);
}
sidebarBody.innerHTML = '';
@@ -588,7 +386,7 @@ async function loadSingleNote(noteId, token = null) {
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);
+ window.SpotNotes.loadNotesForPad(note.notepad.id, note.notepad.name, true, note.owner_id, false, null, note.notepad.can_write);
};
p.appendChild(link);
} else {
@@ -600,8 +398,8 @@ async function loadSingleNote(noteId, token = null) {
noteInfoDiv.appendChild(p);
sidebarBody.appendChild(noteInfoDiv);
- renderMapMarkers();
- map.setView([note.lat, note.lng], 16);
+ window.SpotNotes.renderMapMarkers();
+ window.SpotNotes.map.setView([note.lat, note.lng], 16);
if (window.innerWidth <= 600) {
const sidebar = document.getElementById('sidebar');
@@ -617,72 +415,15 @@ async function loadSingleNote(noteId, token = null) {
}
}
-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 = `No results found.
';
- renderMapMarkers();
+ window.SpotNotes.renderMapMarkers();
return;
} else if (notes.length === 0 && location) {
- sidebarBody.innerHTML = 'Location found on map.
';
- renderMapMarkers();
- map.setView([location.lat, location.lng], 13);
+ sidebarBody.innerHTML = 'Location found on window.SpotNotes.map.
';
+ window.SpotNotes.renderMapMarkers();
+ window.SpotNotes.map.setView([location.lat, location.lng], 13);
return;
}
@@ -729,7 +470,7 @@ window.displaySearchResults = function(notes, location, query) {
};
if (pad.is_favorite) addToSection('favorites');
- if (currentUser && pad.owner_id === currentUser.id) addToSection('mine');
+ if (window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id) addToSection('mine');
if (pad.is_public) addToSection('public');
});
@@ -744,7 +485,7 @@ window.displaySearchResults = function(notes, location, query) {
const headerBtn = document.createElement('button');
headerBtn.className = 'list-section-header';
headerBtn.innerHTML = `${title} ${arrow}`;
- headerBtn.onclick = toggleSection;
+ headerBtn.onclick = window.SpotNotes.toggleSection;
const contentDiv = document.createElement('div');
contentDiv.className = 'list-section-content';
@@ -766,16 +507,16 @@ window.displaySearchResults = function(notes, location, query) {
let icon = pad.is_public ? 'π' : 'π';
let favButton = '';
- if(isLoggedIn){
+ if(window.SpotNotes.isLoggedIn){
const isFavorite = pad.is_favorite;
const action = isFavorite ? 'unfavorite' : 'favorite';
const label = isFavorite ? '♥' : 'β‘';
- favButton = `
-
${icon} ${escapeHtml(pad.name)}
+
${icon} ${window.SpotNotes.escapeHtml(pad.name)}
${favButton}
@@ -790,24 +531,24 @@ window.displaySearchResults = function(notes, location, query) {
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 = `
Edit Delete `;
+ 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 = `
Edit Delete `;
} else if (note.is_public) {
- actionBtn = `
Report `;
+ actionBtn = `
Report `;
}
item.innerHTML = `
-
${escapeHtml(note.title)}
-
${escapeHtml(note.content)}
+
${window.SpotNotes.escapeHtml(note.title)}
+
${window.SpotNotes.escapeHtml(note.content)}
${dateStr} ${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
${actionBtn}
`;
item.addEventListener('click', () => {
- stopTracking();
- map.setView([note.lat, note.lng], 16);
+ window.SpotNotes.stopTracking();
+ window.SpotNotes.map.setView([note.lat, note.lng], 16);
});
contentDiv.appendChild(item);
});
@@ -819,7 +560,7 @@ window.displaySearchResults = function(notes, location, query) {
sidebarBody.appendChild(sectionDiv);
};
- if (isLoggedIn) {
+ if (window.SpotNotes.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);
@@ -827,16 +568,16 @@ window.displaySearchResults = function(notes, location, query) {
if (Object.keys(sections.public).length > 0) renderSection('Public Notepads', sections.public, true);
}
- renderMapMarkers();
+ window.SpotNotes.renderMapMarkers();
if (notes.length > 0) {
- map.setView([notes[0].lat, notes[0].lng], 15);
+ window.SpotNotes.map.setView([notes[0].lat, notes[0].lng], 15);
} else if (location) {
- map.setView([location.lat, location.lng], 13);
+ window.SpotNotes.map.setView([location.lat, location.lng], 13);
}
};
-window.reportNote = async function(noteId) {
+window.SpotNotes.reportNote = async function(noteId) {
if (!confirm("Report this note? It will be hidden from public view.")) return;
try {
@@ -844,27 +585,27 @@ window.reportNote = async function(noteId) {
if (res.ok) {
alert("Note reported and hidden.");
// Refresh current pad
- loadNotesForPad(activePadId, activePadName, activePadIsPublic);
+ window.SpotNotes.loadNotesForPad(window.SpotNotes.activePadId, window.SpotNotes.activePadName, window.SpotNotes.activePadIsPublic);
}
} catch (err) {
console.error("Error reporting note:", err);
}
};
-window.deleteNotepad = async function(padId) {
+window.SpotNotes.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
+ window.SpotNotes.loadNotepads(); // Refresh list
} else {
alert("Failed to delete notepad.");
}
} catch (err) { console.error(err); }
};
-window.openShareSettingsModal = async function(padId, padName) {
+window.SpotNotes.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;
@@ -889,8 +630,8 @@ window.openShareSettingsModal = async function(padId, padName) {
const item = document.createElement('div');
item.className = 'collaborator-item';
item.innerHTML = `
-
${escapeHtml(collab.email)} (${collab.permission})
-
Remove
+
${window.SpotNotes.escapeHtml(collab.email)} (${collab.permission})
+
Remove
`;
listDiv.appendChild(item);
});
@@ -900,14 +641,14 @@ window.openShareSettingsModal = async function(padId, padName) {
}
}
-window.removeCollaborator = async function(padId, userId) {
+window.SpotNotes.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);
+ window.SpotNotes.openShareSettingsModal(padId, padName);
} else {
const err = await res.json();
alert(`Failed to remove collaborator: ${err.error}`);
@@ -918,32 +659,32 @@ window.removeCollaborator = async function(padId, userId) {
}
}
-async function loadAllMyNotes() {
- activePadId = null;
- activePadName = "All My Notes";
- activePadIsPublic = false;
- activePadCanWrite = true;
- if (currentUser) {
- activePadOwnerId = currentUser.id;
+window.SpotNotes.loadAllMyNotes = async function() {
+ window.SpotNotes.activePadId = null;
+ window.SpotNotes.activePadName = "All My Notes";
+ window.SpotNotes.activePadIsPublic = false;
+ window.SpotNotes.activePadCanWrite = true;
+ if (window.SpotNotes.currentUser) {
+ window.SpotNotes.activePadOwnerId = window.SpotNotes.currentUser.id;
}
// Clear existing markers
- currentMarkers.forEach(marker => map.removeLayer(marker));
- currentMarkers = [];
- activeNotes = [];
+ window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
+ window.SpotNotes.currentMarkers = [];
+ window.SpotNotes.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;
+ window.SpotNotes.activeNotes = notes;
- renderMapMarkers();
+ window.SpotNotes.renderMapMarkers();
if (notes.length > 0) {
- // Auto-center map bounds cleanly around all the user's notes
+ // Auto-center window.SpotNotes.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 });
+ window.SpotNotes.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 });
}
} catch (err) {
@@ -951,7 +692,7 @@ async function loadAllMyNotes() {
}
}
-window.shareNotepad = function(padId, token) {
+window.SpotNotes.shareNotepad = function(padId, token) {
let url = `${window.location.origin}/?pad=${padId}`;
if (token) url += `&token=${token}`;
navigator.clipboard.writeText(url).then(() => {
@@ -959,7 +700,7 @@ window.shareNotepad = function(padId, token) {
});
};
-window.shareNote = function(noteId, token) {
+window.SpotNotes.shareNote = function(noteId, token) {
let url = `${window.location.origin}/?note=${noteId}`;
if (token) url += `&token=${token}`;
navigator.clipboard.writeText(url).then(() => {
@@ -967,25 +708,11 @@ window.shareNote = function(noteId, token) {
});
};
-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) {
+
+
+window.SpotNotes.escapeHtml = function(text) {
if (text === null || text === undefined) return '';
return String(text)
.replace(/&/g, "&")
@@ -995,30 +722,82 @@ function escapeHtml(text) {
.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) {
+
+window.SpotNotes.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);
+ if (window.SpotNotes.activePadId) {
+ window.SpotNotes.loadNotesForPad(window.SpotNotes.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();
+ window.SpotNotes.loadNotepads();
}
} else {
alert("Failed to delete note.");
}
} catch (err) { console.error(err); }
-};
\ No newline at end of file
+};
+
+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 = `
+
Edit
+
Delete
+ `;
+ } else if (window.SpotNotes.activePadIsPublic) {
+ actionBtns = `
Report `;
+ }
+
+ 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)}
+
+
+ `;
+ 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).
+};
diff --git a/static/assets/js/index.js.bak b/static/assets/js/index.js.bak
new file mode 100644
index 0000000..d60b096
--- /dev/null
+++ b/static/assets/js/index.js.bak
@@ -0,0 +1,1024 @@
+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 = `
+
+
+
+
+ `;
+ 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 = `
+
+ `;
+
+ 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 = `
${label} `;
+ }
+
+ 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 ? `
+ Add Note ` : '';
+ 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 = `
+
Edit
+
Delete
+ `;
+ } else if (activePadIsPublic) {
+ actionBtns = `
Report `;
+ }
+
+ 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 = `
βοΈ Edit Delete `;
+ } else if (isPublic) {
+ actionBtns = `
Report `;
+ }
+
+ const shareToken = note.share_token || '';
+ const shareBtn = `
π Share `;
+
+ 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 = `${label} `;
+ }
+
+ 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 = `Edit Delete `;
+ } else if (note.is_public) {
+ actionBtn = `Report `;
+ }
+
+ 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})
+ Remove
+ `;
+ 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/map.js b/static/assets/js/map.js
new file mode 100644
index 0000000..47f523e
--- /dev/null
+++ b/static/assets/js/map.js
@@ -0,0 +1,178 @@
+window.SpotNotes = window.SpotNotes || {};
+
+window.SpotNotes.initMap = function() {
+ // Default view (e.g., Oklahoma area if geolocation fails)
+ window.SpotNotes.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;
+ window.SpotNotes.userLocation = { lat: latitude, lng: longitude };
+
+ const centerBtn = document.getElementById('btn-center-user');
+ if (centerBtn) centerBtn.style.display = 'block';
+
+ if (window.SpotNotes.isTracking) {
+ window.SpotNotes.map.setView([latitude, longitude]);
+ } else if (!window.SpotNotes.userMarker && !new URLSearchParams(window.location.search).has('pad') && !new URLSearchParams(window.location.search).has('note')) {
+ window.SpotNotes.map.setView([latitude, longitude], 13);
+ console.log("Map centered on user location:", latitude, longitude);
+ }
+
+ // Add User Location Dot
+ if (window.SpotNotes.userMarker) {
+ window.SpotNotes.userMarker.setLatLng([latitude, longitude]);
+ } else {
+ window.SpotNotes.userMarker = L.circleMarker([latitude, longitude], {
+ radius: 8,
+ fillColor: "#3388ff",
+ color: "#fff",
+ weight: 2,
+ opacity: 1,
+ fillOpacity: 0.8
+ }).addTo(window.SpotNotes.map).bindPopup("You are here");
+ }
+
+ // Location-Based Browser Alerts
+ const userLatLng = L.latLng(latitude, longitude);
+ if (window.SpotNotes.activeNotes && window.SpotNotes.activeNotes.length > 0) {
+ window.SpotNotes.activeNotes.forEach(note => {
+ const noteLatLng = L.latLng(note.lat, note.lng);
+ if (userLatLng.distanceTo(noteLatLng) <= 50) {
+ if (!window.SpotNotes.notifiedNoteIds.has(note.id)) {
+ window.SpotNotes.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(window.SpotNotes.map);
+
+ // Listen for zoom changes to toggle note display
+ window.SpotNotes.map.on('zoomend', () => {
+ if (window.SpotNotes.activeNotes.length > 0) {
+ window.SpotNotes.renderMapMarkers();
+ }
+ });
+
+ window.SpotNotes.map.on('dragstart', window.SpotNotes.stopTracking);
+ window.SpotNotes.map.on('popupopen', window.SpotNotes.stopTracking);
+
+ // Initialize MarkerClusterGroup
+ window.SpotNotes.markerClusterGroup = L.markerClusterGroup({
+ disableClusteringAtZoom: 16
+ });
+ window.SpotNotes.map.addLayer(window.SpotNotes.markerClusterGroup);
+};
+
+window.SpotNotes.renderMapMarkers = function() {
+ // Clear existing
+ if (window.SpotNotes.markerClusterGroup) {
+ window.SpotNotes.markerClusterGroup.clearLayers();
+ }
+
+ // Also remove from map directly just in case (fallback)
+ window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
+ window.SpotNotes.currentMarkers = [];
+
+ const zoom = window.SpotNotes.map.getZoom();
+ const showFull = zoom >= 14; // Zoom threshold
+
+ window.SpotNotes.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 = window.SpotNotes.currentUser && (note.owner_id ? note.owner_id === window.SpotNotes.currentUser.id : window.SpotNotes.activePadOwnerId === window.SpotNotes.currentUser.id);
+ const isPublic = note.is_public !== undefined ? note.is_public : window.SpotNotes.activePadIsPublic;
+ const canWrite = (note.notepad && note.notepad.can_write !== undefined) ? note.notepad.can_write : window.SpotNotes.activePadCanWrite;
+
+ if (canWrite) {
+ 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 = `βοΈ Edit Delete `;
+ } else if (isPublic) {
+ actionBtns = `Report `;
+ }
+
+ const shareToken = note.share_token || '';
+ const shareBtn = `π Share `;
+
+ const dateStr = new Date(note.created_at).toLocaleString();
+
+ const padColor = (note.notepad && note.notepad.color) ? note.notepad.color : '#3388ff';
+
+ const directionsBtn = `πΊοΈ Directions `;
+
+ if (showFull) {
+ const icon = L.divIcon({
+ className: '',
+ html: `
+
${window.SpotNotes.escapeHtml(note.title)}
+
${window.SpotNotes.escapeHtml(note.content)}
+
${dateStr}
+ ${directionsBtn} ${shareBtn} ${actionBtns}
+
`,
+ iconSize: [200, 'auto'],
+ iconAnchor: [100, 0]
+ });
+ marker = L.marker([note.lat, note.lng], { icon: icon });
+ } else {
+ const svgIcon = L.divIcon({
+ className: 'custom-div-icon',
+ html: `
`,
+ iconSize: [20, 20],
+ iconAnchor: [10, 10],
+ popupAnchor: [0, -10]
+ });
+ marker = L.marker([note.lat, note.lng], { icon: svgIcon });
+ marker.bindPopup(`${window.SpotNotes.escapeHtml(note.title)} ${window.SpotNotes.escapeHtml(note.content)}
${dateStr} ${directionsBtn} ${shareBtn} ${actionBtns}`);
+ }
+
+ const bumpZIndex = function() {
+ this.setZIndexOffset(window.SpotNotes.topZIndex++);
+ };
+ marker.on('click', bumpZIndex);
+ marker.on('mousedown', bumpZIndex);
+
+ if (window.SpotNotes.markerClusterGroup) {
+ window.SpotNotes.markerClusterGroup.addLayer(marker);
+ } else {
+ marker.addTo(window.SpotNotes.map);
+ }
+
+ window.SpotNotes.currentMarkers.push(marker);
+ });
+};
+
+window.SpotNotes.stopTracking = function() {
+ window.SpotNotes.isTracking = false;
+ window.SpotNotes.updateTrackingUI();
+};
+
+window.SpotNotes.updateTrackingUI = function() {
+ const btn = document.getElementById('btn-center-user');
+ if (btn) {
+ if (window.SpotNotes.isTracking) {
+ btn.textContent = "Tracking Location...";
+ btn.style.backgroundColor = "#28a745";
+ } else {
+ btn.textContent = "Target My Location";
+ btn.style.backgroundColor = "";
+ }
+ }
+};
\ No newline at end of file
diff --git a/static/assets/js/search.js b/static/assets/js/search.js
index caadbf0..aaab359 100644
--- a/static/assets/js/search.js
+++ b/static/assets/js/search.js
@@ -25,8 +25,8 @@ document.addEventListener('DOMContentLoaded', () => {
// If notes found, display them and stop (don't geocode)
if (notes.length > 0) {
- if (window.displaySearchResults) {
- window.displaySearchResults(notes, null, query);
+ if (window.SpotNotes && window.SpotNotes.displaySearchResults) {
+ window.SpotNotes.displaySearchResults(notes, null, query);
}
return;
}
@@ -48,8 +48,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
// 3. Update UI via index.js helper
- if (window.displaySearchResults) {
- window.displaySearchResults(notes, location, query);
+ if (window.SpotNotes && window.SpotNotes.displaySearchResults) {
+ window.SpotNotes.displaySearchResults(notes, location, query);
}
};
diff --git a/static/assets/js/sidebar.js b/static/assets/js/sidebar.js
new file mode 100644
index 0000000..dfabeed
--- /dev/null
+++ b/static/assets/js/sidebar.js
@@ -0,0 +1,127 @@
+window.SpotNotes = window.SpotNotes || {};
+
+window.SpotNotes.initSidebar = function() {
+ 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 => {
+ const content = section.querySelector('.list-section-content');
+ if (content) 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', () => {
+ window.SpotNotes.isTracking = true;
+ window.SpotNotes.updateTrackingUI();
+ if (window.SpotNotes.userLocation) {
+ window.SpotNotes.map.setView([window.SpotNotes.userLocation.lat, window.SpotNotes.userLocation.lng], 16);
+ }
+ });
+ }
+};
+
+window.SpotNotes.setupPublicToggle = function() {
+ const btn = document.getElementById('btn-toggle-public');
+ const list = document.getElementById('public-notepads-list');
+
+ if (!btn || !list) return;
+
+ 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();
+ window.SpotNotes.renderNotepadList(pads, 'public-notepads-list');
+ } catch (err) {
+ list.innerHTML = 'Error loading public pads.
';
+ }
+ }
+ } else {
+ list.style.display = 'none';
+ btn.innerHTML = 'Public Notepads ▸';
+ }
+ });
+};
+
+window.SpotNotes.renderNotepadList = function(pads, containerId, isFavorite) {
+ const container = document.getElementById(containerId);
+ if (!container) return;
+ 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 = (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 = `${label} `;
+ }
+
+ const deleteBtn = isOwner ?
+ `× `
+ : '';
+
+ const shareToken = pad.share_token || '';
+ const shareBtn = `π `;
+
+ item.innerHTML = `
+
+
${icon} ${window.SpotNotes.escapeHtml(pad.name)}
+
+ ${editBtn} ${manageShareBtn} ${shareBtn} ${favButton}
+ ${deleteBtn}
+
+
${pad.note_count || 0} notes
+
+ `;
+
+ item.addEventListener('click', () => window.SpotNotes.loadNotesForPad(pad.id, pad.name, pad.is_public, pad.owner_id, false, null, pad.can_write));
+ container.appendChild(item);
+ });
+};
+
+window.SpotNotes.toggleSection = function(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('▾', '▸');
+};
diff --git a/static/index.html b/static/index.html
index f9dd9cd..80a1139 100644
--- a/static/index.html
+++ b/static/index.html
@@ -9,6 +9,8 @@
+
+
@@ -100,6 +102,10 @@
Make Public
+