feat: implement core frontend infrastructure, notepad management API, and navigation components

This commit is contained in:
Ben Miller 2026-06-23 14:23:45 -05:00
parent 1d80b47a3d
commit 62e8d38fbc
14 changed files with 769 additions and 1890 deletions

View file

@ -6,6 +6,7 @@ from models.note import Note
from models.collaborator import Collaborator from models.collaborator import Collaborator
from models.user import User from models.user import User
from models.favorite import Favorite from models.favorite import Favorite
from sqlalchemy import func
import csv import csv
from io import StringIO from io import StringIO
@ -68,13 +69,23 @@ def get_notepads():
query = query.filter_by(is_public=True) query = query.filter_by(is_public=True)
pads = query.order_by(Notepad.name).all() pads = query.order_by(Notepad.name).all()
# Optimize note count query (O(1) database hit instead of O(N))
note_counts = dict(db.session.query(Note.notepad_id, func.count(Note.id)).filter(Note.is_hidden == False).group_by(Note.notepad_id).all())
# Optimize write permissions check
collab_write_pad_ids = set()
if user_id:
collab_write_pad_ids = {c.notepad_id for c in Collaborator.query.filter_by(user_id=user_id, permission='write').all()}
results = [] results = []
for pad in pads: for pad in pads:
p_dict = pad.to_dict() note_count = note_counts.get(pad.id, 0)
p_dict = pad.to_dict(note_count=note_count)
if user_id: if user_id:
if pad.user_id == user_id: if pad.user_id == user_id:
p_dict['share_token'] = get_share_token('notepad', pad.id) p_dict['share_token'] = get_share_token('notepad', pad.id)
p_dict['can_write'] = check_pad_write_permission(pad, user_id) p_dict['can_write'] = (pad.user_id == user_id) or (pad.id in collab_write_pad_ids)
else: else:
p_dict['can_write'] = False p_dict['can_write'] = False
results.append(p_dict) results.append(p_dict)
@ -156,7 +167,6 @@ def get_note(note_id):
n_dict['share_token'] = get_share_token('note', note_id) n_dict['share_token'] = get_share_token('note', note_id)
return jsonify(n_dict) return jsonify(n_dict)
@api_bp.route('/notes/mine', methods=['GET']) @api_bp.route('/notes/mine', methods=['GET'])
def get_my_all_notes(): def get_my_all_notes():
user_id = session.get('user_id') user_id = session.get('user_id')
@ -164,10 +174,14 @@ def get_my_all_notes():
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
notes = Note.query.join(Notepad).filter(Notepad.user_id == user_id, Note.is_hidden == False).all() notes = Note.query.join(Notepad).filter(Notepad.user_id == user_id, Note.is_hidden == False).all()
# Eagerly load note counts to avoid N+1 queries when calling pad.to_dict()
note_counts = dict(db.session.query(Note.notepad_id, func.count(Note.id)).filter(Note.is_hidden == False).group_by(Note.notepad_id).all())
results = [] results = []
for note in notes: for note in notes:
n_dict = note.to_dict() n_dict = note.to_dict()
n_dict['notepad'] = note.notepad.to_dict() n_dict['notepad'] = note.notepad.to_dict(note_count=note_counts.get(note.notepad_id, 0))
n_dict['owner_id'] = user_id n_dict['owner_id'] = user_id
n_dict['is_public'] = note.notepad.is_public n_dict['is_public'] = note.notepad.is_public
n_dict['share_token'] = get_share_token('note', note.id) n_dict['share_token'] = get_share_token('note', note.id)

View file

@ -1,9 +1,10 @@
from flask import Blueprint, request, jsonify, session from flask import Blueprint, request, jsonify, session
from sqlalchemy import or_ from sqlalchemy import or_, func
from extensions import db from extensions import db
from models.note import Note from models.note import Note
from models.notepad import Notepad from models.notepad import Notepad
from models.favorite import Favorite from models.favorite import Favorite
from models.collaborator import Collaborator
from .api import check_pad_write_permission from .api import check_pad_write_permission
import urllib.request import urllib.request
import urllib.parse import urllib.parse
@ -45,13 +46,19 @@ def search_notes():
favs = Favorite.query.filter_by(user_id=user_id).all() favs = Favorite.query.filter_by(user_id=user_id).all()
fav_ids = {f.notepad_id for f in favs} fav_ids = {f.notepad_id for f in favs}
# Eagerly fetch note counts and permissions to prevent N+1 queries
note_counts = dict(db.session.query(Note.notepad_id, func.count(Note.id)).filter(Note.is_hidden == False).group_by(Note.notepad_id).all())
collab_write_pad_ids = set()
if user_id:
collab_write_pad_ids = {c.notepad_id for c in Collaborator.query.filter_by(user_id=user_id, permission='write').all()}
# Construct response with extra info for frontend (owner_id, is_public) # Construct response with extra info for frontend (owner_id, is_public)
output = [] output = []
for note, pad in results: for note, pad in results:
n_dict = note.to_dict() n_dict = note.to_dict()
p_dict = pad.to_dict() p_dict = pad.to_dict(note_count=note_counts.get(pad.id, 0))
p_dict['is_favorite'] = pad.id in fav_ids p_dict['is_favorite'] = pad.id in fav_ids
p_dict['can_write'] = check_pad_write_permission(pad, user_id) p_dict['can_write'] = (pad.user_id == user_id) or (pad.id in collab_write_pad_ids)
n_dict['notepad'] = p_dict n_dict['notepad'] = p_dict
n_dict['owner_id'] = pad.user_id # Keep for map marker compatibility n_dict['owner_id'] = pad.user_id # Keep for map marker compatibility

View file

@ -15,12 +15,12 @@ class Notepad(db.Model):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
def to_dict(self): def to_dict(self, note_count=None):
return { return {
'id': self.id, 'id': self.id,
'name': self.name, 'name': self.name,
'is_public': self.is_public, 'is_public': self.is_public,
'owner_id': self.user_id, 'owner_id': self.user_id,
'color': self.color, 'color': self.color,
'note_count': self.notes.filter_by(is_hidden=False).count() 'note_count': note_count if note_count is not None else self.notes.filter_by(is_hidden=False).count()
} }

View file

@ -1,98 +0,0 @@
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')

View file

@ -1,164 +0,0 @@
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

@ -1,46 +1,54 @@
/* Auth & Content Containers */ /* Auth Containers */
.auth-container, .content-container { .auth-container, .content-container {
background: white; background: var(--bg-card);
padding: 2rem; padding: 2.5rem;
border-radius: 8px; border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0,0,0,0.1); box-shadow: var(--shadow-lg);
border: 1px solid var(--border);
width: 100%; width: 100%;
margin: 20px; margin: 40px auto;
transition: var(--transition);
} }
.auth-container { max-width: 400px; } .auth-container {
.content-container { max-width: 800px; margin-top: 20px; } max-width: 440px;
}
/* Forms */ .content-container {
.form-group { margin-bottom: 1rem; } max-width: 800px;
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
} }
.form-group input { /* Auth Header */
width: 100%; .auth-container h2 {
padding: 0.75rem; font-size: 1.5rem;
border: 1px solid #ddd; font-weight: 700;
border-radius: 4px; margin-bottom: 24px;
font-size: 16px; /* Mobile friendly */ text-align: center;
min-height: 44px; color: var(--text-main);
} }
.links { margin-top: 1rem; text-align: center; font-size: 0.9rem; } /* Links */
.links a { color: #007bff; text-decoration: none; display: inline-block; padding: 10px; min-height: 44px; } .links {
.links a:hover { text-decoration: underline; } margin-top: 1.5rem;
text-align: center;
font-size: 0.9rem;
color: var(--text-muted);
}
.links a {
color: var(--primary);
text-decoration: none;
font-weight: 600;
transition: var(--transition);
display: inline-block;
}
.links a:hover {
color: var(--primary-hover);
text-decoration: underline;
}
/* Ensure buttons on content pages are white */ @media (max-width: 768px) {
.content-container .btn { color: white; }
@media (max-width: 600px) {
.auth-container, .content-container { .auth-container, .content-container {
padding: 1.5rem; padding: 2rem 1.5rem;
margin: 10px; margin: 20px 12px;
width: calc(100% - 20px); width: calc(100% - 24px);
} }
} }

View file

@ -1,3 +1,47 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
/* Color Palette */
--primary: #4f46e5; /* Modern Indigo */
--primary-hover: #4338ca;
--primary-light: #e0e7ff;
--secondary: #0ea5e9; /* Ocean sky blue */
--secondary-hover: #0284c7;
/* Backgrounds */
--bg-page: #f8fafc;
--bg-card: #ffffff;
--bg-glass: rgba(255, 255, 255, 0.85);
/* Borders */
--border: #e2e8f0;
--border-glass: rgba(255, 255, 255, 0.4);
--border-focus: #a5b4fc;
/* Text */
--text-main: #0f172a; /* Deep slate */
--text-muted: #64748b; /* Light slate */
--text-on-primary: #ffffff;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--shadow-glass: 0 8px 32px 0 rgba(31, 38, 135, 0.08);
/* Font */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
/* Radii */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-full: 9999px;
/* Transitions */
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Reset and Base Styles */ /* Reset and Base Styles */
* { * {
box-sizing: border-box; box-sizing: border-box;
@ -8,7 +52,9 @@
body, html { body, html {
height: 100%; height: 100%;
width: 100%; width: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: var(--font-sans);
color: var(--text-main);
background-color: var(--bg-page);
overflow: hidden; overflow: hidden;
} }
@ -17,28 +63,122 @@ body.page-body {
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
background: #f4f4f4; background: radial-gradient(circle at 50% 50%, #f1f5f9 0%, #e2e8f0 100%);
overflow: auto; overflow-y: auto;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--text-muted);
border-radius: var(--radius-full);
opacity: 0.6;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-main);
} }
/* Generic Components */ /* Generic Components */
.btn { .btn {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem 1.5rem;
color: white; color: var(--text-on-primary);
border: none; border: 1px solid transparent;
border-radius: 4px; border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
font-size: 1rem; font-family: var(--font-sans);
transition: opacity 0.2s; font-weight: 500;
font-size: 0.95rem;
transition: var(--transition);
min-height: 44px; min-height: 44px;
display: flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 8px;
box-shadow: var(--shadow-sm);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: var(--primary);
}
.btn-primary:hover {
background: var(--primary-hover);
}
.btn-success {
background: #10b981; /* Emerald green */
}
.btn-success:hover {
background: #059669;
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-main);
border-color: var(--border);
margin-top: 10px;
}
.btn-secondary:hover {
background: var(--border);
} }
.btn:hover { opacity: 0.9; }
.btn-primary { background: #007bff; }
.btn-success { background: #28a745; }
.btn-secondary { background: #6c757d; margin-top: 10px; }
.hidden { display: none !important; } .hidden { display: none !important; }
/* Global Forms & Input Elements styling */
.form-group {
margin-bottom: 1.25rem;
width: 100%;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group select,
.form-group textarea,
input.admin-search {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
outline: none;
font-family: var(--font-sans);
font-size: 0.95rem;
color: var(--text-main);
background-color: var(--bg-page);
transition: var(--transition);
min-height: 42px;
}
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="password"]:focus,
.form-group select:focus,
.form-group textarea:focus,
input.admin-search:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
background-color: var(--bg-card);
}

View file

@ -1,49 +1,110 @@
/* Modals */ /* Modals overlay */
.modal { .modal {
position: fixed; position: fixed;
top: 0; left: 0; width: 100%; height: 100%; top: 0;
background: rgba(0,0,0,0.5); left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.4);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 2000; z-index: 2000;
backdrop-filter: blur(2px); backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
animation: modalFadeIn 0.2s ease-out forwards;
} }
.modal.hidden { display: none; } .modal.hidden { display: none !important; }
/* Modal Content Card */
.modal-content { .modal-content {
background: white; background: var(--bg-card);
padding: 25px; padding: 30px;
border-radius: 8px; border-radius: var(--radius-lg);
width: 90%; width: 92%;
max-width: 400px; max-width: 440px;
position: relative; position: relative;
box-shadow: 0 4px 15px rgba(0,0,0,0.2); box-shadow: var(--shadow-lg);
max-height: 90vh; max-height: 85vh;
overflow-y: auto; overflow-y: auto;
border: 1px solid var(--border);
animation: modalScaleUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
} }
@media (max-width: 600px) { /* Close Button */
.close-modal {
position: absolute;
top: 16px;
right: 18px;
font-size: 1.6rem;
cursor: pointer;
color: var(--text-muted);
transition: var(--transition);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-full);
}
.close-modal:hover {
color: var(--text-main);
background-color: var(--border);
}
.modal-content h3 {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 8px;
color: var(--text-main);
}
.modal-content h4 {
font-size: 1rem;
font-weight: 600;
margin: 16px 0 8px 0;
color: var(--text-main);
}
/* Color picker specific */
.form-group input[type="color"] {
width: 50px;
height: 40px;
padding: 0;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
background: none;
}
/* Animations */
@keyframes modalFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalScaleUp {
from { transform: scale(0.96); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
/* Mobile Modals */
@media (max-width: 768px) {
.modal { .modal {
align-items: flex-end; /* Align to bottom for better mobile reach or use full height */ align-items: flex-end; /* Anchor at the bottom on mobile */
} }
.modal-content { .modal-content {
width: 100%; width: 100%;
height: 100%;
max-width: none; max-width: none;
max-height: 100%; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
border-radius: 0; padding: 24px;
padding: 20px; max-height: 80vh;
animation: modalSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes modalSlideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
} }
} }
.close-modal {
position: absolute;
top: 10px; right: 15px;
font-size: 1.5rem;
cursor: pointer;
color: #aaa;
transition: color 0.2s;
}
.close-modal:hover { color: #333; }

View file

@ -4,26 +4,37 @@
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 50px; height: 60px;
background-color: rgba(255, 255, 255, 0.95); background-color: var(--bg-glass);
backdrop-filter: blur(5px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 15px; padding: 0 24px;
z-index: 1000; z-index: 1000;
box-shadow: 0 2px 5px rgba(0,0,0,0.1); box-shadow: var(--shadow-sm);
flex-shrink: 0 border-bottom: 1px solid var(--border);
flex-shrink: 0;
transition: var(--transition);
} }
.brand { .brand {
font-weight: bold; font-weight: 700;
font-size: 1.2rem; font-size: 1.25rem;
color: #333; color: var(--text-main);
text-decoration: none; text-decoration: none;
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center;
transition: var(--transition);
}
.brand img {
transition: var(--transition);
}
.brand:hover img {
transform: scale(1.03);
} }
.nav-links { .nav-links {
@ -31,45 +42,94 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 15px; gap: 20px;
} }
.nav-links a { .nav-links a {
text-decoration: none; text-decoration: none;
color: #555; color: var(--text-muted);
font-size: 0.9rem; font-size: 0.9rem;
transition: color 0.2s; font-weight: 500;
transition: var(--transition);
white-space: nowrap; white-space: nowrap;
position: relative;
padding: 6px 0;
} }
.nav-links a:hover { color: #000; } .nav-links a:hover {
color: var(--primary);
}
.nav-links a::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background: var(--primary);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.25s ease;
}
.nav-links a:hover::after {
transform: scaleX(1);
transform-origin: left;
}
/* Search Bar in Navbar */ /* Search Bar in Navbar */
.nav-search { .nav-search {
flex: 0 1 400px; flex: 0 1 420px;
margin: 0 15px; margin: 0 20px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.nav-search form {
display: flex;
width: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: var(--transition);
}
.nav-search form:focus-within {
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.nav-search input { .nav-search input {
width: 100%; width: 100%;
padding: 8px 10px; padding: 10px 16px;
border: 1px solid #ccc; border: none;
border-radius: 4px 0 0 4px;
outline: none; outline: none;
font-size: 16px; /* Prevents zoom on iOS */ font-family: var(--font-sans);
min-height: 44px; font-size: 0.95rem;
background: transparent;
color: var(--text-main);
min-height: 40px;
} }
.nav-search input::placeholder {
color: var(--text-muted);
}
.nav-search button { .nav-search button {
padding: 5px 12px; padding: 0 16px;
border: 1px solid #ccc; border: none;
border-left: none; background: transparent;
background: #f8f9fa;
border-radius: 0 4px 4px 0;
cursor: pointer; cursor: pointer;
min-height: 44px; font-size: 1.1rem;
color: var(--text-muted);
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
}
.nav-search button:hover {
color: var(--primary);
background-color: var(--bg-page);
} }
.nav-search button:hover { background: #e2e6ea; }
.nav-toggle { .nav-toggle {
display: none; display: none;
@ -77,37 +137,27 @@
border: none; border: none;
font-size: 1.5rem; font-size: 1.5rem;
cursor: pointer; cursor: pointer;
color: #333; color: var(--text-main);
padding: 5px; padding: 5px;
} }
/* Mobile Navbar */ /* Mobile Navbar */
@media (max-width: 600px) { @media (max-width: 768px) {
.navbar { .navbar {
flex-wrap: wrap; flex-wrap: wrap;
height: auto; height: auto;
padding: 10px; padding: 12px 16px;
gap: 10px; gap: 12px;
} }
.brand { .brand {
flex: 1; flex: 1;
order: 1; order: 1;
} }
.nav-toggle {
display: none !important;
}
.nav-links { .nav-links {
display: flex;
flex: 1; flex: 1;
order: 2; order: 2;
justify-content: flex-end; justify-content: flex-end;
align-items: center; gap: 12px;
gap: 10px;
background: transparent;
padding: 0;
}
.nav-links a {
font-size: 0.9rem;
} }
.nav-search { .nav-search {
flex: 1 1 100%; flex: 1 1 100%;

View file

@ -1,40 +1,54 @@
/* Right Collapsible Sidebar */ /* Right Collapsible Floating Sidebar */
.sidebar { .sidebar {
position: absolute; position: absolute;
top: 50px; /* Below navbar */ top: 75px; /* Below navbar (60px) + margin */
right: 0; right: 15px;
bottom: 0; bottom: 15px;
width: 320px; width: 340px;
background-color: white; background-color: var(--bg-glass);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
z-index: 1000; z-index: 1000;
box-shadow: -2px 0 5px rgba(0,0,0,0.1); box-shadow: var(--shadow-lg);
transition: transform 0.3s ease-in-out; border: 1px solid var(--border-glass);
border-radius: var(--radius-lg);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: visible; /* Let toggle stick out */
} }
.sidebar.collapsed { .sidebar.collapsed {
transform: translateX(320px); transform: translateX(calc(100% + 20px));
} }
/* Toggle Button attached to sidebar */ /* Toggle Button attached to sidebar */
.sidebar-toggle { .sidebar-toggle {
position: absolute; position: absolute;
left: -40px; left: -40px;
top: 10px; top: 20px;
width: 40px; width: 40px;
height: 40px; height: 48px;
background-color: white; background-color: var(--bg-glass);
border: none; backdrop-filter: blur(20px);
border-radius: 5px 0 0 5px; -webkit-backdrop-filter: blur(20px);
box-shadow: -2px 0 5px rgba(0,0,0,0.1); border: 1px solid var(--border-glass);
border-right: none;
border-radius: var(--radius-md) 0 0 var(--radius-md);
box-shadow: -4px 4px 12px rgba(0,0,0,0.08);
cursor: pointer; cursor: pointer;
font-weight: bold; font-size: 1.1rem;
color: #555; color: var(--text-muted);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.2rem; transition: var(--transition);
}
.sidebar-toggle:hover {
color: var(--primary);
background-color: var(--bg-card);
width: 42px;
left: -42px;
} }
.sidebar-content { .sidebar-content {
@ -47,138 +61,326 @@
.sidebar-header { .sidebar-header {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center; align-items: stretch;
padding: 15px; padding: 16px 20px;
border-bottom: 1px solid #eee; border-bottom: 1px solid var(--border);
background: #fff;
flex-shrink: 0; flex-shrink: 0;
gap: 8px;
}
.sidebar-header h2 {
font-size: 1.2rem;
font-weight: 700;
color: var(--text-main);
} }
#sidebar-body { #sidebar-body {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
min-height: 0; min-height: 0;
padding: 10px 0;
} }
.btn-new-pad { .btn-new-pad {
background-color: #007bff; background-color: var(--primary);
color: white; color: white;
border: none; border: none;
width: 30px; width: 32px;
height: 30px; height: 32px;
border-radius: 50%; border-radius: var(--radius-full);
cursor: pointer; cursor: pointer;
font-size: 1.2rem; font-size: 1.2rem;
line-height: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: var(--transition);
box-shadow: var(--shadow-sm);
}
.btn-new-pad:hover {
background-color: var(--primary-hover);
transform: scale(1.05);
} }
.btn-center-location { .btn-center-location {
width: calc(100% - 30px); width: calc(100% - 30px);
margin: 15px; margin: 10px 15px;
padding: 10px; padding: 10px;
background-color: #17a2b8; background-color: var(--secondary);
color: white; color: white;
border: none; border: none;
border-radius: 4px; border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: 600;
font-size: 0.9rem;
flex-shrink: 0; flex-shrink: 0;
transition: var(--transition);
box-shadow: var(--shadow-sm);
}
.btn-center-location:hover {
background-color: var(--secondary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
} }
.btn-center-location:hover { background-color: #138496; }
.btn-add-note { .btn-add-note {
background-color: #28a745; background-color: var(--primary);
color: white; color: white;
border: none; border: none;
padding: 5px 10px; padding: 8px 14px;
border-radius: 4px; border-radius: var(--radius-md);
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: var(--transition);
align-self: flex-start;
}
.btn-add-note:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
} }
.btn-add-note:hover { background-color: #218838; }
/* Sidebar Lists & Items */ /* Sidebar Lists & Items */
.list-section { border-bottom: 1px solid #f0f0f0; } .list-section {
border-bottom: 1px solid var(--border);
}
.list-section:last-child {
border-bottom: none;
}
.list-section-header, .list-section-header,
.btn-toggle-section { .btn-toggle-section {
width: 100%; width: 100%;
text-align: left; text-align: left;
background: #fff; background: transparent;
border: none; border: none;
padding: 12px 15px; padding: 14px 20px;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 700;
font-size: 0.85rem; font-size: 0.8rem;
color: #555; color: var(--text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 1px;
margin: 0; margin: 0;
transition: background-color 0.2s;
}
.list-section-header:hover,
.btn-toggle-section:hover {
background-color: #f8f9fa;
color: #333;
}
.notepad-item, .sidebar-note-item {
padding: 10px 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.notepad-item:hover, .sidebar-note-item:hover {
background-color: #fafafa;
}
/* Collaborator List in Share Modal */
.collaborator-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 8px 0; transition: var(--transition);
border-bottom: 1px solid #f0f0f0;
} }
.collaborator-item:last-child { .list-section-header:hover,
border-bottom: none; .btn-toggle-section:hover {
} background-color: rgba(0, 0, 0, 0.02);
.collaborator-item button { color: var(--text-main);
background: none;
border: none;
color: #dc3545;
cursor: pointer;
font-size: 0.8rem;
} }
/* Mobile Sidebar */ /* Notepad Cards inside list */
@media (max-width: 600px) { .notepad-card {
padding: 12px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
cursor: pointer;
transition: var(--transition);
display: flex;
flex-direction: column;
gap: 6px;
}
.notepad-card:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.notepad-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.notepad-info {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.95rem;
color: var(--text-main);
}
.notepad-color-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: var(--radius-full);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.notepad-actions {
display: flex;
align-items: center;
gap: 4px;
}
.btn-action-icon {
border: none;
background: transparent;
cursor: pointer;
font-size: 0.9rem;
padding: 4px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
color: var(--text-muted);
}
.btn-action-icon:hover {
background-color: rgba(0, 0, 0, 0.05);
color: var(--text-main);
transform: scale(1.1);
}
.btn-action-delete:hover {
background-color: #fee2e2;
color: #ef4444;
}
.btn-action-fav {
font-size: 1rem;
}
.btn-action-fav:hover {
color: #ec4899;
}
.notepad-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
color: var(--text-muted);
padding-left: 18px;
}
.badge-count {
background-color: var(--primary-light);
color: var(--primary);
padding: 2px 8px;
border-radius: var(--radius-full);
font-weight: 600;
font-size: 0.75rem;
}
/* Note Cards in active pad view */
.note-card {
padding: 14px 20px;
margin: 8px 12px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition);
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 6px;
}
.note-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--primary-hover);
}
.note-card-title {
font-weight: 700;
font-size: 0.95rem;
color: var(--text-main);
}
.note-card-body {
font-size: 0.85rem;
color: var(--text-muted);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-card-meta {
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.3;
}
.note-card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
margin-top: 6px;
}
.note-card-link {
font-size: 0.75rem;
font-weight: 500;
color: #000000 !important;
text-decoration: none;
transition: var(--transition);
padding: 4px 8px;
border-radius: 4px;
border: none;
cursor: pointer;
display: inline-block;
background-color: #e2e8f0;
}
.note-card-link:hover {
opacity: 0.85;
text-decoration: none;
}
.note-card-link.directions {
background-color: #d1fae5;
color: #065f46 !important;
}
.note-card-link.directions:hover {
background-color: #a7f3d0;
}
.note-card-link.share {
background-color: #6bbbf7;
}
.note-card-link.edit {
background-color: #cce5ff;
}
.note-card-link.delete,
.note-card-link.report {
background-color: #ffcccc;
}
/* Mobile Sidebar Adaptation */
@media (max-width: 768px) {
.sidebar { .sidebar {
width: 100%; width: calc(100% - 24px);
max-width: none; margin: 12px;
top: auto; top: auto;
bottom: 0; bottom: 12px;
left: 0; left: 0;
right: 0; right: 0;
height: 60vh; height: 45vh;
border-radius: 20px 20px 0 0; border-radius: var(--radius-lg);
box-shadow: 0 -4px 15px rgba(0,0,0,0.15);
transform: translateY(0); transform: translateY(0);
transition: transform 0.3s ease-in-out;
} }
.sidebar.collapsed { .sidebar.collapsed {
transform: translateY(100%); transform: translateY(calc(100% + 20px));
} }
.sidebar-toggle { .sidebar-toggle {
left: 50%; left: 50%;
top: -30px; right: auto;
top: -40px;
width: 48px;
height: 40px;
border: 1px solid var(--border-glass);
border-bottom: none;
border-radius: var(--radius-md) var(--radius-md) 0 0;
transform: translateX(-50%) rotate(90deg); transform: translateX(-50%) rotate(90deg);
width: 60px; box-shadow: 0 -4px 10px rgba(0,0,0,0.05);
height: 30px; }
border-radius: 5px 0 0 5px; .sidebar-toggle:hover {
box-shadow: -2px 0 5px rgba(0,0,0,0.1); width: 48px;
left: 50%;
transform: translateX(-50%) rotate(90deg) scale(1.05);
} }
} }

View file

@ -1,339 +0,0 @@
/* Reset and Base Styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body, html {
height: 100%;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
overflow: hidden; /* Prevent scrollbars, map handles panning */
}
/* Map Container */
#map {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
/* Map Crosshair */
#map-crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 900;
}
#map-crosshair::before, #map-crosshair::after {
content: '';
position: absolute;
background: rgba(0,0,0,0.6);
}
#map-crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; }
#map-crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; }
/* Top Navigation Bar */
.navbar {
position: relative;
top: 0;
left: 0;
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(5px);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
z-index: 1000; /* Above map */
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.brand {
font-weight: bold;
font-size: 1.2rem;
color: #333;
text-decoration: none;
}
.nav-links a {
text-decoration: none;
color: #555;
margin-left: 20px;
font-size: 0.9rem;
transition: color 0.2s;
}
.nav-links a:hover {
color: #000;
}
/* Search Bar in Navbar */
.nav-search {
flex: 1;
margin: 0 20px;
display: flex;
max-width: 400px;
}
.nav-search input {
width: 100%;
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px 0 0 4px;
outline: none;
}
.nav-search button {
padding: 5px 10px;
border: 1px solid #ccc;
border-left: none;
background: #f8f9fa;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.nav-search button:hover {
background: #e2e6ea;
}
/* Right Collapsible Sidebar */
.sidebar {
position: absolute;
top: 60px; /* Below navbar */
right: 0;
bottom: 0;
width: 300px;
background-color: white;
z-index: 1000;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
transition: transform 0.3s ease-in-out;
display: flex;
flex-direction: column;
}
.sidebar.collapsed {
transform: translateX(300px);
}
/* Toggle Button attached to sidebar */
.sidebar-toggle {
position: absolute;
left: -40px;
top: 10px;
width: 40px;
height: 40px;
background-color: white;
border: none;
border-radius: 5px 0 0 5px;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
cursor: pointer;
font-weight: bold;
color: #555;
}
.sidebar-content {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.btn-new-pad {
background-color: #007bff;
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
}
/* Center Location Button */
.btn-center-location {
width: 100%;
padding: 10px;
margin-bottom: 15px;
background-color: #17a2b8;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.btn-center-location:hover { background-color: #138496; }
/* Add Note Button (Themed) */
.btn-add-note {
background-color: #28a745;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
cursor: pointer;
}
.btn-add-note:hover { background-color: #218838; }
/* --- Auth & Content Page Styles --- */
/* Use this class on body for login/register/about pages */
body.page-body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
background: #f4f4f4;
overflow: auto; /* Allow scrolling */
}
.auth-container, .content-container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: 100%;
}
.auth-container {
max-width: 400px;
}
.content-container {
max-width: 800px;
margin: 20px auto;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #333;
}
.form-group input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
width: 100%;
padding: 0.75rem;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn:hover { opacity: 0.9; }
.btn-primary { background: #007bff; }
.btn-success { background: #28a745; }
.links { margin-top: 1rem; text-align: center; font-size: 0.9rem; }
/* Ensure buttons on content pages are white */
.content-container .btn { color: white; }
/* --- Modals --- */
.modal {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.modal.hidden { display: none; }
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 400px;
position: relative;
}
.close-modal {
position: absolute;
top: 10px; right: 15px;
font-size: 1.5rem;
cursor: pointer;
color: #aaa;
}
.btn-secondary { background: #6c757d; margin-top: 10px; }
/* Sidebar Note Items */
.sidebar-note-item {
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.sidebar-note-item:hover {
background-color: #f9f9f9;
}
/* Map Note Card (Zoomed In) */
.map-note-card {
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
width: 200px;
font-size: 0.85rem;
border: 1px solid #ddd;
white-space: normal;
}
/* Sidebar Sections */
.list-section {
border-bottom: 1px solid #f0f0f0;
}
.list-section-header,
.btn-toggle-section {
width: 100%;
text-align: left;
background: #fff;
border: none;
padding: 12px 15px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
color: #555;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0;
transition: background-color 0.2s;
}
.list-section-header:hover,
.btn-toggle-section:hover {
background-color: #f8f9fa;
color: #333;
}
.notepad-item:hover {
background-color: #fafafa;
}

View file

@ -133,10 +133,12 @@ window.SpotNotes.loadNotepads = async function(isInitialLoad = false) {
// Reset Sidebar Header // Reset Sidebar Header
const header = document.querySelector('.sidebar-header'); const header = document.querySelector('.sidebar-header');
if (header) { if (header) {
let headerHTML = '<h2>Notepads</h2>'; let headerHTML = `<div style="display:flex; justify-content:space-between; align-items:center; width:100%;">
<h2>Notepads</h2>`;
if (window.SpotNotes.isLoggedIn) { if (window.SpotNotes.isLoggedIn) {
headerHTML += '<button class="btn-new-pad" id="btn-create-pad" title="Create New Pad">+</button>'; headerHTML += '<button class="btn-new-pad" id="btn-create-pad" title="Create New Pad">+</button>';
} }
headerHTML += '</div>';
header.innerHTML = headerHTML; header.innerHTML = headerHTML;
if (window.SpotNotes.isLoggedIn) { if (window.SpotNotes.isLoggedIn) {
@ -295,20 +297,28 @@ window.SpotNotes.loadNotesForPad = async function(padId, padName, isPublic, owne
const header = document.querySelector('.sidebar-header'); const header = document.querySelector('.sidebar-header');
if (header) { if (header) {
const addNoteBtn = window.SpotNotes.activePadCanWrite ? `<button onclick="openCreateNoteModal(${padId})" class="btn-add-note">+ Add Note</button>` : ''; const addNoteBtn = window.SpotNotes.activePadCanWrite ? `<button onclick="openCreateNoteModal(${padId})" class="btn-add-note">+ Add Note</button>` : '';
const manageShareBtn = isOwner ? `<button onclick="window.SpotNotes.openShareSettingsModal(${padId}, '${window.SpotNotes.escapeHtml(currentName)}')" style="border:none; background:none; cursor:pointer; margin-left:5px;" title="Manage Sharing">⚙️</button>` : ''; const manageShareBtn = isOwner ? `<button class="btn-action-icon" onclick="window.SpotNotes.openShareSettingsModal(${padId}, '${window.SpotNotes.escapeHtml(currentName)}')" 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>`; 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>`;
header.innerHTML = ` header.innerHTML = `
<div style="display:flex; align-items:center;"> <div class="sidebar-header-top" style="display:flex; align-items:center; justify-content:space-between; width:100%;">
<button id="btn-back-pads" style="background:none; border:none; font-size:1.5rem; cursor:pointer; margin-right:10px;">&larr;</button> <div style="display:flex; align-items:center; gap:8px; min-width:0;">
<h2 style="font-size:1.2rem; margin:0;">${window.SpotNotes.escapeHtml(currentName)}</h2> <button id="btn-back-pads" class="btn-action-icon" style="font-size:1.25rem; margin-right:4px;">&larr;</button>
<button onclick="window.SpotNotes.shareNotepad(${padId}, '${token || ''}')" style="border:none; background:none; cursor:pointer; margin-left:10px;" title="Share">🔗</button> <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)}</h2>
${manageShareBtn} </div>
${exportBtn} <div class="notepad-actions" style="display:flex; align-items:center; gap:4px; flex-shrink:0;">
${shareBtn}
${manageShareBtn}
${exportBtn}
</div>
</div> </div>
<div style="margin-top: 10px;"> <div style="margin-top: 12px; width:100%;">
<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;"> <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);">
</div>
<div style="margin-top: 10px; width:100%; display:flex; justify-content:space-between; align-items:center;">
${addNoteBtn}
</div> </div>
${addNoteBtn}
`; `;
document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads); document.getElementById('btn-back-pads').addEventListener('click', window.SpotNotes.loadNotepads);
} }
@ -526,25 +536,32 @@ window.SpotNotes.displaySearchResults = function(notes, location, query) {
entry.notes.forEach(note => { entry.notes.forEach(note => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'sidebar-note-item'; item.className = 'note-card';
const dateStr = new Date(note.created_at).toLocaleString(); const dateStr = new Date(note.created_at).toLocaleString();
let actionBtn = ''; // Renaming to actionBtns for consistency let actionBtn = '';
if (note.notepad && note.notepad.can_write) { if (note.notepad && note.notepad.can_write) {
const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '&quot;'); 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'); const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtn = `<button onclick="event.stopPropagation(); openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')" style="font-size:0.7rem; color:#007bff; border:none; background:none; cursor:pointer; text-decoration:underline;">Edit</button> <button onclick="event.stopPropagation(); window.SpotNotes.deleteNote(${note.id})" style="font-size:0.7rem; color:#dc3545; border:none; background:none; cursor:pointer; text-decoration:underline;">Delete</button>`; 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>`;
} else if (note.is_public) { } else if (note.is_public) {
actionBtn = `<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>`; actionBtn = `<button class="note-card-link report" onclick="event.stopPropagation(); window.SpotNotes.reportNote(${note.id})">Report</button>`;
} }
const shareToken = note.share_token || '';
const shareBtn = `<button class="note-card-link share" onclick="event.stopPropagation(); window.SpotNotes.shareNote(${note.id}, '${shareToken}')">Share</button>`;
item.innerHTML = ` item.innerHTML = `
<div style="font-weight:bold;">${window.SpotNotes.escapeHtml(note.title)}</div> <div class="note-card-title">${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 class="note-card-body">${window.SpotNotes.escapeHtml(note.content)}</div>
<div style="font-size:0.7rem; color:#888; margin-top:2px;"> <div class="note-card-meta">
${dateStr} <br> ${note.lat.toFixed(5)}, ${note.lng.toFixed(5)} ${dateStr} <br> ${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
</div> </div>
<div style="text-align:right; margin-top:4px;">${actionBtn}</div> <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}
</div>
`; `;
item.addEventListener('click', () => { item.addEventListener('click', () => {
window.SpotNotes.stopTracking(); window.SpotNotes.stopTracking();
@ -761,32 +778,36 @@ window.SpotNotes.renderActiveNotesList = function(filterText = '') {
filteredNotes.forEach(note => { filteredNotes.forEach(note => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'sidebar-note-item'; item.className = 'note-card';
let actionBtns = ''; let actionBtns = '';
if (window.SpotNotes.activePadCanWrite) { if (window.SpotNotes.activePadCanWrite) {
const escapedTitle = window.SpotNotes.escapeHtml(note.title).replace(/'/g, "\\'").replace(/"/g, '&quot;'); 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'); const escapedContent = window.SpotNotes.escapeHtml(note.content).replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/\n/g, '\\n');
actionBtns = ` 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 class="note-card-link edit" onclick="event.stopPropagation(); openEditNoteModal(${note.id}, '${escapedTitle}', '${escapedContent}')">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> <button class="note-card-link delete" onclick="event.stopPropagation(); window.SpotNotes.deleteNote(${note.id})">Delete</button>
`; `;
} else if (window.SpotNotes.activePadIsPublic) { } 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>`; actionBtns = `<button class="note-card-link report" onclick="event.stopPropagation(); window.SpotNotes.reportNote(${note.id})">Report</button>`;
} }
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>`;
const dateStr = new Date(note.created_at).toLocaleString(); const dateStr = new Date(note.created_at).toLocaleString();
item.innerHTML = ` item.innerHTML = `
<div style="font-weight:bold;">${window.SpotNotes.escapeHtml(note.title)}</div> <div class="note-card-title">${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 class="note-card-body">${window.SpotNotes.escapeHtml(note.content)}</div>
<div style="font-size:0.7rem; color:#888; margin-top:2px;"> <div class="note-card-meta">
${dateStr} <br> ${note.lat.toFixed(5)}, ${note.lng.toFixed(5)} ${dateStr} <br> ${note.lat.toFixed(5)}, ${note.lng.toFixed(5)}
</div> </div>
<div style="text-align:right; margin-top:4px;"> <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();" style="font-size:0.7rem; color:#28a745; text-decoration:underline; margin-right: 5px;">Directions</a> <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>
${actionBtns} ${shareBtn}
</div> ${actionBtns}
</div>
`; `;
item.addEventListener('click', () => { item.addEventListener('click', () => {
window.SpotNotes.stopTracking(); window.SpotNotes.stopTracking();

File diff suppressed because it is too large Load diff

View file

@ -66,50 +66,51 @@ window.SpotNotes.renderNotepadList = function(pads, containerId, isFavorite) {
container.innerHTML = ''; container.innerHTML = '';
if (pads.length === 0) { if (pads.length === 0) {
container.innerHTML = '<p style="padding:10px; color:#999; font-style:italic;">No notepads found.</p>'; container.innerHTML = '<p style="padding: 15px; color: var(--text-muted); font-style: italic; font-size: 0.9rem;">No notepads found.</p>';
return; return;
} }
pads.forEach(pad => { pads.forEach(pad => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'notepad-item'; item.className = 'notepad-card';
item.style.padding = '10px';
item.style.borderBottom = '1px solid #eee';
item.style.cursor = 'pointer';
let icon = pad.is_public ? '🌍' : '🔒'; let icon = pad.is_public ? '🌍' : '🔒';
let favButton = ''; let favButton = '';
const editBtn = (window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id) ? 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>` `<button class="btn-action-icon" onclick="event.stopPropagation(); openEditPadModal(${pad.id}, '${window.SpotNotes.escapeHtml(pad.name)}', ${pad.is_public}, '${pad.color || '#3388ff'}')" title="Edit Notepad">✏️</button>`
: ''; : '';
const isOwner = window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id; const isOwner = window.SpotNotes.currentUser && pad.owner_id === window.SpotNotes.currentUser.id;
const manageShareBtn = isOwner ? 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>` `<button class="btn-action-icon" onclick="event.stopPropagation(); openShareSettingsModal(${pad.id}, '${window.SpotNotes.escapeHtml(pad.name)}')" title="Manage Sharing">⚙️</button>`
: ''; : '';
if(window.SpotNotes.isLoggedIn){ if(window.SpotNotes.isLoggedIn){
const action = isFavorite ? 'unfavorite' : 'favorite'; const action = isFavorite ? 'unfavorite' : 'favorite';
const label = isFavorite ? '&hearts;' : '♡'; 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>`; favButton = `<button class="btn-action-icon btn-action-fav" onclick="event.stopPropagation(); window.SpotNotes.toggleFavorite(${pad.id}, '${action}')" title="${action === 'favorite' ? 'Favorite' : 'Unfavorite'}">${label}</button>`;
} }
const deleteBtn = isOwner ? 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>` `<button class="btn-action-icon btn-action-delete" onclick="event.stopPropagation(); window.SpotNotes.deleteNotepad(${pad.id})" title="Delete Notepad">&times;</button>`
: ''; : '';
const shareToken = pad.share_token || ''; 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>`; const shareBtn = `<button class="btn-action-icon" onclick="event.stopPropagation(); window.SpotNotes.shareNotepad(${pad.id}, '${shareToken}')" title="Share Notepad">🔗</button>`;
item.innerHTML = ` item.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center;"> <div class="notepad-card-header">
<div style="font-weight:bold;">${icon} ${window.SpotNotes.escapeHtml(pad.name)}</div> <div class="notepad-info">
<div style="display: flex; align-items: center;"> <span class="notepad-color-dot" style="background-color: ${pad.color || '#3388ff'}"></span>
${editBtn} ${manageShareBtn} ${shareBtn} ${favButton} <span>${icon} ${window.SpotNotes.escapeHtml(pad.name)}</span>
${deleteBtn}
</div> </div>
<div style="font-size:0.8rem; color:#666;">${pad.note_count || 0} notes</div> <div class="notepad-actions">
${editBtn} ${manageShareBtn} ${shareBtn} ${favButton} ${deleteBtn}
</div>
</div>
<div class="notepad-card-footer">
<span class="badge-count">${pad.note_count || 0} notes</span>
</div> </div>
`; `;