spotnotes.app/static/assets/js/index.js

987 lines
45 KiB
JavaScript
Raw Normal View History

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;
2026-05-12 23:58:18 +00:00
document.addEventListener('DOMContentLoaded', () => {
window.SpotNotes.initMap();
window.SpotNotes.initSidebar();
window.SpotNotes.loadNotepads(true);
2026-05-12 23:58:18 +00:00
// --- Add Collaborator Modal Handler ---
2026-05-12 23:58:18 +00:00
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();
2026-05-12 23:58:18 +00:00
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);
}
2026-05-12 23:58:18 +00:00
const res = await fetch(`/api/notepads/${padId}/collaborators`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, permission, encrypted_key: encryptedKey })
2026-05-12 23:58:18 +00:00
});
if (res.ok) {
form.reset();
const padName = document.getElementById('share-pad-name').textContent;
window.SpotNotes.openShareSettingsModal(padId, padName);
2026-05-12 23:58:18 +00:00
} else {
const err = await res.json();
alert(`Failed to add collaborator: ${err.error}`);
}
} catch (err) {
console.error(err);
alert('An error occurred adding collaborator.');
2026-05-12 23:58:18 +00:00
}
});
}
// --- Edit Notepad Modal Handler ---
2026-05-12 23:58:18 +00:00
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;
2026-05-12 23:58:18 +00:00
try {
const res = await fetch(`/api/notepads/${padId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, is_public: isPublic, color })
2026-05-12 23:58:18 +00:00
});
if (res.ok) {
document.getElementById('modal-edit-pad').classList.add('hidden');
window.SpotNotes.loadNotepads();
2026-05-12 23:58:18 +00:00
} else {
alert('Failed to update notepad.');
}
} catch (err) { console.error(err); }
});
}
// --- Edit Note Modal Handler ---
2026-05-12 23:58:18 +00:00
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();
2026-05-12 23:58:18 +00:00
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;
2026-05-12 23:58:18 +00:00
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 };
}
2026-05-12 23:58:18 +00:00
const res = await fetch(`/api/notes/${noteId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bodyPayload)
2026-05-12 23:58:18 +00:00
});
2026-05-12 23:58:18 +00:00
if (res.ok) {
document.getElementById('modal-edit-note').classList.add('hidden');
if (window.SpotNotes.activePadId) {
window.SpotNotes.loadNotesForPad(window.SpotNotes.activePadId);
2026-05-12 23:58:18 +00:00
} else {
window.SpotNotes.loadNotepads();
2026-05-12 23:58:18 +00:00
}
} else {
alert('Failed to update note.');
}
} catch (err) { console.error(err); }
});
}
});
window.SpotNotes.loadNotepads = async function(isInitialLoad = false) {
2026-05-12 23:58:18 +00:00
// Reset state
window.SpotNotes.activeNotes = [];
window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
window.SpotNotes.currentMarkers = [];
2026-05-12 23:58:18 +00:00
// 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();
}
2026-05-12 23:58:18 +00:00
} catch (e) { console.error(e); }
// Reset Sidebar Header
const header = document.querySelector('.sidebar-header');
if (header) {
let headerHTML = `<div style="display:flex; justify-content:space-between; align-items:center; width:100%;">
<h2>Notepads</h2>`;
if (window.SpotNotes.isLoggedIn) {
2026-05-12 23:58:18 +00:00
headerHTML += '<button class="btn-new-pad" id="btn-create-pad" title="Create New Pad">+</button>';
}
headerHTML += '</div>';
2026-05-12 23:58:18 +00:00
header.innerHTML = headerHTML;
if (window.SpotNotes.isLoggedIn) {
2026-05-12 23:58:18 +00:00
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) {
2026-05-12 23:58:18 +00:00
sidebarBody.innerHTML = `
<div class="list-section" id="favorite-notepads-section">
<button class="list-section-header">Favorite Notepads &#9662;</button>
<div class="list-section-content" id="favorite-notepads-list">
<p style="padding:10px; color:#666;">Loading...</p>
</div>
</div>
<div class="list-section" id="my-notepads-section">
2026-05-12 23:58:18 +00:00
<button class="list-section-header">My Notepads &#9662;</button>
<div class="list-section-content" id="my-notepads-list"><p style="padding:10px; color:#666;">Loading...</p></div>
</div>
<div class="list-section" id="shared-notepads-section">
<button class="list-section-header">Shared With Me &#9662;</button>
<div class="list-section-content" id="shared-notepads-list"><p style="padding:10px; color:#666;">Loading...</p></div>
</div>
<div class="list-section" id="public-notepads-section">
2026-05-12 23:58:18 +00:00
<button id="btn-toggle-public" class="btn-toggle-section">Public Notepads &#9656;</button>
<div id="public-notepads-list" style="display:none;"></div>
</div>
`;
const listSection = document.querySelectorAll('.list-section-header');
listSection.forEach(h => {
h.addEventListener('click', window.SpotNotes.toggleSection);
});
window.SpotNotes.setupPublicToggle();
2026-05-12 23:58:18 +00:00
try {
await window.SpotNotes.loadNotepadsOfType('favorites', 'favorite-notepads-list');
await window.SpotNotes.loadNotepadsOfType('shared', 'shared-notepads-list');
2026-05-12 23:58:18 +00:00
const res = await fetch('/api/notepads?type=mine');
const notepads = await res.json();
window.SpotNotes.renderNotepadList(notepads, 'my-notepads-list');
2026-05-12 23:58:18 +00:00
} catch (err) {
console.error("Failed to load notepads", err);
document.getElementById('my-notepads-list').innerHTML = '<p style="color:red; padding:10px;">Error loading data.</p>';
}
} else {
// Logged out: Only Public Notepads
sidebarBody.innerHTML = `
<div id="public-notepads-section">
<h3 class="list-section-header">Public Notepads</h3>
<div id="public-notepads-list"><p style="padding:10px; color:#666;">Loading...</p></div>
</div>
`;
try {
const res = await fetch('/api/notepads?type=public');
const pads = await res.json();
window.SpotNotes.renderNotepadList(pads, 'public-notepads-list');
2026-05-12 23:58:18 +00:00
} 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();
2026-05-12 23:58:18 +00:00
}
};
2026-05-12 23:58:18 +00:00
window.SpotNotes.toggleFavorite = async function(padId, action) {
2026-05-12 23:58:18 +00:00
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');
2026-05-12 23:58:18 +00:00
} else {
alert(`Failed to ${action} notepad`);
}
} catch (err) {
console.error(err);
}
};
2026-05-12 23:58:18 +00:00
window.SpotNotes.loadNotepadsOfType = async function(type, containerId) {
2026-05-12 23:58:18 +00:00
try {
const res = await fetch(`/api/notepads?type=${type}`);
const notepads = await res.json();
const isFavorite = type === 'favorites';
window.SpotNotes.renderNotepadList(notepads, containerId, isFavorite);
2026-05-12 23:58:18 +00:00
} catch (err) {
console.error(`Failed to load ${type} notepads`, err);
document.getElementById(containerId).innerHTML = '<p style="color:red; padding:10px;">Error loading data.</p>';
}
};
window.SpotNotes.loadNotesForPad = async function(padId, padName, isPublic, ownerId, centerOnFirst = false, token = null, canWrite = undefined, encryptedKey = null) {
window.SpotNotes.activePadId = padId;
2026-05-12 23:58:18 +00:00
// If metadata is missing, fetch it
if (padName === undefined || encryptedKey === undefined) {
2026-05-12 23:58:18 +00:00
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;
}
2026-05-12 23:58:18 +00:00
}
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;
2026-05-12 23:58:18 +00:00
if (canWrite !== undefined) {
window.SpotNotes.activePadCanWrite = canWrite;
2026-05-12 23:58:18 +00:00
} else if (padName !== undefined) {
window.SpotNotes.activePadCanWrite = (window.SpotNotes.currentUser && ownerId === window.SpotNotes.currentUser.id);
2026-05-12 23:58:18 +00:00
}
// 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;
2026-05-12 23:58:18 +00:00
// Clear existing markers
window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
window.SpotNotes.currentMarkers = [];
window.SpotNotes.activeNotes = [];
2026-05-12 23:58:18 +00:00
// Update Sidebar Header
const header = document.querySelector('.sidebar-header');
if (header) {
const addNoteBtn = window.SpotNotes.activePadCanWrite ? `<button onclick="openCreateNoteModal(${padId})" class="btn-add-note">+ Add Note</button>` : '';
const manageShareBtn = isOwner ? `<button class="btn-action-icon" onclick="window.SpotNotes.openShareSettingsModal(${padId}, '${window.SpotNotes.escapeHtml(currentName)}')" title="Manage Sharing">⚙️</button>` : '';
const exportBtn = `<button class="btn-action-icon" onclick="window.SpotNotes.exportCurrentPadCsv(${padId}, '${window.SpotNotes.escapeHtml(currentName)}')" title="Export to CSV">⬇️</button>`;
const shareBtn = `<button class="btn-action-icon" onclick="window.SpotNotes.shareNotepad(${padId}, '${token || ''}')" title="Share Notepad">🔗</button>`;
const encryptionBadge = !isPublic ? '<span title="End-to-End Encrypted at Rest" style="font-size:0.85rem; margin-left:4px;">🔒</span>' : '<span title="Public Unencrypted" style="font-size:0.85rem; margin-left:4px;">🌍</span>';
2026-05-12 23:58:18 +00:00
header.innerHTML = `
<div class="sidebar-header-top" style="display:flex; align-items:center; justify-content:space-between; width:100%;">
<div style="display:flex; align-items:center; gap:8px; min-width:0;">
<button id="btn-back-pads" class="btn-action-icon" style="font-size:1.25rem; margin-right:4px;">&larr;</button>
<h2 style="font-size:1.15rem; margin:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" title="${window.SpotNotes.escapeHtml(currentName)}">${window.SpotNotes.escapeHtml(currentName)} ${encryptionBadge}</h2>
</div>
<div class="notepad-actions" style="display:flex; align-items:center; gap:4px; flex-shrink:0;">
${shareBtn}
${manageShareBtn}
${exportBtn}
</div>
</div>
<div style="margin-top: 12px; width:100%;">
<input type="text" id="filter-active-notes" placeholder="Filter notes..." style="width: 100%; padding: 8px 12px; box-sizing: border-box; border: 1px solid var(--border); border-radius: var(--radius-md); outline:none; transition:var(--transition); font-family:var(--font-sans); font-size:0.9rem; background-color:var(--bg-page);">
2026-05-12 23:58:18 +00:00
</div>
<div style="margin-top: 10px; width:100%; display:flex; justify-content:space-between; align-items:center;">
${addNoteBtn}
</div>
2026-05-12 23:58:18 +00:00
`;
document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads);
2026-05-12 23:58:18 +00:00
}
const sidebarBody = document.getElementById('sidebar-body');
sidebarBody.innerHTML = '<p style="padding:10px;">Loading notes...</p>';
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;
2026-05-12 23:58:18 +00:00
// Render Sidebar List & Markers
window.SpotNotes.renderActiveNotesList();
window.SpotNotes.renderMapMarkers();
2026-05-12 23:58:18 +00:00
const filterInput = document.getElementById('filter-active-notes');
if (filterInput) {
filterInput.addEventListener('keyup', (e) => {
window.SpotNotes.renderActiveNotesList(e.target.value);
2026-05-12 23:58:18 +00:00
});
}
2026-05-12 23:58:18 +00:00
if (centerOnFirst && decryptedNotes.length > 0 && decryptedNotes[0].lat && decryptedNotes[0].lng) {
window.SpotNotes.map.setView([decryptedNotes[0].lat, decryptedNotes[0].lng], 13);
2026-05-12 23:58:18 +00:00
}
} catch (err) {
console.error(err);
sidebarBody.innerHTML = '<p style="color:red; padding:10px;">Error loading notes.</p>';
}
};
2026-05-12 23:58:18 +00:00
window.SpotNotes.loadSingleNote = async function(noteId, token = null) {
2026-05-12 23:58:18 +00:00
// Reset state
window.SpotNotes.activeNotes = [];
window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
window.SpotNotes.currentMarkers = [];
window.SpotNotes.activePadId = null;
2026-05-12 23:58:18 +00:00
const sidebarBody = document.getElementById('sidebar-body');
sidebarBody.innerHTML = '<p style="padding:10px;">Loading note...</p>';
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);
}
2026-05-12 23:58:18 +00:00
window.SpotNotes.activeNotes = [note];
2026-05-12 23:58:18 +00:00
// Update Header
const header = document.querySelector('.sidebar-header');
if (header) {
header.innerHTML = `
<div style="display:flex; align-items:center;">
<button id="btn-back-pads" style="background:none; border:none; font-size:1.5rem; cursor:pointer; margin-right:10px;">&larr;</button>
<h2 style="font-size:1.2rem; margin:0;">Shared Note ${note.is_encrypted ? '🔒' : '🌍'}</h2>
2026-05-12 23:58:18 +00:00
</div>
`;
document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads);
2026-05-12 23:58:18 +00:00
}
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) {
2026-05-12 23:58:18 +00:00
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);
2026-05-12 23:58:18 +00:00
};
p.appendChild(link);
} else {
const strong = document.createElement('strong');
strong.textContent = (note.notepad && note.notepad.name) || 'Private Notepad';
2026-05-12 23:58:18 +00:00
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);
}
2026-05-12 23:58:18 +00:00
if (window.innerWidth <= 768) {
2026-05-12 23:58:18 +00:00
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 = '&lt;';
}
}
} catch (err) {
sidebarBody.innerHTML = '<p style="color:red; padding:10px;">Failed to load note. It may be private or deleted.</p>';
}
};
2026-05-12 23:58:18 +00:00
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;
2026-05-12 23:58:18 +00:00
const header = document.querySelector('.sidebar-header');
if (header) {
header.innerHTML = `
<div style="display:flex; align-items:center;">
<button id="btn-back-pads" style="background:none; border:none; font-size:1.5rem; cursor:pointer; margin-right:10px;">&larr;</button>
<h2 style="font-size:1.2rem; margin:0;">Results: ${window.SpotNotes.escapeHtml(query)}</h2>
2026-05-12 23:58:18 +00:00
</div>
`;
document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads);
2026-05-12 23:58:18 +00:00
}
const sidebarBody = document.getElementById('sidebar-body');
sidebarBody.innerHTML = '';
if (notes.length === 0 && !location) {
sidebarBody.innerHTML = '<p style="padding:10px; color:#666;">No results found.</p>';
window.SpotNotes.renderMapMarkers();
2026-05-12 23:58:18 +00:00
return;
} else if (notes.length === 0 && location) {
sidebarBody.innerHTML = '<p style="padding:10px; color:#666;">Location found on map.</p>';
window.SpotNotes.renderMapMarkers();
window.SpotNotes.map.setView([location.lat, location.lng], 13);
2026-05-12 23:58:18 +00:00
return;
}
const sections = {
favorites: {},
2026-05-12 23:58:18 +00:00
mine: {},
public: {}
};
notes.forEach(note => {
const pad = note.notepad || { id: note.notepad_id, name: 'Notes', is_public: !note.is_encrypted };
2026-05-12 23:58:18 +00:00
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');
2026-05-12 23:58:18 +00:00
if (pad.is_public) addToSection('public');
});
const renderSection = (title, dataMap, isOpen) => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'list-section';
const arrow = isOpen ? '&#9662;' : '&#9656;';
const display = isOpen ? 'block' : 'none';
const headerBtn = document.createElement('button');
headerBtn.className = 'list-section-header';
headerBtn.innerHTML = `${title} ${arrow}`;
headerBtn.onclick = window.SpotNotes.toggleSection;
2026-05-12 23:58:18 +00:00
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 = '<p style="padding:10px; color:#999; font-style:italic; font-size:0.8rem;">No matching notes.</p>';
} 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';
2026-05-12 23:58:18 +00:00
padItem.style.cursor = 'default';
let icon = pad.is_public ? '🌍' : '🔒';
let favButton = '';
if (window.SpotNotes.isLoggedIn) {
2026-05-12 23:58:18 +00:00
const isFavorite = pad.is_favorite;
const action = isFavorite ? 'unfavorite' : 'favorite';
const label = isFavorite ? '&hearts;' : '♡';
favButton = `<button onclick="event.stopPropagation(); window.SpotNotes.toggleFavorite(${pad.id}, '${action}')" style="color:#dc3545; border:none; background:none; cursor:pointer; font-weight:bold; padding:0 5px;">${label}</button>`;
2026-05-12 23:58:18 +00:00
}
padItem.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-weight:bold;">${icon} ${window.SpotNotes.escapeHtml(pad.name)}</div>
2026-05-12 23:58:18 +00:00
<div style="display: flex; align-items: center;">
${favButton}
</div>
</div>
`;
contentDiv.appendChild(padItem);
entry.notes.forEach(note => {
const item = document.createElement('div');
item.className = 'note-card';
2026-05-12 23:58:18 +00:00
const dateStr = new Date(note.created_at).toLocaleString();
let actionBtn = '';
2026-05-12 23:58:18 +00:00
if (note.notepad && note.notepad.can_write) {
const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '&quot;');
const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtn = `<button class="note-card-link edit" onclick="event.stopPropagation(); openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')">Edit</button> <button class="note-card-link delete" onclick="event.stopPropagation(); window.SpotNotes.deleteNote(${note.id})">Delete</button>`;
2026-05-12 23:58:18 +00:00
} else if (note.is_public) {
actionBtn = `<button class="note-card-link report" onclick="event.stopPropagation(); window.SpotNotes.reportNote(${note.id})">Report</button>`;
2026-05-12 23:58:18 +00:00
}
const shareToken = note.share_token || '';
const shareBtn = `<button class="note-card-link share" onclick="event.stopPropagation(); window.SpotNotes.shareNote(${note.id}, '${shareToken}')">Share</button>`;
2026-05-12 23:58:18 +00:00
item.innerHTML = `
<div class="note-card-title">${window.SpotNotes.escapeHtml(note.title)}</div>
<div class="note-card-body">${window.SpotNotes.escapeHtml(note.content)}</div>
<div class="note-card-meta">
${dateStr} <br> ${note.lat ? note.lat.toFixed(5) : ''}, ${note.lng ? note.lng.toFixed(5) : ''}
2026-05-12 23:58:18 +00:00
</div>
<div class="note-card-actions">
<a href="https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target="_blank" onclick="event.stopPropagation();" class="note-card-link directions">Directions</a>
${shareBtn}
${actionBtn}
</div>
2026-05-12 23:58:18 +00:00
`;
item.addEventListener('click', () => {
window.SpotNotes.stopTracking();
if (note.lat && note.lng) window.SpotNotes.map.setView([note.lat, note.lng], 16);
2026-05-12 23:58:18 +00:00
});
contentDiv.appendChild(item);
});
});
}
sectionDiv.appendChild(headerBtn);
sectionDiv.appendChild(contentDiv);
sidebarBody.appendChild(sectionDiv);
};
if (window.SpotNotes.isLoggedIn) {
2026-05-12 23:58:18 +00:00
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();
2026-05-12 23:58:18 +00:00
if (notes.length > 0 && notes[0].lat && notes[0].lng) {
window.SpotNotes.map.setView([notes[0].lat, notes[0].lng], 15);
2026-05-12 23:58:18 +00:00
} else if (location) {
window.SpotNotes.map.setView([location.lat, location.lng], 13);
2026-05-12 23:58:18 +00:00
}
};
window.SpotNotes.reportNote = async function(noteId) {
2026-05-12 23:58:18 +00:00
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);
2026-05-12 23:58:18 +00:00
}
} catch (err) {
console.error("Error reporting note:", err);
}
};
window.SpotNotes.deleteNotepad = async function(padId) {
2026-05-12 23:58:18 +00:00
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();
2026-05-12 23:58:18 +00:00
} else {
alert("Failed to delete notepad.");
}
} catch (err) { console.error(err); }
};
window.SpotNotes.openShareSettingsModal = async function(padId, padName) {
2026-05-12 23:58:18 +00:00
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 = '<p>Loading...</p>';
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 = '<p style="color:#888; font-style:italic;">No collaborators yet.</p>';
} else {
collaborators.forEach(collab => {
const item = document.createElement('div');
item.className = 'collaborator-item';
item.innerHTML = `
<span>${window.SpotNotes.escapeHtml(collab.email)} (${collab.permission})</span>
<button onclick="window.SpotNotes.removeCollaborator(${padId}, ${collab.user_id})">Remove</button>
2026-05-12 23:58:18 +00:00
`;
listDiv.appendChild(item);
});
}
} catch (err) {
listDiv.innerHTML = `<p style="color:red;">${err.message}</p>`;
}
};
2026-05-12 23:58:18 +00:00
window.SpotNotes.removeCollaborator = async function(padId, userId) {
2026-05-12 23:58:18 +00:00
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);
2026-05-12 23:58:18 +00:00
} else {
const err = await res.json();
alert(`Failed to remove collaborator: ${err.error}`);
}
} catch (err) {
console.error(err);
alert('An error occurred.');
}
};
2026-05-12 23:58:18 +00:00
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;
2026-05-12 23:58:18 +00:00
}
window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
window.SpotNotes.currentMarkers = [];
window.SpotNotes.activeNotes = [];
2026-05-12 23:58:18 +00:00
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();
2026-05-12 23:58:18 +00:00
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 });
2026-05-12 23:58:18 +00:00
}
} catch (err) {
console.error("Error loading all my notes:", err);
}
};
2026-05-12 23:58:18 +00:00
// Zero-knowledge link sharing for notepads
window.SpotNotes.shareNotepad = async function(padId, token) {
2026-05-12 23:58:18 +00:00
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}`;
}
}
2026-05-12 23:58:18 +00:00
navigator.clipboard.writeText(url).then(() => {
alert("Notepad link copied to clipboard! (Includes decryption key fragment for private notepads)");
2026-05-12 23:58:18 +00:00
});
};
// Zero-knowledge link sharing for individual notes
window.SpotNotes.shareNote = async function(noteId, token) {
2026-05-12 23:58:18 +00:00
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}`;
}
}
2026-05-12 23:58:18 +00:00
navigator.clipboard.writeText(url).then(() => {
alert("Note link copied to clipboard! (Includes decryption key fragment for private notes)");
2026-05-12 23:58:18 +00:00
});
};
// 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;
}
2026-05-12 23:58:18 +00:00
// 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
]);
});
2026-05-12 23:58:18 +00:00
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) {
2026-05-12 23:58:18 +00:00
if (text === null || text === undefined) return '';
return String(text)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
window.SpotNotes.deleteNote = async function(noteId) {
2026-05-12 23:58:18 +00:00
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);
2026-05-12 23:58:18 +00:00
} else {
alert("Note deleted. Returning to notepad list.");
window.SpotNotes.loadNotepads();
2026-05-12 23:58:18 +00:00
}
} 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 = '<p style="padding:10px; color:#666;">No notes match.</p>';
}
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, '&quot;');
const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtns = `
<button class="note-card-link edit" onclick="event.stopPropagation(); openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')">Edit</button>
<button class="note-card-link delete" onclick="event.stopPropagation(); window.SpotNotes.deleteNote(${note.id})">Delete</button>
`;
} else if (window.SpotNotes.activePadIsPublic) {
actionBtns = `<button class="note-card-link report" onclick="event.stopPropagation(); window.SpotNotes.reportNote(${note.id})">Report</button>`;
}
const shareToken = note.share_token || '';
const shareBtn = `<button class="note-card-link share" onclick="event.stopPropagation(); window.SpotNotes.shareNote(${note.id}, '${shareToken}')" title="Share Note">Share</button>`;
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 = `
<div class="note-card-title">${lockIcon}${window.SpotNotes.escapeHtml(note.title)}</div>
<div class="note-card-body">${window.SpotNotes.escapeHtml(note.content)}</div>
<div class="note-card-meta">
${dateStr} ${coordStr ? '<br>' + coordStr : ''}
</div>
<div class="note-card-actions">
${(note.lat && note.lng) ? `<a href="https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target="_blank" onclick="event.stopPropagation();" class="note-card-link directions">Directions</a>` : ''}
${shareBtn}
${actionBtns}
</div>
`;
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);
}
};