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);
// ── Notes ──────────────────────────────────────────────────────────────
const NOTES_KEY = 'dashboard_notes';
// ── Notes — server-backed so they sync across all devices ─────────────
const API = '/notes.php';
let _notes = [];
function loadNotes() {
return JSON.parse(localStorage.getItem(NOTES_KEY) || '[]');
function escHtml(s) {
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() {
const notes = loadNotes();
const list = document.getElementById('notes-list');
const list = document.getElementById('notes-list');
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>';
return;
}
list.innerHTML = notes.map((n, i) => `
<div class="note-item ${n.done ? 'note-done' : ''}" data-i="${i}">
<button class="note-check" onclick="toggleNote(${i})" title="${n.done ? 'Mark incomplete' : 'Mark done'}">
list.innerHTML = _notes.map(n => `
<div class="note-item ${n.done ? 'note-done' : ''}">
<button class="note-check" onclick="toggleNote('${n.id}')" title="${n.done ? 'Mark incomplete' : 'Mark done'}">
${n.done ? '✅' : '<span class="note-circle"></span>'}
</button>
<div class="note-body">
@@ -433,49 +453,48 @@
${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>
<button class="note-del" onclick="deleteNote(${i})" title="Delete">✕</button>
<button class="note-del" onclick="deleteNote('${n.id}')" title="Delete">✕</button>
</div>`).join('');
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
}
function addNote() {
async function addNote() {
const txt = document.getElementById('note-input').value.trim();
const det = document.getElementById('note-detail').value.trim();
if (!txt) { document.getElementById('note-input').focus(); return; }
const notes = loadNotes();
notes.unshift({ text: txt, detail: det, done: false, ts: Date.now() });
saveNotes(notes);
document.getElementById('note-input').value = '';
document.getElementById('note-detail').value = '';
document.getElementById('note-detail').style.display = 'none';
document.getElementById('note-detail-toggle').textContent = '+ Add details';
renderNotes();
const btn = document.querySelector('.btn-note-add');
btn.disabled = true;
const r = await notesApi('add', { text: txt, detail: det });
btn.disabled = false;
if (r.ok) {
_notes.unshift(r.note);
document.getElementById('note-input').value = '';
document.getElementById('note-detail').value = '';
document.getElementById('note-detail').style.display = 'none';
document.getElementById('note-detail-toggle').textContent = '+ Add details';
renderNotes();
}
}
function toggleNote(i) {
const notes = loadNotes();
notes[i].done = !notes[i].done;
saveNotes(notes);
async function toggleNote(id) {
_notes = _notes.map(n => n.id === id ? {...n, done: !n.done} : n);
renderNotes();
await notesApi('toggle', { id });
}
function deleteNote(i) {
const notes = loadNotes();
notes.splice(i, 1);
saveNotes(notes);
async function deleteNote(id) {
_notes = _notes.filter(n => n.id !== id);
renderNotes();
await notesApi('delete', { id });
}
function clearDone() {
saveNotes(loadNotes().filter(n => !n.done));
async function clearDone() {
_notes = _notes.filter(n => !n.done);
renderNotes();
await notesApi('clear-done');
}
function toggleDetail() {
const d = document.getElementById('note-detail');
const d = document.getElementById('note-detail');
const btn = document.getElementById('note-detail-toggle');
const show = d.style.display === 'none';
d.style.display = show ? 'block' : 'none';
@@ -484,7 +503,11 @@
}
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 => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); addNote(); }
});