1- /* eslint-disable max-len */
2- import { useState } from "react" ;
1+ import { useState , useMemo , useCallback } from "react" ;
32import Image from "next/image" ;
43import { Play , Shuffle , Trash2 , GripVertical , Loader2 } from "lucide-react" ;
54import { 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+
2636export 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 >
0 commit comments