4be489d6d3
Cardio exercises now log a breathing "Gear" (1-5, per Brian MacKenzie) instead of RPE (6-10) as their effort field; strength keeps RPE. An exercise counts as cardio when its equipment type is "cardio" or it carries the "cardio" muscle group (isCardioExercise in lib/exerciseOptions), so the Assault Bike (type "assault bike") qualifies. New nullable SetLog.gear column added by the boot-time guarded ALTER in docker_entrypoint.sh (additive, idempotent); plumbed through all 5 set-write paths, the summary/edit views, and CSV/JSON import-export. Existing rpe data is untouched and still displays. Program/AI target-RPE is unaffected.
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { redirect } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { ChevronLeft } from "lucide-react";
|
|
import { getCurrentUser } from "@/lib/auth";
|
|
import { getExercises } from "@/lib/db/exercises";
|
|
import { getWorkoutById } from "@/lib/db/workouts";
|
|
import WorkoutForm, { EditWorkoutData } from "@/components/workouts/WorkoutForm";
|
|
|
|
export const metadata = {
|
|
title: "Log Workout",
|
|
description: "Log a new workout",
|
|
};
|
|
|
|
export default async function NewWorkoutPage(props: {
|
|
searchParams: Promise<{ edit?: string }>;
|
|
}) {
|
|
const searchParams = await props.searchParams;
|
|
const user = await getCurrentUser();
|
|
if (!user) {
|
|
redirect("/auth/login");
|
|
}
|
|
|
|
const exercises = await getExercises(user.id);
|
|
|
|
// If ?edit=WORKOUT_ID, fetch existing workout for editing
|
|
let editWorkout: EditWorkoutData | undefined;
|
|
if (searchParams.edit) {
|
|
const workout = await getWorkoutById(searchParams.edit);
|
|
if (workout && workout.userId === user.id) {
|
|
// Group sets by exercise
|
|
const grouped: Record<string, EditWorkoutData["exercises"][number]> = {};
|
|
for (const set of workout.setLogs) {
|
|
const exId = set.exercise.id;
|
|
let customMetrics: Record<string, string> | undefined;
|
|
if ((set as any).customMetrics) {
|
|
try {
|
|
customMetrics = JSON.parse((set as any).customMetrics);
|
|
} catch {
|
|
customMetrics = undefined;
|
|
}
|
|
}
|
|
if (!grouped[exId]) {
|
|
grouped[exId] = {
|
|
exercise: set.exercise,
|
|
sets: [],
|
|
};
|
|
}
|
|
grouped[exId].sets.push({
|
|
setNumber: set.setNumber,
|
|
reps: set.reps ?? undefined,
|
|
weight: set.weight ?? undefined,
|
|
rpe: set.rpe ?? undefined,
|
|
gear: set.gear ?? undefined,
|
|
durationSeconds: set.durationSeconds ?? undefined,
|
|
distance: set.distance ?? undefined,
|
|
calories: set.calories ?? undefined,
|
|
watts: set.watts ?? undefined,
|
|
customMetrics,
|
|
notes: set.notes ?? undefined,
|
|
});
|
|
}
|
|
|
|
editWorkout = {
|
|
id: workout.id,
|
|
name: workout.name || "",
|
|
date: workout.date.toISOString(),
|
|
durationMinutes: workout.durationMinutes,
|
|
difficulty: workout.difficulty,
|
|
caloriesBurned: (workout as any).caloriesBurned ?? null,
|
|
notes: workout.notes,
|
|
exercises: Object.values(grouped),
|
|
};
|
|
}
|
|
}
|
|
|
|
const isEditing = !!editWorkout;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#0A0A0A] pb-24 md:pb-8">
|
|
{/* Header */}
|
|
<div className="border-b border-zinc-800 sticky top-0 z-40 bg-[#0A0A0A]">
|
|
<div className="max-w-2xl mx-auto px-4 py-4 flex items-center gap-4">
|
|
<Link
|
|
href={isEditing ? `/main/workouts/${editWorkout!.id}` : "/main/workouts"}
|
|
className="p-2 hover:bg-zinc-900 rounded-lg -ml-2 text-zinc-400 hover:text-white"
|
|
aria-label="Back"
|
|
>
|
|
<ChevronLeft className="w-6 h-6" />
|
|
</Link>
|
|
<h1 className="text-2xl font-display text-white tracking-wider">
|
|
{isEditing ? "Edit Workout" : "Log Workout"}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form */}
|
|
<div className="max-w-2xl mx-auto px-4 py-6 pb-12">
|
|
<WorkoutForm
|
|
exercises={exercises}
|
|
recentlyUsedExercises={[]}
|
|
editWorkout={editWorkout}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|