mirror of
https://github.com/myronblair/web-dashboard
synced 2026-06-30 17:50:10 -05:00
feat: notes section with details, checkoff, timestamps, localStorage persistence
This commit is contained in:
+199
@@ -404,7 +404,206 @@
|
|||||||
now.toLocaleTimeString('en-US', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
now.toLocaleTimeString('en-US', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||||||
}
|
}
|
||||||
tick(); setInterval(tick, 1000);
|
tick(); setInterval(tick, 1000);
|
||||||
|
|
||||||
|
// ── Notes ──────────────────────────────────────────────────────────────
|
||||||
|
const NOTES_KEY = 'dashboard_notes';
|
||||||
|
|
||||||
|
function loadNotes() {
|
||||||
|
return JSON.parse(localStorage.getItem(NOTES_KEY) || '[]');
|
||||||
|
}
|
||||||
|
function saveNotes(notes) {
|
||||||
|
localStorage.setItem(NOTES_KEY, JSON.stringify(notes));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNotes() {
|
||||||
|
const notes = loadNotes();
|
||||||
|
const list = document.getElementById('notes-list');
|
||||||
|
if (!list) return;
|
||||||
|
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'}">
|
||||||
|
${n.done ? '✅' : '<span class="note-circle"></span>'}
|
||||||
|
</button>
|
||||||
|
<div class="note-body">
|
||||||
|
<div class="note-text">${escHtml(n.text)}</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>
|
||||||
|
<button class="note-del" onclick="deleteNote(${i})" title="Delete">✕</button>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNote(i) {
|
||||||
|
const notes = loadNotes();
|
||||||
|
notes[i].done = !notes[i].done;
|
||||||
|
saveNotes(notes);
|
||||||
|
renderNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteNote(i) {
|
||||||
|
const notes = loadNotes();
|
||||||
|
notes.splice(i, 1);
|
||||||
|
saveNotes(notes);
|
||||||
|
renderNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDone() {
|
||||||
|
saveNotes(loadNotes().filter(n => !n.done));
|
||||||
|
renderNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDetail() {
|
||||||
|
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';
|
||||||
|
btn.textContent = show ? '− Hide details' : '+ Add details';
|
||||||
|
if (show) d.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
renderNotes();
|
||||||
|
document.getElementById('note-input').addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); addNote(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- ── NOTES SECTION ─────────────────────────────────────────────────────── -->
|
||||||
|
<style>
|
||||||
|
.notes-wrap {
|
||||||
|
max-width:1400px; margin:2rem auto 0; padding:0 1.5rem 3rem;
|
||||||
|
}
|
||||||
|
.notes-header {
|
||||||
|
display:flex; align-items:center; justify-content:space-between;
|
||||||
|
margin-bottom:1.25rem;
|
||||||
|
}
|
||||||
|
.notes-title {
|
||||||
|
font-size:.75rem; font-weight:700; text-transform:uppercase;
|
||||||
|
letter-spacing:1.5px; color:#8b90a8;
|
||||||
|
}
|
||||||
|
.notes-add {
|
||||||
|
background:var(--surface); border:1px solid var(--border);
|
||||||
|
border-radius:var(--radius); padding:1rem; margin-bottom:1rem;
|
||||||
|
}
|
||||||
|
.notes-add-row {
|
||||||
|
display:flex; gap:.6rem; align-items:center;
|
||||||
|
}
|
||||||
|
#note-input {
|
||||||
|
flex:1; background:var(--surface2); border:1px solid var(--border);
|
||||||
|
border-radius:6px; color:var(--text); padding:.6rem .85rem;
|
||||||
|
font-size:.9rem; font-family:inherit; outline:none;
|
||||||
|
transition:border-color .15s;
|
||||||
|
}
|
||||||
|
#note-input:focus { border-color:var(--accent); }
|
||||||
|
#note-detail {
|
||||||
|
width:100%; margin-top:.6rem; background:var(--surface2);
|
||||||
|
border:1px solid var(--border); border-radius:6px; color:var(--text);
|
||||||
|
padding:.6rem .85rem; font-size:.85rem; font-family:inherit;
|
||||||
|
outline:none; resize:vertical; min-height:70px;
|
||||||
|
transition:border-color .15s; display:none;
|
||||||
|
}
|
||||||
|
#note-detail:focus { border-color:var(--accent); }
|
||||||
|
.btn-note-add {
|
||||||
|
background:var(--accent); color:#fff; border:none; border-radius:6px;
|
||||||
|
padding:.6rem 1.1rem; font-size:.85rem; cursor:pointer; font-weight:600;
|
||||||
|
white-space:nowrap; transition:opacity .15s;
|
||||||
|
}
|
||||||
|
.btn-note-add:hover { opacity:.85; }
|
||||||
|
#note-detail-toggle {
|
||||||
|
background:none; border:none; color:#8b90a8; font-size:.78rem;
|
||||||
|
cursor:pointer; padding:.25rem 0; margin-top:.4rem;
|
||||||
|
transition:color .15s;
|
||||||
|
}
|
||||||
|
#note-detail-toggle:hover { color:var(--text); }
|
||||||
|
.note-item {
|
||||||
|
display:flex; align-items:flex-start; gap:.75rem;
|
||||||
|
padding:.8rem .75rem; border-radius:8px;
|
||||||
|
background:var(--surface); border:1px solid var(--border);
|
||||||
|
margin-bottom:.5rem; transition:opacity .2s, border-color .2s;
|
||||||
|
}
|
||||||
|
.note-item:hover { border-color:var(--accent); }
|
||||||
|
.note-done { opacity:.55; }
|
||||||
|
.note-check {
|
||||||
|
background:none; border:none; cursor:pointer; padding:.1rem;
|
||||||
|
font-size:1.1rem; flex-shrink:0; margin-top:.1rem;
|
||||||
|
}
|
||||||
|
.note-circle {
|
||||||
|
display:inline-block; width:18px; height:18px; border-radius:50%;
|
||||||
|
border:2px solid #8b90a8; vertical-align:middle;
|
||||||
|
transition:border-color .15s;
|
||||||
|
}
|
||||||
|
.note-item:hover .note-circle { border-color:var(--accent); }
|
||||||
|
.note-body { flex:1; min-width:0; }
|
||||||
|
.note-text {
|
||||||
|
font-size:.9rem; color:var(--text); line-height:1.45;
|
||||||
|
word-break:break-word;
|
||||||
|
}
|
||||||
|
.note-done .note-text { text-decoration:line-through; color:#8b90a8; }
|
||||||
|
.note-detail {
|
||||||
|
font-size:.8rem; color:#8b90a8; margin-top:.3rem; line-height:1.5;
|
||||||
|
}
|
||||||
|
.note-meta {
|
||||||
|
font-size:.72rem; color:#5a5f78; margin-top:.35rem;
|
||||||
|
}
|
||||||
|
.note-del {
|
||||||
|
background:none; border:none; color:#5a5f78; cursor:pointer;
|
||||||
|
font-size:.85rem; padding:.1rem .2rem; flex-shrink:0;
|
||||||
|
opacity:0; transition:opacity .15s, color .15s;
|
||||||
|
}
|
||||||
|
.note-item:hover .note-del { opacity:1; }
|
||||||
|
.note-del:hover { color:#ef4444; }
|
||||||
|
.btn-clear {
|
||||||
|
background:none; border:1px solid var(--border); color:#8b90a8;
|
||||||
|
border-radius:5px; padding:.3rem .7rem; font-size:.75rem;
|
||||||
|
cursor:pointer; transition:all .15s;
|
||||||
|
}
|
||||||
|
.btn-clear:hover { border-color:#ef4444; color:#ef4444; }
|
||||||
|
@media(max-width:600px){
|
||||||
|
.notes-wrap { padding:0 1rem 2rem; }
|
||||||
|
.notes-add-row { flex-direction:column; align-items:stretch; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="notes-wrap">
|
||||||
|
<div class="notes-header">
|
||||||
|
<div class="notes-title">📝 Notes</div>
|
||||||
|
<button class="btn-clear" onclick="clearDone()">Clear completed</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notes-add">
|
||||||
|
<div class="notes-add-row">
|
||||||
|
<input id="note-input" type="text" placeholder="Add a note — press Enter to save…">
|
||||||
|
<button class="btn-note-add" onclick="addNote()">Add</button>
|
||||||
|
</div>
|
||||||
|
<button id="note-detail-toggle" onclick="toggleDetail()">+ Add details</button>
|
||||||
|
<textarea id="note-detail" placeholder="Additional details…"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notes-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user