2026-06-02 03:49:38 +00:00
window . SpotNotes = window . SpotNotes || { } ;
// Globals
window . SpotNotes . map = null ;
window . SpotNotes . activeNotes = [ ] ;
window . SpotNotes . userLocation = null ;
window . SpotNotes . activePadName = "" ;
window . SpotNotes . activePadId = null ;
window . SpotNotes . activePadIsPublic = false ;
window . SpotNotes . activePadOwnerId = null ;
window . SpotNotes . activePadCanWrite = false ;
window . SpotNotes . currentUser = null ;
window . SpotNotes . isTracking = false ;
window . SpotNotes . userMarker = null ;
window . SpotNotes . notifiedNoteIds = new Set ( ) ;
window . SpotNotes . isLoggedIn = false ;
window . SpotNotes . currentMarkers = [ ] ;
window . SpotNotes . topZIndex = 1000000 ;
2026-05-12 23:58:18 +00:00
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
2026-06-02 03:49:38 +00:00
window . SpotNotes . initMap ( ) ;
window . SpotNotes . initSidebar ( ) ;
window . SpotNotes . loadNotepads ( true ) ;
2026-05-12 23:58:18 +00:00
const form = document . getElementById ( 'form-add-collaborator' ) ;
if ( form ) {
form . addEventListener ( 'submit' , async ( e ) => {
e . preventDefault ( ) ;
const padId = document . getElementById ( 'share-pad-id' ) . value ;
const email = document . getElementById ( 'collaborator-email' ) . value ;
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 ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . openShareSettingsModal ( padId , padName ) ;
2026-05-12 23:58:18 +00:00
} else {
const err = await res . json ( ) ;
alert ( ` Failed to add collaborator: ${ err . error } ` ) ;
}
} catch ( err ) {
console . error ( err ) ;
alert ( 'An error occurred.' ) ;
}
} ) ;
}
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 ;
2026-06-02 03:49:38 +00:00
const color = document . getElementById ( 'pad-color-edit' ) . value ;
2026-05-12 23:58:18 +00:00
try {
const res = await fetch ( ` /api/notepads/ ${ padId } ` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-06-02 03:49:38 +00:00
body : JSON . stringify ( { name , is _public : isPublic , color } )
2026-05-12 23:58:18 +00:00
} ) ;
if ( res . ok ) {
document . getElementById ( 'modal-edit-pad' ) . classList . add ( 'hidden' ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotepads ( ) ; // Refresh everything
2026-05-12 23:58:18 +00:00
} 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
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . activePadId ) {
window . SpotNotes . loadNotesForPad ( window . SpotNotes . activePadId ) ;
2026-05-12 23:58:18 +00:00
} 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.' ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotepads ( ) ;
2026-05-12 23:58:18 +00:00
}
} else {
alert ( 'Failed to update note.' ) ;
}
} catch ( err ) { console . error ( err ) ; }
} ) ;
}
} ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotepads = async function ( isInitialLoad = false ) {
2026-05-12 23:58:18 +00:00
// Reset state
2026-06-02 03:49:38 +00:00
window . SpotNotes . activeNotes = [ ] ;
window . SpotNotes . currentMarkers . forEach ( marker => window . SpotNotes . map . removeLayer ( marker ) ) ;
window . SpotNotes . currentMarkers = [ ] ;
2026-05-12 23:58:18 +00:00
// Check auth first
try {
const authRes = await fetch ( '/auth/me' ) ;
const authData = await authRes . json ( ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . isLoggedIn = authData . authenticated ;
window . SpotNotes . currentUser = authData . user || null ;
2026-05-12 23:58:18 +00:00
} catch ( e ) { console . error ( e ) ; }
// Reset Sidebar Header
const header = document . querySelector ( '.sidebar-header' ) ;
if ( header ) {
2026-06-23 19:23:45 +00:00
let headerHTML = ` <div style="display:flex; justify-content:space-between; align-items:center; width:100%;">
< h2 > Notepads < / h 2 > ` ;
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . isLoggedIn ) {
2026-05-12 23:58:18 +00:00
headerHTML += '<button class="btn-new-pad" id="btn-create-pad" title="Create New Pad">+</button>' ;
}
2026-06-23 19:23:45 +00:00
headerHTML += '</div>' ;
2026-05-12 23:58:18 +00:00
header . innerHTML = headerHTML ;
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . isLoggedIn ) {
2026-05-12 23:58:18 +00:00
document . getElementById ( 'btn-create-pad' ) . addEventListener ( 'click' , ( ) => {
document . getElementById ( 'modal-create-pad' ) . classList . remove ( 'hidden' ) ;
} ) ;
}
}
const sidebarBody = document . getElementById ( 'sidebar-body' ) ;
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . isLoggedIn ) {
2026-05-12 23:58:18 +00:00
sidebarBody . innerHTML = `
< div class = "list-section" id = "favorite-notepads-section" >
< button class = "list-section-header" > Favorite Notepads & # 9662 ; < / b u t t o n >
< div class = "list-section-content" id = "favorite-notepads-list" >
< p style = "padding:10px; color:#666;" > Loading ... < / p >
< / d i v >
< / d i v >
< div class = "list-section" id = "my-notepads-section" >
< button class = "list-section-header" > My Notepads & # 9662 ; < / b u t t o n >
< div class = "list-section-content" id = "my-notepads-list" > < p style = "padding:10px; color:#666;" > Loading ... < / p > < / d i v >
< / d i v >
< div class = "list-section" id = "shared-notepads-section" >
< button class = "list-section-header" > Shared With Me & # 9662 ; < / b u t t o n >
< div class = "list-section-content" id = "shared-notepads-list" > < p style = "padding:10px; color:#666;" > Loading ... < / p > < / d i v >
< / d i v >
< div class = "list-section" id = "public-notepads-section" >
< button id = "btn-toggle-public" class = "btn-toggle-section" > Public Notepads & # 9656 ; < / b u t t o n >
< div id = "public-notepads-list" style = "display:none;" > < / d i v >
< / d i v >
` ;
const listSection = document . querySelectorAll ( '.list-section-header' ) ;
listSection . forEach ( header => {
2026-06-02 03:49:38 +00:00
header . addEventListener ( 'click' , window . SpotNotes . toggleSection )
2026-05-12 23:58:18 +00:00
} )
2026-06-02 03:49:38 +00:00
window . SpotNotes . setupPublicToggle ( ) ;
2026-05-12 23:58:18 +00:00
try {
2026-06-02 03:49:38 +00:00
await window . SpotNotes . loadNotepadsOfType ( 'favorites' , 'favorite-notepads-list' ) ;
await window . SpotNotes . loadNotepadsOfType ( 'shared' , 'shared-notepads-list' ) ;
2026-05-12 23:58:18 +00:00
const res = await fetch ( '/api/notepads?type=mine' ) ;
const notepads = await res . json ( ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderNotepadList ( notepads , 'my-notepads-list' ) ;
2026-05-12 23:58:18 +00:00
} catch ( err ) {
console . error ( "Failed to load notepads" , err ) ;
document . getElementById ( 'my-notepads-list' ) . innerHTML = '<p style="color:red; padding:10px;">Error loading data.</p>' ;
}
} else {
// Logged out: Only Public Notepads
sidebarBody . innerHTML = `
< div id = "public-notepads-section" >
< h3 class = "list-section-header" > Public Notepads < / h 3 >
< div id = "public-notepads-list" > < p style = "padding:10px; color:#666;" > Loading ... < / p > < / d i v >
< / d i v >
` ;
try {
const res = await fetch ( '/api/notepads?type=public' ) ;
const pads = await res . json ( ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderNotepadList ( pads , 'public-notepads-list' ) ;
2026-05-12 23:58:18 +00:00
} 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' ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotesForPad ( params . get ( 'pad' ) , undefined , undefined , undefined , true , token ) ;
2026-05-12 23:58:18 +00:00
} else if ( params . has ( 'note' ) ) {
const token = params . get ( 'token' ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadSingleNote ( params . get ( 'note' ) , token ) ;
} else if ( window . SpotNotes . isLoggedIn ) {
window . SpotNotes . loadAllMyNotes ( ) ; // Default action for logged in users
2026-05-12 23:58:18 +00:00
} 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 ) {
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotesForPad ( globalPad . id , globalPad . name , true , globalPad . owner _id ) ;
2026-05-12 23:58:18 +00:00
}
} catch ( err ) { }
}
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . toggleFavorite = async function ( padId , action ) {
2026-05-12 23:58:18 +00:00
try {
const res = await fetch ( ` /api/notepads/ ${ padId } / ${ action } ` , { method : 'POST' } ) ;
if ( res . ok ) {
// Refresh the lists
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotepadsOfType ( 'favorites' , 'favorite-notepads-list' ) ;
window . SpotNotes . loadNotepadsOfType ( 'mine' , 'my-notepads-list' ) ;
window . SpotNotes . loadNotepadsOfType ( 'public' , 'public-notepads-list' ) ;
2026-05-12 23:58:18 +00:00
} else {
alert ( ` Failed to ${ action } notepad ` ) ;
}
} catch ( err ) {
console . error ( err ) ;
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotepadsOfType = async function ( type , containerId ) {
2026-05-12 23:58:18 +00:00
try {
const res = await fetch ( ` /api/notepads?type= ${ type } ` ) ;
const notepads = await res . json ( ) ;
const isFavorite = type === 'favorites' ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderNotepadList ( notepads , containerId , isFavorite ) ;
2026-05-12 23:58:18 +00:00
} catch ( err ) {
console . error ( ` Failed to load ${ type } notepads ` , err ) ;
document . getElementById ( containerId ) . innerHTML = '<p style="color:red; padding:10px;">Error loading data.</p>' ;
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotesForPad = async function ( padId , padName , isPublic , ownerId , centerOnFirst = false , token = null , canWrite = undefined ) {
window . SpotNotes . activePadId = padId ;
2026-05-12 23:58:18 +00:00
// 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 ( ) ;
2026-06-02 03:49:38 +00:00
padName = pad . name ; isPublic = pad . is _public ; ownerId = pad . owner _id ; window . SpotNotes . activePadCanWrite = pad . can _write ;
} catch ( e ) { console . error ( e ) ; window . SpotNotes . activePadCanWrite = false ; }
2026-05-12 23:58:18 +00:00
}
2026-06-02 03:49:38 +00:00
if ( padName !== undefined ) window . SpotNotes . activePadName = padName ;
if ( isPublic !== undefined ) window . SpotNotes . activePadIsPublic = isPublic ;
if ( ownerId !== undefined ) window . SpotNotes . activePadOwnerId = ownerId ;
2026-05-12 23:58:18 +00:00
if ( canWrite !== undefined ) {
2026-06-02 03:49:38 +00:00
window . SpotNotes . activePadCanWrite = canWrite ;
2026-05-12 23:58:18 +00:00
} else if ( padName !== undefined ) {
// Fallback if not passed: assume true only if owner
2026-06-02 03:49:38 +00:00
window . SpotNotes . activePadCanWrite = ( window . SpotNotes . currentUser && ownerId === window . SpotNotes . currentUser . id ) ;
2026-05-12 23:58:18 +00:00
}
2026-06-02 03:49:38 +00:00
const currentName = window . SpotNotes . activePadName ;
const isOwner = window . SpotNotes . currentUser && window . SpotNotes . activePadOwnerId === window . SpotNotes . currentUser . id ;
2026-05-12 23:58:18 +00:00
// Clear existing markers
2026-06-02 03:49:38 +00:00
window . SpotNotes . currentMarkers . forEach ( marker => window . SpotNotes . map . removeLayer ( marker ) ) ;
window . SpotNotes . currentMarkers = [ ] ;
window . SpotNotes . activeNotes = [ ] ;
2026-05-12 23:58:18 +00:00
// Update Sidebar Header
const header = document . querySelector ( '.sidebar-header' ) ;
if ( header ) {
2026-06-02 03:49:38 +00:00
const addNoteBtn = window . SpotNotes . activePadCanWrite ? ` <button onclick="openCreateNoteModal( ${ padId } )" class="btn-add-note">+ Add Note</button> ` : '' ;
2026-06-23 19:23:45 +00:00
const manageShareBtn = isOwner ? ` <button class="btn-action-icon" onclick="window.SpotNotes.openShareSettingsModal( ${ padId } , ' ${ window . SpotNotes . escapeHtml ( currentName ) } ')" title="Manage Sharing">⚙️</button> ` : '' ;
const exportBtn = ` <button class="btn-action-icon" onclick="window.location.href='/api/notepads/ ${ padId } /export ${ token ? '?token=' + token : '' } '" title="Export to CSV">⬇️</button> ` ;
const shareBtn = ` <button class="btn-action-icon" onclick="window.SpotNotes.shareNotepad( ${ padId } , ' ${ token || '' } ')" title="Share Notepad">🔗</button> ` ;
2026-05-12 23:58:18 +00:00
header . innerHTML = `
2026-06-23 19:23:45 +00:00
< div class = "sidebar-header-top" style = "display:flex; align-items:center; justify-content:space-between; width:100%;" >
< div style = "display:flex; align-items:center; gap:8px; min-width:0;" >
< button id = "btn-back-pads" class = "btn-action-icon" style = "font-size:1.25rem; margin-right:4px;" > & larr ; < / b u t t o n >
< h2 style = "font-size:1.15rem; margin:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" title = "${window.SpotNotes.escapeHtml(currentName)}" > $ { window . SpotNotes . escapeHtml ( currentName ) } < / h 2 >
< / d i v >
< div class = "notepad-actions" style = "display:flex; align-items:center; gap:4px; flex-shrink:0;" >
$ { shareBtn }
$ { manageShareBtn }
$ { exportBtn }
< / d i v >
< / d i v >
< div style = "margin-top: 12px; width:100%;" >
< input type = "text" id = "filter-active-notes" placeholder = "Filter notes..." style = "width: 100%; padding: 8px 12px; box-sizing: border-box; border: 1px solid var(--border); border-radius: var(--radius-md); outline:none; transition:var(--transition); font-family:var(--font-sans); font-size:0.9rem; background-color:var(--bg-page);" >
2026-05-12 23:58:18 +00:00
< / d i v >
2026-06-23 19:23:45 +00:00
< div style = "margin-top: 10px; width:100%; display:flex; justify-content:space-between; align-items:center;" >
$ { addNoteBtn }
2026-06-02 03:49:38 +00:00
< / d i v >
2026-05-12 23:58:18 +00:00
` ;
2026-06-02 03:49:38 +00:00
document . getElementById ( 'btn-back-pads' ) . addEventListener ( 'click' , window . SpotNotes . loadNotepads ) ;
2026-05-12 23:58:18 +00:00
}
const sidebarBody = document . getElementById ( 'sidebar-body' ) ;
sidebarBody . innerHTML = '<p style="padding:10px;">Loading notes...</p>' ;
try {
const res = await fetch ( ` /api/notepads/ ${ padId } /notes?token= ${ token || '' } ` ) ;
const notes = await res . json ( ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . activeNotes = notes ;
2026-05-12 23:58:18 +00:00
// Render Sidebar List
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderActiveNotesList ( ) ;
window . SpotNotes . renderMapMarkers ( ) ;
2026-05-12 23:58:18 +00:00
2026-06-02 03:49:38 +00:00
const filterInput = document . getElementById ( 'filter-active-notes' ) ;
if ( filterInput ) {
filterInput . addEventListener ( 'keyup' , ( e ) => {
window . SpotNotes . renderActiveNotesList ( e . target . value ) ;
2026-05-12 23:58:18 +00:00
} ) ;
2026-06-02 03:49:38 +00:00
}
2026-05-12 23:58:18 +00:00
if ( centerOnFirst && notes . length > 0 ) {
2026-06-02 03:49:38 +00:00
window . SpotNotes . map . setView ( [ notes [ 0 ] . lat , notes [ 0 ] . lng ] , 13 ) ;
2026-05-12 23:58:18 +00:00
}
} catch ( err ) {
console . error ( err ) ;
sidebarBody . innerHTML = '<p style="color:red; padding:10px;">Error loading notes.</p>' ;
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadSingleNote = async function ( noteId , token = null ) {
2026-05-12 23:58:18 +00:00
// Reset state
2026-06-02 03:49:38 +00:00
window . SpotNotes . activeNotes = [ ] ;
window . SpotNotes . currentMarkers . forEach ( marker => window . SpotNotes . map . removeLayer ( marker ) ) ;
window . SpotNotes . currentMarkers = [ ] ;
window . SpotNotes . activePadId = null ;
2026-05-12 23:58:18 +00:00
const sidebarBody = document . getElementById ( 'sidebar-body' ) ;
sidebarBody . innerHTML = '<p style="padding:10px;">Loading note...</p>' ;
try {
const res = await fetch ( ` /api/notes/ ${ noteId } ?token= ${ token || '' } ` ) ;
if ( ! res . ok ) throw new Error ( "Note not found or private" ) ;
const note = await res . json ( ) ;
2026-06-02 03:49:38 +00:00
// Set active notes to just this one so window.SpotNotes.renderMapMarkers works
window . SpotNotes . activeNotes = [ note ] ;
2026-05-12 23:58:18 +00:00
// Update Header
const header = document . querySelector ( '.sidebar-header' ) ;
if ( header ) {
header . innerHTML = `
< div style = "display:flex; align-items:center;" >
< button id = "btn-back-pads" style = "background:none; border:none; font-size:1.5rem; cursor:pointer; margin-right:10px;" > & larr ; < / b u t t o n >
< h2 style = "font-size:1.2rem; margin:0;" > Shared Note < / h 2 >
< / d i v >
` ;
2026-06-02 03:49:38 +00:00
document . getElementById ( 'btn-back-pads' ) . addEventListener ( 'click' , window . SpotNotes . loadNotepads ) ;
2026-05-12 23:58:18 +00:00
}
sidebarBody . innerHTML = '' ;
const noteInfoDiv = document . createElement ( 'div' ) ;
noteInfoDiv . style . padding = '10px' ;
const p = document . createElement ( 'p' ) ;
p . appendChild ( document . createTextNode ( 'Viewing single note from ' ) ) ;
if ( note . notepad . is _public ) {
const link = document . createElement ( 'a' ) ;
link . href = "#" ;
link . textContent = note . notepad . name ;
link . style . fontWeight = 'bold' ;
link . onclick = ( e ) => {
e . preventDefault ( ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotesForPad ( note . notepad . id , note . notepad . name , true , note . owner _id , false , null , note . notepad . can _write ) ;
2026-05-12 23:58:18 +00:00
} ;
p . appendChild ( link ) ;
} else {
const strong = document . createElement ( 'strong' ) ;
strong . textContent = note . notepad . name ;
p . appendChild ( strong ) ;
}
p . appendChild ( document . createTextNode ( '.' ) ) ;
noteInfoDiv . appendChild ( p ) ;
sidebarBody . appendChild ( noteInfoDiv ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderMapMarkers ( ) ;
window . SpotNotes . map . setView ( [ note . lat , note . lng ] , 16 ) ;
2026-05-12 23:58:18 +00:00
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>' ;
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . displaySearchResults = function ( notes , location , query ) {
2026-05-12 23:58:18 +00:00
// Reset Pad State to "Search Mode"
2026-06-02 03:49:38 +00:00
window . SpotNotes . activePadId = null ;
window . SpotNotes . activePadName = ` Search: ${ query } ` ;
window . SpotNotes . activePadIsPublic = false ; // Mixed results
window . SpotNotes . activePadOwnerId = null ; // Mixed results
window . SpotNotes . activeNotes = notes ;
2026-05-12 23:58:18 +00:00
// 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 ; < / b u t t o n >
2026-06-02 03:49:38 +00:00
< h2 style = "font-size:1.2rem; margin:0;" > Results : $ { window . SpotNotes . escapeHtml ( query ) } < / h 2 >
2026-05-12 23:58:18 +00:00
< / d i v >
` ;
2026-06-02 03:49:38 +00:00
document . getElementById ( 'btn-back-pads' ) . addEventListener ( 'click' , window . SpotNotes . loadNotepads ) ;
2026-05-12 23:58:18 +00:00
}
const sidebarBody = document . getElementById ( 'sidebar-body' ) ;
sidebarBody . innerHTML = '' ;
if ( notes . length === 0 && ! location ) {
sidebarBody . innerHTML = '<p style="padding:10px; color:#666;">No results found.</p>' ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderMapMarkers ( ) ;
2026-05-12 23:58:18 +00:00
return ;
} else if ( notes . length === 0 && location ) {
2026-06-02 03:49:38 +00:00
sidebarBody . innerHTML = '<p style="padding:10px; color:#666;">Location found on window.SpotNotes.map.</p>' ;
window . SpotNotes . renderMapMarkers ( ) ;
window . SpotNotes . map . setView ( [ location . lat , location . lng ] , 13 ) ;
2026-05-12 23:58:18 +00:00
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' ) ;
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . currentUser && pad . owner _id === window . SpotNotes . currentUser . id ) addToSection ( 'mine' ) ;
2026-05-12 23:58:18 +00:00
if ( pad . is _public ) addToSection ( 'public' ) ;
} ) ;
// 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 } ` ;
2026-06-02 03:49:38 +00:00
headerBtn . onclick = window . SpotNotes . toggleSection ;
2026-05-12 23:58:18 +00:00
const contentDiv = document . createElement ( 'div' ) ;
contentDiv . className = 'list-section-content' ;
contentDiv . style . display = display ;
const padIds = Object . keys ( dataMap ) ;
if ( padIds . length === 0 ) {
contentDiv . innerHTML = '<p style="padding:10px; color:#999; font-style:italic; font-size:0.8rem;">No matching notes.</p>' ;
} else {
padIds . forEach ( padId => {
const entry = dataMap [ padId ] ;
const pad = entry . pad ;
// 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 = '' ;
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . isLoggedIn ) {
2026-05-12 23:58:18 +00:00
const isFavorite = pad . is _favorite ;
const action = isFavorite ? 'unfavorite' : 'favorite' ;
const label = isFavorite ? '♥' : '♡' ;
2026-06-02 03:49:38 +00:00
favButton = ` <button onclick="event.stopPropagation(); window.SpotNotes.toggleFavorite( ${ pad . id } , ' ${ action } ')" style="color:#dc3545; border:none; background:none; cursor:pointer; font-weight:bold; padding:0 5px;"> ${ label } </button> ` ;
2026-05-12 23:58:18 +00:00
}
padItem . innerHTML = `
< div style = "display:flex; justify-content:space-between; align-items:center;" >
2026-06-02 03:49:38 +00:00
< div style = "font-weight:bold;" > $ { icon } $ { window . SpotNotes . escapeHtml ( pad . name ) } < / d i v >
2026-05-12 23:58:18 +00:00
< div style = "display: flex; align-items: center;" >
$ { favButton }
< / d i v >
< / d i v >
` ;
contentDiv . appendChild ( padItem ) ;
entry . notes . forEach ( note => {
const item = document . createElement ( 'div' ) ;
2026-06-23 19:23:45 +00:00
item . className = 'note-card' ;
2026-05-12 23:58:18 +00:00
const dateStr = new Date ( note . created _at ) . toLocaleString ( ) ;
2026-06-23 19:23:45 +00:00
let actionBtn = '' ;
2026-05-12 23:58:18 +00:00
if ( note . notepad && note . notepad . can _write ) {
2026-06-02 03:49:38 +00:00
const escapedTitle = window . SpotNotes . escapeHtml ( note . title ) . replace ( /'/g , "\\'" ) . replace ( /"/g , '"' ) ;
const escapedContent = window . SpotNotes . escapeHtml ( note . content ) . replace ( /'/g , "\\'" ) . replace ( /"/g , '"' ) . replace ( /\n/g , '\\n' ) ;
2026-06-23 19:23:45 +00:00
actionBtn = ` <button class="note-card-link edit" onclick="event.stopPropagation(); openEditNoteModal( ${ note . id } , ' ${ escapedTitle } ', ' ${ escapedContent } ')">Edit</button> <button class="note-card-link delete" onclick="event.stopPropagation(); window.SpotNotes.deleteNote( ${ note . id } )">Delete</button> ` ;
2026-05-12 23:58:18 +00:00
} else if ( note . is _public ) {
2026-06-23 19:23:45 +00:00
actionBtn = ` <button class="note-card-link report" onclick="event.stopPropagation(); window.SpotNotes.reportNote( ${ note . id } )">Report</button> ` ;
2026-05-12 23:58:18 +00:00
}
2026-06-23 19:23:45 +00:00
const shareToken = note . share _token || '' ;
const shareBtn = ` <button class="note-card-link share" onclick="event.stopPropagation(); window.SpotNotes.shareNote( ${ note . id } , ' ${ shareToken } ')">Share</button> ` ;
2026-05-12 23:58:18 +00:00
item . innerHTML = `
2026-06-23 19:23:45 +00:00
< div class = "note-card-title" > $ { window . SpotNotes . escapeHtml ( note . title ) } < / d i v >
< div class = "note-card-body" > $ { window . SpotNotes . escapeHtml ( note . content ) } < / d i v >
< div class = "note-card-meta" >
2026-05-12 23:58:18 +00:00
$ { dateStr } < br > $ { note . lat . toFixed ( 5 ) } , $ { note . lng . toFixed ( 5 ) }
< / d i v >
2026-06-23 19:23:45 +00:00
< div class = "note-card-actions" >
< a href = "https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target = "_blank" onclick = "event.stopPropagation();" class = "note-card-link directions" > Directions < / a >
$ { shareBtn }
$ { actionBtn }
< / d i v >
2026-05-12 23:58:18 +00:00
` ;
item . addEventListener ( 'click' , ( ) => {
2026-06-02 03:49:38 +00:00
window . SpotNotes . stopTracking ( ) ;
window . SpotNotes . map . setView ( [ note . lat , note . lng ] , 16 ) ;
2026-05-12 23:58:18 +00:00
} ) ;
contentDiv . appendChild ( item ) ;
} ) ;
} ) ;
}
sectionDiv . appendChild ( headerBtn ) ;
sectionDiv . appendChild ( contentDiv ) ;
sidebarBody . appendChild ( sectionDiv ) ;
} ;
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . isLoggedIn ) {
2026-05-12 23:58:18 +00:00
if ( Object . keys ( sections . favorites ) . length > 0 ) renderSection ( 'Favorite Notepads' , sections . favorites , true ) ;
if ( Object . keys ( sections . mine ) . length > 0 ) renderSection ( 'My Notepads' , sections . mine , true ) ;
if ( Object . keys ( sections . public ) . length > 0 ) renderSection ( 'Public Notepads' , sections . public , true ) ;
} else {
if ( Object . keys ( sections . public ) . length > 0 ) renderSection ( 'Public Notepads' , sections . public , true ) ;
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderMapMarkers ( ) ;
2026-05-12 23:58:18 +00:00
if ( notes . length > 0 ) {
2026-06-02 03:49:38 +00:00
window . SpotNotes . map . setView ( [ notes [ 0 ] . lat , notes [ 0 ] . lng ] , 15 ) ;
2026-05-12 23:58:18 +00:00
} else if ( location ) {
2026-06-02 03:49:38 +00:00
window . SpotNotes . map . setView ( [ location . lat , location . lng ] , 13 ) ;
2026-05-12 23:58:18 +00:00
}
} ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . reportNote = async function ( noteId ) {
2026-05-12 23:58:18 +00:00
if ( ! confirm ( "Report this note? It will be hidden from public view." ) ) return ;
try {
const res = await fetch ( ` /api/notes/ ${ noteId } /report ` , { method : 'POST' } ) ;
if ( res . ok ) {
alert ( "Note reported and hidden." ) ;
// Refresh current pad
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotesForPad ( window . SpotNotes . activePadId , window . SpotNotes . activePadName , window . SpotNotes . activePadIsPublic ) ;
2026-05-12 23:58:18 +00:00
}
} catch ( err ) {
console . error ( "Error reporting note:" , err ) ;
}
} ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . deleteNotepad = async function ( padId ) {
2026-05-12 23:58:18 +00:00
if ( ! confirm ( "Are you sure you want to delete this notepad? All notes inside will be lost." ) ) return ;
try {
const res = await fetch ( ` /api/notepads/ ${ padId } ` , { method : 'DELETE' } ) ;
if ( res . ok ) {
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotepads ( ) ; // Refresh list
2026-05-12 23:58:18 +00:00
} else {
alert ( "Failed to delete notepad." ) ;
}
} catch ( err ) { console . error ( err ) ; }
} ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . openShareSettingsModal = async function ( padId , padName ) {
2026-05-12 23:58:18 +00:00
const modal = document . getElementById ( 'modal-share-settings' ) ;
document . getElementById ( 'share-pad-name' ) . textContent = padName ;
document . getElementById ( 'share-pad-id' ) . value = padId ;
const listDiv = document . getElementById ( 'collaborators-list' ) ;
listDiv . innerHTML = '<p>Loading...</p>' ;
modal . classList . remove ( 'hidden' ) ;
try {
const res = await fetch ( ` /api/notepads/ ${ padId } /collaborators ` ) ;
if ( ! res . ok ) {
const err = await res . json ( ) ;
throw new Error ( err . error || 'Failed to load collaborators' ) ;
}
const collaborators = await res . json ( ) ;
listDiv . innerHTML = '' ;
if ( collaborators . length === 0 ) {
listDiv . innerHTML = '<p style="color:#888; font-style:italic;">No collaborators yet.</p>' ;
} else {
collaborators . forEach ( collab => {
const item = document . createElement ( 'div' ) ;
item . className = 'collaborator-item' ;
item . innerHTML = `
2026-06-02 03:49:38 +00:00
< span > $ { window . SpotNotes . escapeHtml ( collab . email ) } ( $ { collab . permission } ) < / s p a n >
< button onclick = "window.SpotNotes.removeCollaborator(${padId}, ${collab.user_id})" > Remove < / b u t t o n >
2026-05-12 23:58:18 +00:00
` ;
listDiv . appendChild ( item ) ;
} ) ;
}
} catch ( err ) {
listDiv . innerHTML = ` <p style="color:red;"> ${ err . message } </p> ` ;
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . removeCollaborator = async function ( padId , userId ) {
2026-05-12 23:58:18 +00:00
if ( ! confirm ( "Are you sure you want to remove this collaborator?" ) ) return ;
try {
const res = await fetch ( ` /api/notepads/ ${ padId } /collaborators/ ${ userId } ` , { method : 'DELETE' } ) ;
if ( res . ok ) {
const padName = document . getElementById ( 'share-pad-name' ) . textContent ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . openShareSettingsModal ( padId , padName ) ;
2026-05-12 23:58:18 +00:00
} else {
const err = await res . json ( ) ;
alert ( ` Failed to remove collaborator: ${ err . error } ` ) ;
}
} catch ( err ) {
console . error ( err ) ;
alert ( 'An error occurred.' ) ;
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadAllMyNotes = async function ( ) {
window . SpotNotes . activePadId = null ;
window . SpotNotes . activePadName = "All My Notes" ;
window . SpotNotes . activePadIsPublic = false ;
window . SpotNotes . activePadCanWrite = true ;
if ( window . SpotNotes . currentUser ) {
window . SpotNotes . activePadOwnerId = window . SpotNotes . currentUser . id ;
2026-05-12 23:58:18 +00:00
}
// Clear existing markers
2026-06-02 03:49:38 +00:00
window . SpotNotes . currentMarkers . forEach ( marker => window . SpotNotes . map . removeLayer ( marker ) ) ;
window . SpotNotes . currentMarkers = [ ] ;
window . SpotNotes . activeNotes = [ ] ;
2026-05-12 23:58:18 +00:00
try {
const res = await fetch ( '/api/notes/mine' ) ;
if ( ! res . ok ) throw new Error ( "Failed to load" ) ;
const notes = await res . json ( ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . activeNotes = notes ;
2026-05-12 23:58:18 +00:00
2026-06-02 03:49:38 +00:00
window . SpotNotes . renderMapMarkers ( ) ;
2026-05-12 23:58:18 +00:00
if ( notes . length > 0 ) {
2026-06-02 03:49:38 +00:00
// Auto-center window.SpotNotes.map bounds cleanly around all the user's notes
2026-05-12 23:58:18 +00:00
const bounds = L . latLngBounds ( notes . map ( n => [ n . lat , n . lng ] ) ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . map . fitBounds ( bounds , { padding : [ 50 , 50 ] , maxZoom : 15 } ) ;
2026-05-12 23:58:18 +00:00
}
} catch ( err ) {
console . error ( "Error loading all my notes:" , err ) ;
}
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . shareNotepad = function ( padId , token ) {
2026-05-12 23:58:18 +00:00
let url = ` ${ window . location . origin } /?pad= ${ padId } ` ;
if ( token ) url += ` &token= ${ token } ` ;
navigator . clipboard . writeText ( url ) . then ( ( ) => {
alert ( "Notepad link copied to clipboard!" ) ;
} ) ;
} ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . shareNote = function ( noteId , token ) {
2026-05-12 23:58:18 +00:00
let url = ` ${ window . location . origin } /?note= ${ noteId } ` ;
if ( token ) url += ` &token= ${ token } ` ;
navigator . clipboard . writeText ( url ) . then ( ( ) => {
alert ( "Note link copied to clipboard!" ) ;
} ) ;
} ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . escapeHtml = function ( text ) {
2026-05-12 23:58:18 +00:00
if ( text === null || text === undefined ) return '' ;
return String ( text )
. replace ( /&/g , "&" )
. replace ( /</g , "<" )
. replace ( />/g , ">" )
. replace ( /"/g , """ )
. replace ( /'/g , "'" ) ;
}
2026-06-02 03:49:38 +00:00
window . SpotNotes . deleteNote = async function ( noteId ) {
2026-05-12 23:58:18 +00:00
if ( ! confirm ( "Delete this note?" ) ) return ;
try {
const res = await fetch ( ` /api/notes/ ${ noteId } ` , { method : 'DELETE' } ) ;
if ( res . ok ) {
// If we are in a pad view, reload the pad.
2026-06-02 03:49:38 +00:00
if ( window . SpotNotes . activePadId ) {
window . SpotNotes . loadNotesForPad ( window . SpotNotes . activePadId ) ;
2026-05-12 23:58:18 +00:00
} else {
// Otherwise (e.g., search results), the safest refresh is to return to the main list.
alert ( "Note deleted. Returning to notepad list." ) ;
2026-06-02 03:49:38 +00:00
window . SpotNotes . loadNotepads ( ) ;
2026-05-12 23:58:18 +00:00
}
} else {
alert ( "Failed to delete note." ) ;
}
} catch ( err ) { console . error ( err ) ; }
2026-06-02 03:49:38 +00:00
} ;
window . SpotNotes . renderActiveNotesList = function ( filterText = '' ) {
const sidebarBody = document . getElementById ( 'sidebar-body' ) ;
if ( ! sidebarBody ) return ;
sidebarBody . innerHTML = '' ;
const filterLower = filterText . toLowerCase ( ) ;
const filteredNotes = window . SpotNotes . activeNotes . filter ( note => {
return note . title . toLowerCase ( ) . includes ( filterLower ) || note . content . toLowerCase ( ) . includes ( filterLower ) ;
} ) ;
if ( filteredNotes . length === 0 ) {
sidebarBody . innerHTML = '<p style="padding:10px; color:#666;">No notes match.</p>' ;
}
filteredNotes . forEach ( note => {
const item = document . createElement ( 'div' ) ;
2026-06-23 19:23:45 +00:00
item . className = 'note-card' ;
2026-06-02 03:49:38 +00:00
let actionBtns = '' ;
if ( window . SpotNotes . activePadCanWrite ) {
const escapedTitle = window . SpotNotes . escapeHtml ( note . title ) . replace ( /'/g , "\\'" ) . replace ( /"/g , '"' ) ;
const escapedContent = window . SpotNotes . escapeHtml ( note . content ) . replace ( /'/g , "\\'" ) . replace ( /"/g , '"' ) . replace ( /\n/g , '\\n' ) ;
actionBtns = `
2026-06-23 19:23:45 +00:00
< button class = "note-card-link edit" onclick = "event.stopPropagation(); openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')" > Edit < / b u t t o n >
< button class = "note-card-link delete" onclick = "event.stopPropagation(); window.SpotNotes.deleteNote(${note.id})" > Delete < / b u t t o n >
2026-06-02 03:49:38 +00:00
` ;
} else if ( window . SpotNotes . activePadIsPublic ) {
2026-06-23 19:23:45 +00:00
actionBtns = ` <button class="note-card-link report" onclick="event.stopPropagation(); window.SpotNotes.reportNote( ${ note . id } )">Report</button> ` ;
2026-06-02 03:49:38 +00:00
}
2026-06-23 19:23:45 +00:00
const shareToken = note . share _token || '' ;
const shareBtn = ` <button class="note-card-link share" onclick="event.stopPropagation(); window.SpotNotes.shareNote( ${ note . id } , ' ${ shareToken } ')" title="Share Note">Share</button> ` ;
2026-06-02 03:49:38 +00:00
const dateStr = new Date ( note . created _at ) . toLocaleString ( ) ;
item . innerHTML = `
2026-06-23 19:23:45 +00:00
< div class = "note-card-title" > $ { window . SpotNotes . escapeHtml ( note . title ) } < / d i v >
< div class = "note-card-body" > $ { window . SpotNotes . escapeHtml ( note . content ) } < / d i v >
< div class = "note-card-meta" >
2026-06-02 03:49:38 +00:00
$ { dateStr } < br > $ { note . lat . toFixed ( 5 ) } , $ { note . lng . toFixed ( 5 ) }
< / d i v >
2026-06-23 19:23:45 +00:00
< div class = "note-card-actions" >
< a href = "https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target = "_blank" onclick = "event.stopPropagation();" class = "note-card-link directions" > Directions < / a >
$ { shareBtn }
$ { actionBtns }
< / d i v >
2026-06-02 03:49:38 +00:00
` ;
item . addEventListener ( 'click' , ( ) => {
window . SpotNotes . stopTracking ( ) ;
window . SpotNotes . map . setView ( [ note . lat , note . lng ] , 16 ) ;
} ) ;
sidebarBody . appendChild ( item ) ;
} ) ;
// We do NOT call window.SpotNotes.renderMapMarkers() here, because we only want to filter the sidebar, not the map itself.
// Wait, the task says: "filtering the existing activeNotes array before re-rendering the sidebar list".
// It doesn't strictly say to filter the map markers, but maybe we should?
// Usually a sidebar filter only filters the list if it says "Add a simple, fast text input filter floating above the note list in the sidebar... filtering the existing activeNotes array before re-rendering the sidebar list".
// We will leave the map markers as they are to show all active notes. Or wait, if we only render the list, the map shows everything. Let's just leave the map markers as is (calling renderMapMarkers once on load).
} ;