Эх сурвалжийг харах

fix: only show unique options in set data additions (#46)

* fix: only show unique options in set data additions

When adding new data to a set, only show datatypes for options that
haven't already been selected.

* feat(workout-session): refactor workout set schema and labels for better maintainability and clarity
chore(middleware): update matcher regex to simplify route exclusions
refactor(workout-session): consolidate workout set types and units into constants for reuse across components

---------

Co-authored-by: Mathias <mathiasnouzy@gmail.com>
Jonathan 1 сар өмнө
parent
commit
b48317d474

+ 1 - 1
middleware.ts

@@ -28,6 +28,6 @@ export async function middleware(request: NextRequest) {
 
 export const config = {
   matcher: [
-    "/((?!api|static|_next|manifest.json|scripts/pixel.js|favicon.ico|robots.txt|service-worker\\.js|sw.js|apple-touch-icon.png|android-chrome-.*\\.png|images|icons|sitemap.xml).*)",
+    "/((?!api|static|_next|manifest.json|favicon.ico|robots.txt|sw.js|apple-touch-icon.png|android-chrome-.*\\.png|images|icons|sitemap.xml).*)",
   ],
 };

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

@@ -5,17 +5,17 @@ import { ExerciseAttributeValueEnum } from "@prisma/client";
 
 import { workoutSessionStatuses } from "@/shared/lib/workout-session/types/workout-session";
 import { prisma } from "@/shared/lib/prisma";
+import { ALL_WORKOUT_SET_TYPES, WORKOUT_SET_UNITS_TUPLE } from "@/shared/constants/workout-set-types";
 import { actionClient } from "@/shared/api/safe-actions";
 import { serverAuth } from "@/entities/user/model/get-server-session-user";
 
-// Schéma WorkoutSet
 const workoutSetSchema = z.object({
   id: z.string(),
   setIndex: z.number(),
-  types: z.array(z.enum(["TIME", "WEIGHT", "REPS", "BODYWEIGHT", "NA"])),
+  types: z.array(z.enum(ALL_WORKOUT_SET_TYPES)),
   valuesInt: z.array(z.number()).optional(),
   valuesSec: z.array(z.number()).optional(),
-  units: z.array(z.enum(["kg", "lbs"])).optional(),
+  units: z.array(z.enum(WORKOUT_SET_UNITS_TUPLE)).optional(),
   completed: z.boolean(),
 });
 

+ 13 - 0
src/features/workout-session/lib/workout-set-labels.ts

@@ -0,0 +1,13 @@
+import { TFunction } from "locales/client";
+
+import { WorkoutSetType } from "../types/workout-set";
+
+export function getWorkoutSetTypeLabels(t: TFunction): Record<WorkoutSetType, string> {
+  return {
+    TIME: t("workout_builder.session.time"),
+    WEIGHT: t("workout_builder.session.weight"),
+    REPS: t("workout_builder.session.reps"),
+    BODYWEIGHT: t("workout_builder.session.bodyweight"),
+    NA: "N/A",
+  };
+}

+ 0 - 16
src/features/workout-session/schema/workout-session-set.schema.ts

@@ -1,16 +0,0 @@
-import { z } from "zod";
-
-export const workoutSessionSetSchema = z.object({
-  id: z.string(),
-  setIndex: z.number().int().min(0),
-  type: z.enum(["TIME", "WEIGHT", "REPS", "BODYWEIGHT", "NA"]),
-  types: z.array(z.enum(["TIME", "WEIGHT", "REPS", "BODYWEIGHT", "NA"])).optional(),
-  valuesInt: z.array(z.number().int()).optional(),
-  valueSec: z.number().int().min(0).max(59).optional(),
-  valuesSec: z.array(z.number().int().min(0).max(59)).optional(),
-  unit: z.enum(["kg", "lbs"]).optional(),
-  units: z.array(z.enum(["kg", "lbs"])).optional(),
-  completed: z.boolean(),
-});
-
-export type WorkoutSetInput = z.infer<typeof workoutSessionSetSchema>;

+ 42 - 31
src/features/workout-session/ui/workout-session-set.tsx

@@ -1,7 +1,9 @@
 import { Plus, Minus, Trash2 } from "lucide-react";
 
 import { useI18n } from "locales/client";
+import { AVAILABLE_WORKOUT_SET_TYPES, MAX_WORKOUT_SET_COLUMNS } from "@/shared/constants/workout-set-types";
 import { WorkoutSet, WorkoutSetType, WorkoutSetUnit } from "@/features/workout-session/types/workout-set";
+import { getWorkoutSetTypeLabels } from "@/features/workout-session/lib/workout-set-labels";
 import { Button } from "@/components/ui/button";
 
 interface WorkoutSetRowProps {
@@ -15,7 +17,7 @@ interface WorkoutSetRowProps {
 export function WorkoutSessionSet({ set, setIndex, onChange, onFinish, onRemove }: WorkoutSetRowProps) {
   const t = useI18n();
   const types = set.types || [];
-  const maxColumns = 4;
+  const typeLabels = getWorkoutSetTypeLabels(t);
 
   const handleTypeChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLSelectElement>) => {
     const newTypes = [...types];
@@ -42,9 +44,12 @@ export function WorkoutSessionSet({ set, setIndex, onChange, onFinish, onRemove
   };
 
   const addColumn = () => {
-    if (types.length < maxColumns) {
-      const newTypes = [...types, "REPS" as WorkoutSetType];
-      onChange(setIndex, { types: newTypes });
+    if (types.length < MAX_WORKOUT_SET_COLUMNS) {
+      const firstAvailableType = AVAILABLE_WORKOUT_SET_TYPES.find((t) => !types.includes(t));
+      if (firstAvailableType) {
+        const newTypes = [...types, firstAvailableType];
+        onChange(setIndex, { types: newTypes });
+      }
     }
   };
 
@@ -169,38 +174,44 @@ export function WorkoutSessionSet({ set, setIndex, onChange, onFinish, onRemove
 
       {/* Columns of types, stack vertical on mobile, horizontal on md+ */}
       <div className="flex flex-col md:flex-row gap-6 md:gap-2 w-full">
-        {types.map((type, columnIndex) => (
-          <div className="flex flex-col w-full md:w-auto" key={columnIndex}>
-            <div className="flex items-center w-full gap-1 mb-1">
-              <select
-                className="border border-black dark:border-slate-700 rounded font-bold px-1 py-2 text-base w-full bg-white dark:bg-slate-800 dark:text-gray-200 min-w-0 h-10 "
-                disabled={set.completed}
-                onChange={handleTypeChange(columnIndex)}
-                value={type}
-              >
-                <option value="TIME">{t("workout_builder.session.time")}</option>
-                <option value="WEIGHT">{t("workout_builder.session.weight")}</option>
-                <option value="REPS">{t("workout_builder.session.reps")}</option>
-                <option value="BODYWEIGHT">{t("workout_builder.session.bodyweight")}</option>
-              </select>
-              {types.length > 1 && (
-                <Button
-                  className="p-1 h-auto bg-red-500 hover:bg-red-600 dark:bg-red-900 dark:hover:bg-red-800 flex-shrink-0"
-                  onClick={() => removeColumn(columnIndex)}
-                  size="small"
-                  variant="destructive"
+        {types.map((type, columnIndex) => {
+          // An option is available if it's not used by another column, OR it's the current column's type.
+          const availableTypes = AVAILABLE_WORKOUT_SET_TYPES.filter((option) => !types.includes(option) || option === type);
+
+          return (
+            <div className="flex flex-col w-full md:w-auto" key={columnIndex}>
+              <div className="flex items-center w-full gap-1 mb-1">
+                <select
+                  className="border border-black dark:border-slate-700 rounded font-bold px-1 py-2 text-base w-full bg-white dark:bg-slate-800 dark:text-gray-200 min-w-0 h-10 "
+                  disabled={set.completed}
+                  onChange={handleTypeChange(columnIndex)}
+                  value={type}
                 >
-                  <Minus className="h-3 w-3" />
-                </Button>
-              )}
+                  {availableTypes.map((availableType) => (
+                    <option key={availableType} value={availableType}>
+                      {typeLabels[availableType]}
+                    </option>
+                  ))}
+                </select>
+                {types.length > 1 && (
+                  <Button
+                    className="p-1 h-auto bg-red-500 hover:bg-red-600 dark:bg-red-900 dark:hover:bg-red-800 flex-shrink-0"
+                    onClick={() => removeColumn(columnIndex)}
+                    size="small"
+                    variant="destructive"
+                  >
+                    <Minus className="h-3 w-3" />
+                  </Button>
+                )}
+              </div>
+              {renderInputForType(type, columnIndex)}
             </div>
-            {renderInputForType(type, columnIndex)}
-          </div>
-        ))}
+          );
+        })}
       </div>
 
       {/* Add column button */}
-      {types.length < maxColumns && !set.completed && (
+      {types.length < MAX_WORKOUT_SET_COLUMNS && !set.completed && (
         <div className="flex w-full justify-start mt-1">
           <Button
             className="font-bold px-4 py-2 text-sm rounded-xl w-full md:w-auto mt-2"

+ 4 - 0
src/shared/constants/workout-set-types.ts

@@ -0,0 +1,4 @@
+export const AVAILABLE_WORKOUT_SET_TYPES = ["TIME", "WEIGHT", "REPS", "BODYWEIGHT"] as const;
+export const ALL_WORKOUT_SET_TYPES = ["TIME", "WEIGHT", "REPS", "BODYWEIGHT", "NA"] as const;
+export const MAX_WORKOUT_SET_COLUMNS = 4;
+export const WORKOUT_SET_UNITS_TUPLE = ["kg", "lbs"] as const;