Initial commit for Start9 packaging
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user