"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { Card } from "@/components/ui/Card";
import { Badge } from "@/components/ui/Badge";
import { VideoPlayer } from "@/components/workout/VideoPlayer";
import { api } from "@/lib/api";
import type {
GeneratedWorkout,
WorkoutDetail,
Exercise,
PreviewDay,
PreviewExercise,
} from "@/lib/types";
interface DayCardProps {
// Saved mode
workout?: GeneratedWorkout;
detail?: WorkoutDetail;
onUpdate?: () => void;
// Preview mode
previewDay?: PreviewDay;
previewDayIndex?: number;
onPreviewDayChange?: (dayIndex: number, newDay: PreviewDay) => void;
}
function formatTime(seconds: number): string {
const mins = Math.round(seconds / 60);
if (mins < 60) return `${mins}m`;
const h = Math.floor(mins / 60);
const m = mins % 60;
return m > 0 ? `${h}h ${m}m` : `${h}h`;
}
function EditIcon({ className = "" }: { className?: string }) {
return (
);
}
function TrashIcon({ className = "" }: { className?: string }) {
return (
);
}
function XIcon({ className = "" }: { className?: string }) {
return (
);
}
function mediaUrl(path: string): string {
if (typeof window === "undefined") return path;
if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
return `${window.location.protocol}//${window.location.hostname}:8001${path}`;
}
return path;
}
function PlayIcon({ className = "" }: { className?: string }) {
return (
);
}
function VideoModal({ src, title, onClose }: { src: string; title: string; onClose: () => void }) {
return (
e.stopPropagation()}
>
{title}
);
}
function RefreshIcon({ className = "" }: { className?: string }) {
return (
);
}
// Swap exercise modal — works for both preview and saved mode
function SwapModal({
exerciseId,
currentName,
onSwap,
onClose,
}: {
exerciseId: number;
currentName: string;
onSwap: (newExercise: Exercise) => void;
onClose: () => void;
}) {
const [alternatives, setAlternatives] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api
.getSimilarExercises(exerciseId)
.then(setAlternatives)
.catch((err) => console.error("Failed to load alternatives:", err))
.finally(() => setLoading(false));
}, [exerciseId]);
return (
e.stopPropagation()}
>
Replace: {currentName}
{loading ? (
Loading alternatives...
) : alternatives.length === 0 ? (
No alternatives found.
) : (
{alternatives.map((ex) => (
))}
)}
);
}
export function DayCard({
workout,
detail: externalDetail,
onUpdate,
previewDay,
previewDayIndex,
onPreviewDayChange,
}: DayCardProps) {
const isPreview = !!previewDay;
// Saved mode state
const [deleting, setDeleting] = useState(false);
const [fetchedDetail, setFetchedDetail] = useState(null);
const [regenerating, setRegenerating] = useState(false);
// Video preview modal state
const [videoPreview, setVideoPreview] = useState<{ src: string; title: string } | null>(null);
// Swap modal state
const [swapTarget, setSwapTarget] = useState<{
exerciseId: number;
name: string;
// For saved mode
supersetExerciseId?: number;
// For preview mode
supersetIndex?: number;
exerciseIndex?: number;
} | null>(null);
// Saved mode: fetch detail if not provided
useEffect(() => {
if (isPreview) return;
if (externalDetail || !workout?.workout || workout.is_rest_day) return;
let cancelled = false;
api.getWorkoutDetail(workout.workout).then((data) => {
if (!cancelled) setFetchedDetail(data);
}).catch((err) => {
console.error(`[DayCard] Failed to fetch detail for workout ${workout.workout}:`, err);
});
return () => { cancelled = true; };
}, [isPreview, workout?.workout, workout?.is_rest_day, externalDetail]);
const detail = externalDetail || fetchedDetail;
// ============================================
// Preview mode handlers
// ============================================
const handlePreviewDeleteDay = () => {
if (previewDayIndex === undefined || !previewDay || !onPreviewDayChange) return;
onPreviewDayChange(previewDayIndex, {
...previewDay,
is_rest_day: true,
focus_area: "Rest Day",
target_muscles: [],
workout_spec: undefined,
});
};
const handlePreviewDeleteSuperset = (ssIdx: number) => {
if (previewDayIndex === undefined || !previewDay?.workout_spec || !onPreviewDayChange) return;
const newSupersets = previewDay.workout_spec.supersets.filter((_, i) => i !== ssIdx);
onPreviewDayChange(previewDayIndex, {
...previewDay,
workout_spec: { ...previewDay.workout_spec, supersets: newSupersets },
});
};
const handlePreviewDeleteExercise = (ssIdx: number, exIdx: number) => {
if (previewDayIndex === undefined || !previewDay?.workout_spec || !onPreviewDayChange) return;
const newSupersets = previewDay.workout_spec.supersets.map((ss, i) => {
if (i !== ssIdx) return ss;
const newExercises = ss.exercises.filter((_, j) => j !== exIdx);
return { ...ss, exercises: newExercises };
}).filter((ss) => ss.exercises.length > 0);
onPreviewDayChange(previewDayIndex, {
...previewDay,
workout_spec: { ...previewDay.workout_spec, supersets: newSupersets },
});
};
const handlePreviewSwapExercise = (ssIdx: number, exIdx: number, ex: PreviewExercise) => {
setSwapTarget({
exerciseId: ex.exercise_id,
name: ex.exercise_name,
supersetIndex: ssIdx,
exerciseIndex: exIdx,
});
};
const handlePreviewSwapConfirm = (newExercise: Exercise) => {
if (!swapTarget || swapTarget.supersetIndex === undefined || swapTarget.exerciseIndex === undefined) return;
if (previewDayIndex === undefined || !previewDay?.workout_spec || !onPreviewDayChange) return;
const ssIdx = swapTarget.supersetIndex;
const exIdx = swapTarget.exerciseIndex;
const newSupersets = previewDay.workout_spec.supersets.map((ss, i) => {
if (i !== ssIdx) return ss;
const newExercises = ss.exercises.map((ex, j) => {
if (j !== exIdx) return ex;
return {
...ex,
exercise_id: newExercise.id,
exercise_name: newExercise.name,
muscle_groups: newExercise.muscle_groups || "",
};
});
return { ...ss, exercises: newExercises };
});
onPreviewDayChange(previewDayIndex, {
...previewDay,
workout_spec: { ...previewDay.workout_spec, supersets: newSupersets },
});
setSwapTarget(null);
};
const handlePreviewRegenerate = async () => {
if (previewDayIndex === undefined || !previewDay || !onPreviewDayChange) return;
setRegenerating(true);
try {
const newDay = await api.previewDay({
target_muscles: previewDay.target_muscles,
focus_area: previewDay.focus_area,
workout_type_id: previewDay.workout_type_id,
date: previewDay.date,
plan_id: previewDay.plan_id,
});
onPreviewDayChange(previewDayIndex, newDay);
} catch (err) {
console.error("Failed to regenerate day:", err);
} finally {
setRegenerating(false);
}
};
// ============================================
// Saved mode handlers
// ============================================
const handleDeleteDay = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!confirm("Remove this workout day?")) return;
setDeleting(true);
try {
await api.deleteWorkoutDay(workout!.id);
onUpdate?.();
} catch (err) {
console.error("Failed to delete day:", err);
} finally {
setDeleting(false);
}
};
const handleDeleteSuperset = async (e: React.MouseEvent, supersetId: number) => {
e.preventDefault();
e.stopPropagation();
try {
await api.deleteSuperset(supersetId);
onUpdate?.();
} catch (err) {
console.error("Failed to delete superset:", err);
}
};
const handleDeleteExercise = async (e: React.MouseEvent, seId: number) => {
e.preventDefault();
e.stopPropagation();
try {
await api.deleteSupersetExercise(seId);
onUpdate?.();
} catch (err) {
console.error("Failed to delete exercise:", err);
}
};
const handleSavedSwapExercise = (seId: number, exerciseId: number, name: string) => (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setSwapTarget({ supersetExerciseId: seId, exerciseId, name });
};
const handleSavedSwapConfirm = async (newExercise: Exercise) => {
if (!swapTarget?.supersetExerciseId) return;
try {
await api.swapExercise(swapTarget.supersetExerciseId, newExercise.id);
setSwapTarget(null);
onUpdate?.();
} catch (err) {
console.error("Failed to swap exercise:", err);
}
};
// ============================================
// Render: Preview mode
// ============================================
if (isPreview && previewDay) {
if (previewDay.is_rest_day) return null;
const spec = previewDay.workout_spec;
const typeName = previewDay.workout_type_name?.replace(/_/g, " ");
return (
<>
{/* Top-right actions */}
{/* Header */}
{typeName && (
{typeName}
)}
{previewDay.focus_area && (
{previewDay.focus_area}
)}
{spec?.estimated_time && (
{formatTime(spec.estimated_time)}
)}
{previewDay.warnings && previewDay.warnings.length > 0 && (
Warnings
{previewDay.warnings.map((w, idx) => (
- {w}
))}
)}
{/* Supersets */}
{spec && spec.supersets.length > 0 && (
{spec.supersets.map((superset, si) => (
{superset.name}
{superset.rounds}x
{superset.exercises.map((ex, ei) => (
{ex.exercise_name}
{ex.video_url && (
)}
{ex.reps ? `${ex.reps} reps` : ex.duration ? `${ex.duration}s` : ""}
))}
))}
)}
{/* Muscle summary fallback */}
{!spec && previewDay.target_muscles.length > 0 && (
{previewDay.target_muscles.join(", ")}
)}
{swapTarget && (
setSwapTarget(null)}
/>
)}
{videoPreview && (
setVideoPreview(null)}
/>
)}
>
);
}
// ============================================
// Render: Saved mode
// ============================================
if (!workout || workout.is_rest_day) return null;
const typeName = workout.workout_type_name?.replace(/_/g, " ");
const sortedSupersets = detail
? [...detail.supersets].sort((a, b) => a.order - b.order)
: [];
const cardContent = (
{typeName && (
{typeName}
)}
{workout.focus_area && (
{workout.focus_area}
)}
{detail?.estimated_time && (
{formatTime(detail.estimated_time)}
)}
{sortedSupersets.length > 0 && (
{sortedSupersets.map((superset, si) => {
const sortedExercises = [...superset.exercises].sort(
(a, b) => a.order - b.order
);
return (
{superset.name || `Set ${superset.order}`}
{superset.rounds}x
{superset.id && (
)}
{sortedExercises.map((se, ei) => (
{se.exercise.name}
{se.exercise.video_url && (
)}
{se.reps ? `${se.reps} reps` : se.duration ? `${se.duration}s` : ""}
{se.id && (
)}
))}
);
})}
)}
{!detail && workout.target_muscles.length > 0 && (
{workout.target_muscles.join(", ")}
)}
{/* Status badges for saved workouts */}
{workout.status === "accepted" && (
Saved
)}
{workout.status === "completed" && (
Completed
)}
);
const modal = swapTarget ? (
setSwapTarget(null)}
/>
) : null;
const videoModal = videoPreview ? (
setVideoPreview(null)}
/>
) : null;
if (workout.workout) {
return (
<>
{cardContent}
{modal}
{videoModal}
>
);
}
return (
<>
{cardContent}
{modal}
{videoModal}
>
);
}