Files
proof-of-work/proof-of-work/components/programs/StartSessionButton.tsx
T
Keysat 3a5b929284 v1.1.0:1 — Programs UI (manual create / save / follow)
Schema
- Workout.programDayId added (nullable FK to ProgramDay) so a
  Workout logged from a program day can be tied back to the planned
  session for adherence analytics. Compat ALTER in entrypoint adds
  the column + index to existing /data; ON DELETE SET NULL so
  deleting a program doesn't remove historical workouts logged
  against it.
- Back-relation `workouts: Workout[]` added to ProgramDay.

API (proof-of-work/app/api/programs/...)
- GET    /api/programs                       — list user's programs
- POST   /api/programs                       — create with full nested
                                                 weeks/days/exercises
                                                 tree in one transaction
- GET    /api/programs/[id]                  — full tree
- PATCH  /api/programs/[id]                  — update metadata AND/OR
                                                 replace entire weeks
                                                 tree (same shape as
                                                 POST). UI editor + AI
                                                 apply flow share this.
- DELETE /api/programs/[id]                  — cascading
- POST   /api/programs/[id]/days/[dayId]/start
                                              — creates a Workout
                                                 pre-populated with
                                                 empty SetLogs (one per
                                                 planned set), tagged
                                                 with programDayId.

UI (proof-of-work/app/main/programs/...)
- /main/programs               — list with cards, today's-session
                                  callout, "active" badge
- /main/programs/new           — create form using ProgramEditor
- /main/programs/[id]          — detail + edit using same editor;
                                  today's-session card + Start button
                                  if program is active
- ProgramEditor component (components/programs/ProgramEditor.tsx) —
  expandable tree editor for weeks -> days -> exercises with
  per-row sets/reps/RPE/rest/notes fields + library exercise picker
- ProgramActions: delete button
- StartSessionButton: POSTs to start endpoint, redirects to new
  workout

Navigation
- "Programs" link added to bottom nav + sidebar (between Workouts
  and Exercises).
- /main/programs page itself shows the today's-session card; the
  same component pattern can be lifted into the dashboard later
  if we want.

lib/db/programs.ts
- getPrograms, getProgramById, getActivePrograms,
  computeTodaysSessionForProgram, getTodaysSession helpers.
- Today's session math: floor((todayUTC - startDateUTC) / 1day),
  weekNumber = floor(.../7) + 1, dayOfWeek = today.getUTCDay().
  Returns null if not started, past durationWeeks, or no day
  matching today's slot (= rest day).

Tests (tests/routes-programs.test.ts)
- 11 new tests covering: 401 unauthenticated, full-tree create
  with nested weeks+days+exercises, cross-user exerciseId
  rejection, list scoped to actor, GET detail returns 404 for
  another user's program, PATCH replace-tree atomicity,
  cascading DELETE, start-day Workout creation with the right
  number of empty SetLogs + programDayId stamped, start-day
  refused for cross-user program day.
- Total: 96 tests across 11 files.

This is the foundation for v1.1.0:2's AI-generated programs —
the AI will produce the same JSON shape POST /api/programs
already accepts, so the apply path is `editor.tsx + POST
/api/programs` with no new API surface.
2026-05-10 07:15:31 -05:00

56 lines
1.3 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Loader2 } from 'lucide-react';
export default function StartSessionButton({
programId,
dayId,
}: {
programId: string;
dayId: string;
}) {
const router = useRouter();
const [busy, setBusy] = useState(false);
const handleClick = async () => {
setBusy(true);
try {
const res = await fetch(
`/api/programs/${programId}/days/${dayId}/start`,
{ method: 'POST' },
);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
alert(body.error ?? `HTTP ${res.status}`);
setBusy(false);
return;
}
const workout = await res.json();
router.push(`/main/workouts/${workout.id}`);
} catch (e) {
alert((e as Error).message);
setBusy(false);
}
};
return (
<button
type="button"
onClick={handleClick}
disabled={busy}
className="mt-3 inline-block text-xs uppercase tracking-wider px-3 py-1.5 rounded bg-emerald-700 hover:bg-emerald-600 text-white font-bold disabled:opacity-50"
>
{busy ? (
<>
<Loader2 className="inline w-3.5 h-3.5 animate-spin mr-1" />
Starting...
</>
) : (
'Start this session →'
)}
</button>
);
}