|
@@ -1,146 +1,31 @@
|
|
|
"use client";
|
|
|
|
|
|
-import { useState } from "react";
|
|
|
-import { ArrowLeft, ArrowRight, CheckCircle, Zap, Plus } from "lucide-react";
|
|
|
+import { useState, useEffect } from "react";
|
|
|
+import { useRouter } from "next/navigation";
|
|
|
+import Image from "next/image";
|
|
|
|
|
|
import { useI18n } from "locales/client";
|
|
|
+import Trophy from "@public/images/trophy.png";
|
|
|
+import { WorkoutSessionSets } from "@/features/workout-session/ui/workout-session-sets";
|
|
|
+import { WorkoutSessionHeader } from "@/features/workout-session/ui/workout-session-header";
|
|
|
+import { WorkoutBuilderFooter } from "@/features/workout-builder/ui/workout-stepper-footer";
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { StepperStepProps } from "../types";
|
|
|
import { useWorkoutStepper } from "../model/use-workout-stepper";
|
|
|
+import { useWorkoutSession } from "../../workout-session/model/use-workout-session";
|
|
|
import { StepperHeader } from "./stepper-header";
|
|
|
import { MuscleSelection } from "./muscle-selection";
|
|
|
-import { ExerciseListItem } from "./exercise-list-item";
|
|
|
+import { ExercisesSelection } from "./exercises-selection";
|
|
|
import { EquipmentSelection } from "./equipment-selection";
|
|
|
|
|
|
-function NavigationFooter({
|
|
|
- currentStep,
|
|
|
- totalSteps,
|
|
|
- canContinue,
|
|
|
- onPrevious,
|
|
|
- onNext,
|
|
|
- selectedEquipment,
|
|
|
- selectedMuscles,
|
|
|
-}: {
|
|
|
- currentStep: number;
|
|
|
- totalSteps: number;
|
|
|
- canContinue: boolean;
|
|
|
- onPrevious: () => void;
|
|
|
- onNext: () => void;
|
|
|
- selectedEquipment: any[];
|
|
|
- selectedMuscles: any[];
|
|
|
-}) {
|
|
|
- const t = useI18n();
|
|
|
- const isFirstStep = currentStep === 1;
|
|
|
- const isFinalStep = currentStep === totalSteps;
|
|
|
-
|
|
|
- return (
|
|
|
- <div className="w-full">
|
|
|
- {/* Mobile layout - vertical stack */}
|
|
|
- <div className="flex flex-col gap-4 md:hidden">
|
|
|
- {/* Center stats on top for mobile */}
|
|
|
- <div className="flex items-center justify-center">
|
|
|
- <div className="flex items-center gap-4 bg-white dark:bg-slate-800 px-4 py-2 rounded-full dark:border-slate-700 shadow-sm">
|
|
|
- {currentStep === 1 && (
|
|
|
- <div className="flex items-center gap-2 text-sm">
|
|
|
- <Zap className="h-4 w-4 text-emerald-500" />
|
|
|
- <span className="font-medium text-slate-700 dark:text-slate-300">
|
|
|
- {t("workout_builder.stats.equipment_selected", { count: selectedEquipment.length })}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {currentStep === 2 && (
|
|
|
- <div className="flex items-center gap-2 text-sm">
|
|
|
- <CheckCircle className="h-4 w-4 text-blue-500" />
|
|
|
- <span className="font-medium text-slate-700 dark:text-slate-300">
|
|
|
- {t("workout_builder.stats.muscle_selected", { count: selectedMuscles.length })}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Navigation buttons */}
|
|
|
- <div className="flex items-center justify-between gap-3">
|
|
|
- {/* Previous button */}
|
|
|
- <Button className="flex-1" disabled={isFirstStep} onClick={onPrevious} size="default" variant="ghost">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <ArrowLeft className="h-4 w-4" />
|
|
|
- <span className="font-medium">{t("workout_builder.navigation.previous")}</span>
|
|
|
- </div>
|
|
|
- </Button>
|
|
|
-
|
|
|
- {/* Next/Complete button */}
|
|
|
- <Button
|
|
|
- className="flex-1"
|
|
|
- disabled={!canContinue}
|
|
|
- onClick={isFinalStep ? () => console.log("Complete workout!") : onNext}
|
|
|
- size="default"
|
|
|
- variant="default"
|
|
|
- >
|
|
|
- <div className="flex items-center justify-center gap-2">
|
|
|
- <span className="font-semibold">
|
|
|
- {isFinalStep ? t("workout_builder.navigation.complete") : t("workout_builder.navigation.continue")}
|
|
|
- </span>
|
|
|
- {!isFinalStep && <ArrowRight className="h-4 w-4" />}
|
|
|
- {isFinalStep && <CheckCircle className="h-4 w-4" />}
|
|
|
- </div>
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Desktop layout - horizontal */}
|
|
|
- <div className="hidden md:flex items-center justify-between">
|
|
|
- {/* Previous button */}
|
|
|
- <Button disabled={isFirstStep} onClick={onPrevious} size="large" variant="ghost">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <ArrowLeft className="h-4 w-4 transition-transform group-hover:-translate-x-1" />
|
|
|
- <span className="font-medium">{t("workout_builder.navigation.previous")}</span>
|
|
|
- </div>
|
|
|
- </Button>
|
|
|
-
|
|
|
- {/* Center stats */}
|
|
|
- <div className="flex items-center gap-4 bg-white dark:bg-slate-800 px-6 py-3 rounded-full dark:border-slate-700 shadow-sm">
|
|
|
- {currentStep === 1 && (
|
|
|
- <div className="flex items-center gap-2 text-sm">
|
|
|
- <Zap className="h-4 w-4 text-emerald-500" />
|
|
|
- <span className="font-medium text-slate-700 dark:text-slate-300">
|
|
|
- {t("workout_builder.stats.equipment_selected", { count: selectedEquipment.length })}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {currentStep === 2 && (
|
|
|
- <div className="flex items-center gap-2 text-sm">
|
|
|
- <CheckCircle className="h-4 w-4 text-blue-500" />
|
|
|
- <span className="font-medium text-slate-700 dark:text-slate-300">
|
|
|
- {t("workout_builder.stats.muscle_selected", { count: selectedMuscles.length })}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Next/Complete button */}
|
|
|
- <Button
|
|
|
- disabled={!canContinue}
|
|
|
- onClick={isFinalStep ? () => console.log("Complete workout!") : onNext}
|
|
|
- size="large"
|
|
|
- variant="default"
|
|
|
- >
|
|
|
- <div className="relative flex items-center gap-2">
|
|
|
- <span className="font-semibold">
|
|
|
- {isFinalStep ? t("workout_builder.navigation.complete_workout") : t("workout_builder.navigation.continue")}
|
|
|
- </span>
|
|
|
- {!isFinalStep && <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />}
|
|
|
- {isFinalStep && <CheckCircle className="h-4 w-4" />}
|
|
|
- </div>
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- );
|
|
|
-}
|
|
|
+import type { ExerciseWithAttributes } from "../types";
|
|
|
|
|
|
export function WorkoutStepper() {
|
|
|
+ const { loadSessionFromLocal } = useWorkoutSession();
|
|
|
+
|
|
|
const t = useI18n();
|
|
|
+ const router = useRouter();
|
|
|
const {
|
|
|
currentStep,
|
|
|
selectedEquipment,
|
|
@@ -155,13 +40,50 @@ export function WorkoutStepper() {
|
|
|
isLoadingExercises,
|
|
|
exercisesByMuscle,
|
|
|
exercisesError,
|
|
|
+ fetchExercises,
|
|
|
+ exercisesOrder,
|
|
|
} = useWorkoutStepper();
|
|
|
|
|
|
- // État pour les exercices sélectionnés (picked)
|
|
|
- const [pickedExercises, setPickedExercises] = useState<string[]>([]);
|
|
|
+ useEffect(() => {
|
|
|
+ loadSessionFromLocal();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // dnd-kit et flatExercises doivent être avant tout return/condition
|
|
|
+ const [flatExercises, setFlatExercises] = useState<{ id: string; muscle: string; exercise: ExerciseWithAttributes }[]>([]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (exercisesByMuscle.length > 0) {
|
|
|
+ const flat = exercisesByMuscle.flatMap((group) =>
|
|
|
+ group.exercises.map((exercise: ExerciseWithAttributes) => ({
|
|
|
+ id: exercise.id,
|
|
|
+ muscle: group.muscle,
|
|
|
+ exercise,
|
|
|
+ })),
|
|
|
+ );
|
|
|
+ setFlatExercises(flat);
|
|
|
+ }
|
|
|
+ }, [exercisesByMuscle]);
|
|
|
+
|
|
|
+ // Fetch exercises quand on arrive à l'étape 3
|
|
|
+ useEffect(() => {
|
|
|
+ if (currentStep === 3) {
|
|
|
+ fetchExercises();
|
|
|
+ }
|
|
|
+ }, [currentStep, selectedEquipment, selectedMuscles]);
|
|
|
|
|
|
- // Calculer si on peut continuer selon l'étape
|
|
|
- const canContinue = currentStep === 1 ? canProceedToStep2 : currentStep === 2 ? canProceedToStep3 : pickedExercises.length > 0;
|
|
|
+ const {
|
|
|
+ isWorkoutActive,
|
|
|
+ session,
|
|
|
+ startWorkout,
|
|
|
+ currentExercise,
|
|
|
+ formatElapsedTime,
|
|
|
+ isTimerRunning,
|
|
|
+ toggleTimer,
|
|
|
+ resetTimer,
|
|
|
+ quitWorkout,
|
|
|
+ } = useWorkoutSession();
|
|
|
+
|
|
|
+ const canContinue = currentStep === 1 ? canProceedToStep2 : currentStep === 2 ? canProceedToStep3 : exercisesByMuscle.length > 0;
|
|
|
|
|
|
// Actions pour les exercices
|
|
|
const handleShuffleExercise = (exerciseId: string, muscle: string) => {
|
|
@@ -170,7 +92,8 @@ export function WorkoutStepper() {
|
|
|
};
|
|
|
|
|
|
const handlePickExercise = (exerciseId: string) => {
|
|
|
- setPickedExercises((prev) => (prev.includes(exerciseId) ? prev.filter((id) => id !== exerciseId) : [...prev, exerciseId]));
|
|
|
+ // later
|
|
|
+ console.log("Pick exercise:", exerciseId);
|
|
|
};
|
|
|
|
|
|
const handleDeleteExercise = (exerciseId: string, muscle: string) => {
|
|
@@ -183,7 +106,54 @@ export function WorkoutStepper() {
|
|
|
console.log("Add exercise");
|
|
|
};
|
|
|
|
|
|
- // Calculer l'état des étapes avec traductions
|
|
|
+ const orderedExercises = exercisesOrder.length
|
|
|
+ ? exercisesOrder
|
|
|
+ .map((id) => flatExercises.find((item) => item.id === id))
|
|
|
+ .filter(Boolean)
|
|
|
+ .map((item) => item!.exercise)
|
|
|
+ : flatExercises.map((item) => item.exercise);
|
|
|
+
|
|
|
+ const handleStartWorkout = () => {
|
|
|
+ if (orderedExercises.length > 0) {
|
|
|
+ startWorkout(orderedExercises, selectedEquipment, selectedMuscles);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const [showCongrats, setShowCongrats] = useState(false);
|
|
|
+
|
|
|
+ if (showCongrats && !isWorkoutActive) {
|
|
|
+ return (
|
|
|
+ <div className="flex flex-col items-center justify-center py-16">
|
|
|
+ <Image alt="Trophée" className="w-56 h-56" src={Trophy} />
|
|
|
+ <h2 className="text-2xl font-bold mb-2">Bravo, séance terminée ! 🎉</h2>
|
|
|
+ <p className="text-lg text-slate-600 mb-6">Tu as complété tous tes exercices.</p>
|
|
|
+ <Button onClick={() => router.push("/profile")}>{t("commons.go_to_profile")}</Button>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (isWorkoutActive && session) {
|
|
|
+ return (
|
|
|
+ <div className="w-full max-w-6xl mx-auto">
|
|
|
+ {!showCongrats && (
|
|
|
+ <WorkoutSessionHeader
|
|
|
+ currentExerciseIndex={session.exercises.findIndex((exercise) => exercise.id === currentExercise?.id)}
|
|
|
+ elapsedTime={formatElapsedTime()}
|
|
|
+ isTimerRunning={isTimerRunning}
|
|
|
+ onQuitWorkout={quitWorkout}
|
|
|
+ onResetTimer={resetTimer}
|
|
|
+ onSaveAndQuit={() => {
|
|
|
+ // TODO: Implémenter la sauvegarde pour plus tard
|
|
|
+ console.log("Save workout for later");
|
|
|
+ quitWorkout();
|
|
|
+ }}
|
|
|
+ onToggleTimer={toggleTimer}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <WorkoutSessionSets isWorkoutActive={isWorkoutActive} onCongrats={() => setShowCongrats(true)} showCongrats={showCongrats} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
const STEPPER_STEPS: StepperStepProps[] = [
|
|
|
{
|
|
|
stepNumber: 1,
|
|
@@ -214,7 +184,6 @@ export function WorkoutStepper() {
|
|
|
isCompleted: step.stepNumber < currentStep,
|
|
|
}));
|
|
|
|
|
|
- // Rendu du contenu de l'étape actuelle
|
|
|
const renderStepContent = () => {
|
|
|
switch (currentStep) {
|
|
|
case 1:
|
|
@@ -225,66 +194,17 @@ export function WorkoutStepper() {
|
|
|
return <MuscleSelection onToggleMuscle={toggleMuscle} selectedEquipment={selectedEquipment} selectedMuscles={selectedMuscles} />;
|
|
|
case 3:
|
|
|
return (
|
|
|
- <div className="space-y-6">
|
|
|
- {isLoadingExercises ? (
|
|
|
- <div className="text-center py-20">
|
|
|
- <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
|
|
- <p className="mt-4 text-slate-600 dark:text-slate-400">{t("workout_builder.loading.exercises")}</p>
|
|
|
- </div>
|
|
|
- ) : exercisesByMuscle.length > 0 ? (
|
|
|
- <div className="max-w-4xl mx-auto">
|
|
|
- {/* Liste des exercices */}
|
|
|
- <div className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 overflow-hidden">
|
|
|
- {exercisesByMuscle.map((group, groupIndex) => (
|
|
|
- <div key={group.muscle}>
|
|
|
- {group.exercises.map((exercise, exerciseIndex) => (
|
|
|
- <ExerciseListItem
|
|
|
- exercise={exercise}
|
|
|
- isPicked={pickedExercises.includes(exercise.id)}
|
|
|
- key={exercise.id}
|
|
|
- muscle={group.muscle}
|
|
|
- onDelete={handleDeleteExercise}
|
|
|
- onPick={handlePickExercise}
|
|
|
- onShuffle={handleShuffleExercise}
|
|
|
- />
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- ))}
|
|
|
-
|
|
|
- {/* Add exercise button */}
|
|
|
- <div className="border-t border-slate-200 dark:border-slate-800">
|
|
|
- <button
|
|
|
- className="w-full flex items-center gap-3 py-4 px-4 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-950 transition-colors"
|
|
|
- onClick={handleAddExercise}
|
|
|
- >
|
|
|
- <div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center">
|
|
|
- <Plus className="h-4 w-4 text-white" />
|
|
|
- </div>
|
|
|
- <span className="font-medium">Add</span>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Bottom actions */}
|
|
|
- <div className="flex items-center justify-center gap-4 mt-8">
|
|
|
- <Button className="px-8" size="large" variant="outline">
|
|
|
- Save for later
|
|
|
- </Button>
|
|
|
- <Button className="px-8 bg-blue-600 hover:bg-blue-700" size="large">
|
|
|
- Start Workout
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : exercisesError ? (
|
|
|
- <div className="text-center py-20">
|
|
|
- <p className="text-red-600 dark:text-red-400">{t("workout_builder.error.loading_exercises")}</p>
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <div className="text-center py-20">
|
|
|
- <p className="text-slate-600 dark:text-slate-400">{t("workout_builder.no_exercises_found")}</p>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ <ExercisesSelection
|
|
|
+ error={exercisesError}
|
|
|
+ exercisesByMuscle={exercisesByMuscle}
|
|
|
+ isLoading={isLoadingExercises}
|
|
|
+ onAdd={handleAddExercise}
|
|
|
+ onDelete={handleDeleteExercise}
|
|
|
+ onPick={handlePickExercise}
|
|
|
+ onShuffle={handleShuffleExercise}
|
|
|
+ onStartWorkout={handleStartWorkout}
|
|
|
+ t={t}
|
|
|
+ />
|
|
|
);
|
|
|
default:
|
|
|
return null;
|
|
@@ -293,14 +213,11 @@ export function WorkoutStepper() {
|
|
|
|
|
|
return (
|
|
|
<div className="w-full max-w-6xl mx-auto">
|
|
|
- {/* En-tête du stepper */}
|
|
|
<StepperHeader steps={steps} />
|
|
|
|
|
|
- {/* Contenu de l'étape actuelle */}
|
|
|
<div className="min-h-[400px] mb-8">{renderStepContent()}</div>
|
|
|
|
|
|
- {/* Navigation footer gamifiée */}
|
|
|
- <NavigationFooter
|
|
|
+ <WorkoutBuilderFooter
|
|
|
canContinue={canContinue}
|
|
|
currentStep={currentStep}
|
|
|
onNext={nextStep}
|