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/Picker.tsx
// Choosing exercises: search the library and your own exercises, filter by muscle, pick one or
// several, or make a new exercise on the spot.
import { useMemo, useState } from "react";
import { EQUIPMENT, KINDS, MUSCLES, equipmentName, muscleName, type Equipment, type Kind, type Muscle } from "./lib/library";
import { randomId, type Custom } from "./lib/model";
import { allExercises, pastWorkouts, update, useStore } from "./lib/store";
import { Modal } from "./ui";
import { Check, Search } from "./icons";
export const MAX_NAME = 60;
/** Matches every word of the query against the name, muscle and equipment. */
export function matches(q: string, e: { name: string; muscle: Muscle; equipment: Equipment }) {
const hay = `${e.name} ${muscleName(e.muscle)} ${equipmentName(e.equipment)}`.toLowerCase();
return q.toLowerCase().split(/\s+/).filter(Boolean).every((w) => hay.includes(w));
}
export function MuscleChips({ value, onChange }: { value: Muscle | null; onChange: (m: Muscle | null) => void }) {
return (
<div className="chips" role="radiogroup" aria-label="Muscle group">
<button type="button" role="radio" aria-checked={value === null} className="chip-btn" onClick={() => onChange(null)}>All</button>
{MUSCLES.map((m) => (
<button key={m.id} type="button" role="radio" aria-checked={value === m.id} className="chip-btn" onClick={() => onChange(value === m.id ? null : m.id)}>{m.name}</button>
))}
</div>
);
}
export function Picker({ title, multi, onPick, onClose }: { title: string; multi: boolean; onPick: (ids: string[]) => void; onClose: () => void }) {
const s = useStore();
const [q, setQ] = useState("");
const [muscle, setMuscle] = useState<Muscle | null>(null);
const [chosen, setChosen] = useState<string[]>([]);
const [making, setMaking] = useState(false);
const recent = useMemo(() => {
const ids: string[] = [];
for (const w of pastWorkouts(s).slice(0, 12)) for (const i of w.items) if (!ids.includes(i.ex)) ids.push(i.ex);
return ids.slice(0, 8);
}, [s]);
const all = allExercises(s);
const list = all.filter((e) => (!muscle || e.muscle === muscle) && (!q.trim() || matches(q, e)));
const recentList = !q.trim() && !muscle ? recent.map((id) => all.find((e) => e.id === id)).filter((e): e is NonNullable<typeof e> => Boolean(e)) : [];
const pick = (id: string) => {
if (!multi) { onPick([id]); return; }
setChosen((c) => (c.includes(id) ? c.filter((x) => x !== id) : [...c, id]));
};
if (making) {
return (
<Modal title="New exercise" onClose={() => setMaking(false)}>
<CustomForm initialName={q.trim()} onDone={(id) => { setMaking(false); if (id) { if (multi) setChosen((c) => [...c, id]); else onPick([id]); } }} />
</Modal>
);
}
const row = (e: (typeof all)[number]) => {
const on = chosen.includes(e.id);
return (
<li key={e.id}>
<button type="button" className={`pick-row ${on ? "on" : ""}`} aria-pressed={multi ? on : undefined} onClick={() => pick(e.id)}>
<span className="grow">
<strong>{e.name}</strong>
<span className="muted small">{muscleName(e.muscle)} · {equipmentName(e.equipment)}{e.custom ? " · Yours" : ""}</span>
</span>
{multi && <span className="pick-check">{on && <Check size={16} />}</span>}
</button>
</li>
);
};
return (
<Modal title={title} onClose={onClose} footer={multi ? (
<>
<button type="button" className="btn btn-line" onClick={() => setMaking(true)}>New exercise</button>
<button type="button" className="btn grow" disabled={!chosen.length} onClick={() => onPick(chosen)}>
{chosen.length ? `Add ${chosen.length} exercise${chosen.length > 1 ? "s" : ""}` : "Pick exercises"}
</button>
</>
) : undefined}>
<div className="picker">
<label className="search-field">
<Search size={17} />
<input className="input" type="search" value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search 160+ exercises" aria-label="Search exercises" />
</label>
<MuscleChips value={muscle} onChange={setMuscle} />
{recentList.length > 0 && (
<>
<p className="section-label">Recent</p>
<ul className="pick-list">{recentList.map(row)}</ul>
<p className="section-label">All exercises</p>
</>
)}
<ul className="pick-list">{list.map(row)}</ul>
{!list.length && <p className="muted small">No exercise matches "{q}".</p>}
{(!multi || q.trim()) && (
<button type="button" className="link-btn" onClick={() => setMaking(true)}>{q.trim() ? `Make "${q.trim().slice(0, 40)}" an exercise` : "Make a new exercise"}</button>
)}
</div>
</Modal>
);
}
/** Adds or edits one of your own exercises. Calls onDone with its id, or null when cancelled. */
export function CustomForm({ initialName = "", edit, onDone }: { initialName?: string; edit?: Custom; onDone: (id: string | null) => void }) {
const [name, setName] = useState(edit?.name ?? initialName);
const [muscle, setMuscle] = useState<Muscle>(edit?.muscle ?? "chest");
const [equipment, setEquipment] = useState<Equipment>(edit?.equipment ?? "barbell");
const [kind, setKind] = useState<Kind>(edit?.kind ?? "w");
const save = (e: React.FormEvent) => {
e.preventDefault();
const n = name.trim().slice(0, MAX_NAME);
if (!n) return;
const id = edit?.id ?? `c-${randomId(8)}`;
update((s) => { s.custom[id] = { id, name: n, muscle, equipment, kind, custom: true, at: Date.now() }; });
onDone(id);
};
return (
<form className="stack" onSubmit={save}>
<label className="field"><span>Name</span><input className="input" value={name} maxLength={MAX_NAME} required onChange={(e) => setName(e.target.value)} placeholder="Cable Y raise" /></label>
<div className="two-col">
<label className="field"><span>Muscle group</span>
<select className="input" value={muscle} onChange={(e) => setMuscle(e.target.value as Muscle)}>{MUSCLES.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}</select>
</label>
<label className="field"><span>Equipment</span>
<select className="input" value={equipment} onChange={(e) => setEquipment(e.target.value as Equipment)}>{EQUIPMENT.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}</select>
</label>
</div>
<label className="field"><span>What you log</span>
<select className="input" value={kind} onChange={(e) => setKind(e.target.value as Kind)} disabled={Boolean(edit)}>{KINDS.map((k) => <option key={k.id} value={k.id}>{k.name}</option>)}</select>
{edit && <small>Fixed once an exercise exists, so your history keeps its meaning.</small>}
</label>
<div className="row">
<button type="submit" className="btn">{edit ? "Save" : "Add exercise"}</button>
<button type="button" className="btn btn-quiet" onClick={() => onDone(null)}>Cancel</button>
</div>
</form>
);
}
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