144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { ChevronDown, ChevronUp } from "lucide-react";
|
|
import { WorkoutWithSets } from "@/types";
|
|
import { formatSetsSummary } from "@/lib/formatSets";
|
|
|
|
interface WorkoutCardProps {
|
|
workout: WorkoutWithSets;
|
|
}
|
|
|
|
export default function WorkoutCard({ workout }: WorkoutCardProps) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
// Calculate total volume
|
|
const totalVolume = workout.setLogs.reduce((sum, set) => {
|
|
if (set.weight && set.reps) return sum + set.weight * set.reps;
|
|
return sum;
|
|
}, 0);
|
|
|
|
const uniqueExercises = new Set(workout.setLogs.map((s) => s.exerciseId)).size;
|
|
|
|
const date = new Date(workout.date);
|
|
const formattedDate = date.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
});
|
|
|
|
const caloriesBurned = (workout as any).caloriesBurned as number | null | undefined;
|
|
|
|
// Build compact stats chips
|
|
const stats: string[] = [];
|
|
stats.push(`${uniqueExercises} exercise${uniqueExercises !== 1 ? "s" : ""}`);
|
|
const totalSets = workout.setLogs.length;
|
|
stats.push(`${totalSets} set${totalSets !== 1 ? "s" : ""}`);
|
|
if (totalVolume > 0) stats.push(`${Math.round(totalVolume)} lbs`);
|
|
if (workout.durationMinutes) stats.push(`${workout.durationMinutes} min`);
|
|
if (caloriesBurned) stats.push(`${caloriesBurned} cal`);
|
|
if (workout.difficulty) stats.push(`${workout.difficulty}/10`);
|
|
|
|
// Group sets by exercise
|
|
const exerciseGroups = (() => {
|
|
const groups: Array<{
|
|
exerciseId: string;
|
|
exerciseName: string;
|
|
sets: Array<{ weight?: number | null; reps?: number | null; weightUnit?: string | null }>;
|
|
setCount: number;
|
|
}> = [];
|
|
const indexMap = new Map<string, number>();
|
|
|
|
for (const set of workout.setLogs) {
|
|
const existing = indexMap.get(set.exerciseId);
|
|
if (existing !== undefined) {
|
|
groups[existing].sets.push({ weight: set.weight, reps: set.reps, weightUnit: set.weightUnit });
|
|
groups[existing].setCount++;
|
|
} else {
|
|
indexMap.set(set.exerciseId, groups.length);
|
|
groups.push({
|
|
exerciseId: set.exerciseId,
|
|
exerciseName: set.exercise.name,
|
|
sets: [{ weight: set.weight, reps: set.reps, weightUnit: set.weightUnit }],
|
|
setCount: 1,
|
|
});
|
|
}
|
|
}
|
|
return groups;
|
|
})();
|
|
|
|
return (
|
|
<div className="border border-zinc-800 bg-zinc-900 rounded-lg overflow-hidden">
|
|
{/* Single-line card */}
|
|
<div className="flex items-center gap-2 px-3 py-2.5">
|
|
{/* Clickable area — navigates to detail page */}
|
|
<Link
|
|
href={`/main/workouts/${workout.id}`}
|
|
className="flex-1 min-w-0 flex items-center gap-2 hover:opacity-80 transition-opacity"
|
|
>
|
|
<span className="text-xs text-zinc-500 flex-shrink-0">
|
|
{formattedDate}
|
|
</span>
|
|
<span className="font-medium text-white text-sm truncate flex-shrink min-w-0">
|
|
{workout.name || "Unnamed Workout"}
|
|
</span>
|
|
<span className="text-xs text-zinc-500 flex-shrink-0 hidden sm:inline">
|
|
·
|
|
</span>
|
|
<span className="text-xs text-zinc-400 truncate hidden sm:inline">
|
|
{stats.join(" · ")}
|
|
</span>
|
|
</Link>
|
|
|
|
{/* Expand/collapse chevron */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setExpanded((prev) => !prev)}
|
|
className="p-1 rounded-md text-zinc-500 hover:text-zinc-300 hover:bg-zinc-700/50 transition-colors flex-shrink-0"
|
|
aria-label={expanded ? "Hide details" : "Show details"}
|
|
>
|
|
{expanded ? (
|
|
<ChevronUp className="w-4 h-4" />
|
|
) : (
|
|
<ChevronDown className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Mobile stats row (visible only on small screens) */}
|
|
<div className="px-3 pb-2 -mt-1 sm:hidden">
|
|
<Link href={`/main/workouts/${workout.id}`}>
|
|
<span className="text-xs text-zinc-400">
|
|
{stats.join(" · ")}
|
|
</span>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Expanded exercise detail */}
|
|
{expanded && (
|
|
<div className="border-t border-zinc-800 px-3 py-2.5 space-y-1.5">
|
|
{exerciseGroups.map((group) => {
|
|
const summary = formatSetsSummary(group.sets);
|
|
return (
|
|
<div key={group.exerciseId} className="flex items-baseline gap-2">
|
|
<span className="text-sm font-medium text-white truncate flex-shrink min-w-0">
|
|
{group.exerciseName}
|
|
</span>
|
|
<span className="text-xs text-zinc-500 flex-shrink-0">
|
|
{group.setCount}s
|
|
</span>
|
|
{summary && (
|
|
<span className="text-xs text-zinc-400 truncate">
|
|
— {summary}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|