1024 lines
43 KiB
JavaScript
1024 lines
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: '© <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 = '<'; // 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 = '<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 ▾</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 ▾</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 ▾</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 ▸</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 ▾';
|
||
|
|
|
||
|
|
// 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 ▸';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
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 ? '♥' : '♡';
|
||
|
|
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;">×</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;">←</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, '"');
|
||
|
|
const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').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;">←</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 = '<';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
} 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, '"');
|
||
|
|
const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').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;">←</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 ? '▾' : '▸';
|
||
|
|
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 ? '♥' : '♡';
|
||
|
|
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, '"');
|
||
|
|
const escapedContent = escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '"').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, "&")
|
||
|
|
.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); }
|
||
|
|
};
|