2026-06-16 09:43:48 +02:00
|
|
|
|
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";
|
2026-06-16 11:51:10 +02:00
|
|
|
|
import { getLatestAnalysisForTarget, serializeAnalysis } from "@/lib/models/analysis";
|
2026-06-16 09:43:48 +02:00
|
|
|
|
import { getStrengthWorkout, listStrengthWorkouts } from "@/lib/models/strength";
|
2026-06-16 11:11:19 +02:00
|
|
|
|
import { exerciseE1rm, getExerciseHistory } from "@/lib/strength/stats";
|
2026-06-18 11:02:31 +02:00
|
|
|
|
import { getCurrentUserId } from "@/lib/session";
|
2026-06-16 09:43:48 +02:00
|
|
|
|
|
|
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
|
|
|
|
|
|
|
|
const EXERCISE_HISTORY_LIMIT = 8;
|
|
|
|
|
|
|
|
|
|
|
|
export default async function StrengthWorkoutPage({
|
|
|
|
|
|
params,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
params: Promise<{ id: string }>;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const { id } = await params;
|
2026-06-18 11:02:31 +02:00
|
|
|
|
const userId = await getCurrentUserId();
|
|
|
|
|
|
const workout = await getStrengthWorkout(userId, id);
|
2026-06-16 09:43:48 +02:00
|
|
|
|
|
|
|
|
|
|
if (!workout) {
|
|
|
|
|
|
notFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 11:02:31 +02:00
|
|
|
|
const analysis = await getLatestAnalysisForTarget(userId, "strength", workout._id);
|
|
|
|
|
|
const allWorkouts = await listStrengthWorkouts(userId);
|
2026-06-16 09:43:48 +02:00
|
|
|
|
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>
|
|
|
|
|
|
|
2026-06-16 11:51:10 +02:00
|
|
|
|
<AiAnalysisCard targetType="strength" targetId={workout._id.toString()} analysis={analysis ? serializeAnalysis(analysis) : null} />
|
2026-06-16 09:43:48 +02:00
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-3">
|
2026-06-16 11:11:19 +02:00
|
|
|
|
{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>
|
2026-06-16 09:43:48 +02:00
|
|
|
|
</div>
|
2026-06-16 11:11:19 +02:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-06-16 09:43:48 +02:00
|
|
|
|
</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">
|
2026-06-18 11:02:31 +02:00
|
|
|
|
Postęp ćwiczeń
|
|
|
|
|
|
<InfoTooltip text="Wolumen (ciężar × powtórzenia) i maksymalny ciężar na tle poprzednich sesji z tym samym ćwiczeniem." />
|
|
|
|
|
|
</h2>
|
2026-06-16 09:43:48 +02:00
|
|
|
|
<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,
|
2026-06-16 11:11:19 +02:00
|
|
|
|
e1rmKg: point.e1rmKg,
|
2026-06-16 09:43:48 +02:00
|
|
|
|
}))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|