Add free-text day notes with an always-visible notes log

A new 📝 Note quick-action logs a free-text note (with a date, optional
photo, and any text), defaulting to the day you're viewing. Notes are a
distinct "note" event type, so they edit, sync and delete like any other
event, and a new Notes section lists them newest-first across every day —
independent of the day picker — so records like vaccinations stay visible
whatever day you're looking at.
This commit is contained in:
Alexander Heldt
2026-08-02 13:37:39 +00:00
parent 09d9d38c12
commit f9894abfc9
4 changed files with 86 additions and 8 deletions
+71 -8
View File
@@ -21,6 +21,7 @@
"poo": "Poo",
"weight": "Weigh-in",
"training": "Training",
"note": "Note",
};
// ---------- photos: IndexedDB store ----------
@@ -894,6 +895,53 @@
}
}
// Cross-day log of free-text notes (vaccinations, vet visits, milestones…),
// newest first. Unlike History this ignores the day picker so the record is
// always visible regardless of which day you're viewing.
function renderNotes(events) {
const notes = events
.filter(e => e.type === "note")
.sort((a, b) => b.at - a.at);
const list = document.getElementById("notes-list");
const empty = document.getElementById("notes-empty");
list.innerHTML = "";
if (notes.length === 0) { empty.hidden = false; return; }
empty.hidden = true;
const birthday = loadConfig().birthday;
for (const ev of notes) {
const li = document.createElement("li");
li.className = "event";
li.dataset.type = "note";
li.dataset.id = ev.id;
const dateStr = new Date(ev.at).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
const age = formatAgeWeeks(birthday, ev.at);
li.innerHTML = `
<span class="dot"></span>
<span class="note-date">${escapeText(age ? `${dateStr} · ${age}` : dateStr)}</span>
<span class="note-text"></span>
`;
li.querySelector(".note-text").textContent = ev.note || "";
li.addEventListener("click", () => openEditDialog(ev));
for (const pid of photoIdsOf(ev)) {
const img = document.createElement("img");
img.className = "thumb";
img.alt = "photo";
img.loading = "lazy";
img.dataset.photoId = pid;
img.addEventListener("click", (e) => {
e.stopPropagation();
openLightbox(pid);
});
li.appendChild(img);
photoSrc(pid).then(url => { if (url) img.src = url; });
}
list.appendChild(li);
}
}
// ---------- lightbox ----------
const lightbox = document.getElementById("lightbox");
const lightboxImg = document.getElementById("lightbox-img");
@@ -1973,6 +2021,7 @@
renderHourHeatmap(events);
renderTraining(events);
renderWeight(events);
renderNotes(events);
renderHistory(events);
}
@@ -2204,11 +2253,21 @@
function openNoteDialog(type) {
pendingType = type;
noteInput.value = "";
noteTimeEdited = false;
const now = Date.now();
noteDate.value = toDateInput(now);
noteTime.value = toTimeInput(now);
noteTitle.textContent = `Log ${EVENT_LABELS[type]}`;
const isNote = type === "note";
// A note is about a day, so default it to the day you're viewing (at the
// current clock time). The logging types default to "now"; leaving
// noteTimeEdited false lets noteDialogAt() stamp the exact instant.
let base = Date.now();
if (isNote) {
const day = selectedDay();
const now = new Date();
day.setHours(now.getHours(), now.getMinutes(), 0, 0);
base = day.getTime();
}
noteTimeEdited = isNote;
noteDate.value = toDateInput(base);
noteTime.value = toTimeInput(base);
noteTitle.textContent = isNote ? "Add note" : `Log ${EVENT_LABELS[type]}`;
const isWeight = type === "weight";
const isEat = type === "eat";
noteWeightField.hidden = !isWeight;
@@ -2258,6 +2317,10 @@
document.getElementById("note-save").addEventListener("click", async (e) => {
e.preventDefault();
if (!pendingType) { noteDialog.close(); return; }
if (pendingType === "note" && noteInput.value.trim() === "" && notePhotos.length === 0) {
alert("Write something for the note, or add a photo.");
return;
}
let weight;
if (pendingType === "weight") {
weight = parseFloat(noteWeight.value);
@@ -3310,9 +3373,9 @@
document.querySelectorAll("button.action").forEach(btn => {
btn.addEventListener("click", () => {
const type = btn.dataset.type;
// Weigh-ins need a typed value and meals ask for grams, so those two
// keep the full dialog.
if (type === "weight" || type === "eat") { openNoteDialog(type); return; }
// Weigh-ins need a typed value, meals ask for grams, and a note is all
// free text — so these open the full dialog instead of one-tap logging.
if (type === "weight" || type === "eat" || type === "note") { openNoteDialog(type); return; }
quickLog(type);
});
});
+1
View File
@@ -1,4 +1,5 @@
[
{ "date": "2026-08-02", "text": "Added free-text notes: tap 📝 Note to jot down things that happened on a day — vaccinations, vet visits, milestones — with a date, optional photo, and any text. All your notes are collected in a new Notes section that stays visible whatever day you're viewing, so you can see at a glance when things like a tick vaccination were done" },
{ "date": "2026-08-02", "text": "The Daily counts chart now has Pees / Poos / Meals checkboxes so you can focus on just the metrics you care about — untick the rest to see, say, only poos; your choice is remembered" },
{ "date": "2026-08-01", "text": "Logging a pee or poo now sets off 💧/💩 fireworks that shoot up from the bottom of the screen — a little celebration you can switch off in Settings (and it honours a reduced-motion preference)" },
{ "date": "2026-08-01", "text": "Tidied the header on long names and ages — the name now truncates instead of shoving the buttons, and the age reads as a compact \"16 wk · 3 mo 3 wk\"; weight-log rows are a single line again (\"Aug 1 · 16 wk\")" },
+7
View File
@@ -113,6 +113,7 @@
<button class="action pee" data-type="pee">💧 Pee</button>
<button class="action poo" data-type="poo">💩 Poo</button>
<button class="action weight" data-type="weight">⚖️ Weigh-in</button>
<button class="action note" data-type="note">📝 Note</button>
</div>
</section>
@@ -261,6 +262,12 @@
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
</section>
<section class="notes-log" data-panel="notes">
<h2>Notes</h2>
<ul id="notes-list" class="event-list"></ul>
<p id="notes-empty" class="empty">No notes yet. Use the 📝 Note button to jot down things like vaccinations or vet visits — they'll be listed here across every day.</p>
</section>
<section class="history" data-panel="history">
<h2>History</h2>
<ul id="event-list" class="event-list"></ul>
+7
View File
@@ -11,6 +11,7 @@
--poo: #8a5a3b;
--weight: #2bb3a3;
--training: #b04ecf;
--note: #6f7a90;
--danger: #d64545;
--gain: #2e9e5b;
--border: #e9e6f5;
@@ -266,6 +267,7 @@ button.action.eat { background: var(--eat); }
button.action.pee { background: var(--pee); color: #2b240a; }
button.action.poo { background: var(--poo); }
button.action.weight { background: var(--weight); }
button.action.note { background: var(--note); }
/* Unlikely given the current sleep state (see renderActionHints) dimmed
but fully tappable, so corrections are never blocked. */
button.action.unlikely { opacity: 0.4; }
@@ -435,11 +437,16 @@ textarea { resize: vertical; }
.event[data-type="poo"] .dot { background: var(--poo); }
.event[data-type="weight"] .dot { background: var(--weight); }
.event[data-type="training"] .dot { background: var(--training); }
.event[data-type="note"] .dot { background: var(--note); }
.event .time { font-variant-numeric: tabular-nums; color: var(--muted); min-width: 60px; }
.event .label { font-weight: 600; min-width: 110px; }
.event .note { color: var(--muted); font-size: 0.9rem; flex: 1; }
/* Notes log rows: a date instead of a time-of-day, then the note text. */
.event .note-date { font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums; }
.event .note-text { flex: 1; white-space: pre-wrap; overflow-wrap: anywhere; }
.empty {
color: var(--muted);
text-align: center;