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'});
|
||||
}
|
||||
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>
|
||||
|
||||
<!-- ── 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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user