Loading...
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 = `
Loading...
Loading...
Error loading data.
'; } } else { // Logged out: Only Public Notepads sidebarBody.innerHTML = `Loading...
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 && 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 addNoteBtn = window.SpotNotes.activePadCanWrite ? `` : ''; const manageShareBtn = isOwner ? `` : ''; const exportBtn = ``; const shareBtn = ``; const encryptionBadge = !isPublic ? '🔒' : '🌍'; header.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 = `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 = `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 = `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'; 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(); const coordStr = (note.lat !== undefined && note.lat !== 0) ? `${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}` : ''; const lockIcon = note.is_encrypted ? '🔒 ' : ''; item.innerHTML = `