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.activePadEncryptedKey = null; 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); // --- Add Collaborator Modal Handler --- 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.trim(); const permission = document.getElementById('collaborator-permission').value; try { let encryptedKey = null; // If private notepad, encrypt symmetric key with collaborator's public key if (!window.SpotNotes.activePadIsPublic) { const lookupRes = await fetch(`/api/users/lookup?email=${encodeURIComponent(email)}`); if (!lookupRes.ok) { const errData = await lookupRes.json(); alert(errData.error || `User with email "${email}" was not found. Collaborator must be a registered user.`); return; } const collaboratorUser = await lookupRes.json(); if (!collaboratorUser.public_key) { alert(`User "${email}" does not have an active public key registered.`); return; } let padKey = window.SpotCrypto.getCachedPadKey(padId); if (!padKey && window.SpotNotes.activePadEncryptedKey) { padKey = await window.SpotCrypto.unwrapNotepadKey(padId, window.SpotNotes.activePadEncryptedKey); } if (!padKey) { alert('Could not access encryption key for this notepad.'); return; } encryptedKey = await window.SpotCrypto.wrapPadKeyForCollaborator(padKey, collaboratorUser.public_key); } const res = await fetch(`/api/notepads/${padId}/collaborators`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, permission, encrypted_key: encryptedKey }) }); 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 adding collaborator.'); } }); } // --- Edit Notepad Modal Handler --- 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(); } else { alert('Failed to update notepad.'); } } catch (err) { console.error(err); } }); } // --- Edit Note Modal Handler --- const formEditNote = document.getElementById('form-edit-note'); if (formEditNote) { formEditNote.addEventListener('submit', async (e) => { e.preventDefault(); const noteId = parseInt(document.getElementById('note-id-edit').value, 10); const title = document.getElementById('note-title-edit').value.trim(); const content = document.getElementById('note-content-edit').value; const existingNote = window.SpotNotes.activeNotes.find(n => n.id === noteId); const isEncrypted = existingNote ? existingNote.is_encrypted : !window.SpotNotes.activePadIsPublic; try { let bodyPayload = {}; if (isEncrypted) { const targetPadId = (existingNote && existingNote.notepad_id) || window.SpotNotes.activePadId; let padKey = window.SpotCrypto.getCachedPadKey(targetPadId); if (!padKey && window.SpotNotes.activePadEncryptedKey) { padKey = await window.SpotCrypto.unwrapNotepadKey(targetPadId, window.SpotNotes.activePadEncryptedKey); } if (!padKey) { alert('Encryption key not available to edit this private note.'); return; } const encPayload = await window.SpotCrypto.encryptNote({ title, content, lat: existingNote ? existingNote.lat : 0, lng: existingNote ? existingNote.lng : 0 }, padKey); bodyPayload = { title: "🔒 Encrypted Note", content: encPayload }; } else { bodyPayload = { title, content }; } const res = await fetch(`/api/notes/${noteId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyPayload) }); if (res.ok) { document.getElementById('modal-edit-note').classList.add('hidden'); if (window.SpotNotes.activePadId) { window.SpotNotes.loadNotesForPad(window.SpotNotes.activePadId); } else { 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; // Try restoring user's private key from session storage if (window.SpotNotes.isLoggedIn && window.SpotCrypto) { await window.SpotCrypto.restoreUserPrivateKeyFromSession(); } } 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(h => { h.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 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) { 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, encryptedKey = null) { window.SpotNotes.activePadId = padId; // If metadata is missing, fetch it if (padName === undefined || encryptedKey === 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; if (pad.encrypted_key) encryptedKey = pad.encrypted_key; } 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 (encryptedKey !== null) window.SpotNotes.activePadEncryptedKey = encryptedKey; if (canWrite !== undefined) { window.SpotNotes.activePadCanWrite = canWrite; } else if (padName !== undefined) { window.SpotNotes.activePadCanWrite = (window.SpotNotes.currentUser && (isPublic || ownerId === window.SpotNotes.currentUser.id)); } // Check if URL hash has #key=... for link-shared private notes/pads if (window.location.hash.includes('key=')) { const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, '')); const linkKey = hashParams.get('key'); if (linkKey && window.SpotCrypto) { await window.SpotCrypto.importPadKeyFromLink(padId, linkKey); } } // If private and encrypted_key present, unwrap with user's private key if (!isPublic && window.SpotNotes.activePadEncryptedKey && window.SpotCrypto) { await window.SpotCrypto.unwrapNotepadKey(padId, window.SpotNotes.activePadEncryptedKey); } 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 canAddNote = window.SpotNotes.activePadCanWrite || (window.SpotNotes.currentUser && isPublic); const addNoteBtn = canAddNote ? `` : ''; const manageShareBtn = isOwner ? `` : ''; const exportBtn = ``; const shareBtn = ``; const encryptionBadge = !isPublic ? '🔒' : '🌍'; 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(); let decryptedNotes = notes; if (!isPublic && window.SpotCrypto) { let padKey = window.SpotCrypto.getCachedPadKey(padId); if (!padKey && window.SpotNotes.activePadEncryptedKey) { padKey = await window.SpotCrypto.unwrapNotepadKey(padId, window.SpotNotes.activePadEncryptedKey); } decryptedNotes = await window.SpotCrypto.decryptNotes(notes, padKey); } window.SpotNotes.activeNotes = decryptedNotes; // Render Sidebar List & Markers 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 && decryptedNotes.length > 0 && decryptedNotes[0].lat && decryptedNotes[0].lng) { window.SpotNotes.map.setView([decryptedNotes[0].lat, decryptedNotes[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"); let note = await res.json(); // Check hash #key=... for decryption if (note.is_encrypted && window.SpotCrypto) { let padKey = window.SpotCrypto.getCachedPadKey(note.notepad_id); if (!padKey && window.location.hash.includes('key=')) { const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, '')); const linkKey = hashParams.get('key'); if (linkKey) { padKey = await window.SpotCrypto.importPadKeyFromLink(note.notepad_id, linkKey); } } if (!padKey && note.notepad && note.notepad.encrypted_key) { padKey = await window.SpotCrypto.unwrapNotepadKey(note.notepad_id, note.notepad.encrypted_key); } note = await window.SpotCrypto.decryptNote(note, padKey); } window.SpotNotes.activeNotes = [note]; // Update Header const header = document.querySelector('.sidebar-header'); if (header) { header.innerHTML = `

Shared Note ${note.is_encrypted ? '🔒' : '🌍'}

`; 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 && 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 && note.notepad.name) || 'Private Notepad'; p.appendChild(strong); } p.appendChild(document.createTextNode('.')); noteInfoDiv.appendChild(p); sidebarBody.appendChild(noteInfoDiv); window.SpotNotes.renderMapMarkers(); if (note.lat && note.lng) { window.SpotNotes.map.setView([note.lat, note.lng], 16); } if (window.innerWidth <= 768) { 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) { window.SpotNotes.activePadId = null; window.SpotNotes.activePadName = `Search: ${query}`; window.SpotNotes.activePadIsPublic = false; window.SpotNotes.activePadOwnerId = null; window.SpotNotes.activeNotes = notes; 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 map.

'; window.SpotNotes.renderMapMarkers(); window.SpotNotes.map.setView([location.lat, location.lng], 13); return; } const sections = { favorites: {}, mine: {}, public: {} }; notes.forEach(note => { const pad = note.notepad || { id: note.notepad_id, name: 'Notes', is_public: !note.is_encrypted }; 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'); }); 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; const padItem = document.createElement('div'); padItem.className = 'notepad-item'; padItem.style.backgroundColor = '#e9ecef'; 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 ? note.lat.toFixed(5) : ''}, ${note.lng ? note.lng.toFixed(5) : ''}
Directions ${shareBtn} ${actionBtn}
`; item.addEventListener('click', () => { window.SpotNotes.stopTracking(); if (note.lat && note.lng) 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 && notes[0].lat && notes[0].lng) { 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."); 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(); } 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; } 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(); // Decrypt any encrypted notes const decryptedNotes = await Promise.all(notes.map(async (note) => { if (note.is_encrypted && window.SpotCrypto) { let padKey = window.SpotCrypto.getCachedPadKey(note.notepad_id); if (!padKey && note.notepad && note.notepad.encrypted_key) { padKey = await window.SpotCrypto.unwrapNotepadKey(note.notepad_id, note.notepad.encrypted_key); } return await window.SpotCrypto.decryptNote(note, padKey); } return note; })); window.SpotNotes.activeNotes = decryptedNotes; window.SpotNotes.renderMapMarkers(); const validNotes = decryptedNotes.filter(n => n.lat && n.lng); if (validNotes.length > 0) { const bounds = L.latLngBounds(validNotes.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); } }; // Zero-knowledge link sharing for notepads window.SpotNotes.shareNotepad = async function(padId, token) { let url = `${window.location.origin}/?pad=${padId}`; if (token) url += `&token=${token}`; if (!window.SpotNotes.activePadIsPublic && window.SpotCrypto) { let padKey = window.SpotCrypto.getCachedPadKey(padId); if (!padKey && window.SpotNotes.activePadEncryptedKey) { padKey = await window.SpotCrypto.unwrapNotepadKey(padId, window.SpotNotes.activePadEncryptedKey); } if (padKey) { const keyB64 = await window.SpotCrypto.exportPadKeyForLink(padKey); url += `#key=${keyB64}`; } } navigator.clipboard.writeText(url).then(() => { alert("Notepad link copied to clipboard! (Includes decryption key fragment for private notepads)"); }); }; // Zero-knowledge link sharing for individual notes window.SpotNotes.shareNote = async function(noteId, token) { let url = `${window.location.origin}/?note=${noteId}`; if (token) url += `&token=${token}`; const note = window.SpotNotes.activeNotes.find(n => n.id === noteId); if (note && note.is_encrypted && window.SpotCrypto) { const padId = note.notepad_id || window.SpotNotes.activePadId; let padKey = window.SpotCrypto.getCachedPadKey(padId); if (!padKey && window.SpotNotes.activePadEncryptedKey) { padKey = await window.SpotCrypto.unwrapNotepadKey(padId, window.SpotNotes.activePadEncryptedKey); } if (padKey) { const keyB64 = await window.SpotCrypto.exportPadKeyForLink(padKey); url += `#key=${keyB64}`; } } navigator.clipboard.writeText(url).then(() => { alert("Note link copied to clipboard! (Includes decryption key fragment for private notes)"); }); }; // Export decrypted notes client-side for private pads, or server export for public pads window.SpotNotes.exportCurrentPadCsv = function(padId, padName) { if (window.SpotNotes.activePadIsPublic) { const token = new URLSearchParams(window.location.search).get('token') || ''; window.location.href = `/api/notepads/${padId}/export${token ? '?token=' + token : ''}`; return; } // Client-side export for decrypted private notes const rows = [['Note ID', 'Title', 'Content', 'Latitude', 'Longitude', 'Created At']]; window.SpotNotes.activeNotes.forEach(note => { rows.push([ note.id, note.title, note.content, note.lat, note.lng, note.created_at ]); }); const csvContent = "data:text/csv;charset=utf-8," + rows.map(r => r.map(val => `"${String(val ?? '').replace(/"/g, '""')}"`).join(",")).join("\n"); const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", `notepad_${padId}_decrypted_export.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; 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 (window.SpotNotes.activePadId) { window.SpotNotes.loadNotesForPad(window.SpotNotes.activePadId); } else { 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'; const isAuthor = window.SpotNotes.currentUser && (note.user_id && note.user_id === window.SpotNotes.currentUser.id); const isPadOwner = window.SpotNotes.currentUser && (window.SpotNotes.activePadOwnerId === window.SpotNotes.currentUser.id); const isAdmin = window.SpotNotes.currentUser && window.SpotNotes.currentUser.is_admin; const canModify = isAuthor || isPadOwner || isAdmin || (window.SpotNotes.activePadCanWrite && !window.SpotNotes.activePadIsPublic); let actionBtns = ''; if (canModify) { 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(); const coordStr = (note.lat !== undefined && note.lat !== 0) ? `${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}` : ''; const lockIcon = note.is_encrypted ? '🔒 ' : ''; item.innerHTML = `
${lockIcon}${window.SpotNotes.escapeHtml(note.title)}
${window.SpotNotes.escapeHtml(note.content)}
${dateStr} ${coordStr ? '
' + coordStr : ''}
${(note.lat && note.lng) ? `Directions` : ''} ${shareBtn} ${actionBtns}
`; item.addEventListener('click', () => { window.SpotNotes.stopTracking(); if (note.lat && note.lng) { window.SpotNotes.map.setView([note.lat, note.lng], 16); } }); sidebarBody.appendChild(item); }); }; 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(); // Decrypt any encrypted notes if user has access const decryptedNotes = await Promise.all(notes.map(async (note) => { if (note.is_encrypted && window.SpotCrypto) { let padKey = window.SpotCrypto.getCachedPadKey(note.notepad_id); if (!padKey && note.notepad && note.notepad.encrypted_key) { padKey = await window.SpotCrypto.unwrapNotepadKey(note.notepad_id, note.notepad.encrypted_key); } return await window.SpotCrypto.decryptNote(note, padKey); } return note; })); window.SpotNotes.activeNotes = decryptedNotes; window.SpotNotes.renderMapMarkers(); const params = new URLSearchParams(window.location.search); const validCoordsNotes = decryptedNotes.filter(n => n.lat && n.lng); if (validCoordsNotes.length > 0 && !params.has('pad') && !params.has('note')) { const bounds = L.latLngBounds(validCoordsNotes.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); } };