Track five things a day for seventy-five days
The workout, the protein, the calories, the water, and three lines of gratitude. One page per day, a card of seventy-five tiles that fill by how many of the five landed, and a missed day left as a gap rather than a reset. Single Go binary with no dependencies. The log is one JSON file on a mounted volume, written atomically and never silently replaced when it fails to parse, since it is the one thing here that cannot be recreated. Auth is Traefik basic auth at the ingress; the app has no login of its own, so every POST checks Sec-Fetch-Site to stop another origin posting with those credentials. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
93
internal/web/templates/day.html
Normal file
93
internal/web/templates/day.html
Normal file
@@ -0,0 +1,93 @@
|
||||
{{template "head" .}}
|
||||
|
||||
<main class="page">
|
||||
|
||||
<header class="masthead">
|
||||
<nav class="masthead-nav">
|
||||
{{if .PrevDate}}
|
||||
<a class="step" href="/d/{{.PrevDate}}" rel="prev" aria-label="The day before">‹</a>
|
||||
{{else}}
|
||||
<span class="step is-spent" aria-hidden="true">‹</span>
|
||||
{{end}}
|
||||
|
||||
<div class="masthead-day">
|
||||
<p class="eyebrow">Day</p>
|
||||
{{if .View.Number}}
|
||||
<p class="masthead-count">{{.View.Number}}</p>
|
||||
<p class="masthead-of">of {{.Settings.Length}}</p>
|
||||
{{else}}
|
||||
<p class="masthead-count is-spent">·</p>
|
||||
<p class="masthead-of">outside the {{.Settings.Length}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .NextDate}}
|
||||
<a class="step" href="/d/{{.NextDate}}" rel="next" aria-label="The day after">›</a>
|
||||
{{else}}
|
||||
<span class="step is-spent" aria-hidden="true">›</span>
|
||||
{{end}}
|
||||
</nav>
|
||||
|
||||
<p class="masthead-date">
|
||||
{{if .View.IsToday}}<b>Today</b> · {{end}}{{dateLong .View.Time}}
|
||||
{{if not .View.IsToday}}<a class="masthead-back" href="/">Back to today</a>{{end}}
|
||||
</p>
|
||||
|
||||
{{if eq .Status "before"}}
|
||||
<p class="notice">Day 1 is {{dateLong .Settings.Start}}. Nothing counts until then.</p>
|
||||
{{else if eq .Status "after"}}
|
||||
<p class="notice">The {{.Settings.Length}} days are done. {{.Stats.Complete}} of them were perfect.</p>
|
||||
{{end}}
|
||||
</header>
|
||||
|
||||
<form class="log" method="post" action="/d/{{.View.Date}}" data-log>
|
||||
|
||||
<label class="band band-toggle{{if .Day.Workout}} is-met{{end}}" data-band="workout">
|
||||
<input class="band-check" type="checkbox" name="workout" value="1" {{if .Day.Workout}}checked{{end}}>
|
||||
<span class="band-label">Workout</span>
|
||||
<span class="band-state">
|
||||
<span class="state-off">Not yet</span>
|
||||
<span class="state-on">Done</span>
|
||||
</span>
|
||||
<span class="band-tick" aria-hidden="true"></span>
|
||||
</label>
|
||||
|
||||
{{range .Meters}}
|
||||
<div class="band band-meter{{if .Met}} is-met{{end}}{{if .Over}} is-over{{end}}"
|
||||
style="--fill:{{.Pct}}%" data-band="{{.Key}}" data-target="{{.Target}}" data-mode="{{.Mode}}">
|
||||
<span class="band-track" aria-hidden="true"><span class="band-fill"></span></span>
|
||||
<label class="band-label" for="f-{{.Key}}">{{.Label}}</label>
|
||||
<span class="band-entry">
|
||||
<input class="band-input" id="f-{{.Key}}" name="{{.Key}}" type="number"
|
||||
inputmode="numeric" min="0" step="1" value="{{num .Value}}" placeholder="—">
|
||||
<span class="band-unit">{{.Unit}}</span>
|
||||
</span>
|
||||
<span class="band-note">{{.Note}}</span>
|
||||
<span class="band-tick" aria-hidden="true"></span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<fieldset class="gratitude{{if .View.Marks.Gratitude}} is-met{{end}}" data-band="gratitude">
|
||||
<legend>Gratitude</legend>
|
||||
<p class="gratitude-hint">Three lines. Any size.</p>
|
||||
<ol class="gratitude-lines">
|
||||
{{range $i, $line := .Day.Gratitude}}
|
||||
<li>
|
||||
<input class="gratitude-input" type="text" name="gratitude{{add $i 1}}"
|
||||
value="{{$line}}" maxlength="280" autocomplete="off"
|
||||
aria-label="Gratitude {{add $i 1}} of 3">
|
||||
</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
</fieldset>
|
||||
|
||||
<div class="log-foot">
|
||||
<button class="save save-day" type="submit">Save the day</button>
|
||||
<p class="log-status" data-status role="status">{{.Flash}}</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{template "card" .}}
|
||||
|
||||
</main>
|
||||
{{template "foot" .}}
|
||||
12
internal/web/templates/error.html
Normal file
12
internal/web/templates/error.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{{template "head" .}}
|
||||
<main class="page page-narrow">
|
||||
<header class="masthead">
|
||||
<div class="masthead-day">
|
||||
<p class="eyebrow">Stop</p>
|
||||
<p class="masthead-count is-spent">·</p>
|
||||
</div>
|
||||
</header>
|
||||
<p class="alert">{{.Error}}</p>
|
||||
<p><a class="save save-quiet" href="/">Back to today</a></p>
|
||||
</main>
|
||||
{{template "foot" .}}
|
||||
107
internal/web/templates/partials.html
Normal file
107
internal/web/templates/partials.html
Normal file
@@ -0,0 +1,107 @@
|
||||
{{define "head"}}<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>{{.Title}} · 75</title>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#E6EEEF" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#0B1E27" media="(prefers-color-scheme: dark)">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="{{asset "/static/icon.svg"}}" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="{{asset "/static/icon.png"}}">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="75">
|
||||
<link rel="stylesheet" href="{{asset "/static/app.css"}}">
|
||||
</head>
|
||||
<body>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{define "foot"}}
|
||||
<footer class="footer">
|
||||
<a href="/">Today</a>
|
||||
<a href="/settings">Settings</a>
|
||||
</footer>
|
||||
<script src="{{asset "/static/app.js"}}" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{/* The card: seventy-five tiles, each filling from the bottom by how many of
|
||||
the five that day hit. Fifteen across so a phone gets five clean rows. */}}
|
||||
{{define "card"}}
|
||||
<section class="card">
|
||||
<h2 class="card-heading">The card</h2>
|
||||
|
||||
<ol class="card-grid">
|
||||
{{range .Card}}
|
||||
{{if .IsFuture}}
|
||||
<li class="tile tile-ahead" style="--score:0"><span class="tile-fill"></span></li>
|
||||
{{else}}
|
||||
<li class="tile{{if .IsToday}} tile-today{{end}}{{if .Marks.Complete}} tile-full{{end}}" style="--score:{{.Score}}" data-date="{{.Date}}">
|
||||
<a href="/d/{{.Date}}" title="{{tileTitle .}}"><span class="tile-fill"></span><span class="sr-only">{{tileTitle .}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ol>
|
||||
|
||||
<dl class="tally">
|
||||
<div><dt>Perfect</dt><dd>{{.Stats.Complete}}</dd></div>
|
||||
<div><dt>Streak</dt><dd>{{.Stats.Streak}}</dd></div>
|
||||
<div><dt>Left</dt><dd>{{.Stats.Remaining}}</dd></div>
|
||||
</dl>
|
||||
|
||||
<ul class="legend">
|
||||
<li><span>Workout</span><b>{{index .Stats.Marks 0}}</b></li>
|
||||
<li><span>Protein</span><b>{{index .Stats.Marks 1}}</b></li>
|
||||
<li><span>Calories</span><b>{{index .Stats.Marks 2}}</b></li>
|
||||
<li><span>Water</span><b>{{index .Stats.Marks 3}}</b></li>
|
||||
<li><span>Gratitude</span><b>{{index .Stats.Marks 4}}</b></li>
|
||||
</ul>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{/* The settings fields, shared by first-run setup and the settings page. */}}
|
||||
{{define "settings-fields"}}
|
||||
<div class="field">
|
||||
<label for="start_date">Day 1</label>
|
||||
<input id="start_date" name="start_date" type="date" value="{{.Settings.StartDate}}" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="length">Days</label>
|
||||
<input id="length" name="length" type="number" inputmode="numeric" min="1" max="3650" value="{{.Settings.Length}}" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="protein_target">Protein target</label>
|
||||
<div class="field-row">
|
||||
<input id="protein_target" name="protein_target" type="number" inputmode="numeric" min="1" max="1000" value="{{.Settings.ProteinTarget}}" required>
|
||||
<span class="field-unit">grams or more</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="calorie_target">Calorie target</label>
|
||||
<div class="field-row">
|
||||
<input id="calorie_target" name="calorie_target" type="number" inputmode="numeric" min="1" max="20000" value="{{.Settings.CalorieTarget}}" required>
|
||||
<select name="calorie_mode" aria-label="Calorie target direction">
|
||||
<option value="under" {{if ne .Settings.CalorieMode "over"}}selected{{end}}>at most</option>
|
||||
<option value="over" {{if eq .Settings.CalorieMode "over"}}selected{{end}}>at least</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="water_target">Water target</label>
|
||||
<div class="field-row">
|
||||
<input id="water_target" name="water_target" type="number" inputmode="numeric" min="1" max="1000" value="{{.Settings.WaterTarget}}" required>
|
||||
<span class="field-unit">fluid ounces (a gallon is 128)</span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
35
internal/web/templates/settings.html
Normal file
35
internal/web/templates/settings.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{template "head" .}}
|
||||
<main class="page page-narrow">
|
||||
|
||||
<header class="masthead">
|
||||
<div class="masthead-day">
|
||||
<p class="eyebrow">Settings</p>
|
||||
<p class="masthead-count">{{.Settings.Length}}</p>
|
||||
<p class="masthead-of">days from {{.Settings.StartDate}}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{if .Error}}<p class="alert">{{.Error}}</p>{{end}}
|
||||
{{if .Flash}}<p class="flash">{{.Flash}}</p>{{end}}
|
||||
|
||||
<form class="sheet" method="post" action="/settings">
|
||||
{{template "settings-fields" .}}
|
||||
<button class="save" type="submit">Save settings</button>
|
||||
<p class="sheet-note">Targets apply to the whole challenge, past days included. Move day 1 and every entry keeps its date but changes its number.</p>
|
||||
</form>
|
||||
|
||||
<section class="sheet">
|
||||
<h2 class="sheet-heading">Backup</h2>
|
||||
<p class="sheet-note">The log lives in one file on the server. Download a copy before you change anything you cannot retype.</p>
|
||||
<p><a class="save save-quiet" href="/export.json">Download the log</a></p>
|
||||
|
||||
<form method="post" action="/import" enctype="multipart/form-data" class="restore"
|
||||
data-confirm="Restoring replaces every day currently in the log. Continue?">
|
||||
<label for="backup">Restore from a download</label>
|
||||
<input id="backup" name="backup" type="file" accept="application/json,.json" required>
|
||||
<button class="save save-quiet" type="submit">Restore</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
{{template "foot" .}}
|
||||
23
internal/web/templates/setup.html
Normal file
23
internal/web/templates/setup.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{{template "head" .}}
|
||||
<main class="page page-narrow">
|
||||
|
||||
<header class="masthead">
|
||||
<div class="masthead-day">
|
||||
<p class="eyebrow">Starting</p>
|
||||
<p class="masthead-count">75</p>
|
||||
<p class="masthead-of">days</p>
|
||||
</div>
|
||||
<p class="masthead-date">Five things a day. Set what counts.</p>
|
||||
</header>
|
||||
|
||||
{{if .Error}}<p class="alert">{{.Error}}</p>{{end}}
|
||||
|
||||
<form class="sheet" method="post" action="/setup">
|
||||
{{template "settings-fields" .}}
|
||||
<button class="save" type="submit">Start the challenge</button>
|
||||
<p class="sheet-note">All of this is editable later. Changing a target rescores every day against the new one.</p>
|
||||
</form>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user