Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 23e397d8a5d5bf7756362640
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/History.tsx
// History: every finished workout, newest first, and one workout in full with its records. A
// finished workout can be edited, repeated, saved as a routine or deleted.
import { useState } from "react";
import { e1rm, fmtDay, fmtDuration, routineFromWorkout, setLabel, setText, showKg, workoutFromRoutine, workoutStats, RECORD_NAMES, type Workout } from "./lib/model";
import { exerciseOf, pastWorkouts, saveRoutine, update, useStore, workoutRecords } from "./lib/store";
import { navigate } from "./lib/router";
import { A, Confirm, toast } from "./ui";
import { Logger } from "./Logger";
import { useStarter } from "./Workout";
import { Back, Trophy } from "./icons";
const monthName = (ms: number) => new Date(ms).toLocaleDateString(undefined, { month: "long", year: "numeric" });
export function HistoryPage() {
const s = useStore();
const past = pastWorkouts(s);
const recs = workoutRecords(s);
const unit = s.settings.unit;
const months: { name: string; list: Workout[] }[] = [];
for (const w of past) {
const name = monthName(w.start);
if (months[months.length - 1]?.name !== name) months.push({ name, list: [] });
months[months.length - 1].list.push(w);
}
return (
<div className="page">
<header className="page-head"><div><p className="eyebrow">History</p><h1 className="h1">Your workouts</h1></div>{past.length > 0 && <p className="muted small">{past.length} workout{past.length > 1 ? "s" : ""}</p>}</header>
{!past.length && (
<div className="empty">
<img className="art empty-art" src="/art/card-lift.webp" alt="" width="200" height="163" />
<p className="h3">No workouts yet</p>
<p className="muted small">Finished workouts show up here with their sets, time, volume and records.</p>
<A href="/workout" className="btn btn-sm">Start a workout</A>
</div>
)}
{months.map((m) => (
<section key={m.name} className="stack" style={{ ["--gap" as string]: "8px" }}>
<h2 className="section-label">{m.name} <span className="faint">· {m.list.length}</span></h2>
<ul className="history-list">
{m.list.map((w) => {
const st = workoutStats(w, s.custom);
const n = recs.get(w.id)?.length ?? 0;
return (
<li key={w.id}>
<A href={`/history/${w.id}`} className="history-row">
<span className="spread"><strong>{w.name}</strong>{n > 0 && <span className="pr"><Trophy size={14} /> {n} record{n > 1 ? "s" : ""}</span>}</span>
<span className="muted small">{fmtDay(w.start)} · {fmtDuration(st.duration)} · {st.sets} set{st.sets === 1 ? "" : "s"}{st.volume ? ` · ${showKg(st.volume, unit)}` : ""}</span>
<span className="faint small history-ex">{w.items.map((i) => `${i.sets.filter((x) => x.t !== "w").length} × ${exerciseOf(i.ex, s).name}`).join(", ")}</span>
</A>
</li>
);
})}
</ul>
</section>
))}
</div>
);
}
export function WorkoutDetail({ id, saved }: { id: string; saved: boolean }) {
const s = useStore();
const w = s.workouts[id];
const [draft, setDraft] = useState<Workout | null>(null);
const [deleting, setDeleting] = useState(false);
const { start, ask } = useStarter();
const unit = s.settings.unit;
if (!w || w.deleted || !w.end) {
return (
<div className="page">
<A href="/history" className="back-link"><Back /> History</A>
<div className="empty"><p className="h3">This workout is not here</p><p className="muted small">It may have been deleted, or it was logged on another account.</p></div>
</div>
);
}
if (draft) {
return (
<div className="page workout">
<header className="workout-head">
<div className="grow">
<input className="workout-name" value={draft.name} maxLength={60} aria-label="Workout name" onChange={(e) => setDraft({ ...draft, name: e.target.value })} />
<p className="muted small">Editing the workout from {fmtDay(draft.start)}. Unchecked sets are not saved.</p>
</div>
</header>
<Logger w={draft} live={false} change={(fn) => setDraft((d) => { if (!d) return d; const x = structuredClone(d); fn(x); return x; })} />
<div className="workout-foot">
<button type="button" className="btn btn-lg" onClick={() => {
const next = structuredClone(draft);
next.name = next.name.trim() || w.name;
next.items = next.items.map((i) => ({ ...i, sets: i.sets.filter((x) => x.done) })).filter((i) => i.sets.length);
next.at = Date.now();
update((x) => { x.workouts[id] = next; });
setDraft(null);
toast("Workout saved");
}}>Save changes</button>
<button type="button" className="btn btn-quiet" onClick={() => setDraft(null)}>Cancel</button>
</div>
</div>
);
}
const st = workoutStats(w, s.custom);
const recs = workoutRecords(s).get(w.id) ?? [];
const recSets = new Set(recs.map((r) => r.set.id));
return (
<div className="page detail">
<A href="/history" className="back-link"><Back /> History</A>
{saved && (
<div className="notice notice-ok saved-note">
<strong>Workout saved.</strong> {recs.length ? `You set ${recs.length} new record${recs.length > 1 ? "s" : ""}.` : "Nice work."}
</div>
)}
<header className="page-head">
<div>
<p className="eyebrow">{new Date(w.start).toLocaleDateString(undefined, { weekday: "long", day: "numeric", month: "long", year: "numeric" })} · {new Date(w.start).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}</p>
<h1 className="h1">{w.name}</h1>
</div>
</header>
<dl className="stats">
<div><dt>Time</dt><dd>{fmtDuration(st.duration)}</dd></div>
<div><dt>Sets</dt><dd>{st.sets}</dd></div>
<div><dt>Volume</dt><dd>{st.volume ? showKg(st.volume, unit) : "None"}</dd></div>
<div><dt>Records</dt><dd>{recs.length}</dd></div>
</dl>
{recs.length > 0 && (
<section className="card pad stack">
<h2 className="h3 icon-title"><Trophy size={18} /> New records</h2>
<ul className="rec-list">
{recs.map((r, k) => {
const ex = exerciseOf(r.ex, s);
return <li key={k}><A href={`/exercises/${r.ex}`}>{ex.name}</A><span className="muted small">{RECORD_NAMES[r.key]}: {r.key === "e1rm" ? `${showKg(e1rm(r.set.kg ?? 0, r.set.reps ?? 0), unit)} (${setText(r.set, ex.kind, unit)})` : setText(r.set, ex.kind, unit)}</span></li>;
})}
</ul>
</section>
)}
{w.note && <p className="workout-note-text">{w.note}</p>}
<div className="stack">
{w.items.map((item) => {
const ex = exerciseOf(item.ex, s);
return (
<section key={item.id} className={`card pad detail-item ${item.group !== undefined ? "ss" : ""}`}>
<A href={`/exercises/${item.ex}`} className="item-name">{ex.name}</A>
{item.note && <p className="muted small">{item.note}</p>}
<ol className="detail-sets">
{item.sets.map((x, j) => (
<li key={x.id} className={x.t === "w" ? "warm" : ""}>
<span className="set-badge">{setLabel(item, j)}</span>
<span className="grow">{setText(x, ex.kind, unit)}</span>
{ex.kind === "w" && x.t !== "w" && x.kg && x.reps ? <span className="faint small">1RM {showKg(e1rm(x.kg, x.reps), unit)}</span> : null}
{recSets.has(x.id) && <span className="pr" aria-label="Record"><Trophy size={14} /></span>}
</li>
))}
</ol>
</section>
);
})}
</div>
<div className="routine-actions">
<button type="button" className="btn" onClick={() => start(workoutFromRoutine({ ...routineFromWorkout(w), name: w.name }, w.routine))}>Do it again</button>
<button type="button" className="btn btn-line" onClick={() => setDraft(structuredClone(w))}>Edit</button>
{!w.routine || !s.routines[w.routine] || s.routines[w.routine].deleted ? (
<button type="button" className="btn btn-line" onClick={() => { const id = saveRoutine(routineFromWorkout(w)); toast("Saved as a routine"); navigate(`/routines/${id}`); }}>Save as routine</button>
) : (
<A href={`/routines/${w.routine}`} className="btn btn-line">Open routine</A>
)}
<button type="button" className="btn btn-quiet btn-danger" onClick={() => setDeleting(true)}>Delete</button>
</div>
{deleting && <Confirm title="Delete this workout?" text="Its sets and records come out of your history on every device." yes="Delete" danger onYes={() => { update((x) => { x.workouts[id] = { id, name: "", start: w.start, end: w.end, items: [], at: Date.now(), deleted: true }; }); navigate("/history"); }} onClose={() => setDeleting(false)} />}
{ask}
</div>
);
}
Change history
Point OpenApps store links to openapps.free
Add favicon.ico, local share image and openapi.json for links and guides
Bind agent address in policy constants
Add OpenApp contribution strip and OpenApps source links
Bounded source sync
Bounded source sync
Bounded source sync