spotnotes.app/static/assets/js/index.js
2026-05-12 18:58:18 -05:00

1024 lines
No EOL
43 KiB
JavaScript

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: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).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 = '&lt;'; // Point left (open)
} else {
toggleBtn.innerHTML = '&gt;'; // 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 = '<h2>Notepads</h2>';
if (isLoggedIn) {
headerHTML += '<button class="btn-new-pad" id="btn-create-pad" title="Create New Pad">+</button>';
}
header.innerHTML = headerHTML;
if (isLoggedIn) {
document.getElementById('btn-create-pad').addEventListener('click', () => {
document.getElementById('modal-create-pad').classList.remove('hidden');
});
}
}
const sidebarBody = document.getElementById('sidebar-body');
if (isLoggedIn) {
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">
<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">
<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(header => {
header.addEventListener('click',toggleSection)
})
setupPublicToggle();
try {
await loadNotepadsOfType('favorites', 'favorite-notepads-list');
await loadNotepadsOfType('shared', 'shared-notepads-list');
const res = await fetch('/api/notepads?type=mine');
const notepads = await res.json();
renderNotepadList(notepads, 'my-notepads-list');
} 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();
renderNotepadList(pads, 'public-notepads-list');
} catch (err) {
console.error(err);
}
}
// Initial Load Logic (Check for URL params first, else default behavior based on auth)
if (isInitialLoad === true) {
const params = new URLSearchParams(window.location.search);
if (params.has('pad')) {
const token = params.get('token');
loadNotesForPad(params.get('pad'), undefined, undefined, undefined, true, token);
} else if (params.has('note')) {
const token = params.get('token');
loadSingleNote(params.get('note'), token);
} else if (isLoggedIn) {
loadAllMyNotes(); // Default action for logged in users
} else {
try {
const res = await fetch('/api/notepads?type=public');
const pads = await res.json();
const globalPad = pads.find(p => p.name === "Global Public Notes");
if (globalPad) {
loadNotesForPad(globalPad.id, globalPad.name, true, globalPad.owner_id);
}
} catch (err) {}
}
}
}
function setupPublicToggle() {
const btn = document.getElementById('btn-toggle-public');
const list = document.getElementById('public-notepads-list');
btn.addEventListener('click', async () => {
if (list.style.display === 'none') {
list.style.display = 'block';
btn.innerHTML = 'Public Notepads &#9662;';
// Lazy load if empty
if (list.innerHTML === '') {
list.innerHTML = '<p style="padding:10px; color:#666;">Loading...</p>';
try {
const res = await fetch('/api/notepads?type=public');
const pads = await res.json();
renderNotepadList(pads, 'public-notepads-list');
} catch (err) {
list.innerHTML = '<p style="color:red; padding:10px;">Error loading public pads.</p>';
}
}
} else {
list.style.display = 'none';
btn.innerHTML = 'Public Notepads &#9656;';
}
});
}
function renderNotepadList(pads, containerId, isFavorite) {
const container = document.getElementById(containerId);
container.innerHTML = '';
if (pads.length === 0) {
container.innerHTML = '<p style="padding:10px; color:#999; font-style:italic;">No notepads found.</p>';
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) ?
`<button onclick="event.stopPropagation(); openEditPadModal(${pad.id}, '${escapeHtml(pad.name)}', ${pad.is_public})" title="Edit Notepad" style="border:none; background:none; cursor:pointer; padding:0 5px;">✏️</button>`
: '';
const isOwner = currentUser && pad.owner_id === currentUser.id;
const manageShareBtn = isOwner ?
`<button onclick="event.stopPropagation(); openShareSettingsModal(${pad.id}, '${escapeHtml(pad.name)}')" title="Manage Sharing" style="border:none; background:none; cursor:pointer; padding:0 5px;">⚙️</button>`
: '';
if(isLoggedIn){
const action = isFavorite ? 'unfavorite' : 'favorite';
const label = isFavorite ? '&hearts;' : '♡';
favButton = `<button onclick="event.stopPropagation(); toggleFavorite(${pad.id}, '${action}')" style="color:#dc3545; border:none; background:none; cursor:pointer; font-weight:bold; padding:0 5px;">${label}</button>`;
}
const deleteBtn = isOwner ?
`<button onclick="event.stopPropagation(); deleteNotepad(${pad.id})" style="color:#dc3545; border:none; background:none; cursor:pointer; font-weight:bold; padding:0 5px;">&times;</button>`
: '';
const shareToken = pad.share_token || '';
const shareBtn = `<button onclick="event.stopPropagation(); shareNotepad(${pad.id}, '${shareToken}')" title="Share Notepad" style="border:none; background:none; cursor:pointer; padding:0 5px;">🔗</button>`;
item.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-weight:bold;">${icon} ${escapeHtml(pad.name)}</div>
<div style="display: flex; align-items: center;">
${editBtn} ${manageShareBtn} ${shareBtn} ${favButton}
${deleteBtn}
</div>
<div style="font-size:0.8rem; color:#666;">${pad.note_count || 0} notes</div>
</div>
`;
item.addEventListener('click', () => loadNotesForPad(pad.id, pad.name, pad.is_public, pad.owner_id, false, null, pad.can_write));
container.appendChild(item);
});
}
let currentMarkers = [];
let topZIndex = 1000000;
async function toggleFavorite(padId, action) {
try {
const res = await fetch(`/api/notepads/${padId}/${action}`, { method: 'POST' });
if (res.ok) {
// Refresh the lists
loadNotepadsOfType('favorites', 'favorite-notepads-list');
loadNotepadsOfType('mine', 'my-notepads-list');
loadNotepadsOfType('public', 'public-notepads-list');
} else {
alert(`Failed to ${action} notepad`);
}
} catch (err) {
console.error(err);
}
}
async function loadNotepadsOfType(type, containerId) {
try {
const res = await fetch(`/api/notepads?type=${type}`);
const notepads = await res.json();
const isFavorite = type === 'favorites';
renderNotepadList(notepads, containerId, isFavorite);
} catch (err) {
console.error(`Failed to load ${type} notepads`, err);
document.getElementById(containerId).innerHTML = '<p style="color:red; padding:10px;">Error loading data.</p>';
}
}
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 ? `<button onclick="openCreateNoteModal(${padId})" class="btn-add-note">+ Add Note</button>` : '';
const manageShareBtn = isOwner ? `<button onclick="openShareSettingsModal(${padId}, '${escapeHtml(currentName)}')" style="border:none; background:none; cursor:pointer; margin-left:5px;" title="Manage Sharing">⚙️</button>` : '';
const exportBtn = `<button onclick="window.location.href='/api/notepads/${padId}/export${token ? '?token=' + token : ''}'" style="border:none; background:none; cursor:pointer; margin-left:5px;" title="Export to CSV">⬇️</button>`;
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;">${escapeHtml(currentName)}</h2>
<button onclick="shareNotepad(${padId}, '${token || ''}')" style="border:none; background:none; cursor:pointer; margin-left:10px;" title="Share">🔗</button>
${manageShareBtn}
${exportBtn}
</div>
${addNoteBtn}
`;
document.getElementById('btn-back-pads').addEventListener('click', loadNotepads);
}
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();
activeNotes = notes;
// Render Sidebar List
sidebarBody.innerHTML = '';
if (notes.length === 0) {
sidebarBody.innerHTML = '<p style="padding:10px; color:#666;">No notes yet.</p>';
}
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, '&quot;');
const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtns = `
<button onclick="event.stopPropagation(); openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')" style="font-size:0.7rem; color:#007bff; border:none; background:none; cursor:pointer; text-decoration:underline;">Edit</button>
<button onclick="event.stopPropagation(); deleteNote(${note.id})" style="font-size:0.7rem; color:#dc3545; border:none; background:none; cursor:pointer; text-decoration:underline;">Delete</button>
`;
} else if (activePadIsPublic) {
actionBtns = `<button onclick="event.stopPropagation(); reportNote(${note.id})" style="font-size:0.7rem; color:orange; border:none; background:none; cursor:pointer; text-decoration:underline;">Report</button>`;
}
const dateStr = new Date(note.created_at).toLocaleString();
item.innerHTML = `
<div style="font-weight:bold;">${escapeHtml(note.title)}</div>
<div style="font-size:0.8rem; color:#666; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(note.content)}</div>
<div style="font-size:0.7rem; color:#888; margin-top:2px;">
${dateStr} <br> ${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
</div>
<div style="text-align:right; margin-top:4px;">${actionBtns}</div>
`;
item.addEventListener('click', () => {
stopTracking();
map.setView([note.lat, note.lng], 16);
});
sidebarBody.appendChild(item);
});
renderMapMarkers();
if (centerOnFirst && notes.length > 0) {
map.setView([notes[0].lat, notes[0].lng], 13);
}
} catch (err) {
console.error(err);
sidebarBody.innerHTML = '<p style="color:red; padding:10px;">Error loading notes.</p>';
}
}
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 = '<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");
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 = `
<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</h2>
</div>
`;
document.getElementById('btn-back-pads').addEventListener('click', loadNotepads);
}
sidebarBody.innerHTML = '';
const noteInfoDiv = document.createElement('div');
noteInfoDiv.style.padding = '10px';
const p = document.createElement('p');
p.appendChild(document.createTextNode('Viewing single note from '));
if (note.notepad.is_public) {
const link = document.createElement('a');
link.href = "#";
link.textContent = note.notepad.name;
link.style.fontWeight = 'bold';
link.onclick = (e) => {
e.preventDefault();
loadNotesForPad(note.notepad.id, note.notepad.name, true, note.owner_id, false, null, note.notepad.can_write);
};
p.appendChild(link);
} else {
const strong = document.createElement('strong');
strong.textContent = note.notepad.name;
p.appendChild(strong);
}
p.appendChild(document.createTextNode('.'));
noteInfoDiv.appendChild(p);
sidebarBody.appendChild(noteInfoDiv);
renderMapMarkers();
map.setView([note.lat, note.lng], 16);
if (window.innerWidth <= 600) {
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebar-toggle');
if (sidebar && !sidebar.classList.contains('collapsed')) {
sidebar.classList.add('collapsed');
if (toggleBtn) toggleBtn.innerHTML = '&lt;';
}
}
} catch (err) {
sidebarBody.innerHTML = '<p style="color:red; padding:10px;">Failed to load note. It may be private or deleted.</p>';
}
}
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, '&quot;');
const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtns = `<br><button onclick="openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')" class="btn-note-edit">✏️ Edit</button><button onclick="deleteNote(${note.id})" class="btn-note-delete">Delete</button>`;
} else if (isPublic) {
actionBtns = `<br><button onclick="reportNote(${note.id})" class="btn-note-report">Report</button>`;
}
const shareToken = note.share_token || '';
const shareBtn = `<button onclick="shareNote(${note.id}, '${shareToken}')" class="btn-note-share">🔗 Share</button>`;
const dateStr = new Date(note.created_at).toLocaleString();
if (showFull) {
const icon = L.divIcon({
className: '',
html: `<div class="map-note-card">
<div style="font-weight:bold; margin-bottom:4px;">${escapeHtml(note.title)}</div>
<div style="white-space: pre-wrap;">${escapeHtml(note.content)}</div>
<div style="font-size:0.7rem; color:#888; margin-top:4px;">${dateStr}</div>
${shareBtn} ${actionBtns}
</div>`,
iconSize: [200, 'auto'],
iconAnchor: [100, 0]
});
marker = L.marker([note.lat, note.lng], { icon: icon }).addTo(map);
} else {
marker = L.marker([note.lat, note.lng]).addTo(map);
marker.bindPopup(`<b>${escapeHtml(note.title)}</b><br><div style="white-space: pre-wrap;">${escapeHtml(note.content)}</div><span style="font-size:0.7rem; color:#888;">${dateStr}</span><br>${shareBtn} ${actionBtns}`);
}
const bumpZIndex = function() {
this.setZIndexOffset(topZIndex++);
};
marker.on('click', bumpZIndex);
marker.on('mousedown', bumpZIndex);
currentMarkers.push(marker);
});
}
window.displaySearchResults = function(notes, location, query) {
// Reset Pad State to "Search Mode"
activePadId = null;
activePadName = `Search: ${query}`;
activePadIsPublic = false; // Mixed results
activePadOwnerId = null; // Mixed results
activeNotes = notes;
// Update Sidebar 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;">Results: ${escapeHtml(query)}</h2>
</div>
`;
document.getElementById('btn-back-pads').addEventListener('click', loadNotepads);
}
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>';
renderMapMarkers();
return;
} else if (notes.length === 0 && location) {
sidebarBody.innerHTML = '<p style="padding:10px; color:#666;">Location found on map.</p>';
renderMapMarkers();
map.setView([location.lat, location.lng], 13);
return;
}
// Group notes by section and notepad
const sections = {
favorites: {}, // Map<padId, {pad, notes:[]}>
mine: {},
public: {}
};
notes.forEach(note => {
const pad = note.notepad;
// Helper to add to a section
const addToSection = (secKey) => {
if (!sections[secKey][pad.id]) {
sections[secKey][pad.id] = { pad: pad, notes: [] };
}
sections[secKey][pad.id].notes.push(note);
};
if (pad.is_favorite) addToSection('favorites');
if (currentUser && pad.owner_id === currentUser.id) addToSection('mine');
if (pad.is_public) addToSection('public');
});
// Helper function to render a collapsible section
const renderSection = (title, dataMap, isOpen) => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'list-section';
const arrow = isOpen ? '&#9662;' : '&#9656;';
const display = isOpen ? 'block' : 'none';
const headerBtn = document.createElement('button');
headerBtn.className = 'list-section-header';
headerBtn.innerHTML = `${title} ${arrow}`;
headerBtn.onclick = 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 = '<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;
// 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 ? '&hearts;' : '♡';
favButton = `<button onclick="event.stopPropagation(); toggleFavorite(${pad.id}, '${action}')" style="color:#dc3545; border:none; background:none; cursor:pointer; font-weight:bold; padding:0 5px;">${label}</button>`;
}
padItem.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-weight:bold;">${icon} ${escapeHtml(pad.name)}</div>
<div style="display: flex; align-items: center;">
${favButton}
</div>
</div>
`;
contentDiv.appendChild(padItem);
entry.notes.forEach(note => {
const item = document.createElement('div');
item.className = 'sidebar-note-item';
const dateStr = new Date(note.created_at).toLocaleString();
let actionBtn = ''; // Renaming to actionBtns for consistency
if (note.notepad && note.notepad.can_write) {
const escapedTitle = escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '&quot;');
const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtn = `<button onclick="event.stopPropagation(); openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')" style="font-size:0.7rem; color:#007bff; border:none; background:none; cursor:pointer; text-decoration:underline;">Edit</button> <button onclick="event.stopPropagation(); deleteNote(${note.id})" style="font-size:0.7rem; color:#dc3545; border:none; background:none; cursor:pointer; text-decoration:underline;">Delete</button>`;
} else if (note.is_public) {
actionBtn = `<button onclick="event.stopPropagation(); reportNote(${note.id})" style="font-size:0.7rem; color:orange; border:none; background:none; cursor:pointer; text-decoration:underline;">Report</button>`;
}
item.innerHTML = `
<div style="font-weight:bold;">${escapeHtml(note.title)}</div>
<div style="font-size:0.8rem; color:#666; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(note.content)}</div>
<div style="font-size:0.7rem; color:#888; margin-top:2px;">
${dateStr} <br> ${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
</div>
<div style="text-align:right; margin-top:4px;">${actionBtn}</div>
`;
item.addEventListener('click', () => {
stopTracking();
map.setView([note.lat, note.lng], 16);
});
contentDiv.appendChild(item);
});
});
}
sectionDiv.appendChild(headerBtn);
sectionDiv.appendChild(contentDiv);
sidebarBody.appendChild(sectionDiv);
};
if (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);
}
renderMapMarkers();
if (notes.length > 0) {
map.setView([notes[0].lat, notes[0].lng], 15);
} else if (location) {
map.setView([location.lat, location.lng], 13);
}
};
window.reportNote = async function(noteId) {
if (!confirm("Report this note? It will be hidden from public view.")) return;
try {
const res = await fetch(`/api/notes/${noteId}/report`, { method: 'POST' });
if (res.ok) {
alert("Note reported and hidden.");
// Refresh current pad
loadNotesForPad(activePadId, activePadName, activePadIsPublic);
}
} catch (err) {
console.error("Error reporting note:", err);
}
};
window.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) {
loadNotepads(); // Refresh list
} else {
alert("Failed to delete notepad.");
}
} catch (err) { console.error(err); }
};
window.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 = '<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>${escapeHtml(collab.email)} (${collab.permission})</span>
<button onclick="removeCollaborator(${padId}, ${collab.user_id})">Remove</button>
`;
listDiv.appendChild(item);
});
}
} catch (err) {
listDiv.innerHTML = `<p style="color:red;">${err.message}</p>`;
}
}
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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('&#9656;', '&#9662;') : event.target.innerHTML.replace('&#9662;', '&#9656;');
}
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); }
};