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:
+56
-33
@@ -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,'&').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() {
|
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,45 +453,44 @@
|
|||||||
${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,'&').replace(/</g,'<').replace(/>/g,'>').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 });
|
||||||
|
btn.disabled = false;
|
||||||
|
if (r.ok) {
|
||||||
|
_notes.unshift(r.note);
|
||||||
document.getElementById('note-input').value = '';
|
document.getElementById('note-input').value = '';
|
||||||
document.getElementById('note-detail').value = '';
|
document.getElementById('note-detail').value = '';
|
||||||
document.getElementById('note-detail').style.display = 'none';
|
document.getElementById('note-detail').style.display = 'none';
|
||||||
document.getElementById('note-detail-toggle').textContent = '+ Add details';
|
document.getElementById('note-detail-toggle').textContent = '+ Add details';
|
||||||
renderNotes();
|
renderNotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNote(i) {
|
|
||||||
const notes = loadNotes();
|
|
||||||
notes[i].done = !notes[i].done;
|
|
||||||
saveNotes(notes);
|
|
||||||
renderNotes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteNote(i) {
|
async function toggleNote(id) {
|
||||||
const notes = loadNotes();
|
_notes = _notes.map(n => n.id === id ? {...n, done: !n.done} : n);
|
||||||
notes.splice(i, 1);
|
|
||||||
saveNotes(notes);
|
|
||||||
renderNotes();
|
renderNotes();
|
||||||
|
await notesApi('toggle', { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearDone() {
|
async function deleteNote(id) {
|
||||||
saveNotes(loadNotes().filter(n => !n.done));
|
_notes = _notes.filter(n => n.id !== id);
|
||||||
renderNotes();
|
renderNotes();
|
||||||
|
await notesApi('delete', { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearDone() {
|
||||||
|
_notes = _notes.filter(n => !n.done);
|
||||||
|
renderNotes();
|
||||||
|
await notesApi('clear-done');
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDetail() {
|
function toggleDetail() {
|
||||||
@@ -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(); }
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user