Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/CURRENT-STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@

## Mobile-First Premium Polish (v0.10.0 em andamento)

**Branch ativa:** `fix/meus-treinos-kebab` (PR #181 aberto, aguardando review bots)
**PRs mobile-first:** PR #176 (PRD-1) ✅ → PR #179 (PRD-3) ✅ → PR #180 (PRD-2) ✅ → PR #181 (PRD-4) 🟡.
**Branch ativa:** `feat/workout-session-mobile` (PR #182 aberto, aguardando CI)
**PRs mobile-first:** PR #176 (PRD-1) ✅ → PR #179 (PRD-3) ✅ → PR #180 (PRD-2) ✅ → PR #181 (PRD-4) ✅ → PR #182 (PRD-5) 🟡.

### PRD-5 — Workout Session Fullscreen — PR #182 (open)

`src/components/WorkoutSession.tsx`: mobile fullscreen overlay (`fixed inset-x-0 top-0 z-50 h-dvh bg-background`), desktop inline (`md:static md:bg-transparent`). Card vira flex column: `CardContent` scrollável `overflow-y-auto` + `CardFooter` sticky `backdrop-blur` (timer + Próximo/Finalizar acima da URL bar iOS). Series row `data-testid="series-row"`, check buttons `data-testid={`serie-check-${idx}`}` + `aria-label` + `.touch-target` (44px). `animate-[slide-up_0.3s_ease-out] motion-reduce:animate-none`.

`src/app/globals.css`: `@keyframes slide-up` no utilities layer.

E2E `tests/e2e/specs/workout-session.spec.ts`: `div.grid-cols-4` selector → `getByTestId('serie-check-0')` (2 ocorrências). Stable contra refactor de grid/Tailwind purge.

Unit `src/components/WorkoutSession.test.tsx`: Button mock `filterDomProps` passa `data-*`/`aria-*`/`className`. Toggle test sai de svg/`data-variant` filter frágil → `getByTestId('serie-check-0')`. 15/15 pass, 1133/1133 suite.

### PRD-4 — Meus Treinos Kebab + Primary Action — merged #181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: docs/CURRENT-STATE.md: duplicate ### PRD-4 heading. The new merged #181 heading was added but the old PR #181 (open) heading was left in place. Remove the stale duplicate so the PRD-4 feature description lives under a single heading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/CURRENT-STATE.md, line 18:

<comment>docs/CURRENT-STATE.md: duplicate `### PRD-4` heading. The new `merged #181` heading was added but the old `PR #181 (open)` heading was left in place. Remove the stale duplicate so the PRD-4 feature description lives under a single heading.</comment>

<file context>
@@ -2,8 +2,20 @@
+
+Unit `src/components/WorkoutSession.test.tsx`: Button mock `filterDomProps` passa `data-*`/`aria-*`/`className`. Toggle test sai de svg/`data-variant` filter frágil → `getByTestId('serie-check-0')`. 15/15 pass, 1133/1133 suite.
+
+### PRD-4 — Meus Treinos Kebab + Primary Action — merged #181
 
 ### PRD-4 — Meus Treinos Kebab + Primary Action — PR #181 (open)
</file context>


### PRD-4 — Meus Treinos Kebab + Primary Action — PR #181 (open)

Expand Down
8 changes: 8 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@
min-height: 44px;
min-width: 44px;
}
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
}

@media (prefers-reduced-motion: reduce) {
Expand Down
46 changes: 30 additions & 16 deletions src/components/WorkoutSession.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { WorkoutSession } from './WorkoutSession';
import type { Treino } from '@/lib/definitions';
import type { ReactNode } from 'react';

function filterDomProps(props: Record<string, unknown>): Record<string, unknown> {
const domProps: Record<string, unknown> = {};
for (const [key, val] of Object.entries(props)) {
if (
key === 'children' ||
key === 'className' ||
key.startsWith('data-') ||
key.startsWith('aria-')
) {
domProps[key] = val;
}
}
return domProps;
}

vi.mock('react-timer-hook', () => ({
useTimer: () => ({
seconds: 0,
Expand All @@ -23,13 +38,15 @@ vi.mock('@/components/ui/button', () => ({
onClick,
disabled,
variant,
...rest
}: {
children: ReactNode;
onClick?: () => void;
disabled?: boolean;
variant?: string;
[key: string]: unknown;
}) => (
<button onClick={onClick} disabled={disabled} data-variant={variant}>
<button onClick={onClick} disabled={disabled} data-variant={variant} {...filterDomProps(rest)}>
{children}
</button>
),
Expand Down Expand Up @@ -222,21 +239,18 @@ describe('WorkoutSession', () => {
expect(mockOnCancel).toHaveBeenCalled();
});

it('toggles series completion', () => {
it('renders series-row containers with serie-check testids', () => {
render(<WorkoutSession treino={mockTreino} onFinish={mockOnFinish} onCancel={mockOnCancel} />);
const checkButtons = screen
.getAllByRole('button')
.filter(
(btn) => btn.querySelector('svg') !== null && btn.getAttribute('data-variant') === 'outline'
);
if (checkButtons.length > 0) {
fireEvent.click(checkButtons[0]);
}
const updatedCheckButtons = screen
.getAllByRole('button')
.filter(
(btn) => btn.querySelector('svg') !== null && btn.getAttribute('data-variant') === 'default'
);
expect(updatedCheckButtons.length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByTestId('series-row')).toHaveLength(3);
expect(screen.getByTestId('serie-check-0')).toBeTruthy();
expect(screen.getByTestId('serie-check-2')).toBeTruthy();
});

it('toggles series completion via serie-check testid', () => {
render(<WorkoutSession treino={mockTreino} onFinish={mockOnFinish} onCancel={mockOnCancel} />);
const firstCheck = screen.getByTestId('serie-check-0');
expect(firstCheck.getAttribute('data-variant')).toBe('outline');
fireEvent.click(firstCheck);
expect(screen.getByTestId('serie-check-0').getAttribute('data-variant')).toBe('default');
});
});
161 changes: 85 additions & 76 deletions src/components/WorkoutSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,89 +226,98 @@ export function WorkoutSession({ treino, onFinish, onCancel }: Readonly<WorkoutS
}

// --- Tela de Sessão Ativa ---

// ponytail: mobile fullscreen overlay; desktop inline. slide-up anim via globals keyframe.
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle className="text-2xl text-center">
{exercicioAtual.exercicioOriginal.nomeExercicio}
</CardTitle>
<div className="text-center text-muted-foreground">
Exercício {exercicioAtualIndex + 1} de {exerciciosEmSessao.length}
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Descrição do Exercício Planejado */}
<div className="text-center bg-secondary text-secondary-foreground rounded-lg p-3">
<p className="font-bold">
Planejado: {exercicioAtual.exercicioOriginal.series} séries x{' '}
{exercicioAtual.exercicioOriginal.repeticoes} reps
</p>
{exercicioAtual.exercicioOriginal.observacoes && (
<p className="text-sm">Obs: {exercicioAtual.exercicioOriginal.observacoes}</p>
)}
</div>

{/* Tabela de Séries */}
<div className="space-y-4">
{exercicioAtual.seriesExecutadas.map((serie) => (
<div key={serie.id} className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Série {serie.serieNumero}</Label>
<Input
type="number"
placeholder="Peso (kg)"
onChange={(e) => handleSerieChange(serie.id, 'peso', e.target.value)}
/>
<Input
type="number"
placeholder="Reps"
onChange={(e) => handleSerieChange(serie.id, 'repeticoesFeitas', e.target.value)}
/>
<Button
size="icon"
variant={serie.concluido ? 'default' : 'outline'}
onClick={() => handleSerieToggle(serie.id)}
<div className="fixed inset-x-0 top-0 z-50 flex h-dvh flex-col bg-background md:static md:z-auto md:h-auto md:bg-transparent animate-[slide-up_0.3s_ease-out] motion-reduce:animate-none">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The slide-up animation plays on desktop too, which may cause visual overlap with adjacent page content during the 0.3s mount animation. Since the desktop layout is intended to be inline (md:static), consider adding md:animate-none to suppress the animation on desktop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/WorkoutSession.tsx, line 231:

<comment>The slide-up animation plays on desktop too, which may cause visual overlap with adjacent page content during the 0.3s mount animation. Since the desktop layout is intended to be inline (`md:static`), consider adding `md:animate-none` to suppress the animation on desktop.</comment>

<file context>
@@ -226,89 +226,98 @@ export function WorkoutSession({ treino, onFinish, onCancel }: Readonly<WorkoutS
-                size="icon"
-                variant={serie.concluido ? 'default' : 'outline'}
-                onClick={() => handleSerieToggle(serie.id)}
+    <div className="fixed inset-x-0 top-0 z-50 flex h-dvh flex-col bg-background md:static md:z-auto md:h-auto md:bg-transparent animate-[slide-up_0.3s_ease-out] motion-reduce:animate-none">
+      <Card className="flex h-full w-full flex-col md:max-w-2xl md:mx-auto md:h-auto">
+        <CardHeader className="shrink-0">
</file context>

<Card className="flex h-full w-full flex-col md:max-w-2xl md:mx-auto md:h-auto">
<CardHeader className="shrink-0">
<CardTitle className="text-2xl text-center">
{exercicioAtual.exercicioOriginal.nomeExercicio}
</CardTitle>
<div className="text-center text-muted-foreground">
Exercício {exercicioAtualIndex + 1} de {exerciciosEmSessao.length}
</div>
</CardHeader>
<CardContent className="flex-1 space-y-6 overflow-y-auto">
{/* Descrição do Exercício Planejado */}
<div className="text-center bg-secondary text-secondary-foreground rounded-lg p-3">
<p className="font-bold">
Planejado: {exercicioAtual.exercicioOriginal.series} séries x{' '}
{exercicioAtual.exercicioOriginal.repeticoes} reps
</p>
{exercicioAtual.exercicioOriginal.observacoes && (
<p className="text-sm">Obs: {exercicioAtual.exercicioOriginal.observacoes}</p>
)}
</div>

{/* Tabela de Séries */}
<div className="space-y-4">
{exercicioAtual.seriesExecutadas.map((serie, serieIdx) => (
<div
key={serie.id}
data-testid="series-row"
className="grid grid-cols-4 items-center gap-2"
>
<Check className="h-4 w-4" />
</Button>
</div>
))}
</div>

{/* Cronômetro de Descanso */}
{isRunning && (
<div className="flex items-center justify-center gap-2 text-2xl font-bold text-primary">
<Timer />
<span>
{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
</span>
<Label className="text-right">Série {serie.serieNumero}</Label>
<Input
type="number"
placeholder="Peso (kg)"
onChange={(e) => handleSerieChange(serie.id, 'peso', e.target.value)}
/>
<Input
type="number"
placeholder="Reps"
onChange={(e) => handleSerieChange(serie.id, 'repeticoesFeitas', e.target.value)}
/>
<Button
size="icon"
variant={serie.concluido ? 'default' : 'outline'}
onClick={() => handleSerieToggle(serie.id)}
data-testid={`serie-check-${serieIdx}`}
aria-label={`Marcar série ${serie.serieNumero}`}
className="touch-target"
>
<Check className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</CardContent>

<CardFooter className="flex justify-between">
<Button
variant="outline"
onClick={handleExercicioAnterior}
disabled={exercicioAtualIndex === 0}
>
<ArrowLeft className="mr-2 h-4 w-4" /> Anterior
</Button>
{/* Cronômetro de Descanso */}
{isRunning && (
<div className="flex items-center justify-center gap-2 text-2xl font-bold text-primary">
<Timer />
<span>
{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
</span>
</div>
)}
</CardContent>

{exercicioAtualIndex === exerciciosEmSessao.length - 1 ? (
<CardFooter className="sticky bottom-0 flex shrink-0 justify-between bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 md:bg-transparent md:backdrop-blur-none">
<Button
onClick={handleFinalizarTreino}
className="bg-green-600 hover:bg-green-700"
disabled={isFinishing}
variant="outline"
onClick={handleExercicioAnterior}
disabled={exercicioAtualIndex === 0}
>
Finalizar Treino
</Button>
) : (
<Button onClick={handleProximoExercicio}>
Próximo <ArrowRight className="ml-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Anterior
</Button>
)}
</CardFooter>
</Card>

{exercicioAtualIndex === exerciciosEmSessao.length - 1 ? (
<Button
onClick={handleFinalizarTreino}
className="bg-green-600 hover:bg-green-700"
disabled={isFinishing}
>
Finalizar Treino
</Button>
) : (
<Button onClick={handleProximoExercicio}>
Próximo <ArrowRight className="ml-2 h-4 w-4" />
</Button>
)}
</CardFooter>
</Card>
</div>
);
}
8 changes: 4 additions & 4 deletions tests/e2e/specs/workout-session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ test.describe('Workout session — completion flow', () => {
await expect(iniciarButton).toBeVisible({ timeout: 15_000 });
await iniciarButton.click();

// The WorkoutSession card should now be visible — wait for the first series row
// Buttons inside div.grid-cols-4 are the series check buttons (icon-only, no text)
const seriesCheckButton = page.locator('div.grid-cols-4').getByRole('button').first();
// The WorkoutSession card should now be visible — wait for the first series check button
// data-testid="serie-check-0" is the icon-only check button for series 0 of current exercise
const seriesCheckButton = page.getByTestId('serie-check-0');
await expect(seriesCheckButton).toBeVisible({ timeout: 10_000 });
await seriesCheckButton.click();

Expand All @@ -33,7 +33,7 @@ test.describe('Workout session — completion flow', () => {
throw new Error('Workout navigation loop exceeded max exercises — UI regression?');
await proximoButton.click();
// Mark a series on the next exercise too
const nextCheckButton = page.locator('div.grid-cols-4').getByRole('button').first();
const nextCheckButton = page.getByTestId('serie-check-0');
if (await nextCheckButton.isVisible()) await nextCheckButton.click();
}

Expand Down
Loading