fix(ui): pódio de resultado sem cards vazios em corrida solo/dupla (#11) - #26
Conversation
Adapta Results a ranked.length < 3 (§III.7): - Solo (1 jogador): card 'herói' centrado com medalha + WPM/precisão/erros/tempo, sem os dois placeholders escuros do 2º/3º lugar. - Dupla (2 jogadores): pódio de 2 colunas, sem o card vazio do 3º. - 3+: layout de pódio inalterado (mesmos springs/tokens). Extrai PodiumCard/SoloHero e gateia as animações de entrada com useReducedMotion (o CSS global só cobre animações CSS, não o animate JS do Framer Motion). Closes #11 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@caioross is attempting to deploy a commit to the caioross' projects team on Vercel, but is not a member of this team. To resolve this issue, you can:
To read more about collaboration on Vercel, click here. |
There was a problem hiding this comment.
Code Review
This pull request refactors the Results component to improve the podium layout, introducing a dedicated SoloHero view for single-player races and a reusable PodiumCard component for multiplayer races, while also integrating accessibility support via useReducedMotion. The feedback suggests filtering screenOrder to prevent potential runtime errors if the podium is empty, and updating the type of the reduced prop from boolean | null to boolean | undefined to align with the return type of useReducedMotion.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // Ordem na tela: 2º, 1º, 3º (só as colocações que têm jogador). | ||
| const solo = ranked.length === 1; | ||
| const duo = ranked.length === 2; | ||
| const screenOrder = duo ? [1, 0] : [1, 0, 2]; |
There was a problem hiding this comment.
Se a lista de jogadores (ranked ou podium) estiver vazia por qualquer motivo (por exemplo, durante transições de estado ou carregamento), o código tentará renderizar o pódio padrão de 3 colunas ([1, 0, 2]). Como podium[rankIdx] será undefined, acessar podium[rankIdx].id causará um erro de execução (TypeError: Cannot read properties of undefined).
Para evitar isso e garantir uma programação defensiva, podemos filtrar o screenOrder para conter apenas os índices que realmente existem no podium.
| const screenOrder = duo ? [1, 0] : [1, 0, 2]; | |
| const screenOrder = (duo ? [1, 0] : [1, 0, 2]).filter(idx => podium[idx] !== undefined); |
| function PodiumCard({ | ||
| player, | ||
| meta, | ||
| col, | ||
| isMe, | ||
| reduced | ||
| }: { | ||
| player: Player; | ||
| meta: (typeof PODIUM_META)[number]; | ||
| col: number; | ||
| isMe: boolean; | ||
| reduced: boolean | null; | ||
| }) { |
There was a problem hiding this comment.
O hook useReducedMotion() do framer-motion retorna boolean ou undefined (especialmente em ambientes SSR). Tipar a propriedade reduced como boolean | null pode causar incompatibilidades de tipo se o TypeScript estiver configurado com strictNullChecks estrito, além de não ser o padrão idiomático em React (onde preferimos undefined ou propriedades opcionais ?).
Podemos alterar a tipagem para reduced?: boolean (ou boolean | undefined).
| function PodiumCard({ | |
| player, | |
| meta, | |
| col, | |
| isMe, | |
| reduced | |
| }: { | |
| player: Player; | |
| meta: (typeof PODIUM_META)[number]; | |
| col: number; | |
| isMe: boolean; | |
| reduced: boolean | null; | |
| }) { | |
| function PodiumCard({ | |
| player, | |
| meta, | |
| col, | |
| isMe, | |
| reduced | |
| }: { | |
| player: Player; | |
| meta: (typeof PODIUM_META)[number]; | |
| col: number; | |
| isMe: boolean; | |
| reduced?: boolean; | |
| }) { |
| function SoloHero({ | ||
| player, | ||
| startedAt, | ||
| isMe, | ||
| reduced | ||
| }: { | ||
| player: Player; | ||
| startedAt: number; | ||
| isMe: boolean; | ||
| reduced: boolean | null; | ||
| }) { |
There was a problem hiding this comment.
Da mesma forma que no PodiumCard, o hook useReducedMotion() retorna boolean | undefined. É mais idiomático e seguro tipar a propriedade reduced como opcional (reduced?: boolean) em vez de boolean | null.
| function SoloHero({ | |
| player, | |
| startedAt, | |
| isMe, | |
| reduced | |
| }: { | |
| player: Player; | |
| startedAt: number; | |
| isMe: boolean; | |
| reduced: boolean | null; | |
| }) { | |
| function SoloHero({ | |
| player, | |
| startedAt, | |
| isMe, | |
| reduced | |
| }: { | |
| player: Player; | |
| startedAt: number; | |
| isMe: boolean; | |
| reduced?: boolean; | |
| }) { |
Parecer PR Doctor — APROVADO (normal §7.3) ✅ · merge aguarda sessão supervisionadaDiff lido inteiro. UI-pura em
Nota honesta (não bloqueia): o caminho 3+ não é literalmente idêntico como o corpo diz — o Gate: CI
|
Parecer PR Doctor — APROVADO (NORMAL, HANDBOOK §7.3)Diff lido inteiro ( Corretude verificada:
|
Contexto
Feedback da persona O Iniciante (discussion #9): no fim de uma partida solo, os cards de 2º/3º lugar do pódio ficam vazios/escuros — anti-clímax no pico emocional da tela de resultado, e todo jogador solo bate nisso. Causa: o pódio renderizava sempre 3 colunas fixas
[1,0,2]e caía no fallbackif (!p) return <div className="card" .../>quandopodium[idx]eraundefined.O que mudou (
src/components/Results.tsx)Ramifica por
ranked.lengthantes do bloco do pódio:SoloHero— card "herói" centrado com medalha 🥇 + WPM / precisão / erros / tempo, sem nenhum placeholder vazio.Detalhes:
PodiumCard(uma coluna, só recebe jogador existente — o fallback vazio deixou de existir) eSoloHero/SoloStat.screenOrder) em vez do array fixo.prefers-reduced-motionviauseReducedMotion()— o CSS global (globals.css) só neutraliza animações/transições CSS, não oanimateJS do Framer Motion. Sob movimento normal o visual do caminho 3+ é idêntico (sem regressão).Critérios de aceitação
ranked.map) e botão "ver ranking global" intactos.prefers-reduced-motionrespeitado nas animações novas.Validação (gate §6)
pnpm typecheck→ verdepnpm build→ verde (rota/roomcompilada)node .../validate-metrics.mjs→ 24/24node scripts/validate-persistence.mjs→ 33/33.eslintrc; CI não roda lint)Verificação em browser: a tela
Resultsfica atrás de uma corrida concluída (solo/dupla/3+), inviável de dirigir de ponta a ponta num run headless (exige digitar o snippet inteiro + múltiplos clientes). A rota compilou no build e a mudança é ramificação simples, type-safe, sobre markup já existente. Vale um olhar visual do revisor nos três estados.Riscos
Baixo — UI-pura, pós-corrida, zero impacto na área sagrada (input da
textarea). Sem novas deps, sem tocar engine/persistência/Realtime.Closes #11