Files
puppy-tracker/src/index.html
T
Alexander Heldt 0009e63d23 Freeze the day picker at the top of the page
The day bar moves above everything else and sticks while scrolling, so
switching days never requires scrolling back up. A fixed strip painted
over the safe-area inset keeps content from peeking through above the
stuck bar on notched phones in standalone mode.
2026-07-13 20:39:06 +00:00

392 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#7c5cff" />
<title>Puppy Tracker</title>
<link rel="manifest" href="manifest.json" />
<link rel="icon" href="icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="icon.svg" />
<link rel="stylesheet" href="style.css" />
<script>
// Apply a saved theme before first paint so there's no light/dark flash.
// No saved choice → leave it to the prefers-color-scheme media query.
(function () {
try {
var t = localStorage.getItem("puppy-tracker:theme");
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
} catch (e) {}
})();
</script>
</head>
<body>
<!-- Shown when a newer build's service worker is waiting. "Reload" activates
it and refreshes onto the new assets; "Later" dismisses until next time.
Suppressed on the very first install (see app.js). -->
<div id="update-banner" class="update-banner" hidden role="status" aria-live="polite">
<div class="update-banner-row">
<span class="update-banner-msg">A new version is available</span>
<span class="update-banner-actions">
<button type="button" id="update-reload" class="update-banner-btn">Reload</button>
<button type="button" id="update-later" class="update-banner-btn ghostish">Later</button>
</span>
</div>
<!-- What the waiting build adds over the running one; filled by app.js
from the diff between the cached and the fresh changelog.json. -->
<ul id="update-changelog" class="update-changelog" hidden></ul>
</div>
<!-- Login / register gate. Shown until the session check succeeds; the app
(#app) stays hidden behind it so no puppy data paints while logged out. -->
<div id="auth-screen" class="auth-screen" hidden>
<div class="auth-card">
<h1>🐶 Puppy Tracker</h1>
<p class="auth-sub" id="auth-sub">Sign in to continue</p>
<form id="auth-form">
<label>Email
<input type="email" id="auth-email" autocomplete="username" required />
</label>
<label>Password
<input type="password" id="auth-password" autocomplete="current-password" required minlength="8" />
</label>
<label id="auth-invite-field" hidden>Invite code
<input type="text" id="auth-invite" autocomplete="off" placeholder="Ask the owner for this" />
</label>
<p id="auth-error" class="auth-error" hidden></p>
<button type="submit" id="auth-submit">Sign in</button>
</form>
<p class="auth-toggle">
<span id="auth-toggle-text">No account yet?</span>
<button type="button" id="auth-toggle-btn" class="linklike">Create one</button>
</p>
</div>
</div>
<div id="app" hidden>
<header>
<div class="title">
<h1 id="app-title">🐶 Puppy Tracker</h1>
<div id="puppy-age" class="puppy-age" hidden></div>
</div>
<div class="header-actions">
<button type="button" id="settings-btn" class="ghost icon-btn" aria-label="Settings" title="Settings">⚙️</button>
<button type="button" id="logout-btn" class="ghost icon-btn" aria-label="Log out" title="Log out">🚪</button>
<div id="online-status" class="status-pill"></div>
</div>
</header>
<main>
<section class="day-bar">
<button type="button" id="day-prev" class="ghost" aria-label="Previous day"></button>
<input type="date" id="day-picker" />
<button type="button" id="day-today" class="ghost">Today</button>
<button type="button" id="day-next" class="ghost" aria-label="Next day"></button>
</section>
<section id="big-clock" class="big-clock" hidden>
<div class="bc-label" id="bc-label"></div>
<div class="bc-time" id="bc-time">0:00</div>
<div class="bc-since" id="bc-since"></div>
</section>
<section class="quick-actions">
<h2>Log event</h2>
<div class="grid">
<button class="action sleep" data-type="sleep-start">😴 Sleep start</button>
<button class="action sleep" data-type="sleep-end">⏰ Sleep end</button>
<button class="action eat" data-type="eat">🍽️ Ate</button>
<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>
</div>
</section>
<section class="training" data-panel="training">
<h2>Training</h2>
<ul id="training-list" class="training-list"></ul>
<p id="training-empty" class="empty">No exercises yet. Add one to start tracking training.</p>
<button type="button" id="exercise-add" class="ghost training-add">Add exercise</button>
<div class="chart training-chart" id="training-chart-wrap" hidden>
<div class="chart-title">Consistency (last 14 days)</div>
<svg id="chart-training" class="chart-svg" viewBox="0 0 320 60" role="img" aria-label="Training sessions per exercise per day over the last 14 days"></svg>
<p class="muted-note">Darker = more sessions that day. Tap a cell to open that day.</p>
</div>
</section>
<section class="overview" data-panel="overview">
<h2 id="overview-title">Today's overview</h2>
<div class="stats">
<div class="stat">
<div class="stat-label">Sleep</div>
<div class="stat-value" id="stat-sleep">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Awake</div>
<div class="stat-value" id="stat-awake">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Meals</div>
<div class="stat-value" id="stat-meals">0</div>
<div class="stat-sub" id="stat-meals-grams" hidden></div>
</div>
<div class="stat">
<div class="stat-label">Pees</div>
<div class="stat-value" id="stat-pees">0</div>
</div>
<div class="stat">
<div class="stat-label">Poos</div>
<div class="stat-value" id="stat-poos">0</div>
</div>
<div class="stat">
<div class="stat-label">Training</div>
<div class="stat-value" id="stat-training">0</div>
</div>
</div>
<div class="lasts">
<div class="last-row"><span>Last pee</span><span id="last-pee"></span></div>
<div class="last-row"><span>Last poo</span><span id="last-poo"></span></div>
<div class="last-row"><span>Last meal</span><span id="last-eat"></span></div>
<div class="last-row"><span>Last sleep</span><span id="last-sleep"></span></div>
</div>
</section>
<section class="timing" data-panel="timing">
<h2>Bathroom timing <span class="muted-note">(last 7 days)</span></h2>
<div class="lasts">
<div class="last-row"><span>Typical time between pees</span><span id="gap-pee"></span></div>
<div class="last-row"><span>Shortest between pees</span><span id="gap-pee-min"></span></div>
<div class="last-row"><span>Typical time between poos</span><span id="gap-poo"></span></div>
<div class="last-row"><span>Shortest between poos</span><span id="gap-poo-min"></span></div>
</div>
<p class="muted-note timing-hint" id="timing-hint"></p>
</section>
<section class="sleep" data-panel="sleep-windows">
<h2>Sleep windows</h2>
<ul id="sleep-list" class="wake-list"></ul>
<p id="sleep-empty" class="empty">No sleep windows yet for this day.</p>
</section>
<section class="wake" data-panel="wake-windows">
<h2>Wake windows</h2>
<ul id="wake-list" class="wake-list"></ul>
<p id="wake-empty" class="empty">No wake windows yet for this day.</p>
</section>
<section class="weekly" data-panel="weekly">
<h2>Last 7 days</h2>
<div class="chart">
<div class="chart-title">Sleep (hours)</div>
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day for the last 7 days"></svg>
</div>
<div class="chart">
<div class="chart-title">Daily counts</div>
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day for the last 7 days"></svg>
<div class="legend">
<span class="lg pee"><span class="sw"></span>Pees</span>
<span class="lg poo"><span class="sw"></span>Poos</span>
<span class="lg eat"><span class="sw"></span>Meals</span>
</div>
</div>
<div class="chart" id="grams-chart-wrap" hidden>
<div class="chart-title">Food (grams)</div>
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day for the last 7 days"></svg>
</div>
</section>
<section class="patterns" data-panel="sleep-timeline">
<h2><span id="sleep-timeline-title">When sleeping</span> <span class="muted-note">(last 14 days)</span></h2>
<svg id="chart-sleep-timeline" class="chart-svg" viewBox="0 0 320 228" role="img" aria-label="Sleep periods per day over the last 14 days"></svg>
<p class="muted-note">Each row is a day, midnight to midnight; shaded = asleep. Tap a row to open that day.</p>
</section>
<section class="patterns" data-panel="hour-heatmap">
<h2>By hour of day <span class="muted-note">(last 14 days)</span></h2>
<svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day over the last 14 days"></svg>
<p class="muted-note">Darker = happens more often at that hour.</p>
</section>
<section class="weight" data-panel="weight">
<h2>Weight</h2>
<div class="weight-summary">
<div class="stat">
<div class="stat-label">Latest</div>
<div class="stat-value" id="weight-latest"></div>
</div>
<div class="stat">
<div class="stat-label">Since last</div>
<div class="stat-value" id="weight-change"></div>
</div>
</div>
<div class="chart">
<div class="chart-title">Weight (kg)</div>
<svg id="chart-weight" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Weight in kilograms over time"></svg>
<p id="weight-point-info" class="muted-note weight-point-info"></p>
</div>
<ul id="weight-list" class="wake-list"></ul>
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
</section>
<section class="history" data-panel="history">
<h2>History</h2>
<ul id="event-list" class="event-list"></ul>
<p id="empty-state" class="empty">No events logged for this day.</p>
</section>
</main>
<footer class="app-footer">
<button type="button" id="changelog-btn" class="linklike">Changelog</button>
</footer>
</div><!-- /#app -->
<dialog id="changelog-dialog">
<form method="dialog" id="changelog-form">
<h3>Changelog</h3>
<ul id="changelog-list" class="changelog-list"></ul>
<p id="changelog-empty" class="empty" hidden>No changelog available.</p>
<menu>
<button value="close">Close</button>
</menu>
</form>
</dialog>
<dialog id="settings-dialog">
<form method="dialog" id="settings-form">
<h3>Puppy settings</h3>
<label>Name
<input type="text" id="settings-name" placeholder="e.g. Rex" autocomplete="off" />
</label>
<label>Birthday
<input type="date" id="settings-birthday" />
</label>
<label class="toggle-row">
<span>Dark mode</span>
<input type="checkbox" id="settings-theme" role="switch" class="switch" />
</label>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button value="save" id="settings-save">Save</button>
</menu>
<hr class="settings-sep" />
<button type="button" id="delete-account-btn" class="danger danger-block">Delete account…</button>
</form>
</dialog>
<dialog id="delete-account-dialog">
<form method="dialog" id="delete-account-form">
<h3>Delete account</h3>
<p class="danger-text">
This permanently deletes your account and <strong>all its data</strong>
every event, photo and your puppy profile. This can't be undone.
</p>
<label>Confirm your password
<input type="password" id="delete-account-password" autocomplete="current-password" />
</label>
<p id="delete-account-error" class="auth-error" hidden></p>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button type="button" id="delete-account-confirm" class="danger">Delete forever</button>
</menu>
</form>
</dialog>
<dialog id="exercise-dialog">
<form method="dialog" id="exercise-form">
<h3 id="exercise-title">Add exercise</h3>
<label>Name
<input type="text" id="exercise-name" placeholder="e.g. Sit" autocomplete="off" />
</label>
<label>How to do it
<textarea id="exercise-note" rows="5" placeholder="Reminder for how to train it, e.g. lure with a treat, mark the moment the butt touches the ground, reward"></textarea>
</label>
<menu>
<!-- type="button" keeps Save the form's default button, so Enter /
the iOS keyboard's "Go" saves instead of silently hitting the
(hidden) Delete button via implicit form submission. -->
<button type="button" value="delete" id="exercise-delete" class="danger" hidden>Delete</button>
<button type="button" value="cancel" class="ghost">Cancel</button>
<button value="save" id="exercise-save">Save</button>
</menu>
</form>
</dialog>
<dialog id="note-dialog">
<form method="dialog" id="note-form">
<h3 id="note-title">Add note</h3>
<label>Time
<div class="time-row">
<input type="date" id="note-date" />
<input type="time" id="note-time" lang="en-GB" />
<button type="button" id="note-time-now" class="ghost">Now</button>
</div>
</label>
<label id="note-weight-field" hidden>Weight (kg)
<input type="number" id="note-weight" inputmode="decimal" step="0.01" min="0" placeholder="e.g. 5.2" />
</label>
<label id="note-grams-field" hidden>Amount (g)
<input type="number" id="note-grams" inputmode="numeric" step="1" min="0" placeholder="e.g. 80 — leave empty if unknown" />
</label>
<label>Note
<textarea id="note-input" rows="4" placeholder="e.g. pee was instant, poo took 5min, ate 300g raw food"></textarea>
</label>
<div class="photo-field">
<input type="file" id="note-photo-input" accept="image/*" multiple hidden />
<button type="button" id="note-photo-btn" class="ghost">📷 Add photos</button>
<div id="note-photo-preview" class="photo-preview" hidden></div>
</div>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button value="save" id="note-save">Save</button>
</menu>
</form>
</dialog>
<dialog id="edit-dialog">
<form method="dialog" id="edit-form">
<h3>Edit event</h3>
<label>Time
<div class="time-row">
<input type="date" id="edit-date" />
<input type="time" id="edit-time" lang="en-GB" />
</div>
</label>
<label id="edit-weight-field" hidden>Weight (kg)
<input type="number" id="edit-weight" inputmode="decimal" step="0.01" min="0" />
</label>
<label id="edit-grams-field" hidden>Amount (g)
<input type="number" id="edit-grams" inputmode="numeric" step="1" min="0" />
</label>
<label>Note
<textarea id="edit-note" rows="4"></textarea>
</label>
<div class="photo-field">
<input type="file" id="edit-photo-input" accept="image/*" multiple hidden />
<button type="button" id="edit-photo-btn" class="ghost">📷 Add photos</button>
<div id="edit-photo-preview" class="photo-preview" hidden></div>
</div>
<menu>
<button value="delete" id="edit-delete" class="danger">Delete</button>
<button value="cancel" class="ghost">Cancel</button>
<button value="save">Save</button>
</menu>
</form>
</dialog>
<dialog id="lightbox">
<button type="button" id="lightbox-close" class="lightbox-close" aria-label="Close">×</button>
<img id="lightbox-img" alt="" />
</dialog>
<!-- Brief confirmation after a one-tap quick log, with Undo / Add note. -->
<div id="snackbar" class="snackbar" hidden role="status" aria-live="polite">
<span id="snackbar-msg" class="snackbar-msg"></span>
<button type="button" id="snackbar-note" class="snackbar-action">Add note</button>
<button type="button" id="snackbar-undo" class="snackbar-action">Undo</button>
</div>
<script src="app.js"></script>
</body>
</html>