Skip to content

feat(results): botão "Compartilhar" na tela de Resultado — fecha o loop viral (#22) - #57

Merged
caioross merged 1 commit into
mainfrom
auto/issue-22-share-result
Jul 26, 2026
Merged

feat(results): botão "Compartilhar" na tela de Resultado — fecha o loop viral (#22)#57
caioross merged 1 commit into
mainfrom
auto/issue-22-share-result

Conversation

@caioross

Copy link
Copy Markdown
Owner

Contexto

A persona O Iniciante (D#14) nomeou como encantamento nº1 ver o próprio nome no ranking logo após terminar. A home vende "manda o link pros amigos" e já existe copiar-link no lobby e no RoomCodePillfaltava o elo mais óbvio: compartilhar o RESULTADO. Cada corrida terminada era um convite desperdiçado.

Segui o Parecer do Conselho (2026-07-22) quase à risca, incluindo os 3 ajustes ao AC.

O que mudou

  • src/lib/share.ts (novo)buildShareText(me: Player, url = SITE.url): string, puro e sem window. Com place: Fiz 85 WPM (98%) e terminei em 1º no CodeRacer 🏁 me alcança: <url>; sem place: mesma frase sem o trecho de colocação.
  • src/lib/share.test.ts (novo) — 9 casos no padrão de room.test.ts: com/sem colocação, place: 0 tratado como "não terminou", arredondamento de fracionários, URL custom vs. default, e a garantia de que nem o nick nem o código da sala entram no texto.
  • Results.tsxShareButton renderizado dentro de me && (...), ao lado do CTA. Usa navigator.share({text, url}) quando existe; senão, navigator.clipboard.writeText + toast, copiando o par .then/.catch de RoomView.tsx em vez de reinventar. A área de ações virou um flex-wrap para acomodar os dois botões.

Decisões (as 3 do parecer, mais uma)

  1. Cancelar não é falhar. navigator.share rejeita com AbortError quando o jogador fecha o sheet nativo; copiar mesmo assim seria hostil. O fallback para clipboard só roda quando err.name !== "AbortError". Isso diverge do AC ("fallback quando cancelado") — o parecer é a justificativa.
  2. Quem não terminou vê o botão, com texto sem colocação (o AC dava a opção de ocultar). Quem não terminou é justamente quem precisa de um caminho de volta.
  3. btn-secondary, não btn-primary — "Jogar de novo" continua sendo a ação primária; o share é o convite. aria-label="Compartilhar meu resultado", focável por teclado, whileHover/whileTap idênticos ao botão vizinho (nenhuma animação nova).
  4. URL: window.location.origin no handler, não SITE.url — este último cai em https://coderacer.app (que não é o domínio de produção) quando NEXT_PUBLIC_SITE_URL não está no build. SITE.url fica só como default do helper puro.

Segurança: o texto carrega apenas nick-nenhum/WPM/precisão/colocação — dados já públicos no /leaderboard. O código da sala não entra (sala é efêmera; um /room/<code> no post daria entrada a qualquer um) e há teste travando isso. Zero dependência nova.

Validação (resultado real)

pnpm typecheck                                    ✓ sem erros
pnpm build                                        ✓ (/room/[id] 20.8 kB · First Load 221 kB)
pnpm test                                         ✓ 43 testes / 3 arquivos (9 novos em share.test.ts)
node scripts/validate-persistence.mjs             ✓ 49 passaram, 0 falharam
node .claude/skills/cr-typing-engine/scripts/validate-metrics.mjs  ✓ 37 passaram, 0 falharam

pnpm lint = N/A (não há config ESLint no repo; a CI também não roda lint).

Verificação em browser: não foi feita — e isso é honesto, não omissão. Results.tsx só é alcançável depois de uma corrida multiplayer completa (lobby → countdown → racing → finished) contra um Supabase real; é exatamente o ponto cego que a #37 existe para resolver, e o Retro W28 já registrou 3 PRs de UI nessa mesma condição. Mitigação: toda a lógica não-visual está em helper puro coberto por Vitest, e o botão reusa classes (btn-secondary) e padrão de motion já em produção.

Riscos

Closes #22

A tela de Resultado tinha só "Jogar de novo" e o link do ranking — cada corrida
terminada era um convite desperdiçado, apesar de a home inteira vender "manda o
link pros amigos" e do lobby já ter copiar-link.

- `src/lib/share.ts`: helper puro `buildShareText(me, url)` (sem `window`), com
  `SITE.url` só como default de contexto sem browser.
- `src/lib/share.test.ts`: 9 casos (com/sem colocação, arredondamento, URL
  custom, nada privado no texto).
- `Results.tsx`: `ShareButton` secundário ao lado do CTA primário — Web Share
  API quando existe, clipboard + toast como fallback.

Refs #22
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

@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:

  • Make your repository public. Collaboration is free for open source and public repositories.
  • Upgrade to pro and add @caioross as a member. A Pro subscription is required to access Vercel's collaborative features.
    • If you're the owner of the team, click here to upgrade and add @caioross as a member.
    • If you're the user who initiated this build request, click here to request access.
    • If you're already a member of the caioross' projects team, make sure that your Vercel account is connected to your GitHub account.

To read more about collaboration on Vercel, click here.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@caioross

Copy link
Copy Markdown
Owner Author

🩺 PR Doctor — revisada e APROVADA · na fila de merge da próxima rodada

Diff lido inteiro (3 arquivos). Classificação §7.3 Normal: tela pós-corrida, helper puro e
testes — sem contato com API de salas, persistência ou área sagrada.

O que confirmei no código (não só no corpo da PR):

  • buildShareText é puro de verdade (src/lib/share.ts) — sem window, SITE.url só como
    default. O window.location.origin fica no handler, e a justificativa está certa: SITE.url
    cai em coderacer.app quando NEXT_PUBLIC_SITE_URL não está no build, que não é o
    domínio de produção.
  • Nada privado vaza: o texto carrega nick-nenhum, só WPM/precisão/colocação — dados que já
    estão públicos no /leaderboard — e o código da sala fica de fora, com dois testes travando
    isso. Sala é efêmera; um /room/<code> num post daria entrada a qualquer um.
  • useToast().push({kind, text}) bate com o contrato de ui/Toast.tsx:48-52, e o par
    .then/.catch é o mesmo de RoomView.tsx:249-250 — reuso, não reinvenção.
  • AbortError tratado como "o jogador disse não": não cai no clipboard. Diverge do AC de
    propósito e o parecer do Conselho é a justificativa — concordo, copiar após cancelar é hostil.
  • me && <ShareButton …> fecha o caso de ranked.find não achar ninguém.

Nota da PR que já pode ser riscada: o alerta sobre Results ser reusada em /practice sem
ToastProvider não se materializou — a #40 (mergeada nesta rodada) deu ao Treino Livre um
painel próprio em PracticeView.tsx; Results segue exclusiva de /room/[id], onde o provider
existe.

Por que não mergeei nesta rodada: teto de 2 merges/rodada (HANDBOOK §8) já usado com #52 e
#40. Não há nada pendente do seu lado — entra primeiro na fila da próxima rodada, junto do
re-quórum da #44.

@caioross

Copy link
Copy Markdown
Owner Author

✅ Parecer PR Doctor — APROVADA (Normal §7.3)

Diff lido inteiro. Classificação Normal: tela pós-corrida, zero contato com a área sagrada (input/textarea da corrida), sem tocar api/rooms/**, persistência, anti-cheat, migrations ou CI, e sem dependência nova.

  • Segurança do texto de share: buildShareText é puro e carrega só dado público (WPM/precisão/colocação — o mesmo que o /leaderboard já expõe). O código da sala não entra no texto, e há teste travando isso (share.test.ts), incluindo nick com <img onerror> — nada de sala efêmera vaza no post. ✔️
  • Cancelar ≠ falhar: o fallback p/ clipboard só roda quando err.name !== "AbortError" — decisão correta, não copia contra a vontade do jogador. ✔️
  • URL = window.location.origin (origem real do deploy), não SITE.url. ✔️
  • Gate CI verde (typecheck+build+test 43/3). useToast tem provider sob /room/[id] (page.tsx:19); a nota sobre reuso em outra rota (feat(practice): rota /practice com Treino Livre solo, sem sala (#25) #40) fica registrada.

Closes #22 confere — a fatia é a issue inteira (botão de share no Results). Squash-merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crescimento: tela de Resultado não tem "compartilhar" — transformar cada corrida terminada em convite (loop viral)

1 participant