0009e63d23
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.
392 lines
18 KiB
HTML
392 lines
18 KiB
HTML
<!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>
|