Repository navigation
Description
Primeira fatia do epic #24 (Practice/Solo · §V.5). Menor valor entregável: dar ao visitante solitário algo para jogar em um clique, sem sala.
Problema
Para digitar qualquer coisa hoje é preciso criar/entrar numa sala multiplayer (src/components/HomeView.tsx → API rooms → router.push('/room/<code>'), l.90). O visitante que chega sozinho não tem caminho jogável imediato — fricção nº1 da persona O Iniciante (discussions #9/#14). A engine já roda solo (o ranking global aceita corrida solo), então falta só desacoplar o Race do useRoom e expô-lo numa rota própria.
Escopo (só Treino livre)
- Nova rota
/practice(src/app/practice/page.tsx) que renderiza a corrida sem sala e sem Realtime (semuseRoom, sem broadcast, sem presença). - Seletor de linguagem + dificuldade antes de começar (reusar os controles/estilos do modal de criar sala em
HomeView; dificuldade default Fácil, coerente com Onboarding: dificuldade nasce em "Médio" e atropela o Iniciante — primeira sala deveria nascer no Fácil (§0.3) #16 e §0.3). - Ao terminar: mostrar as stats pessoais (WPM/precisão/erros/tempo) e um botão "treinar de novo" (novo snippet da mesma config) + link "criar sala para jogar com amigos".
- CTA na home: um botão/atalho "treinar →" que leva a
/practicesem criar sala (HomeView, perto do bloco "Criar sala", ~l.294).
Acceptance criteria (verificáveis)
/practicecarrega e inicia uma corrida solo sem nenhuma chamada à APIroomsnem canal Supabase Realtime (verificar na aba Network: zero request a/api/rooms).- Trocar linguagem/dificuldade no seletor troca o snippet exibido; a corrida começa Fácil por default.
- A digitação preserva a área sagrada: input a 60fps, sem trabalho pesado competindo com a
textarea; feedback de erro char-a-char idêntico aoRaceatual (CodeDisplay). prefers-reduced-motionrespeitado (sem animações de entrada quando ativo).- Ao concluir, as stats aparecem e "treinar de novo" reinicia com novo snippet sem recarregar a página.
- Home tem CTA "treinar →" visível que navega para
/practicesem criar sala. pnpm typecheck+pnpm buildverdes;node scripts/validate-metrics.mjsverde (engine reutilizada, não regredida).
Dica de abordagem
Extrair de src/components/Race.tsx a parte que só depende de snippet + onFinish (métricas, CodeDisplay, CodeEditor, anti-paste) para um wrapper standalone PracticeRace, deixando o Race multiplayer chamando o mesmo núcleo. Evitar duplicar a lógica de métricas — a honestidade de WPM/precisão precisa continuar validada por validate-metrics. Ghost (PB) e bots ficam para as fatias 3 e 2 do #24 — não implementar aqui.
Fora de escopo
Ghost/PB, bots, persistência de melhor resultado, metadata/SEO da landing (fatia 4 do #24).
Activity
caioross commentedon Jul 12, 2026
Nota do Curador (do [Feedback] O Iniciante · D#27): enquanto a rota
/practicenão sai, uma microcopy barata no botão da home reduz a fricção de descoberta quase de graça — algo como "Criar sala — dá pra jogar sozinho também". Candidata a fatia mínima/quick-win desta issue (ou PR próprio pequeno emarea:ui) antes do modo completo. A dor real do Iniciante é descobrir que o solo existe.caioross commentedon Jul 15, 2026
🏛️ Parecer do Conselho · 2026-07-14
Veredito: APROVADA PARA EXECUÇÃO
src/components/Race.tsxé acoplado aroom: RoomState(usaroom.snippet,room.startedAt,room.players,room.chate renderizaRaceTrack+FloatingChat, ambos multiplayer-only). Mas o núcleo de digitação é autocontido:typed/errors/totalKeystrokes,correctChars(l.49-53),wpm(l.55),accuracy(l.56-57),progress(l.58),handleInput(l.74-96),noPaste(l.99) +CodeDisplay/CodeEditor. Caminho limpo: extrair esse núcleo para um componente presentacional semroom(ex.:TypingCorecom propscode/language/onFinish?) e montar/practicereusando-o sem RaceTrack/FloatingChat/broadcast. Casa com Engine/Testes: extrair métricas puras de Race.tsx para src/lib/metrics.ts + testes de honestidade (WPM/precisão) #32 (extrair métricas puras p/src/lib/metrics.ts) — coordene: se Engine/Testes: extrair métricas puras de Race.tsx para src/lib/metrics.ts + testes de honestidade (WPM/precisão) #32 landar antes,TypingCoreconsomemetrics.ts; senão, cálculo inline e Engine/Testes: extrair métricas puras de Race.tsx para src/lib/metrics.ts + testes de honestidade (WPM/precisão) #32 refatora depois. Cross-link no epic Practice/Solo mode (§V.5): treinar sem sala — modo inteiro a fatiar #24.src/lib/languages.tsdiz "Snippets live on the server" epickSnippeté server-side (src/lib/snippets.ts). Practice é client-side sem sala → precisa obter snippet via (a) novoGET /api/snippet?language=&difficulty=leve reusandopickSnippet, ou (b) expor snippets ao cliente. Recomendo (a) — 1 fonte de verdade, sem inchar o bundle. Registrar a decisão na PR.matches/scoresnem toca leaderboard. Confirmar que oTypingCoreem modo practice não chama nenhuma rota de persistência (senão vira forja trivial). Não persistir PB de treino nesta fatia.prefers-reduced-motion, tokens dodocs/UI-AAA-OVERHAUL.md. Sem countdown de sala: iniciar o cronômetro no 1º keystroke (elimina espera). Botões reiniciar/trocar snippet.CodeEditor/handleInputmantém a latência de input; Practice remove RaceTrack/FloatingChat/broadcast → mais leve que a corrida. Manter o tick denowem 200ms (l.38-42); não adicionar loops.Plano sugerido:
Race.tsx→src/components/TypingCore.tsx(propscode,language,onFinish?), semroom.Race.tsxpassa a comporTypingCore+ track/chat (refactor sem mudança de comportamento no multiplayer — rode o app e confira a corrida).GET /api/snippet?language=&difficulty=reusandopickSnippet+ allowlist (ver Segurança/API: language e difficulty entram no banco sem allowlist e vazam para matches/scores #35).src/app/practice/page.tsx: escolhe lang/dificuldade, busca snippet, renderizaTypingCore; start no 1º keystroke; zero persistência.src/app/page.tsx) para/practice.validate-metricsverde se tocar cálculo.caioross commentedon Jul 16, 2026
Resolvida na PR #40 seguindo o plano do 🏛️ Parecer (opção (a) p/ snippet:
GET /api/snippetserver-side): núcleo de digitação extraído paraTypingCore(Race multiplayer intacto, extração 1:1), rota/practicecom seletor (default Fácil), cronômetro no 1º keystroke, stats + treinar de novo sem reload, CTA na home. Verificado no browser: zero chamadas a/api/rooms/Realtime no practice; gate 100% verde (typecheck, build, persistence 33/33, metrics 27/27).refactor(practice): TypingCore e /api/snippet reusam as libs já na main
feat(practice): rota /practice com Treino Livre solo, sem sala (#25) (#…