Skip to content

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

Closed

Description

@caioross

Contexto

A persona O Iniciante (Feedback #14) confirmou que o encantamento nº1 é ver o próprio nome no ranking global logo após terminar — "sensação de progresso pura, me deu vontade de jogar de novo". A home inteira já vende "manda o link pros amigos", e existe copiar-link no lobby (Lobby.tsx:41-44) e no RoomView.tsx:247-250. Só falta o elo mais óbvio do loop viral: compartilhar o RESULTADO.

Hoje a tela pós-corrida (Results.tsx, área de ações linhas 137-160) tem só "Jogar de novo" e "ver ranking global" — nenhum botão de compartilhar. Cada corrida terminada é um convite desperdiçado.

O que fazer

Botão "Compartilhar resultado" na área de ações do Results.tsx (ao lado de "Jogar de novo"), que vira o WPM/precisão/colocação do jogador (me já disponível em Results.tsx:33, com place/wpm/accuracy) num texto curto + link do site (SITE.url em src/lib/site.ts).

Acceptance criteria (verificáveis)

  • Botão de compartilhar visível na área de ações (Results.tsx:137-160), focável por teclado e com aria-label.
  • Ao clicar em desktop: copia p/ a área de transferência um texto tipo Fiz 85 WPM (98%) e terminei em 1º no CodeRacer 🏁 me alcança: https://code-racer-three.vercel.app + toast de confirmação (reusar o padrão de clipboard já existente, não reinventar).
  • Se navigator.share existir (mobile): usa a Web Share API (sheet nativo do SO) com fallback automático p/ clipboard quando indisponível ou cancelado.
  • Comportamento definido p/ quem não terminou (me.place ausente): texto sem "terminei em Xº" (ex.: só WPM/precisão) OU botão oculto — escolha uma e documente no PR.
  • prefers-reduced-motion respeitado (nenhuma animação nova obrigatória).
  • Zero impacto na área sagrada (input da corrida): Results é tela pós-corrida; nada compete com a textarea.

Dica de abordagem

Helper puro buildShareText(me) (testável) + handler onShare que tenta navigator.share({ text, url }) e no catch/ausência cai no navigator.clipboard.writeText — mesmo par try/catch de RoomView.tsx:247-250. Nada de dado sensível no texto (só nick/WPM/precisão/colocação, todos já públicos no ranking).

Activity

  1. added
    P2Prioridade normal
    area:uiInterface, design system, animação, game feel
    on Jul 11, 2026
  2. caioross commented on Jul 22, 2026

    @caioross
    OwnerAuthor

    🏛️ Parecer do Conselho · 2026-07-22

    Veredito: APROVADA PARA EXECUÇÃO (com 3 ajustes ao AC — ver UX e Engenharia)

    Revisado contra origin/main (7236d47). Correção de referência: a área de ações do Results.tsx é hoje l.134-156 (não 137-160) — o botão entra ao lado do motion.button "Jogar de novo" (l.136-144), acima do link "ver ranking global" (l.150-155).

    • Engenharia: me vem de Results.tsx:41 (ranked.find(p => p.id === meId)) e é Player | undefined — renderize a ação dentro de um me && (...), como a headline já faz em l.64. Helper puro buildShareText(me: Player, url: string): string em src/lib/share.ts (arquivo novo, não utils.ts) + teste Vitest no padrão de src/lib/room.test.ts — a fundação de testes já está de pé (Testes: instalar Vitest + primeiro suite na fronteira anti-cheat (sanitizeResults) e rodar no CI #31/Engine/Testes: extrair métricas puras de Race.tsx para src/lib/metrics.ts + testes de honestidade (WPM/precisão) #32/Testes/Persistência: extrair mapeamento puro de persistMatch (results→matches/scores) e cobrir com Vitest #50), e um helper puro sem teste agora seria regressão de convenção.
      Armadilha (URL errada no texto compartilhado): SITE.url é process.env.NEXT_PUBLIC_SITE_URL com fallback https://coderacer.app (src/lib/site.ts:8,14) — um domínio que não é o de produção (code-racer-three.vercel.app). Se a env não estiver setada no build da Vercel, todo share aponta para o lugar errado. Faça como o RoomCodePill já faz: window.location.origin no handler (RoomView.tsx:57), com SITE.url só como default do helper puro (que não tem window).
      Clipboard: copie o par exato de RoomView.tsx:56-62 — navigator.clipboard.writeText(txt).then(() => toast.push({kind:"success",...})).catch(() => toast.push({kind:"error",...})). Não reinvente.
    • Segurança/Anti-cheat: vetor nulo — nick/WPM/precisão/colocação já são públicos no /leaderboard. Duas regras: (1) não inclua o código da sala nem /room/<code> no texto — a sala é efêmera e o link dá entrada a qualquer um; o AC pede o link do site, mantenha assim. (2) O nick vem do jogador e é interpolado em string — React escapa na tela, mas o texto vai para clipboard/navigator.share; sanitizeResults já limita nome a MAX_NAME_LEN (src/lib/room.ts:102) na persistência, e aqui basta não injetar HTML/markdown. Zero dependência nova (navigator.share é nativo) — nada que encoste em §7.1.
    • UX/Game-feel: ajuste 1 — cancelar não é falhar. navigator.share rejeita com AbortError quando o usuário fecha o sheet; cair no clipboard nesse caso é hostil (ele disse não). Fallback só quando navigator.share é indisponível ou err.name !== "AbortError". Documente essa escolha no PR (o AC diz "cancelado" — este parecer é a justificativa para divergir).
      Ajuste 2 — quem não terminou vê o botão. O AC dá a opção "ocultar"; escolha texto sem colocação, nunca ocultar: quem não terminou é justamente quem precisa de um caminho de volta, e Results.tsx:66-69 já tem as duas microcopy (// você terminou em Xº / // você não terminou esta corrida) — espelhe o tom.
      Ajuste 3 — hierarquia visual. Use btn-secondary/btn-ghost, não btn-primary: "Jogar de novo" (l.140) tem de continuar sendo a ação primária; o share é o convite, não o CTA. aria-label explícito + whileHover/whileTap idênticos a l.137-138 (o useReducedMotion de l.30 já está disponível se precisar suprimir). Nenhuma animação nova obrigatória.
      Nota de tooling: useToast() funciona dentro de Results porque a árvore está sob <ToastProvider> em src/app/room/[id]/page.tsx:19 — mas o provider não está em src/app/layout.tsx. Se a tela for reusada fora dessa rota (ex.: /practice, PR feat(practice): rota /practice com Treino Livre solo, sem sala (#25) #40), useToast cai no fallback silencioso (Toast.tsx:48-52) e o AC "toast de confirmação" falha sem erro. Confira antes de declarar verde.
    • Performance: zero na área sagrada — Results é tela pós-corrida, nada compete com a textarea. Sem lib nova, sem re-render extra relevante (um useState de "copiado" no máximo).
    • Produto: alto e barato — fecha o loop viral que a persona O Iniciante nomeou como encantamento nº1 (D#14) e que a home já promete. P2 está correto. Ordem importa: esta PR mexe exatamente nas telas que a Infra/UI: harness só-de-dev para verificar telas pós-corrida (Results/pódio) sem jogar #37 torna verificáveis; sem o harness ela repete o padrão do Retro W28 (UI mergeada sem verificação em browser). Sugestão ao Resolvedor: Infra/UI: harness só-de-dev para verificar telas pós-corrida (Results/pódio) sem jogar #37 antes de Crescimento: tela de Resultado não tem "compartilhar" — transformar cada corrida terminada em convite (loop viral) #22 — e, se pegar Crescimento: tela de Resultado não tem "compartilhar" — transformar cada corrida terminada em convite (loop viral) #22 primeiro, verifique manualmente numa corrida solo real e diga isso no PR.

    Plano sugerido:

    1. src/lib/share.ts — buildShareText(me, url = SITE.url): com me.place → Fiz ${Math.round(me.wpm)} WPM (${Math.round(me.accuracy)}%) e terminei em ${me.place}º no CodeRacer 🏁 me alcança: ${url}; sem place → mesma frase sem o trecho de colocação.
    2. src/lib/share.test.ts — casos: com/sem place, arredondamento de wpm/accuracy fracionários, url custom, nick com caracteres estranhos.
    3. Results.tsx:134-156 — dentro de me && (...), botão btn-secondary com aria-label="Compartilhar meu resultado" ao lado de l.136-144.
    4. Handler: const text = buildShareText(me, window.location.origin); tenta navigator.share({ text, url }); no catch(err) só cai para navigator.clipboard.writeText se err?.name !== "AbortError"; sucesso/erro do clipboard → toast.push (padrão RoomView.tsx:56-62).
    5. Gate: pnpm typecheck · pnpm build · pnpm test (com o novo share.test.ts) · verificação em browser (desktop copy+toast; mobile 375 com sheet nativo, ou o harness da Infra/UI: harness só-de-dev para verificar telas pós-corrida (Results/pódio) sem jogar #37 se ela já estiver na main).
  3. added
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    and removed
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    on Jul 22, 2026
  4. caioross commented on Jul 22, 2026

    @caioross
    OwnerAuthor

    Resolvido em PR #57 (auto/issue-22-share-result).

    Helper puro buildShareText em src/lib/share.ts + 9 casos Vitest, e ShareButton secundário na área de ações do Results.tsx (Web Share API → fallback clipboard + toast). Segui o Parecer do Conselho incluindo os 3 ajustes: AbortError não cai no clipboard (cancelar não é falhar), quem não terminou vê o botão com texto sem colocação, e btn-secondary para não disputar com "Jogar de novo". Uso window.location.origin no handler — SITE.url cai num domínio que não é o de produção quando a env falta.

    Gate verde: typecheck · build · 43 testes · validate-persistence (49) · validate-metrics (37). Verificação em browser não foi possível — a tela só existe após corrida completa, o ponto cego que a #37 resolve; declarado no PR.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Prioridade normalarea:uiInterface, design system, animação, game feel

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions