window.SpotNotes = window.SpotNotes || {}; // Globals window.SpotNotes.map = null; window.SpotNotes.activeNotes = []; window.SpotNotes.userLocation = null; window.SpotNotes.activePadName = ""; window.SpotNotes.activePadId = null; window.SpotNotes.activePadIsPublic = false; window.SpotNotes.activePadOwnerId = null; window.SpotNotes.activePadCanWrite = false; window.SpotNotes.currentUser = null; window.SpotNotes.isTracking = false; window.SpotNotes.userMarker = null; window.SpotNotes.notifiedNoteIds = new Set(); window.SpotNotes.isLoggedIn = false; window.SpotNotes.currentMarkers = []; window.SpotNotes.topZIndex = 1000000; document.addEventListener('DOMContentLoaded', () => { window.SpotNotes.initMap(); window.SpotNotes.initSidebar(); window.SpotNotes.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; window.SpotNotes.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; const color = document.getElementById('pad-color-edit').value; try { const res = await fetch(`/api/notepads/${padId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, is_public: isPublic, color }) }); if (res.ok) { document.getElementById('modal-edit-pad').classList.add('hidden'); window.SpotNotes.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 (window.SpotNotes.activePadId) { window.SpotNotes.loadNotesForPad(window.SpotNotes.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.'); window.SpotNotes.loadNotepads(); } } else { alert('Failed to update note.'); } } catch (err) { console.error(err); } }); } }); window.SpotNotes.loadNotepads = async function(isInitialLoad = false) { // Reset state window.SpotNotes.activeNotes = []; window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker)); window.SpotNotes.currentMarkers = []; // Check auth first try { const authRes = await fetch('/auth/me'); const authData = await authRes.json(); window.SpotNotes.isLoggedIn = authData.authenticated; window.SpotNotes.currentUser = authData.user || null; } catch (e) { console.error(e); } // Reset Sidebar Header const header = document.querySelector('.sidebar-header'); if (header) { let headerHTML = `

Notepads

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

Loading...

Loading...

Loading...

`; const listSection = document.querySelectorAll('.list-section-header'); listSection.forEach(header => { header.addEventListener('click',window.SpotNotes.toggleSection) }) window.SpotNotes.setupPublicToggle(); try { 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(); 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.

'; } } 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(); window.SpotNotes.renderNotepadList(pads, 'public-notepads-list'); } catch (err) { console.error(err); } } // Initial / Default Load Logic: when no notepad is selected, show default notes on map const params = new URLSearchParams(window.location.search); if (isInitialLoad === true && params.has('pad')) { const token = params.get('token'); window.SpotNotes.loadNotesForPad(params.get('pad'), undefined, undefined, undefined, true, token); } else if (isInitialLoad === true && params.has('note')) { const token = params.get('token'); window.SpotNotes.loadSingleNote(params.get('note'), token); } else { await window.SpotNotes.loadDefaultNotes(); } } window.SpotNotes.toggleFavorite = async function(padId, action) { try { const res = await fetch(`/api/notepads/${padId}/${action}`, { method: 'POST' }); if (res.ok) { // Refresh the lists 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`); } } catch (err) { console.error(err); } } 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'; window.SpotNotes.renderNotepadList(notepads, containerId, isFavorite); } catch (err) { console.error(`Failed to load ${type} notepads`, err); document.getElementById(containerId).innerHTML = '

Error loading data.

'; } } 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; window.SpotNotes.activePadCanWrite = pad.can_write; } catch(e) { console.error(e); window.SpotNotes.activePadCanWrite = false; } } if (padName !== undefined) window.SpotNotes.activePadName = padName; if (isPublic !== undefined) window.SpotNotes.activePadIsPublic = isPublic; if (ownerId !== undefined) window.SpotNotes.activePadOwnerId = ownerId; if (canWrite !== undefined) { window.SpotNotes.activePadCanWrite = canWrite; } else if (padName !== undefined) { // Fallback if not passed: assume true only if owner window.SpotNotes.activePadCanWrite = (window.SpotNotes.currentUser && ownerId === window.SpotNotes.currentUser.id); } const currentName = window.SpotNotes.activePadName; const isOwner = window.SpotNotes.currentUser && window.SpotNotes.activePadOwnerId === window.SpotNotes.currentUser.id; // Clear existing markers 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 = window.SpotNotes.activePadCanWrite ? `` : ''; const manageShareBtn = isOwner ? `` : ''; const exportBtn = ``; const shareBtn = ``; header.innerHTML = `
${addNoteBtn}
`; document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.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(); window.SpotNotes.activeNotes = notes; // Render Sidebar List 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); }); } if (centerOnFirst && notes.length > 0) { window.SpotNotes.map.setView([notes[0].lat, notes[0].lng], 13); } } catch (err) { console.error(err); sidebarBody.innerHTML = '

Error loading notes.

'; } } window.SpotNotes.loadSingleNote = async function(noteId, token = null) { // Reset state 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...

'; 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 window.SpotNotes.renderMapMarkers works window.SpotNotes.activeNotes = [note]; // Update Header const header = document.querySelector('.sidebar-header'); if (header) { header.innerHTML = `

Shared Note

`; document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.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(); window.SpotNotes.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); window.SpotNotes.renderMapMarkers(); window.SpotNotes.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.

'; } } window.SpotNotes.displaySearchResults = function(notes, location, query) { // Reset Pad State to "Search Mode" window.SpotNotes.activePadId = null; window.SpotNotes.activePadName = `Search: ${query}`; window.SpotNotes.activePadIsPublic = false; // Mixed results window.SpotNotes.activePadOwnerId = null; // Mixed results window.SpotNotes.activeNotes = notes; // Update Sidebar Header const header = document.querySelector('.sidebar-header'); if (header) { header.innerHTML = `

Results: ${window.SpotNotes.escapeHtml(query)}

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

No results found.

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

Location found on window.SpotNotes.map.

'; window.SpotNotes.renderMapMarkers(); window.SpotNotes.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 (window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.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 = window.SpotNotes.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(window.SpotNotes.isLoggedIn){ const isFavorite = pad.is_favorite; const action = isFavorite ? 'unfavorite' : 'favorite'; const label = isFavorite ? '♥' : '♡'; favButton = ``; } padItem.innerHTML = `
${icon} ${window.SpotNotes.escapeHtml(pad.name)}
${favButton}
`; contentDiv.appendChild(padItem); entry.notes.forEach(note => { const item = document.createElement('div'); item.className = 'note-card'; const dateStr = new Date(note.created_at).toLocaleString(); 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 = ` `; } else if (note.is_public) { actionBtn = ``; } const shareToken = note.share_token || ''; const shareBtn = ``; item.innerHTML = `
${window.SpotNotes.escapeHtml(note.title)}
${window.SpotNotes.escapeHtml(note.content)}
${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
Directions ${shareBtn} ${actionBtn}
`; item.addEventListener('click', () => { window.SpotNotes.stopTracking(); window.SpotNotes.map.setView([note.lat, note.lng], 16); }); contentDiv.appendChild(item); }); }); } sectionDiv.appendChild(headerBtn); sectionDiv.appendChild(contentDiv); sidebarBody.appendChild(sectionDiv); }; 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); } else { if (Object.keys(sections.public).length > 0) renderSection('Public Notepads', sections.public, true); } window.SpotNotes.renderMapMarkers(); if (notes.length > 0) { window.SpotNotes.map.setView([notes[0].lat, notes[0].lng], 15); } else if (location) { window.SpotNotes.map.setView([location.lat, location.lng], 13); } }; window.SpotNotes.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 window.SpotNotes.loadNotesForPad(window.SpotNotes.activePadId, window.SpotNotes.activePadName, window.SpotNotes.activePadIsPublic); } } catch (err) { console.error("Error reporting note:", err); } }; 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) { window.SpotNotes.loadNotepads(); // Refresh list } else { alert("Failed to delete notepad."); } } catch (err) { console.error(err); } }; 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; 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 = ` ${window.SpotNotes.escapeHtml(collab.email)} (${collab.permission}) `; listDiv.appendChild(item); }); } } catch (err) { listDiv.innerHTML = `

${err.message}

`; } } 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; window.SpotNotes.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.'); } } 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 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(); window.SpotNotes.activeNotes = notes; window.SpotNotes.renderMapMarkers(); if (notes.length > 0) { // Auto-center window.SpotNotes.map bounds cleanly around all the user's notes const bounds = L.latLngBounds(notes.map(n => [n.lat, n.lng])); window.SpotNotes.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 }); } } catch (err) { console.error("Error loading all my notes:", err); } } window.SpotNotes.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.SpotNotes.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!"); }); }; window.SpotNotes.escapeHtml = function(text) { if (text === null || text === undefined) return ''; return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } 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 (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."); window.SpotNotes.loadNotepads(); } } else { alert("Failed to delete note."); } } catch (err) { console.error(err); } }; 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 = '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 = ``; } 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)}
${dateStr}
${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
Directions ${shareBtn} ${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). }; window.SpotNotes.loadDefaultNotes = async function() { try { const res = await fetch('/api/notes/default'); if (!res.ok) throw new Error("Failed to load"); const notes = await res.json(); window.SpotNotes.activeNotes = notes; window.SpotNotes.renderMapMarkers(); // Also auto-center map bounds cleanly if we have notes and no pad/note query params const params = new URLSearchParams(window.location.search); if (notes.length > 0 && !params.has('pad') && !params.has('note')) { const bounds = L.latLngBounds(notes.map(n => [n.lat, n.lng])); window.SpotNotes.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 }); } } catch (err) { console.error("Error loading default notes:", err); } };