lift-free.openapps.xyz

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.
Owned app · rev f9f4e37d6945
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.
← All files
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
f9f4e37d69 · 17 hours ago
Add favicon.ico, local share image and openapi.json for links and guides
2796c0063c · 17 hours ago
Bind agent address in policy constants
cf80a3ca66 · 19 hours ago
Add OpenApp contribution strip and OpenApps source links
5e4cc2b9f3 · 19 hours ago
Bounded source sync
ca4a3d3631 · 1 day ago
Bounded source sync
7351063f4d · 1 day ago
Bounded source sync
0ce3238c46 · 1 day ago