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:
+71
-8
@@ -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,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\")" },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user