feat: notes section with details, checkoff, timestamps, localStorage persistence

This commit is contained in:
2026-06-22 05:12:19 +00:00
parent 067ba0cbe8
commit 99132f168e
+199
View File
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').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>