code refactoring.. addition of marker clumping and search box in the notes tab

This commit is contained in:
Ben Miller 2026-06-01 22:49:38 -05:00
parent 46d1af6d41
commit 6c582b059c
13 changed files with 1892 additions and 486 deletions

View file

@ -1,40 +1,10 @@
# SpotNotes Future Implementation & Refactoring Plan # SpotNotes Future Implementation & Refactoring Plan
## 1. Feature: Mobile UX & Responsive Overhaul
## 1. Refactoring `index.js` (Reducing Complexity)
- **Status:** **[To Do]** - **Status:** **[To Do]**
- **Action Plan:** - **Action Plan:**
- `index.js` is currently a monolithic file (~1000 lines) handling global state, Leaflet map initialization, sidebar DOM manipulation, and raw API calls simultaneously. - **Audit & Consistency:** Review every application state (Login, Register, Admin Dashboard, Map View) to ensure visual parity with the desktop experience while optimizing for vertical screen constraints.
- **Extract Map Logic:** Create `static/assets/js/map.js` to encapsulate `initMap`, `renderMapMarkers`, geolocation tracking, and all Leaflet-specific code. - **Compact Layout & Spacing:** Re-evaluate the spacing and layering of icons, buttons, and UI elements. Aim for a "small and tight" aesthetic that maximizes map visibility without cramping everything into an unreadable jumble.
- **Extract UI/Sidebar Logic:** Create `static/assets/js/sidebar.js` to handle `initSidebar`, `renderNotepadList`, HTML generation for note list items, and DOM toggling. - **Sidebar to Bottom-Sheet:** On mobile devices, explore transitioning the right-side drawer into a "Bottom-Sheet" component (sliding up from the bottom) to provide a more ergonomic, thumb-reachable interface for note lists.
- **Extract API & State Logic:** Keep the core data fetching (`loadNotepads`, `loadNotesForPad`, `loadAllMyNotes`) in `index.js` (or a dedicated `data.js`) to act as the central controller. - **Enhanced Touch Targets:** Standardize all interactive elements (buttons, icons, notepad list items) to a minimum 44x44px touch target to prevent accidental clicks and improve accessibility.
- **Namespace Globals:** Group global variables (`activeNotes`, `activePadId`, `currentUser`, `map`, etc.) into a single `window.SpotNotes` state object to prevent scope pollution and make dependencies explicitly clear across the new script files. - **Responsive Modals:** Refactor modal windows to occupy 100% of the viewport height/width on small screens, ensuring that form inputs and the "Add Note" textarea remain fully visible above the virtual keyboard.
- **Mobile Navigation:** Consolidate the desktop navbar into a streamlined mobile header, utilizing a hamburger menu or action bar to keep the search functionality and account links accessible but unobtrusive.
## 2. Feature: "Get Directions" Button
- **Status:** **[To Do]**
- **Action Plan:**
- **Low-Hanging Fruit:** Add a quick "Directions" or "Navigate" link/button to the map marker popups and sidebar note items.
- Clicking the button will easily open a new tab mapping directly to the note's coordinates: `https://www.google.com/maps/dir/?api=1&destination={lat},{lng}`.
- Extremely useful UX for a location-based app, requiring absolutely zero backend changes.
## 3. Feature: Map Marker Clustering
- **Status:** **[To Do]**
- **Action Plan:**
- **Low-Hanging Fruit:** Integrate the official `Leaflet.markercluster` plugin.
- Currently, viewing an "All My Notes" view with hundreds of notes in the same city creates a messy, overlapping visual experience and impacts browser performance.
- Clustering groups nearby markers into a single circle with a count, automatically expanding when the user clicks or zooms in.
- Implementation requires just adding the CDN links to `index.html` and wrapping the markers in a `L.markerClusterGroup()` inside the `renderMapMarkers` loop.
## 4. Feature: Sort & Filter Active Notes
- **Status:** **[To Do]**
- **Action Plan:**
- **Low-Hanging Fruit:** Add a simple, fast text input filter floating above the note list in the sidebar whenever a notepad is actively viewed.
- Allows users to instantly search for a specific note's title or content within a massive notepad *without* triggering a slow, global backend search.
- Implementation is 100% frontend logic: listening to the input's `keyup` event and filtering the existing `activeNotes` array before re-rendering the sidebar list.
## 5. Feature: Custom Marker Colors per Notepad
- **Status:** **[To Do]**
- **Action Plan:**
- Update the `Notepad` database model to include an optional `color` field (defaulting to the standard Leaflet blue).
- Add a simple HTML `<input type="color">` selector to the Create/Edit Notepad modals.
- Update the Leaflet marker icon logic to apply a CSS tint/filter (or use customized SVG icons) matching the notepad's selected color.
- This allows users to visually distinguish between different notes (e.g., "Restaurants" vs "Birding Spots") instantly when looking at the map in mixed views like "All My Notes".

View file

@ -230,6 +230,7 @@ def update_notepad(pad_id):
data = request.get_json() data = request.get_json()
notepad.name = data.get('name', notepad.name) notepad.name = data.get('name', notepad.name)
notepad.is_public = data.get('is_public', notepad.is_public) notepad.is_public = data.get('is_public', notepad.is_public)
notepad.color = data.get('color', notepad.color)
db.session.commit() db.session.commit()
return jsonify(notepad.to_dict()) return jsonify(notepad.to_dict())

View file

@ -6,6 +6,7 @@ class Notepad(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False) name = db.Column(db.String(100), nullable=False)
is_public = db.Column(db.Boolean, default=False) is_public = db.Column(db.Boolean, default=False)
color = db.Column(db.String(7), default="#3388ff")
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

98
refactor.py Normal file
View file

@ -0,0 +1,98 @@
import os
import re
base_dir = r'z:\source\forgejo\spotnotes.app\static\assets\js'
index_path = os.path.join(base_dir, 'index.js')
with open(index_path, 'r', encoding='utf-8') as f:
content = f.read()
# Remove the blocks that were moved to map.js and sidebar.js
# initMap
content = re.sub(r'function initMap\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# initSidebar
content = re.sub(r'function initSidebar\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# renderMapMarkers
content = re.sub(r'function renderMapMarkers\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# stopTracking
content = re.sub(r'function stopTracking\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# updateTrackingUI
content = re.sub(r'function updateTrackingUI\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# renderNotepadList
content = re.sub(r'function renderNotepadList\(pads, containerId, isFavorite\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# setupPublicToggle
content = re.sub(r'function setupPublicToggle\(\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# toggleSection
content = re.sub(r'function toggleSection\(event\).*?^}', '', content, flags=re.MULTILINE | re.DOTALL)
# Now, wrap the whole thing or replace globals
# It's easier to rewrite index.js using a structured output or targeted regexes.
# Let's replace the variable declarations:
globals_to_replace = [
'map', 'activeNotes', 'userLocation', 'activePadName', 'activePadId',
'activePadIsPublic', 'activePadOwnerId', 'activePadCanWrite', 'currentUser',
'isTracking', 'userMarker', 'notifiedNoteIds', 'isLoggedIn', 'currentMarkers', 'topZIndex'
]
# Remove the var declarations
for var in globals_to_replace:
content = re.sub(r'let\s+' + var + r'\s*=[^;]+;\n?', '', content)
# Prepend window.SpotNotes. to usages
for var in globals_to_replace:
# Use negative lookbehind/lookahead to avoid matching substring or properties if any
content = re.sub(r'(?<!\w|\.)' + var + r'(?!\w)', f'window.SpotNotes.{var}', content)
# Some function names
functions_to_namespace = [
'loadNotepads', 'loadNotepadsOfType', 'loadNotesForPad', 'loadSingleNote',
'loadAllMyNotes', 'toggleFavorite', 'shareNotepad', 'shareNote',
'deleteNotepad', 'deleteNote', 'reportNote', 'openShareSettingsModal',
'removeCollaborator', 'displaySearchResults', 'escapeHtml'
]
for func in functions_to_namespace:
# replace function definition
content = re.sub(r'async function ' + func + r'\(', f'window.SpotNotes.{func} = async function(', content)
content = re.sub(r'function ' + func + r'\(', f'window.SpotNotes.{func} = function(', content)
content = re.sub(r'window\.' + func + r'\s*=\s*async function\(', f'window.SpotNotes.{func} = async function(', content)
content = re.sub(r'window\.' + func + r'\s*=\s*function\(', f'window.SpotNotes.{func} = function(', content)
# replace usages
content = re.sub(r'(?<!\w|\.)' + func + r'\(', f'window.SpotNotes.{func}(', content)
# also replace usages as callbacks, e.g. .addEventListener('click', loadNotepads)
content = re.sub(r'(?<!\w|\.)' + func + r'(?!\w|\()', f'window.SpotNotes.{func}', content)
# Also fix the initMap / initSidebar / setupPublicToggle usages
for func in ['initMap', 'initSidebar', 'renderMapMarkers', 'stopTracking', 'updateTrackingUI', 'renderNotepadList', 'setupPublicToggle', 'toggleSection']:
content = re.sub(r'(?<!\w|\.)' + func + r'\(', f'window.SpotNotes.{func}(', content)
content = re.sub(r'(?<!\w|\.)' + func + r'(?!\w|\()', f'window.SpotNotes.{func}', content)
# Add initialization block for state
init_block = """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;
"""
with open(index_path, 'w', encoding='utf-8') as f:
f.write(init_block + content)
print('Refactored index.js')

164
refactor_filter.py Normal file
View file

@ -0,0 +1,164 @@
import os
index_path = r'z:\source\forgejo\spotnotes.app\static\assets\js\index.js'
with open(index_path, 'r', encoding='utf-8') as f:
content = f.read()
# Add the filter input to the header
header_replace = """
<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;">${window.SpotNotes.escapeHtml(currentName)}</h2>
<button onclick="window.SpotNotes.shareNotepad(${padId}, '${token || ''}')" style="border:none; background:none; cursor:pointer; margin-left:10px;" title="Share">🔗</button>
${manageShareBtn}
${exportBtn}
</div>
<div style="margin-top: 10px;">
<input type="text" id="filter-active-notes" placeholder="Filter notes..." style="width: 100%; padding: 5px; margin-bottom: 5px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px;">
</div>
${addNoteBtn}
"""
content = content.replace("""
<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;">${window.SpotNotes.escapeHtml(currentName)}</h2>
<button onclick="window.SpotNotes.shareNotepad(${padId}, '${token || ''}')" style="border:none; background:none; cursor:pointer; margin-left:10px;" title="Share">🔗</button>
${manageShareBtn}
${exportBtn}
</div>
${addNoteBtn}
""", header_replace)
# Now, extract the rendering logic into a new function `window.SpotNotes.renderActiveNotesList`
old_render_block = """
// 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 (window.SpotNotes.activePadCanWrite) {
const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\\\'").replace(/"/g, '&quot;').replace(/\\n/g, '\\\\n');
actionBtns = `
<button 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(); window.SpotNotes.deleteNote(${note.id})" style="font-size:0.7rem; color:#dc3545; border:none; background:none; cursor:pointer; text-decoration:underline;">Delete</button>
`;
} else if (window.SpotNotes.activePadIsPublic) {
actionBtns = `<button onclick="event.stopPropagation(); window.SpotNotes.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;">${window.SpotNotes.escapeHtml(note.title)}</div>
<div style="font-size:0.8rem; color:#666; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${window.SpotNotes.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;">
<a href="https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target="_blank" onclick="event.stopPropagation();" style="font-size:0.7rem; color:#28a745; text-decoration:underline; margin-right: 5px;">Directions</a>
${actionBtns}
</div>
`;
item.addEventListener('click', () => {
window.SpotNotes.stopTracking();
window.SpotNotes.map.setView([note.lat, note.lng], 16);
});
sidebarBody.appendChild(item);
});
window.SpotNotes.renderMapMarkers();
"""
new_render_call = """
// Render Sidebar List
window.SpotNotes.renderActiveNotesList();
"""
content = content.replace(old_render_block, new_render_call)
# Add event listener for the filter input inside loadNotesForPad
filter_listener = """
const filterInput = document.getElementById('filter-active-notes');
if (filterInput) {
filterInput.addEventListener('keyup', (e) => {
window.SpotNotes.renderActiveNotesList(e.target.value);
});
}
"""
content = content.replace(new_render_call, new_render_call + filter_listener)
# Now append the function window.SpotNotes.renderActiveNotesList to the end of the file
render_function = """
window.SpotNotes.renderActiveNotesList = function(filterText = '') {
const sidebarBody = document.getElementById('sidebar-body');
if (!sidebarBody) return;
sidebarBody.innerHTML = '';
const filterLower = filterText.toLowerCase();
const filteredNotes = window.SpotNotes.activeNotes.filter(note => {
return note.title.toLowerCase().includes(filterLower) || note.content.toLowerCase().includes(filterLower);
});
if (filteredNotes.length === 0) {
sidebarBody.innerHTML = '<p style="padding:10px; color:#666;">No notes match.</p>';
}
filteredNotes.forEach(note => {
const item = document.createElement('div');
item.className = 'sidebar-note-item';
let actionBtns = '';
if (window.SpotNotes.activePadCanWrite) {
const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\\\'").replace(/"/g, '&quot;').replace(/\\n/g, '\\\\n');
actionBtns = `
<button 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(); window.SpotNotes.deleteNote(${note.id})" style="font-size:0.7rem; color:#dc3545; border:none; background:none; cursor:pointer; text-decoration:underline;">Delete</button>
`;
} else if (window.SpotNotes.activePadIsPublic) {
actionBtns = `<button onclick="event.stopPropagation(); window.SpotNotes.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;">${window.SpotNotes.escapeHtml(note.title)}</div>
<div style="font-size:0.8rem; color:#666; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${window.SpotNotes.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;">
<a href="https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target="_blank" onclick="event.stopPropagation();" style="font-size:0.7rem; color:#28a745; text-decoration:underline; margin-right: 5px;">Directions</a>
${actionBtns}
</div>
`;
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).
};
"""
content += "\n" + render_function
with open(index_path, 'w', encoding='utf-8') as f:
f.write(content)
print('Filtered active notes logic applied')

View file

@ -21,12 +21,13 @@ function initModals() {
e.preventDefault(); e.preventDefault();
const name = document.getElementById('pad-name').value; const name = document.getElementById('pad-name').value;
const isPublic = document.getElementById('pad-public').checked; const isPublic = document.getElementById('pad-public').checked;
const color = document.getElementById('pad-color').value;
try { try {
const res = await fetch('/api/notepads', { const res = await fetch('/api/notepads', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, is_public: isPublic }) body: JSON.stringify({ name, is_public: isPublic, color: color })
}); });
if (res.ok) { if (res.ok) {
@ -34,7 +35,7 @@ function initModals() {
modalPad.classList.add('hidden'); modalPad.classList.add('hidden');
formPad.reset(); formPad.reset();
// Transition to the new notepad // Transition to the new notepad
if (typeof loadNotesForPad === 'function') loadNotesForPad(newPad.id, newPad.name, newPad.is_public, newPad.owner_id); if (typeof window.SpotNotes.loadNotesForPad === 'function') window.SpotNotes.loadNotesForPad(newPad.id, newPad.name, newPad.is_public, newPad.owner_id);
} else { } else {
alert('Failed to create notepad'); alert('Failed to create notepad');
} }
@ -60,7 +61,7 @@ function initModals() {
const content = document.getElementById('note-content').value; const content = document.getElementById('note-content').value;
// Use current map center // Use current map center
const center = map.getCenter(); const center = window.SpotNotes.map.getCenter();
try { try {
const res = await fetch('/api/notes', { const res = await fetch('/api/notes', {
@ -79,7 +80,7 @@ function initModals() {
modalNote.classList.add('hidden'); modalNote.classList.add('hidden');
formNote.reset(); formNote.reset();
// Refresh notes // Refresh notes
if (typeof loadNotesForPad === 'function') loadNotesForPad(activeNotepadId); if (typeof window.SpotNotes.loadNotesForPad === 'function') window.SpotNotes.loadNotesForPad(activeNotepadId);
} else { } else {
alert('Failed to create note'); alert('Failed to create note');
} }
@ -98,10 +99,11 @@ function initModals() {
} }
// Helper to open edit notepad modal (called from index.js) // Helper to open edit notepad modal (called from index.js)
window.openEditPadModal = function(id, currentName, currentPublic) { window.openEditPadModal = function(id, currentName, currentPublic, currentColor = '#3388ff') {
document.getElementById('pad-id-edit').value = id; document.getElementById('pad-id-edit').value = id;
document.getElementById('pad-name-edit').value = currentName; document.getElementById('pad-name-edit').value = currentName;
document.getElementById('pad-public-edit').checked = currentPublic; document.getElementById('pad-public-edit').checked = currentPublic;
document.getElementById('pad-color-edit').value = currentColor;
document.getElementById('modal-edit-pad').classList.remove('hidden'); document.getElementById('modal-edit-pad').classList.remove('hidden');
}; };
@ -118,3 +120,5 @@ function openCreateNoteModal(notepadId) {
activeNotepadId = notepadId; activeNotepadId = notepadId;
document.getElementById('modal-create-note').classList.remove('hidden'); document.getElementById('modal-create-note').classList.remove('hidden');
} }
en');
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

178
static/assets/js/map.js Normal file
View file

@ -0,0 +1,178 @@
window.SpotNotes = window.SpotNotes || {};
window.SpotNotes.initMap = function() {
// Default view (e.g., Oklahoma area if geolocation fails)
window.SpotNotes.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;
window.SpotNotes.userLocation = { lat: latitude, lng: longitude };
const centerBtn = document.getElementById('btn-center-user');
if (centerBtn) centerBtn.style.display = 'block';
if (window.SpotNotes.isTracking) {
window.SpotNotes.map.setView([latitude, longitude]);
} else if (!window.SpotNotes.userMarker && !new URLSearchParams(window.location.search).has('pad') && !new URLSearchParams(window.location.search).has('note')) {
window.SpotNotes.map.setView([latitude, longitude], 13);
console.log("Map centered on user location:", latitude, longitude);
}
// Add User Location Dot
if (window.SpotNotes.userMarker) {
window.SpotNotes.userMarker.setLatLng([latitude, longitude]);
} else {
window.SpotNotes.userMarker = L.circleMarker([latitude, longitude], {
radius: 8,
fillColor: "#3388ff",
color: "#fff",
weight: 2,
opacity: 1,
fillOpacity: 0.8
}).addTo(window.SpotNotes.map).bindPopup("You are here");
}
// Location-Based Browser Alerts
const userLatLng = L.latLng(latitude, longitude);
if (window.SpotNotes.activeNotes && window.SpotNotes.activeNotes.length > 0) {
window.SpotNotes.activeNotes.forEach(note => {
const noteLatLng = L.latLng(note.lat, note.lng);
if (userLatLng.distanceTo(noteLatLng) <= 50) {
if (!window.SpotNotes.notifiedNoteIds.has(note.id)) {
window.SpotNotes.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(window.SpotNotes.map);
// Listen for zoom changes to toggle note display
window.SpotNotes.map.on('zoomend', () => {
if (window.SpotNotes.activeNotes.length > 0) {
window.SpotNotes.renderMapMarkers();
}
});
window.SpotNotes.map.on('dragstart', window.SpotNotes.stopTracking);
window.SpotNotes.map.on('popupopen', window.SpotNotes.stopTracking);
// Initialize MarkerClusterGroup
window.SpotNotes.markerClusterGroup = L.markerClusterGroup({
disableClusteringAtZoom: 16
});
window.SpotNotes.map.addLayer(window.SpotNotes.markerClusterGroup);
};
window.SpotNotes.renderMapMarkers = function() {
// Clear existing
if (window.SpotNotes.markerClusterGroup) {
window.SpotNotes.markerClusterGroup.clearLayers();
}
// Also remove from map directly just in case (fallback)
window.SpotNotes.currentMarkers.forEach(marker => window.SpotNotes.map.removeLayer(marker));
window.SpotNotes.currentMarkers = [];
const zoom = window.SpotNotes.map.getZoom();
const showFull = zoom >= 14; // Zoom threshold
window.SpotNotes.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 = window.SpotNotes.currentUser && (note.owner_id ? note.owner_id === window.SpotNotes.currentUser.id : window.SpotNotes.activePadOwnerId === window.SpotNotes.currentUser.id);
const isPublic = note.is_public !== undefined ? note.is_public : window.SpotNotes.activePadIsPublic;
const canWrite = (note.notepad && note.notepad.can_write !== undefined) ? note.notepad.can_write : window.SpotNotes.activePadCanWrite;
if (canWrite) {
const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '&quot;');
const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtns = `<br><button onclick="openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')" class="btn-note-edit">✏️ Edit</button><button onclick="window.SpotNotes.deleteNote(${note.id})" class="btn-note-delete">Delete</button>`;
} else if (isPublic) {
actionBtns = `<br><button onclick="window.SpotNotes.reportNote(${note.id})" class="btn-note-report">Report</button>`;
}
const shareToken = note.share_token || '';
const shareBtn = `<button onclick="window.SpotNotes.shareNote(${note.id}, '${shareToken}')" class="btn-note-share">🔗 Share</button>`;
const dateStr = new Date(note.created_at).toLocaleString();
const padColor = (note.notepad && note.notepad.color) ? note.notepad.color : '#3388ff';
const directionsBtn = `<a href="https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target="_blank" class="btn-note-directions" style="display:inline-block; margin-top:5px; margin-right:5px; font-size:0.8rem; text-decoration:none;">🗺️ Directions</a>`;
if (showFull) {
const icon = L.divIcon({
className: '',
html: `<div class="map-note-card" style="border-top: 4px solid ${padColor};">
<div style="font-weight:bold; margin-bottom:4px;">${window.SpotNotes.escapeHtml(note.title)}</div>
<div style="white-space: pre-wrap;">${window.SpotNotes.escapeHtml(note.content)}</div>
<div style="font-size:0.7rem; color:#888; margin-top:4px;">${dateStr}</div>
${directionsBtn} ${shareBtn} ${actionBtns}
</div>`,
iconSize: [200, 'auto'],
iconAnchor: [100, 0]
});
marker = L.marker([note.lat, note.lng], { icon: icon });
} else {
const svgIcon = L.divIcon({
className: 'custom-div-icon',
html: `<div style="background-color: ${padColor}; width: 16px; height: 16px; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 4px rgba(0,0,0,0.5);"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10],
popupAnchor: [0, -10]
});
marker = L.marker([note.lat, note.lng], { icon: svgIcon });
marker.bindPopup(`<b>${window.SpotNotes.escapeHtml(note.title)}</b><br><div style="white-space: pre-wrap;">${window.SpotNotes.escapeHtml(note.content)}</div><span style="font-size:0.7rem; color:#888;">${dateStr}</span><br>${directionsBtn} ${shareBtn} ${actionBtns}`);
}
const bumpZIndex = function() {
this.setZIndexOffset(window.SpotNotes.topZIndex++);
};
marker.on('click', bumpZIndex);
marker.on('mousedown', bumpZIndex);
if (window.SpotNotes.markerClusterGroup) {
window.SpotNotes.markerClusterGroup.addLayer(marker);
} else {
marker.addTo(window.SpotNotes.map);
}
window.SpotNotes.currentMarkers.push(marker);
});
};
window.SpotNotes.stopTracking = function() {
window.SpotNotes.isTracking = false;
window.SpotNotes.updateTrackingUI();
};
window.SpotNotes.updateTrackingUI = function() {
const btn = document.getElementById('btn-center-user');
if (btn) {
if (window.SpotNotes.isTracking) {
btn.textContent = "Tracking Location...";
btn.style.backgroundColor = "#28a745";
} else {
btn.textContent = "Target My Location";
btn.style.backgroundColor = "";
}
}
};

View file

@ -25,8 +25,8 @@ document.addEventListener('DOMContentLoaded', () => {
// If notes found, display them and stop (don't geocode) // If notes found, display them and stop (don't geocode)
if (notes.length > 0) { if (notes.length > 0) {
if (window.displaySearchResults) { if (window.SpotNotes && window.SpotNotes.displaySearchResults) {
window.displaySearchResults(notes, null, query); window.SpotNotes.displaySearchResults(notes, null, query);
} }
return; return;
} }
@ -48,8 +48,8 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// 3. Update UI via index.js helper // 3. Update UI via index.js helper
if (window.displaySearchResults) { if (window.SpotNotes && window.SpotNotes.displaySearchResults) {
window.displaySearchResults(notes, location, query); window.SpotNotes.displaySearchResults(notes, location, query);
} }
}; };

127
static/assets/js/sidebar.js Normal file
View file

@ -0,0 +1,127 @@
window.SpotNotes = window.SpotNotes || {};
window.SpotNotes.initSidebar = function() {
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 => {
const content = section.querySelector('.list-section-content');
if (content) 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', () => {
window.SpotNotes.isTracking = true;
window.SpotNotes.updateTrackingUI();
if (window.SpotNotes.userLocation) {
window.SpotNotes.map.setView([window.SpotNotes.userLocation.lat, window.SpotNotes.userLocation.lng], 16);
}
});
}
};
window.SpotNotes.setupPublicToggle = function() {
const btn = document.getElementById('btn-toggle-public');
const list = document.getElementById('public-notepads-list');
if (!btn || !list) return;
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();
window.SpotNotes.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;';
}
});
};
window.SpotNotes.renderNotepadList = function(pads, containerId, isFavorite) {
const container = document.getElementById(containerId);
if (!container) return;
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 = (window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id) ?
`<button onclick="event.stopPropagation(); openEditPadModal(${pad.id}, '${window.SpotNotes.escapeHtml(pad.name)}', ${pad.is_public}, '${pad.color || '#3388ff'}')" title="Edit Notepad" style="border:none; background:none; cursor:pointer; padding:0 5px;">✏️</button>`
: '';
const isOwner = window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id;
const manageShareBtn = isOwner ?
`<button onclick="event.stopPropagation(); openShareSettingsModal(${pad.id}, '${window.SpotNotes.escapeHtml(pad.name)}')" title="Manage Sharing" style="border:none; background:none; cursor:pointer; padding:0 5px;">⚙️</button>`
: '';
if(window.SpotNotes.isLoggedIn){
const action = isFavorite ? 'unfavorite' : 'favorite';
const label = isFavorite ? '&hearts;' : '♡';
favButton = `<button onclick="event.stopPropagation(); window.SpotNotes.toggleFavorite(${pad.id}, '${action}')" style="color:#dc3545; border:none; background:none; cursor:pointer; font-weight:bold; padding:0 5px;">${label}</button>`;
}
const deleteBtn = isOwner ?
`<button onclick="event.stopPropagation(); window.SpotNotes.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(); window.SpotNotes.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} ${window.SpotNotes.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', () => window.SpotNotes.loadNotesForPad(pad.id, pad.name, pad.is_public, pad.owner_id, false, null, pad.can_write));
container.appendChild(item);
});
};
window.SpotNotes.toggleSection = function(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;');
};

View file

@ -9,6 +9,8 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/> crossorigin=""/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" />
<!-- Custom Styles --> <!-- Custom Styles -->
<link rel="stylesheet" href="/assets/css/base.css"> <link rel="stylesheet" href="/assets/css/base.css">
@ -100,6 +102,10 @@
<input type="checkbox" id="pad-public-edit" style="width:auto;"> Make Public <input type="checkbox" id="pad-public-edit" style="width:auto;"> Make Public
</label> </label>
</div> </div>
<div class="form-group">
<label>Marker Color</label>
<input type="color" id="pad-color-edit" value="#3388ff">
</div>
<button type="submit" class="btn btn-primary">Save Changes</button> <button type="submit" class="btn btn-primary">Save Changes</button>
</form> </form>
</div> </div>
@ -177,7 +183,10 @@
<!-- Scripts --> <!-- Scripts -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
<script src="/assets/js/nav.js"></script> <script src="/assets/js/nav.js"></script>
<script src="/assets/js/map.js"></script>
<script src="/assets/js/sidebar.js"></script>
<script src="/assets/js/index.js"></script> <script src="/assets/js/index.js"></script>
<script src="/assets/js/actions.js"></script> <script src="/assets/js/actions.js"></script>
<script src="/assets/js/search.js"></script> <script src="/assets/js/search.js"></script>

51
update_map_colors.py Normal file
View file

@ -0,0 +1,51 @@
import os
import re
path = r'z:\source\forgejo\spotnotes.app\static\assets\js\map.js'
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
new_render = """ const shareToken = note.share_token || '';
const shareBtn = `<button onclick="window.SpotNotes.shareNote(${note.id}, '${shareToken}')" class="btn-note-share">🔗 Share</button>`;
const dateStr = new Date(note.created_at).toLocaleString();
const padColor = (note.notepad && note.notepad.color) ? note.notepad.color : '#3388ff';
const directionsBtn = `<a href="https://www.google.com/maps/dir/?api=1&destination=${note.lat},${note.lng}" target="_blank" class="btn-note-directions" style="display:inline-block; margin-top:5px; margin-right:5px; font-size:0.8rem; text-decoration:none;">🗺️ Directions</a>`;
if (showFull) {
const icon = L.divIcon({
className: '',
html: `<div class="map-note-card" style="border-top: 4px solid ${padColor};">
<div style="font-weight:bold; margin-bottom:4px;">${window.SpotNotes.escapeHtml(note.title)}</div>
<div style="white-space: pre-wrap;">${window.SpotNotes.escapeHtml(note.content)}</div>
<div style="font-size:0.7rem; color:#888; margin-top:4px;">${dateStr}</div>
${directionsBtn} ${shareBtn} ${actionBtns}
</div>`,
iconSize: [200, 'auto'],
iconAnchor: [100, 0]
});
marker = L.marker([note.lat, note.lng], { icon: icon });
} else {
const svgIcon = L.divIcon({
className: 'custom-div-icon',
html: `<div style="background-color: ${padColor}; width: 16px; height: 16px; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 4px rgba(0,0,0,0.5);"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10],
popupAnchor: [0, -10]
});
marker = L.marker([note.lat, note.lng], { icon: svgIcon });
marker.bindPopup(`<b>${window.SpotNotes.escapeHtml(note.title)}</b><br><div style="white-space: pre-wrap;">${window.SpotNotes.escapeHtml(note.content)}</div><span style="font-size:0.7rem; color:#888;">${dateStr}</span><br>${directionsBtn} ${shareBtn} ${actionBtns}`);
}"""
# Extract old chunk to replace
old_chunk_start = "const shareToken = note.share_token || '';"
old_chunk_end = "marker.bindPopup(`<b>${window.SpotNotes.escapeHtml(note.title)}</b><br><div style=\"white-space: pre-wrap;\">${window.SpotNotes.escapeHtml(note.content)}</div><span style=\"font-size:0.7rem; color:#888;\">${dateStr}</span><br>${directionsBtn} ${shareBtn} ${actionBtns}`);\n }"
pattern = re.escape(old_chunk_start) + r'.*?' + re.escape(old_chunk_end)
content = re.sub(pattern, new_render, content, flags=re.DOTALL)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
print('Marker logic updated with colors.')