feat: notes backed by server API — syncs across all devices, reloads on tab focus

This commit is contained in:
2026-06-22 05:16:04 +00:00
parent 99132f168e
commit 88fc966dd6
+61 -38
View File
@@ -405,27 +405,47 @@
} }
tick(); setInterval(tick, 1000); tick(); setInterval(tick, 1000);
// ── Notes ────────────────────────────────────────────────────────────── // ── Notes — server-backed so they sync across all devices ─────────────
const NOTES_KEY = 'dashboard_notes'; const API = '/notes.php';
let _notes = [];
function loadNotes() { function escHtml(s) {
return JSON.parse(localStorage.getItem(NOTES_KEY) || '[]'); return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
} }
function saveNotes(notes) {
localStorage.setItem(NOTES_KEY, JSON.stringify(notes)); async function notesApi(action, body = {}) {
try {
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, ...body }),
credentials: 'same-origin',
});
return await r.json();
} catch (e) { return { ok: false }; }
}
async function fetchNotes() {
const el = document.getElementById('notes-list');
if (el && !_notes.length) el.innerHTML = '<div style="color:#8b90a8;font-size:.85rem">Loading…</div>';
try {
const r = await fetch(API + '?action=list', { credentials: 'same-origin' });
const d = await r.json();
_notes = d.notes || [];
} catch (e) { _notes = []; }
renderNotes();
} }
function renderNotes() { function renderNotes() {
const notes = loadNotes(); const list = document.getElementById('notes-list');
const list = document.getElementById('notes-list');
if (!list) return; if (!list) return;
if (!notes.length) { if (!_notes.length) {
list.innerHTML = '<div style="color:#8b90a8;font-size:.85rem;padding:.5rem 0">No notes yet. Add one above.</div>'; list.innerHTML = '<div style="color:#8b90a8;font-size:.85rem;padding:.5rem 0">No notes yet. Add one above.</div>';
return; return;
} }
list.innerHTML = notes.map((n, i) => ` list.innerHTML = _notes.map(n => `
<div class="note-item ${n.done ? 'note-done' : ''}" data-i="${i}"> <div class="note-item ${n.done ? 'note-done' : ''}">
<button class="note-check" onclick="toggleNote(${i})" title="${n.done ? 'Mark incomplete' : 'Mark done'}"> <button class="note-check" onclick="toggleNote('${n.id}')" title="${n.done ? 'Mark incomplete' : 'Mark done'}">
${n.done ? '✅' : '<span class="note-circle"></span>'} ${n.done ? '✅' : '<span class="note-circle"></span>'}
</button> </button>
<div class="note-body"> <div class="note-body">
@@ -433,49 +453,48 @@
${n.detail ? `<div class="note-detail">${escHtml(n.detail)}</div>` : ''} ${n.detail ? `<div class="note-detail">${escHtml(n.detail)}</div>` : ''}
<div class="note-meta">${new Date(n.ts).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}</div> <div class="note-meta">${new Date(n.ts).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}</div>
</div> </div>
<button class="note-del" onclick="deleteNote(${i})" title="Delete">✕</button> <button class="note-del" onclick="deleteNote('${n.id}')" title="Delete">✕</button>
</div>`).join(''); </div>`).join('');
} }
function escHtml(s) { async function addNote() {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
}
function addNote() {
const txt = document.getElementById('note-input').value.trim(); const txt = document.getElementById('note-input').value.trim();
const det = document.getElementById('note-detail').value.trim(); const det = document.getElementById('note-detail').value.trim();
if (!txt) { document.getElementById('note-input').focus(); return; } if (!txt) { document.getElementById('note-input').focus(); return; }
const notes = loadNotes(); const btn = document.querySelector('.btn-note-add');
notes.unshift({ text: txt, detail: det, done: false, ts: Date.now() }); btn.disabled = true;
saveNotes(notes); const r = await notesApi('add', { text: txt, detail: det });
document.getElementById('note-input').value = ''; btn.disabled = false;
document.getElementById('note-detail').value = ''; if (r.ok) {
document.getElementById('note-detail').style.display = 'none'; _notes.unshift(r.note);
document.getElementById('note-detail-toggle').textContent = '+ Add details'; document.getElementById('note-input').value = '';
renderNotes(); document.getElementById('note-detail').value = '';
document.getElementById('note-detail').style.display = 'none';
document.getElementById('note-detail-toggle').textContent = '+ Add details';
renderNotes();
}
} }
function toggleNote(i) { async function toggleNote(id) {
const notes = loadNotes(); _notes = _notes.map(n => n.id === id ? {...n, done: !n.done} : n);
notes[i].done = !notes[i].done;
saveNotes(notes);
renderNotes(); renderNotes();
await notesApi('toggle', { id });
} }
function deleteNote(i) { async function deleteNote(id) {
const notes = loadNotes(); _notes = _notes.filter(n => n.id !== id);
notes.splice(i, 1);
saveNotes(notes);
renderNotes(); renderNotes();
await notesApi('delete', { id });
} }
function clearDone() { async function clearDone() {
saveNotes(loadNotes().filter(n => !n.done)); _notes = _notes.filter(n => !n.done);
renderNotes(); renderNotes();
await notesApi('clear-done');
} }
function toggleDetail() { function toggleDetail() {
const d = document.getElementById('note-detail'); const d = document.getElementById('note-detail');
const btn = document.getElementById('note-detail-toggle'); const btn = document.getElementById('note-detail-toggle');
const show = d.style.display === 'none'; const show = d.style.display === 'none';
d.style.display = show ? 'block' : 'none'; d.style.display = show ? 'block' : 'none';
@@ -484,7 +503,11 @@
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
renderNotes(); fetchNotes();
// Re-sync when tab becomes visible (switching back from another device)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') fetchNotes();
});
document.getElementById('note-input').addEventListener('keydown', e => { document.getElementById('note-input').addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); addNote(); } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); addNote(); }
}); });