Skip to content

Commit 626eefd

Browse files
authored
chore/exercise selection perf (#52)
* refactor(exercise-list-item.tsx): optimize component by using useMemo and useCallback for performance improvements and remove redundant code feat(exercise-list-item.tsx): add muscle configuration constants for better styling management and improve muscle title handling * refactor(exercises-selection.tsx): optimize exercise selection logic using useMemo and useCallback for better performance and readability * fix(exercise-list-item.tsx): change zIndex value from undefined to 1 when not dragging to ensure proper stacking context * feat(exercise-list-item): enhance drag handle for better touch support and improve hover effects for responsiveness feat(exercises-selection): add touch sensor support for drag-and-drop functionality and refactor drag end logic for clarity * style(exercise-list-item.tsx): update button styles for better responsiveness and visual consistency style(exercise-list-item.tsx): adjust hover effects and sizes for improved user experience fix(exercises-selection.tsx): reduce touch sensor delay for quicker interaction responsiveness * chore(ui): remove console.log statements from exercise video modal, workout session store, and workout session list for cleaner code and improved performance style(exercise-list-item): adjust button styles for consistency in min-height and min-width across different screen sizes * refactor(workout-builder): rename isShuffling to shufflingExerciseId for better clarity and update related components to use the new state structure * style(workout-session-header.tsx): update styles for workout session header components to enhance layout and visual consistency * feat(workout-session.store): enhance addSet function to copy types and units from the last set for consistency in workout sessions
1 parent 9a28a4d commit 626eefd

File tree

9 files changed

+184
-162
lines changed

9 files changed

+184
-162
lines changed

src/features/workout-builder/model/use-workout-stepper.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export function useWorkoutStepper() {
1010
exercisesByMuscle,
1111
isLoadingExercises,
1212
exercisesError,
13+
exercisesOrder,
14+
shufflingExerciseId,
1315
setStep,
1416
nextStep,
1517
prevStep,
@@ -18,12 +20,11 @@ export function useWorkoutStepper() {
1820
toggleMuscle,
1921
clearMuscles,
2022
fetchExercises,
21-
exercisesOrder,
2223
setExercisesOrder,
2324
shuffleExercise,
2425
pickExercise,
25-
isShuffling,
2626
deleteExercise,
27+
loadFromSession,
2728
} = useWorkoutBuilderStore();
2829

2930
const canProceedToStep2 = selectedEquipment.length > 0;
@@ -68,12 +69,15 @@ export function useWorkoutStepper() {
6869
shuffleExercise,
6970

7071
// additional
71-
isShuffling,
72+
shufflingExerciseId,
7273

7374
// pick
7475
pickExercise,
7576

7677
// delete
7778
deleteExercise,
79+
80+
// load
81+
loadFromSession,
7882
};
7983
}

src/features/workout-builder/model/workout-builder.store.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface WorkoutBuilderState {
1515
isLoadingExercises: boolean;
1616
exercisesError: any; //TODO: type this
1717
exercisesOrder: string[];
18-
isShuffling: boolean;
18+
shufflingExerciseId: string | null;
1919

2020
// Actions
2121
setStep: (step: WorkoutBuilderStep) => void;
@@ -49,7 +49,7 @@ export const useWorkoutBuilderStore = create<WorkoutBuilderState>((set, get) =>
4949
isLoadingExercises: false,
5050
exercisesError: null,
5151
exercisesOrder: [],
52-
isShuffling: false,
52+
shufflingExerciseId: null,
5353

5454
setStep: (step) => set({ currentStep: step }),
5555
nextStep: () => set((state) => ({ currentStep: Math.min(state.currentStep + 1, 3) as WorkoutBuilderStep })),
@@ -108,7 +108,7 @@ export const useWorkoutBuilderStore = create<WorkoutBuilderState>((set, get) =>
108108
})),
109109

110110
shuffleExercise: async (exerciseId, muscle) => {
111-
set({ isShuffling: true });
111+
set({ shufflingExerciseId: exerciseId });
112112
try {
113113
const { selectedEquipment, exercisesByMuscle } = get();
114114

@@ -144,7 +144,7 @@ export const useWorkoutBuilderStore = create<WorkoutBuilderState>((set, get) =>
144144
console.error("Error shuffling exercise:", error);
145145
throw error;
146146
} finally {
147-
set({ isShuffling: false });
147+
set({ shufflingExerciseId: null });
148148
}
149149
},
150150

src/features/workout-builder/ui/exercise-list-item.tsx

Lines changed: 65 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
/* eslint-disable max-len */
2-
import { useState } from "react";
1+
import { useState, useMemo, useCallback } from "react";
32
import Image from "next/image";
43
import { Play, Shuffle, Trash2, GripVertical, Loader2 } from "lucide-react";
54
import { CSS } from "@dnd-kit/utilities";
@@ -23,98 +22,106 @@ interface ExerciseListItemProps {
2322
isShuffling?: boolean;
2423
}
2524

25+
const MUSCLE_CONFIGS: Record<string, { color: string; bg: string }> = {
26+
ABDOMINALS: { color: "text-red-600 dark:text-red-400", bg: "bg-red-50 dark:bg-red-950/50" },
27+
BICEPS: { color: "text-purple-600 dark:text-purple-400", bg: "bg-purple-50 dark:bg-purple-950/50" },
28+
BACK: { color: "text-blue-600 dark:text-blue-400", bg: "bg-blue-50 dark:bg-blue-950/50" },
29+
CHEST: { color: "text-green-600 dark:text-green-400", bg: "bg-green-50 dark:bg-green-950/50" },
30+
SHOULDERS: { color: "text-orange-600 dark:text-orange-400", bg: "bg-orange-50 dark:bg-orange-950/50" },
31+
OBLIQUES: { color: "text-pink-600 dark:text-pink-400", bg: "bg-pink-50 dark:bg-pink-950/50" },
32+
};
33+
34+
const DEFAULT_MUSCLE_CONFIG = { color: "text-slate-600 dark:text-slate-400", bg: "bg-slate-50 dark:bg-slate-950/50" };
35+
2636
export function ExerciseListItem({ exercise, muscle, onShuffle, onPick, onDelete, isShuffling }: ExerciseListItemProps) {
2737
const t = useI18n();
28-
const [isHovered, setIsHovered] = useState(false);
2938
const locale = useCurrentLocale();
30-
const exerciseName = locale === "fr" ? exercise.name : exercise.nameEn;
3139
const [showVideo, setShowVideo] = useState(false);
3240
const [showPickModal, setShowPickModal] = useState(false);
3341

34-
// dnd-kit sortable
42+
const exerciseName = useMemo(() => (locale === "fr" ? exercise.name : exercise.nameEn), [locale, exercise.name, exercise.nameEn]);
43+
const muscleConfig = useMemo(() => MUSCLE_CONFIGS[muscle] || DEFAULT_MUSCLE_CONFIG, [muscle]);
44+
3545
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: exercise.id });
3646

37-
const style = {
38-
transform: CSS.Transform.toString(transform),
39-
transition,
40-
zIndex: isDragging ? 50 : undefined,
41-
boxShadow: isDragging ? "0 4px 16px 0 rgba(0,0,0,0.10)" : undefined,
42-
};
47+
const style = useMemo(
48+
() => ({
49+
transform: CSS.Transform.toString(transform),
50+
transition,
51+
zIndex: isDragging ? 50 : 1,
52+
boxShadow: isDragging ? "0 4px 16px 0 rgba(0,0,0,0.10)" : undefined,
53+
}),
54+
[transform, transition, isDragging],
55+
);
4356

44-
const handleOpenVideo = () => {
57+
// Mémoriser les handlers
58+
const handleOpenVideo = useCallback(() => {
4559
setShowVideo(true);
46-
};
47-
48-
const _handleOpenPickModal = () => {
49-
setShowPickModal(true);
50-
};
60+
}, []);
5161

52-
const handleClosePickModal = () => {
62+
const handleClosePickModal = useCallback(() => {
5363
setShowPickModal(false);
54-
};
64+
}, []);
5565

56-
const handleConfirmPick = () => {
66+
const handleConfirmPick = useCallback(() => {
5767
onPick(exercise.id);
58-
};
59-
60-
// Déterminer la couleur du muscle
61-
const getMuscleConfig = (muscle: string) => {
62-
const configs: Record<string, { color: string; bg: string }> = {
63-
ABDOMINALS: { color: "text-red-600 dark:text-red-400", bg: "bg-red-50 dark:bg-red-950/50" },
64-
BICEPS: { color: "text-purple-600 dark:text-purple-400", bg: "bg-purple-50 dark:bg-purple-950/50" },
65-
BACK: { color: "text-blue-600 dark:text-blue-400", bg: "bg-blue-50 dark:bg-blue-950/50" },
66-
CHEST: { color: "text-green-600 dark:text-green-400", bg: "bg-green-50 dark:bg-green-950/50" },
67-
SHOULDERS: { color: "text-orange-600 dark:text-orange-400", bg: "bg-orange-50 dark:bg-orange-950/50" },
68-
OBLIQUES: { color: "text-pink-600 dark:text-pink-400", bg: "bg-pink-50 dark:bg-pink-950/50" },
69-
};
70-
return configs[muscle] || { color: "text-slate-600 dark:text-slate-400", bg: "bg-slate-50 dark:bg-slate-950/50" };
71-
};
72-
73-
const muscleConfig = getMuscleConfig(muscle);
68+
}, [onPick, exercise.id]);
69+
70+
const handleShuffle = useCallback(() => {
71+
onShuffle(exercise.id, muscle);
72+
}, [onShuffle, exercise.id, muscle]);
73+
74+
const handleDelete = useCallback(() => {
75+
onDelete(exercise.id, muscle);
76+
}, [onDelete, exercise.id, muscle]);
77+
78+
const muscleTitle = useMemo(() => t(("workout_builder.muscles." + muscle.toLowerCase()) as keyof typeof t), [t, muscle]);
7479

7580
return (
7681
<div
7782
className={`
7883
group relative overflow-hidden transition-all duration-300 ease-out
79-
bg-white dark:bg-slate-900 hover:bg-slate-50 dark:hover:bg-slate-800/70
84+
bg-white dark:bg-slate-900 sm:hover:bg-slate-50 dark:sm:hover:bg-slate-800/70
8085
border-b border-slate-200 dark:border-slate-700/50
81-
${isHovered ? "shadow-lg shadow-slate-200/50 dark:shadow-slate-900/50" : ""}
86+
sm:hover:shadow-lg sm:hover:shadow-slate-200/50 dark:sm:hover:shadow-slate-900/50
8287
${isDragging ? "ring-2 ring-blue-400" : ""}
8388
`}
84-
onMouseEnter={() => setIsHovered(true)}
85-
onMouseLeave={() => setIsHovered(false)}
8689
ref={setNodeRef}
8790
style={style}
8891
>
8992
<div className="relative flex items-center justify-between py-2 px-2">
9093
<div className="flex items-center gap-2 sm:gap-4 flex-1 min-w-0">
9194
{/* Drag handle */}
92-
<GripVertical
93-
className="h-5 w-5 text-slate-400 dark:text-slate-500 cursor-grab active:cursor-grabbing"
95+
<div
96+
className="flex items-center justify-center p-2 -m-2 touch-manipulation cursor-grab active:cursor-grabbing"
9497
{...attributes}
9598
{...listeners}
96-
/>
99+
>
100+
<GripVertical className="h-6 w-6 sm:h-5 sm:w-5 text-slate-400 dark:text-slate-500" />
101+
</div>
97102

98103
{exercise.fullVideoImageUrl && (
99104
<div className="relative w-10 h-10 rounded-lg overflow-hidden shrink-0 bg-slate-200 dark:bg-slate-800 cursor-pointer border border-slate-200 dark:border-slate-700/50">
100105
<Image
101106
alt={exerciseName ?? ""}
102107
className="w-full h-full object-cover scale-[1.5]"
103108
height={40}
109+
loading="lazy"
104110
onError={(e) => {
105111
e.currentTarget.style.display = "none";
106112
}}
113+
priority={false}
107114
src={exercise.fullVideoImageUrl}
108115
width={40}
109116
/>
110-
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
117+
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 sm:group-hover:opacity-100 transition-opacity duration-200">
111118
<Play className="h-3 w-3 text-white fill-current" onClick={handleOpenVideo} />
112119
</div>
113120
</div>
114121
)}
115122

116123
{/* Badge muscle avec animation */}
117-
<InlineTooltip className="cursor-pointer" title={t(("workout_builder.muscles." + muscle.toLowerCase()) as keyof typeof t)}>
124+
<InlineTooltip className="cursor-pointer" title={muscleTitle}>
118125
<div
119126
className={`
120127
relative flex items-center justify-center w-5 h-5 rounded-sm font-bold text-xs shrink-0
@@ -129,46 +136,39 @@ export function ExerciseListItem({ exercise, muscle, onShuffle, onPick, onDelete
129136

130137
{/* Nom de l'exercice avec indicateurs */}
131138
<InlineTooltip className="tooltip tooltip-bottom z-50 max-w-[300px]" title={exerciseName || ""}>
132-
<div className="flex-1 min-w-0 ">
139+
<div className="flex-1 min-w-0 items">
133140
<div className="flex items-center gap-3 mb-1">
134141
<h3 className="font-semibold text-slate-900 dark:text-slate-200 md:truncate text-sm">{exerciseName}</h3>
135142
</div>
136143
</div>
137144
</InlineTooltip>
138145
</div>
139146

140-
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
147+
<div className="flex items-center gap-1 sm:gap-2 shrink-0 ml-1">
141148
{/* Bouton shuffle */}
142149
<Button
143-
className="p-1 sm:p-2"
150+
className="p-2 sm:p-2 min-h-[44px] min-w-[44px] sm:min-h-min sm:min-w-min touch-manipulation"
144151
disabled={isShuffling}
145-
onClick={() => onShuffle(exercise.id, muscle)}
152+
onClick={handleShuffle}
146153
size="small"
147154
variant="outline"
148155
>
149-
{isShuffling ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Shuffle className="h-3.5 w-3.5" />}
150-
<span className="hidden sm:inline">{t("workout_builder.exercise.shuffle")}</span>
156+
{isShuffling ? (
157+
<Loader2 className="h-4 w-4 sm:h-3.5 sm:w-3.5 animate-spin" />
158+
) : (
159+
<Shuffle className="h-4 w-4 sm:h-3.5 sm:w-3.5" />
160+
)}
161+
<span className="hidden sm:inline ml-1">{t("workout_builder.exercise.shuffle")}</span>
151162
</Button>
152163

153-
{/* Bouton pick */}
154-
{/* TODO: V2 */}
155-
{/* <Button
156-
className="p-1 sm:p-2 bg-blue-50 dark:bg-blue-950/50 hover:bg-blue-100 dark:hover:bg-blue-950 text-blue-600 dark:text-blue-400 border-2 border-blue-200 dark:border-blue-800"
157-
onClick={handleOpenPickModal}
158-
size="small"
159-
>
160-
<Star className="h-3.5 w-3.5" />
161-
<span className="hidden sm:inline">{t("workout_builder.exercise.pick")}</span>
162-
</Button> */}
163-
164164
{/* Bouton delete */}
165165
<Button
166-
className="p-1 sm:p-2 bg-red-50 dark:bg-red-950/50 hover:bg-red-100 dark:hover:bg-red-950 text-red-600 dark:text-red-400 border-0 rounded-lg group-hover:opacity-100 transition-all duration-200 hover:scale-110"
167-
onClick={() => onDelete(exercise.id, muscle)}
166+
className="p-2 sm:p-2 min-h-[44px] min-w-[44px] sm:min-h-min sm:min-w-min bg-red-50 dark:bg-red-950/50 sm:hover:bg-red-100 dark:sm:hover:bg-red-950 text-red-600 dark:text-red-400 border-0 rounded-lg opacity-100 sm:group-hover:opacity-100 transition-all duration-200 sm:hover:scale-110 touch-manipulation"
167+
onClick={handleDelete}
168168
size="small"
169169
variant="ghost"
170170
>
171-
<Trash2 className="h-3.5 w-3.5" />
171+
<Trash2 className="h-4 w-4 sm:h-3.5 sm:w-3.5" />
172172
</Button>
173173
</div>
174174
</div>

src/features/workout-builder/ui/exercise-video-modal.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ interface ExerciseVideoModalProps {
1212
}
1313

1414
export function ExerciseVideoModal({ open, onOpenChange, exercise }: ExerciseVideoModalProps) {
15-
console.log("exercise:", exercise);
1615
const t = useI18n();
1716
const locale = useCurrentLocale();
1817
const title = locale === "fr" ? exercise.name : exercise.nameEn || exercise.name;

0 commit comments

Comments
 (0)