ソースを参照

feat(workout-session): add weight conversion functionality to support volume calculations in different units and persist user preferences
fix(workout-session-header): update total volume display to reflect selected weight unit and implement localStorage for user preference management

Mathias 1 ヶ月 前
コミット
21b2d75cfc

+ 32 - 0
src/features/workout-session/model/workout-session.store.ts

@@ -2,6 +2,7 @@ import { create } from "zustand";
 
 import { workoutSessionLocal } from "@/shared/lib/workout-session/workout-session.local";
 import { WorkoutSession } from "@/shared/lib/workout-session/types/workout-session";
+import { convertWeight, type WeightUnit } from "@/shared/lib/weight-conversion";
 import { WorkoutSessionExercise, WorkoutSet } from "@/features/workout-session/types/workout-set";
 import { useWorkoutBuilderStore } from "@/features/workout-builder/model/workout-builder.store";
 
@@ -49,6 +50,7 @@ interface WorkoutSessionState {
   getExercisesCompleted: () => number;
   getTotalExercises: () => number;
   getTotalVolume: () => number;
+  getTotalVolumeInUnit: (unit: WeightUnit) => number;
   loadSessionFromLocal: () => void;
 }
 
@@ -327,6 +329,36 @@ export const useWorkoutSessionStore = create<WorkoutSessionState>((set, get) =>
     return Math.round(totalVolume);
   },
 
+  getTotalVolumeInUnit: (unit: WeightUnit) => {
+    const { session } = get();
+    if (!session) return 0;
+
+    let totalVolume = 0;
+
+    session.exercises.forEach((exercise) => {
+      exercise.sets.forEach((set) => {
+        // Vérifier si le set est complété et contient REPS et WEIGHT
+        if (set.completed && set.types.includes("REPS") && set.types.includes("WEIGHT") && set.valuesInt) {
+          const repsIndex = set.types.indexOf("REPS");
+          const weightIndex = set.types.indexOf("WEIGHT");
+
+          const reps = set.valuesInt[repsIndex] || 0;
+          const weight = set.valuesInt[weightIndex] || 0;
+
+          // Déterminer l'unité de poids originale de la série
+          const originalUnit: WeightUnit = set.units && set.units[weightIndex] === "lbs" ? "lbs" : "kg";
+
+          // Convertir vers l'unité demandée
+          const convertedWeight = convertWeight(weight, originalUnit, unit);
+
+          totalVolume += reps * convertedWeight;
+        }
+      });
+    });
+
+    return Math.round(totalVolume * 10) / 10; // Arrondir à 1 décimale
+  },
+
   formatElapsedTime: () => {
     const { elapsedTime } = get();
     const hours = Math.floor(elapsedTime / 3600);

+ 47 - 5
src/features/workout-session/ui/workout-session-header.tsx

@@ -1,9 +1,10 @@
 "use client";
 
-import { useState } from "react";
+import { useState, useEffect } from "react";
 import { Clock, Play, Pause, RotateCcw, X, Target, Weight } from "lucide-react";
 
 import { useCurrentLocale, useI18n } from "locales/client";
+import { type WeightUnit } from "@/shared/lib/weight-conversion";
 import { cn } from "@/shared/lib/utils";
 import { useWorkoutSession } from "@/features/workout-session/model/use-workout-session";
 import { Timer } from "@/components/ui/timer";
@@ -31,11 +32,26 @@ export function WorkoutSessionHeader({
   const t = useI18n();
   const [showQuitDialog, setShowQuitDialog] = useState(false);
   const [resetCount, setResetCount] = useState(0);
+  const [volumeUnit, setVolumeUnit] = useState<WeightUnit>("kg");
   const locale = useCurrentLocale();
-  const { getExercisesCompleted, getTotalExercises, session, getTotalVolume } = useWorkoutSession();
+  const { getExercisesCompleted, getTotalExercises, session, getTotalVolume, getTotalVolumeInUnit } = useWorkoutSession();
   const exercisesCompleted = getExercisesCompleted();
   const totalExercises = getTotalExercises();
-  const totalVolume = getTotalVolume();
+  const totalVolume = getTotalVolumeInUnit(volumeUnit);
+
+  // Load volume unit preference from localStorage
+  useEffect(() => {
+    const savedUnit = localStorage.getItem("volumeUnit") as WeightUnit;
+    if (savedUnit === "kg" || savedUnit === "lbs") {
+      setVolumeUnit(savedUnit);
+    }
+  }, []);
+
+  // Save volume unit preference to localStorage
+  const handleVolumeUnitChange = (unit: WeightUnit) => {
+    setVolumeUnit(unit);
+    localStorage.setItem("volumeUnit", unit);
+  };
 
   const handleQuitClick = () => {
     setShowQuitDialog(true);
@@ -158,8 +174,34 @@ export function WorkoutSessionHeader({
               </div>
 
               <div className="text-center">
-                <div className="text-2xl font-bold text-slate-900 dark:text-white mb-1">{totalVolume}</div>
-                <span className="text-sm text-slate-400">kg</span>
+                <div className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
+                  {totalVolume.toFixed(volumeUnit === "lbs" ? 1 : 0)}
+                </div>
+                <div className="flex items-center justify-center gap-1">
+                  <button
+                    className={cn(
+                      "text-xs px-2 py-1 rounded transition-colors",
+                      volumeUnit === "kg"
+                        ? "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-100"
+                        : "text-slate-400 hover:text-slate-600 dark:hover:text-slate-300",
+                    )}
+                    onClick={() => handleVolumeUnitChange("kg")}
+                  >
+                    kg
+                  </button>
+                  <span className="text-slate-300 dark:text-slate-600">|</span>
+                  <button
+                    className={cn(
+                      "text-xs px-2 py-1 rounded transition-colors",
+                      volumeUnit === "lbs"
+                        ? "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-100"
+                        : "text-slate-400 hover:text-slate-600 dark:hover:text-slate-300",
+                    )}
+                    onClick={() => handleVolumeUnitChange("lbs")}
+                  >
+                    lbs
+                  </button>
+                </div>
               </div>
             </div>
           </div>

+ 59 - 0
src/shared/lib/weight-conversion.ts

@@ -0,0 +1,59 @@
+export const WEIGHT_CONVERSION = {
+  LBS_TO_KG: 0.453592,
+  KG_TO_LBS: 2.20462,
+} as const;
+
+export type WeightUnit = "kg" | "lbs";
+
+export function convertWeight(weight: number, fromUnit: WeightUnit, toUnit: WeightUnit): number {
+  if (fromUnit === toUnit) return weight;
+
+  if (fromUnit === "lbs" && toUnit === "kg") {
+    return weight * WEIGHT_CONVERSION.LBS_TO_KG;
+  }
+
+  if (fromUnit === "kg" && toUnit === "lbs") {
+    return weight * WEIGHT_CONVERSION.KG_TO_LBS;
+  }
+
+  return weight;
+}
+
+export function formatWeight(weight: number, unit: WeightUnit, decimals: number = 1): string {
+  return `${weight.toFixed(decimals)} ${unit}`;
+}
+
+export function convertVolumeToUnit(
+  exercises: Array<{
+    sets: Array<{
+      completed: boolean;
+      types: string[];
+      valuesInt?: number[];
+      units?: string[];
+    }>;
+  }>,
+  targetUnit: WeightUnit,
+): number {
+  let totalVolume = 0;
+
+  exercises.forEach((exercise) => {
+    exercise.sets.forEach((set) => {
+      if (set.completed && set.types.includes("REPS") && set.types.includes("WEIGHT") && set.valuesInt) {
+        const repsIndex = set.types.indexOf("REPS");
+        const weightIndex = set.types.indexOf("WEIGHT");
+
+        const reps = set.valuesInt[repsIndex] || 0;
+        const weight = set.valuesInt[weightIndex] || 0;
+
+        // set unit
+        const originalUnit: WeightUnit = set.units && set.units[weightIndex] === "lbs" ? "lbs" : "kg";
+
+        const convertedWeight = convertWeight(weight, originalUnit, targetUnit);
+
+        totalVolume += reps * convertedWeight;
+      }
+    });
+  });
+
+  return Math.round(totalVolume * 10) / 10; // round to 1 decimal
+}