diff --git a/src/app.js b/src/app.js index fbc4480..9b1e0e9 100644 --- a/src/app.js +++ b/src/app.js @@ -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 = ` + + ${escapeText(age ? `${dateStr} Β· ${age}` : dateStr)} + + `; + 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); }); }); diff --git a/src/changelog.json b/src/changelog.json index a109b2f..14cf02d 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -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\")" }, diff --git a/src/index.html b/src/index.html index d57e449..fe6730d 100644 --- a/src/index.html +++ b/src/index.html @@ -113,6 +113,7 @@ + @@ -261,6 +262,12 @@

No weigh-ins logged yet.

+
+

Notes

+ +

No notes yet. Use the πŸ“ Note button to jot down things like vaccinations or vet visits β€” they'll be listed here across every day.

+
+

History

diff --git a/src/style.css b/src/style.css index 454fa6c..daac039 100644 --- a/src/style.css +++ b/src/style.css @@ -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;