Loading...
document.addEventListener('DOMContentLoaded', () => { initMap(); initSidebar(); loadNotepads(true); const form = document.getElementById('form-add-collaborator'); if (form) { form.addEventListener('submit', async (e) => { e.preventDefault(); const padId = document.getElementById('share-pad-id').value; const email = document.getElementById('collaborator-email').value; const permission = document.getElementById('collaborator-permission').value; try { const res = await fetch(`/api/notepads/${padId}/collaborators`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, permission }) }); if (res.ok) { form.reset(); const padName = document.getElementById('share-pad-name').textContent; openShareSettingsModal(padId, padName); } else { const err = await res.json(); alert(`Failed to add collaborator: ${err.error}`); } } catch (err) { console.error(err); alert('An error occurred.'); } }); } const formEditPad = document.getElementById('form-edit-pad'); if (formEditPad) { formEditPad.addEventListener('submit', async (e) => { e.preventDefault(); const padId = document.getElementById('pad-id-edit').value; const name = document.getElementById('pad-name-edit').value; const isPublic = document.getElementById('pad-public-edit').checked; try { const res = await fetch(`/api/notepads/${padId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, is_public: isPublic }) }); if (res.ok) { document.getElementById('modal-edit-pad').classList.add('hidden'); loadNotepads(); // Refresh everything } else { alert('Failed to update notepad.'); } } catch (err) { console.error(err); } }); } const formEditNote = document.getElementById('form-edit-note'); if (formEditNote) { formEditNote.addEventListener('submit', async (e) => { e.preventDefault(); const noteId = document.getElementById('note-id-edit').value; const title = document.getElementById('note-title-edit').value; const content = document.getElementById('note-content-edit').value; try { const res = await fetch(`/api/notes/${noteId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content }) }); if (res.ok) { document.getElementById('modal-edit-note').classList.add('hidden'); // Refresh current view if (activePadId) { loadNotesForPad(activePadId); } else { // This was likely from a search result, which is harder to refresh. // A full reload of notepads is a safe fallback. alert('Note updated. Refreshing list.'); loadNotepads(); } } else { alert('Failed to update note.'); } } catch (err) { console.error(err); } }); } }); let map; let activeNotes = []; let userLocation = null; let activePadName = ""; let activePadId = null; let activePadIsPublic = false; let activePadOwnerId = null; let activePadCanWrite = false; let currentUser = null; let isTracking = false; let userMarker = null; let notifiedNoteIds = new Set(); let isLoggedIn = false; function initMap() { // Default view (e.g., Oklahoma area if geolocation fails) map = L.map('map').setView([35.6528, -97.4781], 10); // Ask for location if (navigator.geolocation) { navigator.geolocation.watchPosition((position) => { const { latitude, longitude } = position.coords; userLocation = { lat: latitude, lng: longitude }; const centerBtn = document.getElementById('btn-center-user'); if (centerBtn) centerBtn.style.display = 'block'; if (isTracking) { map.setView([latitude, longitude]); } else if (!userMarker && !new URLSearchParams(window.location.search).has('pad') && !new URLSearchParams(window.location.search).has('note')) { // Only auto-center if we aren't loading a shared item // First fix map.setView([latitude, longitude], 13); console.log("Map centered on user location:", latitude, longitude); } // Add User Location Dot if (userMarker) { userMarker.setLatLng([latitude, longitude]); } else { userMarker = L.circleMarker([latitude, longitude], { radius: 8, fillColor: "#3388ff", color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.8 }).addTo(map).bindPopup("You are here"); } // Location-Based Browser Alerts const userLatLng = L.latLng(latitude, longitude); if (activeNotes && activeNotes.length > 0) { activeNotes.forEach(note => { const noteLatLng = L.latLng(note.lat, note.lng); if (userLatLng.distanceTo(noteLatLng) <= 50) { if (!notifiedNoteIds.has(note.id)) { notifiedNoteIds.add(note.id); alert(`You are near a note!\n\n${note.title}\n${note.content}`); } } }); } }, (err) => { console.warn("Geolocation error:", err); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }); } // Add OpenStreetMap tiles L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); // Listen for zoom changes to toggle note display map.on('zoomend', () => { if (activeNotes.length > 0) { renderMapMarkers(); } }); map.on('dragstart', stopTracking); map.on('popupopen', stopTracking); } function initSidebar() { const sidebar = document.getElementById('sidebar'); const toggleBtn = document.getElementById('sidebar-toggle'); toggleBtn.addEventListener('click', () => { sidebar.classList.toggle('collapsed'); // Change arrow direction based on state if (sidebar.classList.contains('collapsed')) { const sections = document.querySelectorAll('.list-section'); sections.forEach(section => { section.querySelector('.list-section-content').style.display = 'none'; }); toggleBtn.innerHTML = '<'; // Point left (open) } else { toggleBtn.innerHTML = '>'; // Point right (close) } }); const centerBtn = document.getElementById('btn-center-user'); if (centerBtn) { centerBtn.addEventListener('click', () => { isTracking = true; updateTrackingUI(); if (userLocation) { map.setView([userLocation.lat, userLocation.lng], 16); } }); } } async function loadNotepads(isInitialLoad = false) { // Reset state activeNotes = []; currentMarkers.forEach(marker => map.removeLayer(marker)); currentMarkers = []; // Check auth first try { const authRes = await fetch('/auth/me'); const authData = await authRes.json(); isLoggedIn = authData.authenticated; currentUser = authData.user || null; } catch (e) { console.error(e); } // Reset Sidebar Header const header = document.querySelector('.sidebar-header'); if (header) { let headerHTML = '
Loading...
Loading...
Error loading data.
'; } } else { // Logged out: Only Public Notepads sidebarBody.innerHTML = `Loading...
Loading...
'; try { const res = await fetch('/api/notepads?type=public'); const pads = await res.json(); renderNotepadList(pads, 'public-notepads-list'); } catch (err) { list.innerHTML = 'Error loading public pads.
'; } } } else { list.style.display = 'none'; btn.innerHTML = 'Public Notepads ▸'; } }); } function renderNotepadList(pads, containerId, isFavorite) { const container = document.getElementById(containerId); container.innerHTML = ''; if (pads.length === 0) { container.innerHTML = 'No notepads found.
'; return; } pads.forEach(pad => { const item = document.createElement('div'); item.className = 'notepad-item'; item.style.padding = '10px'; item.style.borderBottom = '1px solid #eee'; item.style.cursor = 'pointer'; let icon = pad.is_public ? '🌍' : '🔒'; let favButton = ''; const editBtn = (currentUser && pad.owner_id === currentUser.id) ? `` : ''; const isOwner = currentUser && pad.owner_id === currentUser.id; const manageShareBtn = isOwner ? `` : ''; if(isLoggedIn){ const action = isFavorite ? 'unfavorite' : 'favorite'; const label = isFavorite ? '♥' : '♡'; favButton = ``; } const deleteBtn = isOwner ? `` : ''; const shareToken = pad.share_token || ''; const shareBtn = ``; item.innerHTML = `Error loading data.
'; } } async function loadNotesForPad(padId, padName, isPublic, ownerId, centerOnFirst = false, token = null, canWrite = undefined) { activePadId = padId; // If metadata is missing (e.g. from URL share), fetch it if (padName === undefined) { try { const res = await fetch(`/api/notepads/${padId}?token=${token || ''}`); const pad = await res.json(); padName = pad.name; isPublic = pad.is_public; ownerId = pad.owner_id; activePadCanWrite = pad.can_write; } catch(e) { console.error(e); activePadCanWrite = false; } } if (padName !== undefined) activePadName = padName; if (isPublic !== undefined) activePadIsPublic = isPublic; if (ownerId !== undefined) activePadOwnerId = ownerId; if (canWrite !== undefined) { activePadCanWrite = canWrite; } else if (padName !== undefined) { // Fallback if not passed: assume true only if owner activePadCanWrite = (currentUser && ownerId === currentUser.id); } const currentName = activePadName; const isOwner = currentUser && activePadOwnerId === currentUser.id; // Clear existing markers currentMarkers.forEach(marker => map.removeLayer(marker)); currentMarkers = []; activeNotes = []; // Update Sidebar Header const header = document.querySelector('.sidebar-header'); if (header) { const addNoteBtn = activePadCanWrite ? `` : ''; const manageShareBtn = isOwner ? `` : ''; const exportBtn = ``; header.innerHTML = `Loading notes...
'; try { const res = await fetch(`/api/notepads/${padId}/notes?token=${token || ''}`); const notes = await res.json(); activeNotes = notes; // Render Sidebar List sidebarBody.innerHTML = ''; if (notes.length === 0) { sidebarBody.innerHTML = 'No notes yet.
'; } notes.forEach(note => { const item = document.createElement('div'); item.className = 'sidebar-note-item'; let actionBtns = ''; if (activePadCanWrite) { const escapedTitle = escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '"'); const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); actionBtns = ` `; } else if (activePadIsPublic) { actionBtns = ``; } const dateStr = new Date(note.created_at).toLocaleString(); item.innerHTML = `Error loading notes.
'; } } async function loadSingleNote(noteId, token = null) { // Reset state activeNotes = []; currentMarkers.forEach(marker => map.removeLayer(marker)); currentMarkers = []; activePadId = null; const sidebarBody = document.getElementById('sidebar-body'); sidebarBody.innerHTML = 'Loading note...
'; try { const res = await fetch(`/api/notes/${noteId}?token=${token || ''}`); if (!res.ok) throw new Error("Note not found or private"); const note = await res.json(); // Set active notes to just this one so renderMapMarkers works activeNotes = [note]; // Update Header const header = document.querySelector('.sidebar-header'); if (header) { header.innerHTML = `Failed to load note. It may be private or deleted.
'; } } function renderMapMarkers() { // Clear existing currentMarkers.forEach(marker => map.removeLayer(marker)); currentMarkers = []; const zoom = map.getZoom(); const showFull = zoom >= 14; // Zoom threshold activeNotes.forEach(note => { let marker; let actionBtns = ''; // Determine permissions: Use note-specific info if available (search results), else fall back to pad globals const isOwner = currentUser && (note.owner_id ? note.owner_id === currentUser.id : activePadOwnerId === currentUser.id); const isPublic = note.is_public !== undefined ? note.is_public : activePadIsPublic; const canWrite = (note.notepad && note.notepad.can_write !== undefined) ? note.notepad.can_write : activePadCanWrite; if (canWrite) { const escapedTitle = escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '"'); const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); actionBtns = `No results found.
'; renderMapMarkers(); return; } else if (notes.length === 0 && location) { sidebarBody.innerHTML = 'Location found on map.
'; renderMapMarkers(); map.setView([location.lat, location.lng], 13); return; } // Group notes by section and notepad const sections = { favorites: {}, // MapNo matching notes.
'; } else { padIds.forEach(padId => { const entry = dataMap[padId]; const pad = entry.pad; // Notepad Header, styled like the default view const padItem = document.createElement('div'); padItem.className = 'notepad-item'; padItem.style.backgroundColor = '#e9ecef'; // Differentiate as a sub-header padItem.style.cursor = 'default'; let icon = pad.is_public ? '🌍' : '🔒'; let favButton = ''; if(isLoggedIn){ const isFavorite = pad.is_favorite; const action = isFavorite ? 'unfavorite' : 'favorite'; const label = isFavorite ? '♥' : '♡'; favButton = ``; } 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 = ` ${escapeHtml(collab.email)} (${collab.permission}) `; listDiv.appendChild(item); }); } } catch (err) { listDiv.innerHTML = `${err.message}
`; } } window.removeCollaborator = async function(padId, userId) { if (!confirm("Are you sure you want to remove this collaborator?")) return; try { const res = await fetch(`/api/notepads/${padId}/collaborators/${userId}`, { method: 'DELETE' }); if (res.ok) { const padName = document.getElementById('share-pad-name').textContent; openShareSettingsModal(padId, padName); } else { const err = await res.json(); alert(`Failed to remove collaborator: ${err.error}`); } } catch (err) { console.error(err); alert('An error occurred.'); } } async function loadAllMyNotes() { activePadId = null; activePadName = "All My Notes"; activePadIsPublic = false; activePadCanWrite = true; if (currentUser) { activePadOwnerId = currentUser.id; } // Clear existing markers currentMarkers.forEach(marker => map.removeLayer(marker)); currentMarkers = []; activeNotes = []; try { const res = await fetch('/api/notes/mine'); if (!res.ok) throw new Error("Failed to load"); const notes = await res.json(); activeNotes = notes; renderMapMarkers(); if (notes.length > 0) { // Auto-center map bounds cleanly around all the user's notes const bounds = L.latLngBounds(notes.map(n => [n.lat, n.lng])); map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 }); } } catch (err) { console.error("Error loading all my notes:", err); } } window.shareNotepad = function(padId, token) { let url = `${window.location.origin}/?pad=${padId}`; if (token) url += `&token=${token}`; navigator.clipboard.writeText(url).then(() => { alert("Notepad link copied to clipboard!"); }); }; window.shareNote = function(noteId, token) { let url = `${window.location.origin}/?note=${noteId}`; if (token) url += `&token=${token}`; navigator.clipboard.writeText(url).then(() => { alert("Note link copied to clipboard!"); }); }; function stopTracking() { isTracking = false; updateTrackingUI(); } function updateTrackingUI() { const btn = document.getElementById('btn-center-user'); if (btn) { if (isTracking) { btn.textContent = "Tracking Location..."; btn.style.backgroundColor = "#28a745"; } else { btn.textContent = "Target My Location"; btn.style.backgroundColor = ""; } } } function escapeHtml(text) { if (text === null || text === undefined) return ''; return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function toggleSection(event) { const section = event.target.closest('.list-section'); const content = section.querySelector('.list-section-content'); const isCollapsed = content.style.display === 'none'; content.style.display = isCollapsed ? 'block' : 'none'; event.target.innerHTML = isCollapsed ? event.target.innerHTML.replace('▸', '▾') : event.target.innerHTML.replace('▾', '▸'); } window.deleteNote = async function(noteId) { if (!confirm("Delete this note?")) return; try { const res = await fetch(`/api/notes/${noteId}`, { method: 'DELETE' }); if (res.ok) { // If we are in a pad view, reload the pad. if (activePadId) { loadNotesForPad(activePadId); } else { // Otherwise (e.g., search results), the safest refresh is to return to the main list. alert("Note deleted. Returning to notepad list."); loadNotepads(); } } else { alert("Failed to delete note."); } } catch (err) { console.error(err); } };