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",
|
"poo": "Poo",
|
||||||
"weight": "Weigh-in",
|
"weight": "Weigh-in",
|
||||||
"training": "Training",
|
"training": "Training",
|
||||||
|
"note": "Note",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- photos: IndexedDB store ----------
|
// ---------- 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 ----------
|
// ---------- lightbox ----------
|
||||||
const lightbox = document.getElementById("lightbox");
|
const lightbox = document.getElementById("lightbox");
|
||||||
const lightboxImg = document.getElementById("lightbox-img");
|
const lightboxImg = document.getElementById("lightbox-img");
|
||||||
@@ -1973,6 +2021,7 @@
|
|||||||
renderHourHeatmap(events);
|
renderHourHeatmap(events);
|
||||||
renderTraining(events);
|
renderTraining(events);
|
||||||
renderWeight(events);
|
renderWeight(events);
|
||||||
|
renderNotes(events);
|
||||||
renderHistory(events);
|
renderHistory(events);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2204,11 +2253,21 @@
|
|||||||
function openNoteDialog(type) {
|
function openNoteDialog(type) {
|
||||||
pendingType = type;
|
pendingType = type;
|
||||||
noteInput.value = "";
|
noteInput.value = "";
|
||||||
noteTimeEdited = false;
|
const isNote = type === "note";
|
||||||
const now = Date.now();
|
// A note is about a day, so default it to the day you're viewing (at the
|
||||||
noteDate.value = toDateInput(now);
|
// current clock time). The logging types default to "now"; leaving
|
||||||
noteTime.value = toTimeInput(now);
|
// noteTimeEdited false lets noteDialogAt() stamp the exact instant.
|
||||||
noteTitle.textContent = `Log ${EVENT_LABELS[type]}`;
|
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 isWeight = type === "weight";
|
||||||
const isEat = type === "eat";
|
const isEat = type === "eat";
|
||||||
noteWeightField.hidden = !isWeight;
|
noteWeightField.hidden = !isWeight;
|
||||||
@@ -2258,6 +2317,10 @@
|
|||||||
document.getElementById("note-save").addEventListener("click", async (e) => {
|
document.getElementById("note-save").addEventListener("click", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!pendingType) { noteDialog.close(); return; }
|
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;
|
let weight;
|
||||||
if (pendingType === "weight") {
|
if (pendingType === "weight") {
|
||||||
weight = parseFloat(noteWeight.value);
|
weight = parseFloat(noteWeight.value);
|
||||||
@@ -3310,9 +3373,9 @@
|
|||||||
document.querySelectorAll("button.action").forEach(btn => {
|
document.querySelectorAll("button.action").forEach(btn => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
const type = btn.dataset.type;
|
const type = btn.dataset.type;
|
||||||
// Weigh-ins need a typed value and meals ask for grams, so those two
|
// Weigh-ins need a typed value, meals ask for grams, and a note is all
|
||||||
// keep the full dialog.
|
// free text — so these open the full dialog instead of one-tap logging.
|
||||||
if (type === "weight" || type === "eat") { openNoteDialog(type); return; }
|
if (type === "weight" || type === "eat" || type === "note") { openNoteDialog(type); return; }
|
||||||
quickLog(type);
|
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-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": "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\")" },
|
{ "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 pee" data-type="pee">💧 Pee</button>
|
||||||
<button class="action poo" data-type="poo">💩 Poo</button>
|
<button class="action poo" data-type="poo">💩 Poo</button>
|
||||||
<button class="action weight" data-type="weight">⚖️ Weigh-in</button>
|
<button class="action weight" data-type="weight">⚖️ Weigh-in</button>
|
||||||
|
<button class="action note" data-type="note">📝 Note</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -261,6 +262,12 @@
|
|||||||
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
|
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
|
||||||
</section>
|
</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">
|
<section class="history" data-panel="history">
|
||||||
<h2>History</h2>
|
<h2>History</h2>
|
||||||
<ul id="event-list" class="event-list"></ul>
|
<ul id="event-list" class="event-list"></ul>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
--poo: #8a5a3b;
|
--poo: #8a5a3b;
|
||||||
--weight: #2bb3a3;
|
--weight: #2bb3a3;
|
||||||
--training: #b04ecf;
|
--training: #b04ecf;
|
||||||
|
--note: #6f7a90;
|
||||||
--danger: #d64545;
|
--danger: #d64545;
|
||||||
--gain: #2e9e5b;
|
--gain: #2e9e5b;
|
||||||
--border: #e9e6f5;
|
--border: #e9e6f5;
|
||||||
@@ -266,6 +267,7 @@ button.action.eat { background: var(--eat); }
|
|||||||
button.action.pee { background: var(--pee); color: #2b240a; }
|
button.action.pee { background: var(--pee); color: #2b240a; }
|
||||||
button.action.poo { background: var(--poo); }
|
button.action.poo { background: var(--poo); }
|
||||||
button.action.weight { background: var(--weight); }
|
button.action.weight { background: var(--weight); }
|
||||||
|
button.action.note { background: var(--note); }
|
||||||
/* Unlikely given the current sleep state (see renderActionHints) — dimmed
|
/* Unlikely given the current sleep state (see renderActionHints) — dimmed
|
||||||
but fully tappable, so corrections are never blocked. */
|
but fully tappable, so corrections are never blocked. */
|
||||||
button.action.unlikely { opacity: 0.4; }
|
button.action.unlikely { opacity: 0.4; }
|
||||||
@@ -435,11 +437,16 @@ textarea { resize: vertical; }
|
|||||||
.event[data-type="poo"] .dot { background: var(--poo); }
|
.event[data-type="poo"] .dot { background: var(--poo); }
|
||||||
.event[data-type="weight"] .dot { background: var(--weight); }
|
.event[data-type="weight"] .dot { background: var(--weight); }
|
||||||
.event[data-type="training"] .dot { background: var(--training); }
|
.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 .time { font-variant-numeric: tabular-nums; color: var(--muted); min-width: 60px; }
|
||||||
.event .label { font-weight: 600; min-width: 110px; }
|
.event .label { font-weight: 600; min-width: 110px; }
|
||||||
.event .note { color: var(--muted); font-size: 0.9rem; flex: 1; }
|
.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 {
|
.empty {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user