"use client"; import { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import { formatSetsSummary } from "@/lib/formatSets"; import { ChevronLeft, Loader, Pencil, Trash2, Check, X, Dumbbell, TrendingUp, Calendar, } from "lucide-react"; import { Exercise as PrismaExercise } from "@prisma/client"; // Extended type until Prisma client is regenerated with new fields type Exercise = PrismaExercise & { inputFields?: string; defaultWeightUnit?: string | null; }; const EXERCISE_TYPES = [ "barbell", "dumbbell", "machine", "cable", "bodyweight", "cardio", "kettlebell", "other", ]; const MUSCLE_GROUPS = [ "chest", "back", "shoulders", "quads", "hamstrings", "glutes", "biceps", "triceps", "forearms", "core", "calves", "full body", "cardio", ]; const INPUT_FIELD_OPTIONS = [ { value: "sets", label: "Sets" }, { value: "reps", label: "Reps" }, { value: "weight", label: "Weight" }, { value: "duration", label: "Duration" }, { value: "distance", label: "Distance" }, { value: "calories", label: "Calories" }, { value: "notes", label: "Notes" }, ]; interface WorkoutHistory { workout: { id: string; date: string; name: string | null }; sets: Array<{ id: string; setNumber: number; reps: number | null; weight: number | null; weightUnit: string; rpe: number | null; durationSeconds: number | null; distance: number | null; calories: number | null; notes: string | null; }>; } export default function ExerciseDetailPage() { const params = useParams(); const router = useRouter(); const exerciseId = params.id as string; const [exercise, setExercise] = useState(null); const [history, setHistory] = useState([]); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); // Edit form state const [editName, setEditName] = useState(""); const [editType, setEditType] = useState(""); const [editMuscleGroups, setEditMuscleGroups] = useState([]); const [editInputFields, setEditInputFields] = useState([]); const [editDefaultUnit, setEditDefaultUnit] = useState(null); // Custom "+" add state const [addingType, setAddingType] = useState(false); const [newTypeText, setNewTypeText] = useState(""); const [addingMuscle, setAddingMuscle] = useState(false); const [newMuscleText, setNewMuscleText] = useState(""); const [addingField, setAddingField] = useState(false); const [newFieldText, setNewFieldText] = useState(""); const [customTypes, setCustomTypes] = useState([]); const [customMuscles, setCustomMuscles] = useState([]); const [customFields, setCustomFields] = useState<{ value: string; label: string }[]>([]); useEffect(() => { fetchExercise(); }, [exerciseId]); const fetchExercise = async () => { try { const res = await fetch(`/api/exercises/${exerciseId}`); if (!res.ok) throw new Error("Not found"); const data = await res.json(); setExercise(data.exercise); setHistory(data.history || []); // Populate edit form setEditName(data.exercise.name); setEditType(data.exercise.type); const mg = JSON.parse(data.exercise.muscleGroups || "[]") as string[]; setEditMuscleGroups(mg); const ifs = JSON.parse(data.exercise.inputFields || '["sets","reps","weight"]') as string[]; setEditInputFields(ifs); setEditDefaultUnit(data.exercise.defaultWeightUnit); // Detect custom values not in default lists const knownTypes = EXERCISE_TYPES; if (!knownTypes.includes(data.exercise.type)) { setCustomTypes((prev) => prev.includes(data.exercise.type) ? prev : [...prev, data.exercise.type]); } const knownMuscles = MUSCLE_GROUPS; mg.forEach((m: string) => { if (!knownMuscles.includes(m)) { setCustomMuscles((prev) => prev.includes(m) ? prev : [...prev, m]); } }); const knownFields = INPUT_FIELD_OPTIONS.map((f) => f.value); ifs.forEach((f: string) => { if (!knownFields.includes(f)) { setCustomFields((prev) => prev.some((cf) => cf.value === f) ? prev : [...prev, { value: f, label: f.charAt(0).toUpperCase() + f.slice(1) }] ); } }); } catch (err) { console.error(err); } finally { setLoading(false); } }; const handleSave = async () => { setSaving(true); try { const res = await fetch(`/api/exercises/${exerciseId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editName, type: editType, muscleGroups: editMuscleGroups, inputFields: editInputFields, defaultWeightUnit: editDefaultUnit, }), }); if (!res.ok) throw new Error("Failed to save"); const updated = await res.json(); setExercise(updated); setEditing(false); } catch (err) { console.error(err); alert("Failed to save changes"); } finally { setSaving(false); } }; const handleDelete = async () => { if (!confirm("Delete this exercise and all its history?")) return; setDeleting(true); try { const res = await fetch(`/api/exercises/${exerciseId}`, { method: "DELETE", }); if (!res.ok) throw new Error("Failed to delete"); router.push("/main/exercises"); } catch (err) { console.error(err); alert("Failed to delete exercise"); setDeleting(false); } }; const toggleMuscleGroup = (group: string) => { setEditMuscleGroups((prev) => prev.includes(group) ? prev.filter((g) => g !== group) : [...prev, group] ); }; const toggleInputField = (field: string) => { setEditInputFields((prev) => prev.includes(field) ? prev.filter((f) => f !== field) : [...prev, field] ); }; if (loading) { return (
); } if (!exercise) { return (
Back

Exercise not found

); } const muscleGroups = JSON.parse(exercise.muscleGroups || "[]") as string[]; const inputFields = JSON.parse( exercise.inputFields || '["sets","reps","weight"]' ) as string[]; return (
{/* Header */}

{exercise.name}

{!editing && (
)}
{/* Edit Mode */} {editing ? (
setEditName(e.target.value)} className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-white/20" />
{[...EXERCISE_TYPES, ...customTypes].map((type) => ( ))} {addingType ? (
{ e.preventDefault(); const val = newTypeText.trim().toLowerCase(); if (val && !EXERCISE_TYPES.includes(val) && !customTypes.includes(val)) { setCustomTypes((p) => [...p, val]); setEditType(val); } setNewTypeText(""); setAddingType(false); }} className="flex items-center gap-1" > setNewTypeText(e.target.value)} onBlur={() => { const val = newTypeText.trim().toLowerCase(); if (val && !EXERCISE_TYPES.includes(val) && !customTypes.includes(val)) { setCustomTypes((p) => [...p, val]); setEditType(val); } setNewTypeText(""); setAddingType(false); }} placeholder="New type" className="w-24 px-2 py-1.5 bg-zinc-800 border border-zinc-600 rounded-lg text-sm text-white focus:outline-none focus:ring-1 focus:ring-white/30" />
) : ( )}
{[...MUSCLE_GROUPS, ...customMuscles].map((group) => ( ))} {addingMuscle ? (
{ e.preventDefault(); const val = newMuscleText.trim().toLowerCase(); if (val && !MUSCLE_GROUPS.includes(val) && !customMuscles.includes(val)) { setCustomMuscles((p) => [...p, val]); } if (val && !editMuscleGroups.includes(val)) { setEditMuscleGroups((p) => [...p, val]); } setNewMuscleText(""); setAddingMuscle(false); }} className="flex items-center gap-1" > setNewMuscleText(e.target.value)} onBlur={() => { const val = newMuscleText.trim().toLowerCase(); if (val && !MUSCLE_GROUPS.includes(val) && !customMuscles.includes(val)) { setCustomMuscles((p) => [...p, val]); } if (val && !editMuscleGroups.includes(val)) { setEditMuscleGroups((p) => [...p, val]); } setNewMuscleText(""); setAddingMuscle(false); }} placeholder="New group" className="w-24 px-2 py-1.5 bg-zinc-800 border border-zinc-600 rounded-lg text-sm text-white focus:outline-none focus:ring-1 focus:ring-white/30" />
) : ( )}

Choose which fields are relevant when logging this exercise

{[...INPUT_FIELD_OPTIONS, ...customFields].map((field) => ( ))} {addingField ? (
{ e.preventDefault(); const val = newFieldText.trim().toLowerCase(); if ( val && !INPUT_FIELD_OPTIONS.some((f) => f.value === val) && !customFields.some((f) => f.value === val) ) { setCustomFields((p) => [ ...p, { value: val, label: val.charAt(0).toUpperCase() + val.slice(1) }, ]); } if (val && !editInputFields.includes(val)) { setEditInputFields((p) => [...p, val]); } setNewFieldText(""); setAddingField(false); }} className="flex items-center gap-1" > setNewFieldText(e.target.value)} onBlur={() => { const val = newFieldText.trim().toLowerCase(); if ( val && !INPUT_FIELD_OPTIONS.some((f) => f.value === val) && !customFields.some((f) => f.value === val) ) { setCustomFields((p) => [ ...p, { value: val, label: val.charAt(0).toUpperCase() + val.slice(1) }, ]); } if (val && !editInputFields.includes(val)) { setEditInputFields((p) => [...p, val]); } setNewFieldText(""); setAddingField(false); }} placeholder="New field" className="w-24 px-2 py-1.5 bg-zinc-800 border border-zinc-600 rounded-lg text-sm text-white focus:outline-none focus:ring-1 focus:ring-white/30" />
) : ( )}
{[ { value: null, label: "User Default" }, { value: "lbs", label: "Pounds" }, { value: "kg", label: "Kilograms" }, ].map((opt) => ( ))}
) : ( /* View Mode */

{exercise.name}

{exercise.type.charAt(0).toUpperCase() + exercise.type.slice(1)}
{muscleGroups.length > 0 && (
{muscleGroups.map((group) => ( {group.charAt(0).toUpperCase() + group.slice(1)} ))}
)}

Tracked Fields

{inputFields.map((field) => ( {field.charAt(0).toUpperCase() + field.slice(1)} ))}
{exercise.defaultWeightUnit && (

Default unit:{" "} {exercise.defaultWeightUnit}

)}
)} {/* History */}

History

{history.length === 0 ? (

No history yet

Start logging this exercise to see your progress

) : (
{history.map((entry) => { const summary = formatSetsSummary( entry.sets.map((s: any) => ({ weight: s.weight, reps: s.reps, weightUnit: s.weightUnit })) ); const dateStr = new Date(entry.workout.date).toLocaleDateString( "en-US", { month: "short", day: "numeric" } ); return ( {dateStr} · {entry.sets.length} {entry.sets.length === 1 ? "set" : "sets"} {summary && ( <> · {summary} )} ); })}
)}
); }