Files
knur-app/app/strength/[id]/page.tsx
Dominik Klarkowski 21e5db3409 init
2026-06-16 11:11:19 +02:00

111 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { notFound } from "next/navigation";
import { AiAnalysisCard } from "@/components/ai-analysis-card";
import { ExerciseProgressChart } from "@/components/exercise-progress-chart";
import { InfoTooltip } from "@/components/info-tooltip";
import { formatDate, formatDateShort } from "@/lib/format";
import { getLatestAnalysisForTarget } from "@/lib/models/analysis";
import { getStrengthWorkout, listStrengthWorkouts } from "@/lib/models/strength";
import { exerciseE1rm, getExerciseHistory } from "@/lib/strength/stats";
export const dynamic = "force-dynamic";
const EXERCISE_HISTORY_LIMIT = 8;
export default async function StrengthWorkoutPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const workout = await getStrengthWorkout(id);
if (!workout) {
notFound();
}
const analysis = await getLatestAnalysisForTarget("strength", workout._id);
const allWorkouts = await listStrengthWorkouts();
const pastWorkouts = allWorkouts.filter((w) => w.date <= workout.date);
const exercisesWithHistory = workout.exercises.map((exercise) => ({
exercise,
history: getExerciseHistory(exercise.name, pastWorkouts, EXERCISE_HISTORY_LIMIT),
}));
return (
<div className="flex flex-col gap-5">
<div>
<h1 className="text-2xl font-bold text-fg">{workout.name}</h1>
<p className="mt-1 text-sm text-fg/60">{formatDate(workout.date)}</p>
{workout.notes ? <p className="mt-1.5 text-sm text-fg/70">{workout.notes}</p> : null}
</div>
<AiAnalysisCard targetType="strength" targetId={workout._id.toString()} analysis={analysis} />
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-3">
{exercisesWithHistory.map(({ exercise }, index) => {
const e1rm = exerciseE1rm(exercise);
return (
<div key={index} className="flex flex-col gap-2 rounded-lg border border-muted/40 bg-surface px-3 py-2.5">
<p className="text-xs font-semibold text-fg">{exercise.name}</p>
{exercise.notes ? <p className="text-xs text-fg/50 italic">{exercise.notes}</p> : null}
<div className="flex flex-wrap gap-1">
{exercise.sets.map((set) => {
const pct =
e1rm && set.weightKg != null
? Math.round((set.weightKg / e1rm) * 100)
: null;
return (
<span key={set.order} className="rounded bg-bg px-1.5 py-0.5 text-xs text-fg/70">
{set.reps ?? "?"}×{set.weightKg !== undefined ? `${set.weightKg} kg` : "—"}
{pct !== null ? (
<span className="ml-1 text-fg/40">{pct}%</span>
) : null}
</span>
);
})}
</div>
</div>
);
})}
</div>
{exercisesWithHistory.some(({ history }) => history.length >= 2) ? (
<div className="flex flex-col gap-3">
<h2 className="flex items-center gap-1.5 text-sm font-semibold text-fg/70">
Postęp ćwiczeń
<InfoTooltip text="Wolumen (ciężar × powtórzenia) i maksymalny ciężar na tle poprzednich sesji z tym samym ćwiczeniem." />
</h2>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{exercisesWithHistory
.filter(({ history }) => history.length >= 2)
.map(({ exercise, history }) => (
<ExerciseProgressChart
key={exercise.name}
name={exercise.name}
data={history.map((point) => ({
label: formatDateShort(point.date),
volumeKg: point.volumeKg,
topWeightKg: point.topWeightKg,
e1rmKg: point.e1rmKg,
}))}
/>
))}
</div>
</div>
) : null}
{workout.sourceUrl ? (
<a
href={workout.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-fg/40 hover:text-accent"
>
{workout.sourceUrl}
</a>
) : null}
</div>
);
}