Ver código fonte

Merge pull request #13 from Snouzy/style/improvments

style/improvments
Mat B. 1 mês atrás
pai
commit
543d06d954

+ 5 - 3
app/[locale]/profile/page.tsx

@@ -1,6 +1,7 @@
 "use client";
 import { useRouter } from "next/navigation";
 
+import { useI18n } from "locales/client";
 import { workoutSessionLocal } from "@/shared/lib/workout-session/workout-session.local";
 import { WorkoutSessionList } from "@/features/workout-session/ui/workout-session-list";
 import { WorkoutSessionHeatmap } from "@/features/workout-session/ui/workout-session-heatmap";
@@ -8,6 +9,7 @@ import { Button } from "@/components/ui/button";
 
 export default function ProfilePage() {
   const router = useRouter();
+  const t = useI18n();
 
   const sessions = typeof window !== "undefined" ? workoutSessionLocal.getAll() : [];
   const values: Record<string, number> = {};
@@ -23,10 +25,10 @@ export default function ProfilePage() {
   return (
     <div>
       <WorkoutSessionHeatmap until={until} values={values} />
-      <WorkoutSessionList onSelect={(id) => router.push(`/workout-builder?sessionId=${id}`)} />
+      <WorkoutSessionList onSelect={(id) => router.push(`/?sessionId=${id}`)} />
       <div className="mt-8 flex justify-center">
-        <Button onClick={() => router.push("/workout-builder")} size="large">
-          Nouvelle séance
+        <Button onClick={() => router.push("/")} size="large">
+          {t("profile.new_workout")}
         </Button>
       </div>
     </div>

+ 11 - 0
locales/en.ts

@@ -10,6 +10,10 @@ export default {
     invalid_credentials: "Invalid credentials or account does not exist",
   },
 
+  profile: {
+    new_workout: "New Workout",
+  },
+
   // Release Notes
   release_notes: {
     title: "What's New",
@@ -153,6 +157,9 @@ export default {
       exercise_selection_description: "This step will show you personalized exercise recommendations.",
     },
     session: {
+      congrats: "Congratulations, workout finished! 🎉",
+      congrats_subtitle: "You've done it !",
+      see_instructions: "See instructions",
       finish_set: "Finish Set",
       finish_session: "Finish Session",
       bodyweight: "Bodyweight",
@@ -168,8 +175,10 @@ export default {
       set_number_singular: "Set {number}",
       set_number_plural_singular: "Sets {number}",
       workout_in_progress: "Workout in Progress",
+      started_at: "Started at",
       quit_workout: "Quit Workout",
       elapsed_time: "Elapsed Time",
+      chronometer: "Chronometer",
       exercise_progress: "Exercise Progress",
       current_exercise: "Current Exercise",
       complete: "Complete",
@@ -282,5 +291,7 @@ export default {
     donate: "Donate",
     my_account: "My account",
     dashboard: "Dashboard",
+    home: "Home",
+    changelog: "Changelog",
   },
 } as const;

+ 11 - 0
locales/fr.ts

@@ -10,6 +10,10 @@ export default {
     invalid_credentials: "Identifiants invalides ou compte inexistant",
   },
 
+  profile: {
+    new_workout: "Nouvelle séance",
+  },
+
   // Release Notes
   release_notes: {
     title: "Nouveautés",
@@ -153,6 +157,9 @@ export default {
       exercise_selection_description: "Cette étape vous montrera des recommandations d'exercices personnalisées.",
     },
     session: {
+      congrats: "Bravo, séance terminée ! 🎉",
+      congrats_subtitle: "Tu l'as fait !",
+      see_instructions: "Voir les instructions",
       finish_set: "Valider la série",
       finish_session: "Terminer la séance",
       bodyweight: "Poids du corps",
@@ -168,8 +175,10 @@ export default {
       set_number_singular: "Série {number}",
       set_number_plural_singular: "Séries {number}",
       workout_in_progress: "Entraînement en cours",
+      started_at: "Débuté à",
       quit_workout: "Quitter l'Entraînement",
       elapsed_time: "Temps écoulé",
+      chronometer: "Chronomètre",
       total_workout_time: "Temps total d'entraînement",
       exercise_progress: "Progression",
       current_exercise: "Exercice actuel",
@@ -283,5 +292,7 @@ export default {
     donate: "Faire un don",
     my_account: "Mon compte",
     dashboard: "Tableau de bord",
+    home: "Accueil",
+    changelog: "Annonces & notes de version",
   },
 } as const;

+ 47 - 0
src/components/ui/timer.tsx

@@ -0,0 +1,47 @@
+import { useEffect, useRef, useState } from "react";
+
+export function Timer({
+  isRunning,
+  initialSeconds = 0,
+  onChange,
+}: {
+  isRunning: boolean;
+  initialSeconds?: number;
+  onChange?: (seconds: number) => void;
+}) {
+  const [seconds, setSeconds] = useState(initialSeconds);
+  const intervalRef = useRef<NodeJS.Timeout | null>(null);
+
+  useEffect(() => {
+    setSeconds(initialSeconds);
+  }, [initialSeconds]);
+
+  useEffect(() => {
+    if (isRunning) {
+      intervalRef.current = setInterval(() => {
+        setSeconds((s) => {
+          const next = s + 1;
+          onChange?.(next);
+          return next;
+        });
+      }, 1000);
+    } else if (intervalRef.current) {
+      clearInterval(intervalRef.current);
+      intervalRef.current = null;
+    }
+    return () => {
+      if (intervalRef.current) clearInterval(intervalRef.current);
+    };
+  }, [isRunning, onChange]);
+
+  // Format mm:ss ou hh:mm:ss
+  const format = () => {
+    const h = Math.floor(seconds / 3600);
+    const m = Math.floor((seconds % 3600) / 60);
+    const s = seconds % 60;
+    if (h > 0) return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
+    return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
+  };
+
+  return <span>{format()}</span>;
+}

+ 2 - 2
src/features/layout/Header.tsx

@@ -46,8 +46,8 @@ export const Header = () => {
 
       {/* User Menu */}
       <div className="navbar-end">
-        <Link aria-label="Accueil" className="hover:bg-slate-100 rounded-full p-2 transition" href="/">
-          <InlineTooltip title="Accueil">
+        <Link aria-label={t("commons.home")} className="hover:bg-slate-100 rounded-full p-2 transition" href="/">
+          <InlineTooltip title={t("commons.home")}>
             <Home className="w-6 h-6 text-blue-500" />
           </InlineTooltip>
         </Link>

+ 1 - 1
src/features/release-notes/ui/release-notes-dialog.tsx

@@ -19,7 +19,7 @@ export function ReleaseNotesDialog() {
     <Dialog>
       <DialogTrigger asChild>
         <Button aria-label={t("release_notes.release_notes")} className="rounded-full hover:bg-slate-100" size="small" variant="ghost">
-          <InlineTooltip title="Annonces / Changelog">
+          <InlineTooltip title={t("commons.changelog")}>
             <Bell className="text-blue-500 h-6 w-6" />
           </InlineTooltip>
         </Button>

+ 18 - 0
src/features/workout-builder/model/workout-builder.store.ts

@@ -24,6 +24,12 @@ interface WorkoutBuilderState {
   clearMuscles: () => void;
   fetchExercises: () => Promise<void>;
   setExercisesOrder: (order: string[]) => void;
+  loadFromSession: (params: {
+    equipment: ExerciseAttributeValueEnum[];
+    muscles: ExerciseAttributeValueEnum[];
+    exercisesByMuscle: any[];
+    exercisesOrder: string[];
+  }) => void;
 }
 
 export const useWorkoutBuilderStore = create<WorkoutBuilderState>((set, get) => ({
@@ -74,4 +80,16 @@ export const useWorkoutBuilderStore = create<WorkoutBuilderState>((set, get) =>
   },
 
   setExercisesOrder: (order) => set({ exercisesOrder: order }),
+
+  loadFromSession: ({ equipment, muscles, exercisesByMuscle, exercisesOrder }) => {
+    set({
+      selectedEquipment: equipment,
+      selectedMuscles: muscles,
+      exercisesByMuscle,
+      exercisesOrder,
+      currentStep: 3,
+      isLoadingExercises: false,
+      exercisesError: null,
+    });
+  },
 }));

+ 3 - 9
src/features/workout-builder/ui/quit-workout-dialog.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { AlertTriangle, Save, Trash2 } from "lucide-react";
+import { AlertTriangle, Trash2 } from "lucide-react";
 
 import { useI18n } from "locales/client";
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -64,18 +64,12 @@ export function QuitWorkoutDialog({
 
         {/* Action Buttons */}
         <div className="space-y-3">
-          {/* Save and Quit */}
-          <Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={onQuitWithSave} size="large">
-            <Save className="h-4 w-4 mr-2" />
-            {t("workout_builder.session.save_and_quit")}
-          </Button>
-
           {/* Quit without saving */}
           <Button
-            className="w-full border-red-500/30 text-red-400 hover:bg-red-500/10 hover:border-red-500"
+            className="w-full bg-red-500/10 border-red-500/30 text-red-400 hover:bg-red-500/10 hover:border-red-500"
             onClick={onQuitWithoutSave}
             size="large"
-            variant="outline"
+            variant="default"
           >
             <Trash2 className="h-4 w-4 mr-2" />
             {t("workout_builder.session.quit_without_save")}

+ 4 - 4
src/features/workout-builder/ui/workout-stepper.tsx

@@ -66,10 +66,10 @@ export function WorkoutStepper() {
 
   // Fetch exercises quand on arrive à l'étape 3
   useEffect(() => {
-    if (currentStep === 3) {
+    if (currentStep === 3 && exercisesByMuscle.length === 0) {
       fetchExercises();
     }
-  }, [currentStep, selectedEquipment, selectedMuscles]);
+  }, [currentStep, selectedEquipment, selectedMuscles, exercisesByMuscle.length]);
 
   const {
     isWorkoutActive,
@@ -125,8 +125,8 @@ export function WorkoutStepper() {
     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>
+        <h2 className="text-2xl font-bold mb-2">{t("workout_builder.session.congrats")}</h2>
+        <p className="text-lg text-slate-600 mb-6">{t("workout_builder.session.congrats_subtitle")}</p>
         <Button onClick={() => router.push("/profile")}>{t("commons.go_to_profile")}</Button>
       </div>
     );

+ 1 - 1
src/features/workout-session/model/workout-session.store.ts

@@ -90,7 +90,7 @@ export const useWorkoutSessionStore = create<WorkoutSessionState>((set, get) =>
     set({
       session: newSession,
       elapsedTime: 0,
-      isTimerRunning: true,
+      isTimerRunning: false,
       isWorkoutActive: true,
       currentExerciseIndex: 0,
       currentExercise: sessionExercises[0],

+ 21 - 20
src/features/workout-session/ui/workout-session-header.tsx

@@ -3,9 +3,10 @@
 import { useState } from "react";
 import { Clock, Play, Pause, RotateCcw, X, Target } from "lucide-react";
 
-import { useI18n } from "locales/client";
+import { useCurrentLocale, useI18n } from "locales/client";
 import { cn } from "@/shared/lib/utils";
 import { useWorkoutSession } from "@/features/workout-session/model/use-workout-session";
+import { Timer } from "@/components/ui/timer";
 import { Button } from "@/components/ui/button";
 
 import { QuitWorkoutDialog } from "../../workout-builder/ui/quit-workout-dialog";
@@ -31,8 +32,9 @@ export function WorkoutSessionHeader({
 }: WorkoutSessionHeaderProps) {
   const t = useI18n();
   const [showQuitDialog, setShowQuitDialog] = useState(false);
-
-  const { getExercisesCompleted, getTotalExercises } = useWorkoutSession();
+  const [resetCount, setResetCount] = useState(0);
+  const locale = useCurrentLocale();
+  const { getExercisesCompleted, getTotalExercises, session } = useWorkoutSession();
   const exercisesCompleted = getExercisesCompleted();
   const totalExercises = getTotalExercises();
 
@@ -50,17 +52,21 @@ export function WorkoutSessionHeader({
     setShowQuitDialog(false);
   };
 
+  const handleReset = () => {
+    onResetTimer();
+    setResetCount((c) => c + 1);
+  };
+
   return (
     <>
       <div className="w-full mb-8">
-        {/* Minimal header, fond blanc en clair, dégradé en dark */}
         <div className="rounded-xl p-3 bg-slate-50">
-          {/* Top row - Status et Quit button */}
           <div className="flex items-center justify-between mb-4">
             <div className="flex items-center gap-2">
-              <div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></div>
+              <div className="w-2 h-2 rounded-full bg-emerald-400 animate-ping"></div>
               <span className="text-emerald-400 font-semibold text-xs uppercase tracking-wider">
-                {t("workout_builder.session.workout_in_progress")}
+                {t("workout_builder.session.started_at")}{" "}
+                {new Date(session?.startedAt || "").toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" })}
               </span>
             </div>
 
@@ -74,24 +80,23 @@ export function WorkoutSessionHeader({
             </Button>
           </div>
 
-          {/* Main content - Cards */}
-          <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
-            {/* Card 1: Temps écoulé */}
+          <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
+            {/* Card 1: elapsed time */}
             <div className="bg-white dark:bg-gradient-to-br dark:from-slate-800/80 dark:to-slate-700/80 rounded-lg p-3 border border-slate-100 dark:border-slate-600/30">
               <div className="flex items-center gap-2 mb-2">
                 <div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center">
                   <Clock className="h-4 w-4 text-blue-400" />
                 </div>
                 <div>
-                  <h3 className="text-slate-700 dark:text-white font-semibold text-base">{t("workout_builder.session.elapsed_time")}</h3>
+                  <h3 className="text-slate-700 dark:text-white font-semibold text-base">{t("workout_builder.session.chronometer")}</h3>
                 </div>
               </div>
 
-              {/* Chrono display - Large et centré */}
               <div className="text-center">
-                <div className="text-2xl font-mono font-bold text-slate-900 dark:text-white mb-2 tracking-wider">{elapsedTime}</div>
+                <div className="text-2xl font-mono font-bold text-slate-900 dark:text-white mb-2 tracking-wider">
+                  <Timer initialSeconds={typeof elapsedTime === "number" ? elapsedTime : 0} isRunning={isTimerRunning} key={resetCount} />
+                </div>
 
-                {/* Timer controls */}
                 <div className="flex items-center justify-center gap-2">
                   <Button
                     className={cn(
@@ -105,7 +110,7 @@ export function WorkoutSessionHeader({
 
                   <Button
                     className="w-8 h-8 rounded-full p-0 border-slate-200 text-slate-400 hover:bg-slate-100 dark:border-slate-600 hover:dark:bg-slate-700"
-                    onClick={onResetTimer}
+                    onClick={handleReset}
                     variant="outline"
                   >
                     <RotateCcw className="h-4 w-4" />
@@ -114,7 +119,7 @@ export function WorkoutSessionHeader({
               </div>
             </div>
 
-            {/* Card 2: Progression */}
+            {/* Card 2: progress */}
             <div className="bg-white dark:bg-gradient-to-br dark:from-slate-800/80 dark:to-slate-700/80 rounded-lg p-3 border border-slate-100 dark:border-slate-600/30">
               <div className="flex items-center gap-2 mb-2">
                 <div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center">
@@ -128,13 +133,11 @@ export function WorkoutSessionHeader({
               </div>
 
               <div className="space-y-2">
-                {/* Progress display */}
                 <div className="flex items-center justify-between">
                   <span className="text-lg font-bold text-slate-900 dark:text-white">{exercisesCompleted}</span>
                   <span className="text-slate-400">/ {totalExercises}</span>
                 </div>
 
-                {/* Progress bar */}
                 <div className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2 overflow-hidden">
                   <div
                     className="h-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all duration-500 ease-out"
@@ -142,7 +145,6 @@ export function WorkoutSessionHeader({
                   />
                 </div>
 
-                {/* Percentage */}
                 <div className="text-center">
                   <span className="text-xs text-slate-400">
                     {Math.round((exercisesCompleted / totalExercises) * 100)}% {t("workout_builder.session.complete")}
@@ -154,7 +156,6 @@ export function WorkoutSessionHeader({
         </div>
       </div>
 
-      {/* Dialog de confirmation pour quitter */}
       <QuitWorkoutDialog
         elapsedTime={elapsedTime}
         exercisesCompleted={exercisesCompleted}

+ 41 - 23
src/features/workout-session/ui/workout-session-list.tsx

@@ -1,8 +1,10 @@
 import { useState } from "react";
+import { useRouter } from "next/navigation";
 import { Repeat2, Trash2 } from "lucide-react";
 
 import { useCurrentLocale, useI18n } from "locales/client";
 import { workoutSessionLocal } from "@/shared/lib/workout-session/workout-session.local";
+import { useWorkoutBuilderStore } from "@/features/workout-builder/model/workout-builder.store";
 import { InlineTooltip } from "@/components/ui/tooltip";
 import { Button } from "@/components/ui/button";
 
@@ -20,6 +22,8 @@ const BADGE_COLORS = [
 export function WorkoutSessionList({ onSelect }: { onSelect: (id: string) => void }) {
   const locale = useCurrentLocale();
   const t = useI18n();
+  const router = useRouter();
+  const loadFromSession = useWorkoutBuilderStore((s) => s.loadFromSession);
 
   const [sessions, setSessions] = useState<WorkoutSession[]>(() =>
     workoutSessionLocal.getAll().sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()),
@@ -33,30 +37,44 @@ export function WorkoutSessionList({ onSelect }: { onSelect: (id: string) => voi
   const handleRepeat = (id: string) => {
     const sessionToCopy = sessions.find((s) => s.id === id);
     if (!sessionToCopy) return;
-    // Deep copy des exercices et sets, reset des champs nécessaires
-    const newExercises = sessionToCopy.exercises.map((ex, idx) => ({
-      ...ex,
-      sets: ex.sets.map((set, setIdx) => ({
-        ...set,
-        id: `${ex.id}-set-${setIdx + 1}-${Date.now()}`,
-        completed: false,
-      })),
+    // prepare data for the builder
+
+    const allEquipment = Array.from(
+      new Set(
+        sessionToCopy.exercises
+          .flatMap((ex) =>
+            ex.attributes?.filter((attr) => attr.attributeName?.name === "EQUIPMENT").map((attr) => attr.attributeValue.value),
+          )
+          .filter(Boolean),
+      ),
+    );
+
+    const allMuscles = Array.from(
+      new Set(
+        sessionToCopy.exercises
+          .flatMap((ex) =>
+            ex.attributes?.filter((attr) => attr.attributeName?.name === "PRIMARY_MUSCLE").map((attr) => attr.attributeValue.value),
+          )
+          .filter(Boolean),
+      ),
+    );
+    const exercisesByMuscle = allMuscles.map((muscle) => ({
+      muscle,
+      exercises: sessionToCopy.exercises.filter((ex) =>
+        ex.attributes?.some((attr) => attr.attributeName?.name === "PRIMARY_MUSCLE" && attr.attributeValue.value === muscle),
+      ),
     }));
-    const newSession: WorkoutSession = {
-      ...sessionToCopy,
-      id: `${Date.now()}`,
-      startedAt: new Date().toISOString(),
-      endedAt: undefined,
-      duration: 0,
-      status: "active",
-      currentExerciseIndex: 0,
-      isActive: true,
-      exercises: newExercises,
-    };
-    workoutSessionLocal.add(newSession);
-    workoutSessionLocal.setCurrent(newSession.id);
-    setSessions(workoutSessionLocal.getAll().sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()));
-    onSelect(newSession.id);
+
+    const exercisesOrder = sessionToCopy.exercises.map((ex) => ex.id);
+
+    // 5. inject in the builder and go step 3
+    loadFromSession({
+      equipment: allEquipment,
+      muscles: allMuscles,
+      exercisesByMuscle,
+      exercisesOrder,
+    });
+    router.push("/");
   };
 
   return (

+ 1 - 6
src/features/workout-session/ui/workout-session-sets.tsx

@@ -32,11 +32,6 @@ export function WorkoutSessionSets({
   const exerciseDetailsMap = Object.fromEntries(session?.exercises.map((ex) => [ex.id, ex]) || []);
   const [videoModal, setVideoModal] = useState<{ open: boolean; exerciseId?: string }>({ open: false });
 
-  // Calcul de la progression (exercices terminés / total)
-  const totalExercises = session?.exercises.length || 0;
-  const completedExercises = session?.exercises.filter((ex) => ex.sets.length > 0 && ex.sets.every((set) => set.completed)).length || 0;
-  const progressPercent = totalExercises > 0 ? Math.round((completedExercises / totalExercises) * 100) : 0;
-
   if (showCongrats) {
     return (
       <div className="flex flex-col items-center justify-center py-16">
@@ -149,7 +144,7 @@ export function WorkoutSessionSets({
                         setVideoModal({ open: true, exerciseId: ex.id });
                       }}
                     >
-                      Voir les instructions
+                      {t("workout_builder.session.see_instructions")}
                     </span>
                   )}
                   {/* Fallback: description si pas d'introduction */}