@@ -13,7 +13,24 @@ import {
1313import { DIAS_DA_SEMANA } from '@/lib/constants' ;
1414import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
1515import { Button } from '@/components/ui/button' ;
16- import { PlusCircle , Trash2 , Pencil , FileSignature , User , Play , Sparkles } from 'lucide-react' ;
16+ import {
17+ PlusCircle ,
18+ Trash2 ,
19+ Pencil ,
20+ FileSignature ,
21+ User ,
22+ Play ,
23+ Sparkles ,
24+ Dumbbell ,
25+ MoreVertical ,
26+ } from 'lucide-react' ;
27+ import {
28+ DropdownMenu ,
29+ DropdownMenuContent ,
30+ DropdownMenuItem ,
31+ DropdownMenuSeparator ,
32+ DropdownMenuTrigger ,
33+ } from '@/components/ui/dropdown-menu' ;
1734import type { Treino , HistoricoTreino } from '@/lib/definitions' ;
1835import { useAppNotification } from '@/hooks/use-app-notification' ;
1936import { WorkoutSession } from '@/components/WorkoutSession' ;
@@ -82,14 +99,20 @@ export default function MeusTreinosClient({
8299 const [ showPlanBanner , setShowPlanBanner ] = useState ( false ) ;
83100 const [ bannerPlanName , setBannerPlanName ] = useState < string | null > ( null ) ;
84101
102+ /* eslint-disable react-hooks/set-state-in-effect -- ponytail: setState-on-prop-change is the legitimate derived-banner-from-hook pattern; reducer adds ceremony for no gain. */
85103 useEffect ( ( ) => {
86104 if ( planName ) {
87105 setBannerPlanName ( planName ) ;
88106 setShowPlanBanner ( true ) ;
89- const timer = setTimeout ( ( ) => setShowPlanBanner ( false ) , 30000 ) ;
90- return ( ) => clearTimeout ( timer ) ;
91107 }
92108 } , [ planName ] ) ;
109+ /* eslint-enable react-hooks/set-state-in-effect */
110+
111+ useEffect ( ( ) => {
112+ if ( ! showPlanBanner ) return ;
113+ const timer = setTimeout ( ( ) => setShowPlanBanner ( false ) , 30000 ) ;
114+ return ( ) => clearTimeout ( timer ) ;
115+ } , [ bannerPlanName , showPlanBanner ] ) ;
93116
94117 const [ treinoEmSessao , setTreinoEmSessao ] = useState < Treino | null > ( null ) ;
95118 const [ editingTreinoId , setEditingTreinoId ] = useState < string | null > ( null ) ;
@@ -127,8 +150,9 @@ export default function MeusTreinosClient({
127150 { treinos . map ( ( treino ) => (
128151 < div
129152 key = { treino . id }
153+ data-testid = "treino-card"
130154 className = { cn (
131- 'rounded-lg border p-4 transition-all flex flex-col sm:flex-row justify-between sm:items-center gap-4 ' ,
155+ 'rounded-lg border p-4 transition-all flex flex-col gap-3 ' ,
132156 treino . diaSemana !== null && 'bg-accent/10 border-accent'
133157 ) }
134158 >
@@ -146,19 +170,54 @@ export default function MeusTreinosClient({
146170 </ div >
147171 ) : (
148172 < >
149- < div className = "flex-1" >
150- < div className = "flex items-center gap-3" >
151- < h3 className = "font-bold text-base" > { treino . objetivo } </ h3 >
152- { treino . diaSemana !== null && < Badge > { getDiaLabel ( treino . diaSemana ) } </ Badge > }
153- { treino . instrutorId && treino . instrutorId !== userId && (
154- < Badge variant = "secondary" > Do Personal</ Badge >
155- ) }
173+ < div className = "flex items-start justify-between gap-2" >
174+ < div className = "flex-1 min-w-0" >
175+ < div className = "flex items-center gap-2 flex-wrap" >
176+ < h3 className = "font-headline font-bold text-base" > { treino . objetivo } </ h3 >
177+ { treino . diaSemana !== null && < Badge > { getDiaLabel ( treino . diaSemana ) } </ Badge > }
178+ { treino . instrutorId && treino . instrutorId !== userId && (
179+ < Badge variant = "secondary" > Do Personal</ Badge >
180+ ) }
181+ </ div >
182+ < p className = "text-sm text-muted-foreground" >
183+ { treino . exercicios . length } exercícios
184+ </ p >
156185 </ div >
157- < p className = "text-sm text-muted-foreground" >
158- { treino . exercicios . length } exercícios
159- </ p >
186+ { allowEditing && (
187+ < DropdownMenu >
188+ < DropdownMenuTrigger asChild >
189+ < Button
190+ variant = "ghost"
191+ size = "icon"
192+ className = "h-9 w-9 shrink-0"
193+ data-testid = "treino-kebab"
194+ aria-label = "Ações do treino"
195+ >
196+ < MoreVertical className = "h-4 w-4" />
197+ </ Button >
198+ </ DropdownMenuTrigger >
199+ < DropdownMenuContent align = "end" >
200+ < DropdownMenuItem
201+ data-testid = "editar-treino"
202+ onClick = { ( ) => handleEditLocal ( treino ) }
203+ >
204+ < Pencil className = "mr-2 h-4 w-4" />
205+ Editar
206+ </ DropdownMenuItem >
207+ < DropdownMenuSeparator />
208+ < DropdownMenuItem
209+ data-testid = "excluir-treino"
210+ className = "text-destructive focus:text-destructive"
211+ onClick = { ( ) => openDeleteAlert ( treino ) }
212+ >
213+ < Trash2 className = "mr-2 h-4 w-4" />
214+ Excluir
215+ </ DropdownMenuItem >
216+ </ DropdownMenuContent >
217+ </ DropdownMenu >
218+ ) }
160219 </ div >
161- < div className = "flex items-center gap-2 flex-wrap " >
220+ < div className = "flex flex-col sm:flex-row gap-2 " >
162221 < Select
163222 value = { treino . diaSemana === null ? 'nenhum' : String ( treino . diaSemana ) }
164223 onValueChange = { ( value ) => handleDayChange ( treino . id , value ) }
@@ -175,36 +234,39 @@ export default function MeusTreinosClient({
175234 ) ) }
176235 </ SelectContent >
177236 </ Select >
178- < Button size = "sm" onClick = { ( ) => setTreinoEmSessao ( treino ) } >
237+ < Button
238+ size = "sm"
239+ className = "flex-1 sm:flex-none"
240+ data-testid = "iniciar-treino"
241+ onClick = { ( ) => setTreinoEmSessao ( treino ) }
242+ >
179243 < Play className = "mr-2 h-4 w-4" />
180- Iniciar
244+ Iniciar Treino
181245 </ Button >
182- { allowEditing && (
183- < >
184- < Button variant = "secondary" size = "sm" onClick = { ( ) => handleEditLocal ( treino ) } >
185- < Pencil className = "mr-2 h-4 w-4" />
186- Editar
187- </ Button >
188- < Button
189- variant = "destructive"
190- size = "sm"
191- onClick = { ( ) => openDeleteAlert ( treino ) }
192- >
193- < Trash2 className = "mr-2 h-4 w-4" />
194- Excluir
195- </ Button >
196- </ >
197- ) }
198246 </ div >
199247 </ >
200248 ) }
201249 </ div >
202250 ) ) }
203251 { treinos . length === 0 && (
204- < div className = "text-center text-sm text-muted-foreground py-10" >
205- { allowEditing
206- ? 'Você ainda não criou nenhum treino.'
207- : 'Nenhum treino prescrito pelo seu personal ainda.' }
252+ < div className = "flex flex-col items-center justify-center gap-3 py-16 text-center" >
253+ < div className = "rounded-full bg-muted/30 p-4" >
254+ < Dumbbell className = "h-8 w-8 text-muted-foreground" />
255+ </ div >
256+ < div >
257+ < p className = "font-medium text-foreground" > Nenhum treino ainda</ p >
258+ < p className = "text-sm text-muted-foreground mt-1" >
259+ { allowEditing
260+ ? 'Gere um plano com IA ou crie manualmente para começar.'
261+ : 'Seu personal ainda não prescreveu treinos.' }
262+ </ p >
263+ </ div >
264+ { allowEditing && (
265+ < Button variant = "outline" size = "sm" onClick = { ( ) => setEditingTreinoId ( '__new__' ) } >
266+ < PlusCircle className = "mr-2 h-4 w-4" />
267+ Criar primeiro treino
268+ </ Button >
269+ ) }
208270 </ div >
209271 ) }
210272 </ CardContent >
0 commit comments