Initial commit for Start9 packaging

This commit is contained in:
MacPro
2026-02-28 09:27:26 -06:00
commit 1b64c45c52
124 changed files with 15671 additions and 0 deletions
@@ -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>
);
}