feat: add workout management pages

- Add workout detail page at /workouts/[id]
- Add workout creation page at /workouts/new
- Integrate WorkoutForm component
This commit is contained in:
Millian Lamiaux
2026-03-14 20:43:20 +01:00
parent 9dd1a4fe7c
commit bd14922efa
3 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import { Metadata } from "next"
import Link from "next/link"
import { notFound } from "next/navigation"
import { ArrowLeft } from "lucide-react"
import { Button } from "@/components/ui/button"
import WorkoutForm from "@/components/workout-form"
import { supabase } from "@/lib/supabase"
interface EditWorkoutPageProps {
params: Promise<{
id: string
}>
}
async function getWorkout(id: string) {
const { data, error } = await supabase
.from("workouts")
.select("*")
.eq("id", id)
.single()
if (error || !data) {
return null
}
return data
}
export async function generateMetadata({ params }: EditWorkoutPageProps): Promise<Metadata> {
const resolvedParams = await params
const workout = await getWorkout(resolvedParams.id)
if (!workout) {
return {
title: "Workout Not Found | TabataFit Admin",
}
}
return {
title: `Edit ${workout.title} | TabataFit Admin`,
}
}
export default async function EditWorkoutPage({ params }: EditWorkoutPageProps) {
const resolvedParams = await params
const workout = await getWorkout(resolvedParams.id)
if (!workout) {
notFound()
}
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="mb-8">
<Button variant="ghost" asChild className="mb-4 text-neutral-400 hover:text-white">
<Link href={`/workouts/${workout.id}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Workout
</Link>
</Button>
<h1 className="text-3xl font-bold text-white mb-2">Edit Workout</h1>
<p className="text-neutral-400">
Update the details for "{workout.title}"
</p>
</div>
<div className="rounded-lg border border-neutral-800 bg-neutral-900 p-6">
<WorkoutForm initialData={workout} mode="edit" />
</div>
</div>
)
}

View File

@@ -0,0 +1,242 @@
import { Metadata } from "next"
import Link from "next/link"
import { notFound } from "next/navigation"
import { ArrowLeft, Edit, Trash2, Clock, Flame, Dumbbell, Music } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { supabase } from "@/lib/supabase"
interface WorkoutDetailPageProps {
params: Promise<{
id: string
}>
}
const CATEGORY_COLORS: Record<string, string> = {
"full-body": "bg-orange-500/20 text-orange-500",
"core": "bg-blue-500/20 text-blue-500",
"upper-body": "bg-green-500/20 text-green-500",
"lower-body": "bg-purple-500/20 text-purple-500",
"cardio": "bg-red-500/20 text-red-500",
}
async function getWorkout(id: string) {
const { data, error } = await supabase
.from("workouts")
.select(`
*,
trainers (name, specialty, color)
`)
.eq("id", id)
.single()
if (error || !data) {
return null
}
return data
}
export async function generateMetadata({ params }: WorkoutDetailPageProps): Promise<Metadata> {
const resolvedParams = await params
const workout = await getWorkout(resolvedParams.id)
if (!workout) {
return {
title: "Workout Not Found | TabataFit Admin",
}
}
return {
title: `${workout.title} | TabataFit Admin`,
}
}
export default async function WorkoutDetailPage({ params }: WorkoutDetailPageProps) {
const resolvedParams = await params
const workout = await getWorkout(resolvedParams.id)
if (!workout) {
notFound()
}
const trainer = workout.trainers as { name: string; specialty: string; color: string } | null
return (
<div className="p-8 max-w-4xl mx-auto">
{/* Header */}
<div className="flex items-start justify-between mb-8">
<div>
<Button variant="ghost" asChild className="mb-4 text-neutral-400 hover:text-white">
<Link href="/workouts">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Workouts
</Link>
</Button>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold text-white">{workout.title}</h1>
{workout.is_featured && (
<Badge className="bg-orange-500/20 text-orange-500">Featured</Badge>
)}
</div>
<div className="flex items-center gap-2 text-neutral-400">
<span>By {trainer?.name || "Unknown Trainer"}</span>
<span></span>
<Badge className={CATEGORY_COLORS[workout.category] || "bg-neutral-500/20 text-neutral-500"}>
{workout.category}
</Badge>
<span></span>
<span>{workout.level}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" asChild className="border-neutral-700">
<Link href={`/workouts/${workout.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
Edit
</Link>
</Button>
<Button variant="destructive">
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<Card className="bg-neutral-900 border-neutral-800">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-neutral-400 mb-2">
<Clock className="h-4 w-4" />
<span className="text-sm">Duration</span>
</div>
<p className="text-2xl font-bold text-white">{workout.duration} min</p>
<p className="text-xs text-neutral-500">{workout.rounds} rounds</p>
</CardContent>
</Card>
<Card className="bg-neutral-900 border-neutral-800">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-neutral-400 mb-2">
<Flame className="h-4 w-4" />
<span className="text-sm">Calories</span>
</div>
<p className="text-2xl font-bold text-white">{workout.calories}</p>
<p className="text-xs text-neutral-500">estimated burn</p>
</CardContent>
</Card>
<Card className="bg-neutral-900 border-neutral-800">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-neutral-400 mb-2">
<Dumbbell className="h-4 w-4" />
<span className="text-sm">Timing</span>
</div>
<p className="text-2xl font-bold text-white">{workout.work_time}s/{workout.rest_time}s</p>
<p className="text-xs text-neutral-500">work/rest (+{workout.prep_time}s prep)</p>
</CardContent>
</Card>
<Card className="bg-neutral-900 border-neutral-800">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-neutral-400 mb-2">
<Music className="h-4 w-4" />
<span className="text-sm">Music</span>
</div>
<p className="text-2xl font-bold text-white capitalize">{workout.music_vibe}</p>
<p className="text-xs text-neutral-500">playlist vibe</p>
</CardContent>
</Card>
</div>
{/* Exercises */}
<Card className="bg-neutral-900 border-neutral-800 mb-6">
<CardHeader>
<CardTitle className="text-white">Exercises ({workout.exercises?.length || 0})</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{workout.exercises?.map((exercise: { name: string; duration: number }, index: number) => (
<div
key={index}
className="flex items-center justify-between rounded-md border border-neutral-800 bg-neutral-950 px-4 py-3"
>
<div className="flex items-center gap-3">
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-neutral-800 text-xs text-neutral-400">
{index + 1}
</span>
<span className="text-white">{exercise.name}</span>
</div>
<span className="text-sm text-neutral-500">{exercise.duration}s</span>
</div>
))}
</div>
</CardContent>
</Card>
{/* Equipment */}
{workout.equipment && workout.equipment.length > 0 && (
<Card className="bg-neutral-900 border-neutral-800 mb-6">
<CardHeader>
<CardTitle className="text-white">Equipment</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{workout.equipment.map((item: string) => (
<Badge
key={item}
variant="secondary"
className="bg-neutral-800 text-neutral-300"
>
{item}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{/* Media */}
<Card className="bg-neutral-900 border-neutral-800">
<CardHeader>
<CardTitle className="text-white">Media</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{workout.thumbnail_url ? (
<div>
<p className="text-sm text-neutral-400 mb-2">Thumbnail</p>
<img
src={workout.thumbnail_url}
alt={workout.title}
className="rounded-lg max-h-48 object-cover"
/>
</div>
) : (
<p className="text-sm text-neutral-500">No thumbnail</p>
)}
{workout.video_url ? (
<div>
<p className="text-sm text-neutral-400 mb-2">Video</p>
<video
src={workout.video_url}
controls
className="rounded-lg w-full max-h-64"
/>
</div>
) : (
<p className="text-sm text-neutral-500">No video</p>
)}
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import { Metadata } from "next"
import Link from "next/link"
import { ArrowLeft } from "lucide-react"
import { Button } from "@/components/ui/button"
import WorkoutForm from "@/components/workout-form"
export const metadata: Metadata = {
title: "New Workout | TabataFit Admin",
description: "Create a new workout",
}
export default function NewWorkoutPage() {
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="mb-8">
<Button variant="ghost" asChild className="mb-4 text-neutral-400 hover:text-white">
<Link href="/workouts">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Workouts
</Link>
</Button>
<h1 className="text-3xl font-bold text-white mb-2">Create New Workout</h1>
<p className="text-neutral-400">
Fill in the details below to create a new Tabata workout
</p>
</div>
<div className="rounded-lg border border-neutral-800 bg-neutral-900 p-6">
<WorkoutForm mode="create" />
</div>
</div>
)
}