Pārlūkot izejas kodu

feat(auth-layout): update layout styles to use flexbox for better alignment
feat(locales): add abdominals muscle translation to English locale
feat(prisma): add muscles column to workout_sessions table for better tracking
feat(workout-builder): enhance workout builder to include muscle data in state management
feat(workout-session): implement muscle tracking in workout sessions for improved functionality
style(workout-session-set): change button style to rounded-xl for better aesthetics
fix(workout-session-list): ensure muscles are

Mathias 1 mēnesi atpakaļ
vecāks
revīzija
35951d86cf

+ 1 - 1
app/[locale]/auth/(auth-layout)/layout.tsx

@@ -23,7 +23,7 @@ export default async function AuthLayout(props: LayoutParams<{}>) {
 
   return (
     <>
-      <div>
+      <div className="h-full flex">
         {searchParams.error && (
           <Alert className="mb-4" variant="error">
             <AlertTitle>{translatedError}</AlertTitle>

+ 1 - 1
locales/en.ts

@@ -108,8 +108,8 @@ export default {
       },
     },
     muscles: {
-      abdominals: "Abdominals",
       back: "Back",
+      abdominals: "Abdominals",
       biceps: "Biceps",
       triceps: "Triceps",
       chest: "Chest",

+ 2 - 0
prisma/migrations/20250615160343_add_muscle_to_a_workout_session/migration.sql

@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "workout_sessions" ADD COLUMN     "muscles" "ExerciseAttributeValueEnum"[] DEFAULT ARRAY[]::"ExerciseAttributeValueEnum"[];

+ 3 - 2
prisma/schema.prisma

@@ -267,13 +267,14 @@ enum ExerciseAttributeValueEnum {
 }
 
 model WorkoutSession {
-  id        String                   @id @default(cuid())
+  id        String                       @id @default(cuid())
   userId    String
-  user      User                     @relation(fields: [userId], references: [id])
+  user      User                         @relation(fields: [userId], references: [id])
   startedAt DateTime
   endedAt   DateTime?
   duration  Int? // en secondes
   exercises WorkoutSessionExercise[]
+  muscles   ExerciseAttributeValueEnum[] @default([])
 
   @@map("workout_sessions")
 }

+ 5 - 2
src/features/workout-builder/model/workout-builder.store.ts

@@ -1,5 +1,5 @@
 import { create } from "zustand";
-import { ExerciseAttributeValueEnum } from "@prisma/client";
+import { ExerciseAttributeValueEnum, WorkoutSessionExercise } from "@prisma/client";
 
 import { WorkoutBuilderStep } from "../types";
 import { getExercisesAction } from "./get-exercises.action";
@@ -27,7 +27,10 @@ interface WorkoutBuilderState {
   loadFromSession: (params: {
     equipment: ExerciseAttributeValueEnum[];
     muscles: ExerciseAttributeValueEnum[];
-    exercisesByMuscle: any[];
+    exercisesByMuscle: {
+      muscle: ExerciseAttributeValueEnum;
+      exercises: WorkoutSessionExercise[];
+    }[];
     exercisesOrder: string[];
   }) => void;
 }

+ 1 - 0
src/features/workout-builder/types/index.ts

@@ -35,6 +35,7 @@ export interface EquipmentItem {
 
 // Types pour les exercices avec leurs attributs
 export type ExerciseWithAttributes = Exercise & {
+  order: number;
   attributes: (ExerciseAttribute & {
     attributeName: ExerciseAttributeName;
     attributeValue: ExerciseAttributeValue;

+ 12 - 5
src/features/workout-builder/ui/exercises-selection.tsx

@@ -69,14 +69,21 @@ export const ExercisesSelection = ({
     }
   };
 
-  return (
-    <div className="space-y-6">
-      {isLoading ? (
+  if (isLoading) {
+    return (
+      <div className="space-y-6">
         <div className="text-center">
           <Loader2 className="h-8 w-8 animate-spin mx-auto text-blue-600" />
           <p className="mt-4 text-slate-600 dark:text-slate-400">{t("workout_builder.loading.exercises")}</p>
         </div>
-      ) : flatExercises.length > 0 ? (
+      </div>
+    );
+  }
+  console.log("flatExercises:", flatExercises);
+
+  return (
+    <div className="space-y-6">
+      {flatExercises.length > 0 ? (
         <div className="max-w-4xl mx-auto">
           {/* Liste des exercices drag and drop */}
           <DndContext collisionDetection={closestCenter} modifiers={[restrictToVerticalAxis]} onDragEnd={handleDragEnd} sensors={sensors}>
@@ -85,7 +92,7 @@ export const ExercisesSelection = ({
                 {flatExercises.map((item) => (
                   <ExerciseListItem
                     exercise={item.exercise}
-                    key={item.id}
+                    key={`${item.id}-${item.exercise.order}`}
                     muscle={item.muscle}
                     onDelete={onDelete}
                     onPick={onPick}

+ 29 - 5
src/features/workout-builder/ui/workout-stepper.tsx

@@ -1,8 +1,10 @@
 "use client";
 
 import { useState, useEffect } from "react";
+import { useQueryState } from "nuqs";
 import { useRouter } from "next/navigation";
 import Image from "next/image";
+import { ExerciseAttributeValueEnum } from "@prisma/client";
 
 import { useI18n } from "locales/client";
 import Trophy from "@public/images/trophy.png";
@@ -26,6 +28,7 @@ export function WorkoutStepper() {
 
   const t = useI18n();
   const router = useRouter();
+  const [fromSession, setFromSession] = useQueryState("fromSession");
   const {
     currentStep,
     selectedEquipment,
@@ -62,12 +65,12 @@ export function WorkoutStepper() {
       setFlatExercises(flat);
     }
   }, [exercisesByMuscle]);
-  // FIXME : when i go back to step 2, the exercises are not fetched anymore
+
   useEffect(() => {
-    if (currentStep === 3 && exercisesByMuscle.length === 0) {
+    if (currentStep === 3 && !fromSession) {
       fetchExercises();
     }
-  }, [currentStep, selectedEquipment, selectedMuscles, exercisesByMuscle.length]);
+  }, [currentStep, selectedEquipment, selectedMuscles, fromSession]);
 
   const {
     isWorkoutActive,
@@ -123,6 +126,21 @@ export function WorkoutStepper() {
     router.push("/profile");
   };
 
+  const handleToggleEquipment = (equipment: ExerciseAttributeValueEnum) => {
+    toggleEquipment(equipment);
+    if (fromSession) setFromSession(null);
+  };
+
+  const handleClearEquipment = () => {
+    clearEquipment();
+    if (fromSession) setFromSession(null);
+  };
+
+  const handleToggleMuscle = (muscle: ExerciseAttributeValueEnum) => {
+    toggleMuscle(muscle);
+    if (fromSession) setFromSession(null);
+  };
+
   if (showCongrats && !isWorkoutActive) {
     return (
       <div className="flex flex-col items-center justify-center py-16 h-full">
@@ -186,10 +204,16 @@ export function WorkoutStepper() {
     switch (currentStep) {
       case 1:
         return (
-          <EquipmentSelection onClearEquipment={clearEquipment} onToggleEquipment={toggleEquipment} selectedEquipment={selectedEquipment} />
+          <EquipmentSelection
+            onClearEquipment={handleClearEquipment}
+            onToggleEquipment={handleToggleEquipment}
+            selectedEquipment={selectedEquipment}
+          />
         );
       case 2:
-        return <MuscleSelection onToggleMuscle={toggleMuscle} selectedEquipment={selectedEquipment} selectedMuscles={selectedMuscles} />;
+        return (
+          <MuscleSelection onToggleMuscle={handleToggleMuscle} selectedEquipment={selectedEquipment} selectedMuscles={selectedMuscles} />
+        );
       case 3:
         return (
           <ExercisesSelection

+ 4 - 0
src/features/workout-session/actions/sync-workout-sessions.action.ts

@@ -1,6 +1,7 @@
 "use server";
 
 import { z } from "zod";
+import { ExerciseAttributeValueEnum } from "@prisma/client";
 
 import { workoutSessionStatuses } from "@/shared/lib/workout-session/types/workout-session";
 import { prisma } from "@/shared/lib/prisma";
@@ -31,6 +32,7 @@ const syncWorkoutSessionSchema = z.object({
     endedAt: z.string().optional(),
     exercises: z.array(workoutSessionExerciseSchema),
     status: z.enum(workoutSessionStatuses),
+    muscles: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),
   }),
 });
 
@@ -44,6 +46,7 @@ export const syncWorkoutSessionAction = actionClient.schema(syncWorkoutSessionSc
       where: { id: session.id },
       create: {
         ...sessionData,
+        muscles: session.muscles,
         exercises: {
           create: session.exercises.map((exercise) => ({
             order: exercise.order,
@@ -66,6 +69,7 @@ export const syncWorkoutSessionAction = actionClient.schema(syncWorkoutSessionSc
         startedAt: sessionData.startedAt,
         endedAt: sessionData.endedAt,
         userId: sessionData.userId,
+        muscles: session.muscles,
         exercises: {
           deleteMany: {},
           create: session.exercises.map((exercise) => ({

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

@@ -63,7 +63,7 @@ export const useWorkoutSessionStore = create<WorkoutSessionState>((set, get) =>
   totalExercises: 0,
   progressPercent: 0,
 
-  startWorkout: (exercises, _equipment, _muscles) => {
+  startWorkout: (exercises, _equipment, muscles) => {
     const sessionExercises: WorkoutSessionExercise[] = exercises.map((ex, idx) => ({
       ...ex,
       order: idx,
@@ -86,6 +86,7 @@ export const useWorkoutSessionStore = create<WorkoutSessionState>((set, get) =>
       startedAt: new Date().toISOString(),
       exercises: sessionExercises,
       status: "active",
+      muscles,
     };
 
     workoutSessionLocal.add(newSession);

+ 45 - 16
src/features/workout-session/ui/workout-session-list.tsx

@@ -38,6 +38,7 @@ export function WorkoutSessionList() {
     const sessionToCopy = sessions.find((s) => s.id === id);
     if (!sessionToCopy) return;
     // prepare data for the builder
+    console.log("sessionToCopy.exercises:", sessionToCopy.exercises);
 
     const allEquipment = Array.from(
       new Set(
@@ -49,21 +50,36 @@ export function WorkoutSessionList() {
       ),
     );
 
-    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),
-      ),
-    }));
+    // Utilise les muscles stockés dans la session, sinon fallback sur les muscles primaires des exercices
+    const allMuscles =
+      sessionToCopy.muscles && sessionToCopy.muscles.length > 0
+        ? sessionToCopy.muscles
+        : Array.from(
+            new Set(
+              sessionToCopy.exercises
+                .flatMap((ex) =>
+                  ex.attributes?.filter((attr) => attr.attributeName?.name === "PRIMARY_MUSCLE").map((attr) => attr.attributeValue.value),
+                )
+                .filter(Boolean),
+            ),
+          );
+    console.log("allMuscles:", allMuscles);
+
+    // Pour répéter exactement la même séance, on garde tous les exercices dans l'ordre exact
+    const exercisesByMuscle = [
+      {
+        muscle: allMuscles[0] || "FULL_BODY", // Utilise le premier muscle sélectionné ou FULL_BODY par défaut
+        exercises: sessionToCopy.exercises
+          .sort((a, b) => a.order - b.order) // Trie par ordre original
+          .map((ex) => ({
+            ...ex,
+            id: ex.id,
+            workoutSessionId: sessionToCopy.id,
+            exerciseId: ex.id,
+            order: ex.order,
+          })),
+      },
+    ];
 
     const exercisesOrder = sessionToCopy.exercises.map((ex) => ex.id);
 
@@ -74,7 +90,7 @@ export function WorkoutSessionList() {
       exercisesByMuscle,
       exercisesOrder,
     });
-    router.push("/");
+    router.push("/?fromSession=1");
   };
 
   return (
@@ -107,6 +123,19 @@ export function WorkoutSessionList() {
                     {new Date(session.endedAt).toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" })}
                   </span>
                 )}
+                {session.muscles && session.muscles.length > 0 && (
+                  <div className="flex flex-wrap gap-1 mt-1 justify-center">
+                    {session.muscles.map((muscle, idx) => (
+                      <span
+                        // eslint-disable-next-line max-len
+                        className={`inline-block border rounded-full px-2 py-0.5 text-xs font-semibold ${BADGE_COLORS[idx % BADGE_COLORS.length]}`}
+                        key={muscle}
+                      >
+                        {t(("workout_builder.muscles." + muscle.toLowerCase()) as keyof typeof t)}
+                      </span>
+                    ))}
+                  </div>
+                )}
                 {session.status === "active" && (
                   <div className="relative mt-1">
                     <span className="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 border border-emerald-300 text-xs font-semibold">

+ 1 - 1
src/features/workout-session/ui/workout-session-set.tsx

@@ -227,7 +227,7 @@ export function WorkoutSessionSet({ set, setIndex, onChange, onFinish, onRemove
         </Button>
         {set.completed && (
           <Button
-            className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold px-4 py-2 text-sm rounded flex-1 border border-gray-300"
+            className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold px-4 py-2 text-sm rounded-xl flex-1 border border-gray-300"
             onClick={handleEdit}
             variant="outline"
           >

+ 3 - 0
src/shared/lib/workout-session/types/workout-session.ts

@@ -1,3 +1,5 @@
+import { ExerciseAttributeValueEnum } from "@prisma/client";
+
 import { WorkoutSessionExercise } from "@/features/workout-session/types/workout-set";
 
 export const workoutSessionStatuses = ["active", "completed", "synced"] as const;
@@ -14,4 +16,5 @@ export interface WorkoutSession {
   currentExerciseIndex?: number;
   isActive?: boolean;
   serverId?: string; // If synced
+  muscles: ExerciseAttributeValueEnum[];
 }

+ 1 - 2
src/shared/lib/workout-session/use-workout-session.service.ts

@@ -54,11 +54,10 @@ export const useWorkoutSessionService = () => {
 
   const add = async (session: WorkoutSession) => {
     if (userId) {
-      // Utiliser l'action de synchronisation
       const result = await syncWorkoutSessionAction({
         session: {
           ...session,
-          userId: "current-user-id", // TODO: passer le vrai userId
+          userId,
           status: "synced",
         },
       });