3a5b929284
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.
56 lines
1.3 KiB
TypeScript
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>
|
|
);
|
|
}
|