mirror of
https://github.com/myronblair/web-dashboard
synced 2026-06-30 17:50:10 -05:00
feat: notes backed by server API — syncs across all devices, reloads on tab focus
This commit is contained in:
+61
-38
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').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,'&').replace(/</g,'<').replace(/>/g,'>').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(); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user