Skip to content

Commit fac628b

Browse files
feat(ux): inline editor + visual grouper + scroll fixes for meus treinos (#175)
* docs: meus treinos UX improvements design spec Co-Authored-By: Claude <noreply@anthropic.com> * docs: detailed implementation plan for meus treinos UX improvements Co-Authored-By: Claude <noreply@anthropic.com> * feat(workout-editor): add compact prop to drop outer Card wrapper - Add compact?: boolean prop (default false) to WorkoutEditor - Extract JSX into const content, conditionally wrap in div vs Card - Add 2 tests: compact omits Card, default renders Card Co-Authored-By: Claude <noreply@anthropic.com> * feat(ux): expose planName from useWorkoutGeneration for visual grouper banner * feat(ux): inline editor inside treino cards, remove bottom editor - editingTreinoId state drives inline WorkoutEditor per card - handleEditLocal replaces scrollTo behavior - Criar Manual button moved above list, opens inline editor - Removed bottom-of-page conditional editor - Removed window.scrollTo from use-workout-crud Co-Authored-By: Claude <noreply@anthropic.com> * fix(test): update use-workout-crud tests for inline editor changes Co-Authored-By: Claude <noreply@anthropic.com> * feat(ux): visual grouper banner + after-gen scroll anchor Co-Authored-By: Claude <noreply@anthropic.com> * test(ux): update meus-treinos-client tests for planName banner Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): reset planName before each generation Ensures banner re-triggers on every AI gen, not just first time. Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): add plan description to banner card Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): retry scroll with requestAnimationFrame until DOM ready Replaces fixed 300ms setTimeout with rAF loop that retries until #treinos-pessoais anchor exists in DOM. Eliminates race condition on slow devices / slow RSC renders. Co-Authored-By: Claude <noreply@anthropic.com> * fix(auth): resolve Prisma alunoId vs Auth UUID in delete+update authorization deleteTreinoAction and updateTreinoDayAction compared treino.alunoId (Prisma ID) against user.id (Auth UUID), always mismatching after PR #174 FK fix. Resolve aluno.id by email before auth check. Co-Authored-By: Claude <noreply@anthropic.com> * fix(review): address PR #175 review findings — all severity levels - rAF scroll capped at 20 attempts (prevents infinite loop) - handleDelete + handleDayChange: surface server errors to user - replace inline prisma.aluno.findUnique with resolveAlunoId() for auth - add onSuccess to useCallback dep array Co-Authored-By: Claude <noreply@anthropic.com> * fix(type): make onSuccess optional in useWorkoutGeneration Pre-existing tests don't pass onSuccess — made optional to avoid 12 TS2345 errors. Caller (meus-treinos-client) always passes it. Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): keep editor open on save failure + dead code cleanup handleSave returns boolean. Client closes editor only on success. Removed unused isFormVisible, editingTreino, handleEdit from hook. Kept __new__ sentinel for now. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Emiya Kiritsugu <emiyakiritsugu3@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1118d92 commit fac628b

10 files changed

Lines changed: 786 additions & 161 deletions

docs/superpowers/plans/2026-07-05-meus-treinos-ux-improvements.md

Lines changed: 431 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Design: Meus Treinos UX Improvements
2+
3+
**Date:** 2026-07-05
4+
**Branch:** `fix/meus-treinos-ux-improvements`
5+
**Scope:** UX only — zero schema/API changes
6+
7+
## Problem
8+
9+
1. **Scroll paradox** — clicking "Editar" on a treino card runs `window.scrollTo({top:0})` but the editor appears at page bottom. User loses context, must scroll down manually.
10+
2. **No visual grouping** — AI generates 3-day split (e.g. Peito/Triceps + Costas/Biceps + Pernas/Ombros). Three independent cards with no visual connection.
11+
3. **After-gen feedback gap**`router.refresh()` fixed (commit 1118d92), but no visual anchor after generation completes.
12+
13+
## Design
14+
15+
### 1. Inline Editor
16+
17+
Clicking "Editar" on a card renders `WorkoutEditor` **inside the card**, replacing exercise list. No scroll. Cancel/Save collapses back to normal view.
18+
19+
**Card states:**
20+
21+
```
22+
NORMAL: EDITING:
23+
┌─────────────────────────────┐ ┌──────────────────────────────────┐
24+
│ Peito/Triceps [Seg] [▶][✏][🗑]│ │ ✏ Editando: Peito/Triceps │
25+
│ 5 exercicios │ │ ┌──────────────────────────────┐ │
26+
│ │ │ │ WorkoutEditor (exercises) │ │
27+
│ Dropdown agendamento │ │ └──────────────────────────────┘ │
28+
└─────────────────────────────┘ │ [Cancelar] [Salvar] │
29+
└──────────────────────────────────┘
30+
```
31+
32+
**Files:**
33+
- `card-treino.tsx` — add `isEditing` state + inline render of WorkoutEditor
34+
- `workout-editor.tsx` — add `compact?: boolean` prop to suppress outer Card wrapper
35+
- `meus-treinos-client.tsx` — remove bottom-of-page editor, wire inline mode
36+
37+
### 2. Visual Grouper — Plano Semanal
38+
39+
After AI generation, banner appears above "Meus Treinos Pessoais" for 30s.
40+
41+
**Detection:** `useWorkoutGeneration` exposes `planName` from `streamWorkoutPlan` return. Client stores in state, renders banner, auto-clears after 30s.
42+
43+
```tsx
44+
<Card className="bg-gradient-to-r from-primary/5 to-secondary/5 border-primary/20">
45+
<CardHeader>
46+
<CardTitle className="flex items-center gap-2">
47+
<Sparkles className="text-primary" />
48+
Plano Semanal: {planName}
49+
</CardTitle>
50+
<CardDescription>{diaLabels.join('')}</CardDescription>
51+
</CardHeader>
52+
</Card>
53+
```
54+
55+
Cards part of plan get `border-l-2 border-primary/30 ml-2 pl-4` timeline visual.
56+
57+
### 3. After-gen Scroll
58+
59+
Scroll smooth to `#treinos-pessoais` after `router.refresh()`.
60+
61+
### 4. Remove Bottom Editor
62+
63+
"Criar Novo Treino Manualmente" button moves **above** treino list. When clicked, renders `WorkoutEditor` inline above first card. Bottom conditional editor removed.
64+
65+
## Files Changed
66+
67+
| File | Change | Risk |
68+
|---|---|---|
69+
| `card-treino.tsx` | +isEditing state, inline WorkoutEditor render | Low |
70+
| `workout-editor.tsx` | +`compact?: boolean` prop | Low |
71+
| `meus-treinos-client.tsx` | -bottom editor, +banner, +planName, +scroll anchor | Medium |
72+
| `use-workout-generation.ts` | +expose planName from hook return | Low |
73+
| `card-treino.test.tsx` | +inline edit tests | Low |
74+
| `meus-treinos-client.test.tsx` | +banner tests | Low |
75+
76+
## Out of Scope
77+
78+
- PlanoSemanal Prisma model
79+
- Plan-wide progress tracking
80+
- 1-day = 1-card data model change
81+
82+
## Success Criteria
83+
84+
1. Click "Editar" → editor appears inside same card, zero page jump
85+
2. "Cancelar" / "Salvar" → card returns to normal
86+
3. AI gen: banner 30s + day sequence + scroll to treino list
87+
4. "Criar Manual" above list, opens inline editor
88+
5. All quality gates pass

src/app/aluno/meus-treinos/meus-treinos-client.test.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
33
import MeusTreinosClient from './meus-treinos-client';
44
import type { Treino } from '@/lib/definitions';
55
import type { ReactNode } from 'react';
6+
import { useWorkoutGeneration } from '@/hooks/use-workout-generation';
67

78
vi.mock('next/navigation', () => ({
89
useRouter: () => ({
@@ -55,10 +56,11 @@ vi.mock('@/hooks/use-workout-crud', () => ({
5556
}));
5657

5758
vi.mock('@/hooks/use-workout-generation', () => ({
58-
useWorkoutGeneration: () => ({
59+
useWorkoutGeneration: vi.fn(() => ({
5960
isGenerating: false,
6061
handleGenerate: vi.fn(),
61-
}),
62+
planName: null,
63+
})),
6264
}));
6365

6466
vi.mock('@/lib/actions/treinos', () => ({
@@ -194,4 +196,15 @@ describe('MeusTreinosClient', () => {
194196
render(<MeusTreinosClient initialTreinos={[]} userId="user-1" />);
195197
expect(screen.getByText('Criar Novo Treino Manualmente')).toBeTruthy();
196198
});
199+
200+
it('renders plan banner when planName is set', () => {
201+
vi.mocked(useWorkoutGeneration).mockReturnValue({
202+
isGenerating: false,
203+
handleGenerate: vi.fn(),
204+
planName: 'Hipertrofia 3x',
205+
});
206+
render(<MeusTreinosClient initialTreinos={mockTreinos} userId="user-1" />);
207+
expect(screen.getByText(/Plano Semanal/)).toBeTruthy();
208+
expect(screen.getByText(/Hipertrofia 3x/)).toBeTruthy();
209+
});
197210
});

0 commit comments

Comments
 (0)