diff --git a/docs/changelog/2026-09-20-s22.md b/docs/changelog/2026-09-20-s22.md new file mode 100644 index 00000000..688f0a1d --- /dev/null +++ b/docs/changelog/2026-09-20-s22.md @@ -0,0 +1 @@ +**S22 — Jingles e rádio na home: a seção de som (2026-09-20):** o som do mandato chega à vitrine — a home pública ganha, entre "Nossa história" e "Faça parte", a seção "A trilha da nossa caminhada" (portada classe-a-classe do artefato aprovado `docs/plans/jingles-radio-homepage-ui-design.html`, Cenas 01–07) com a **Rádio Jorge Solla 1313** e até **3 jingles publicados** do S21, sem rota nova e sem tocar `/jingles` nem o rodapé. A rádio é um `RadioFacade` click-to-load: no estado `facade` não existe iframe, script ou request de terceiro — só o botão "Ouvir a rádio" e o link "Abrir no Zeno" —; o clique monta o widget oficial do zeno.fm e o card passa por `loading` (`role="status"` + `aria-busy` + spinner, com o foco do teclado movido para a região) até `loaded` no `onLoad`, com o **mesmo footprint reservado nos três estados** (330px mobile / 312px desktop, medido 522=522 e 314=314 no app real) — zero CLS, e o iframe nunca remonta (sem refetch). Os jingles reusam o core do S21, **extraído behavior-preserving** de `JinglePlayer` para `JingleCards` (exclusividade, `preload="none"`, download pelo slug e contrato `article[data-jingle]`/`data-state` intactos, com `headingLevel` para o outline da home e o anel de foco único em `focusRing.ts`): grade `md:grid-cols-3`, até 3 exibidos e "Ver todos os jingles" → `/jingles` **somente** quando há mais de 3; com zero publicados a grade e o link somem e a rádio permanece (fail-closed, mesmo kill switch do S21) na variante compacta da Cena 07 (`max-w-[880px]`, coluna de 240px, copy própria). A home segue estática: uma leitura `getPublishedJingleItems()` (cache tag `jingles`) alimenta a seção e o flag do rodapé (`slice(0,3)` em memória; nenhum `payload.find` novo). e2e: `frontendJingles` ganhou a seção da home (facade inerte com zero request `zeno.fm` **e** zero request de MP3 antes do gesto, widget de teste via `page.route`, `data-radio-state` até `loaded`, 4 publicados → 3 cards + handoff, kill switch → só rádio) e o `frontend` pinou a ordem (`sound = story+1`, `cards = sound+1`) e o overflow 390; unit: `radioFacade` (facade sem iframe → clique monta → `onLoad` não remonta) e `jingleHomeSection` (0/1/3/>3, "Ver todos", contrato da grade). Sem migration (nenhum schema; leitura pública sem PII/Consent). Crítica final do `designer` **CERTIFICADO** (tier `openai/gpt-5.6-sol`) contra o app renderizado em 390/1280 nos estados facade, carregando, carregado e zero — após uma primeira passada com 6 ajustes (escala do título, facade mobile da Cena 02, arte/wave, CLS, variante zero, header mobile), todos aplicados. `pnpm gate:fast` verde (4224 unit), e2e local selecionado verde (`frontendJingles` 4/4, `frontend` 2/2), simplify com 2 revisores (11 achados: 9 resolvidos no lugar, 2 descartados com respaldo; nenhum débito novo). diff --git a/docs/plans/jingles-radio-homepage-impl.md b/docs/plans/jingles-radio-homepage-impl.md new file mode 100644 index 00000000..23a4f585 --- /dev/null +++ b/docs/plans/jingles-radio-homepage-impl.md @@ -0,0 +1,140 @@ +# Impl: Jingles e rádio na home — a seção de som (S22) + +Status: aprovado — modo --auto +Atualizado em: 2026-09-20 +Issue: #1228 +Intenção: docs/plans/jingles-radio-homepage.md +Appetite restante: herdado (~1 dia eng; uma seção nova na home, sem rota nova) — sem ajuste de escopo; cortes em Rabbit holes. + +## Leitura da intenção + +- **Outcome:** a home passa a ter uma seção de som curta com a Rádio Jorge Solla 1313 (player do zeno só após o clique) e até 3 jingles publicados (capa, play um por vez, download MP3), linkando `/jingles` quando houver mais; zero jingles publicados deixa a rádio e esconde a grade (fail-closed); nada de terceiro, áudio ou PII antes do gesto do visitante. +- **O que NÃO negociar:** `/jingles` continua canônica, indexável e no rodapé (não mudar player nem página do S21); facade click-to-load da rádio (zero request de terceiro antes do clique; link externo como alternativa); até 3 jingles + "Ver todos" só quando houver mais; posição entre `CampaignStorySection` e `CampaignCardsSection`; fonte única é a collection `jingle` via `getPublishedJingleItems()` (sem collection/mídia nova); home estática (nada de `payload.find` cru); kill switch do S21 preservado; sem formulário/consent/PII. +- **O que reavaliar (hipóteses do plano de intenção):** (a) o `JinglePlayer` do S21 é reusável como está? — não: ele hardcoda o shell de página inteira (`JinglePlayer.tsx:134-135,261`) e a grade; a hipótese correta é extrair o core; (b) o fixture `e2eTest.ts` "aborta requests externos"? — não em geral: ele só roteia `youtube-nocookie` (`:108`) e `facebook.net` (`:114`); o iframe do zeno exige `page.route` próprio no spec, senão vira rede real/erro de console no guard; (c) o e2e da home mede overflow? — sim (`frontend.e2e.spec.ts:553-557`), mas não da seção nova: ela ganha asserção própria. + +## Abordagem recomendada + +```mermaid +flowchart LR + RSC["(home)/page.tsx (RSC)
getPublishedJingleItems() · cache tag jingles"] --> SEC["JingleHomeSection (server)
data-home-section='sound'"] + RSC -- "slice(0,3)" --> GRID["JingleCards (client, extraído)
article[data-jingle] · data-state · preload=none"] + RSC -- ">>> 3" --> ALL["Link 'Ver todos os jingles' → /jingles"] + SEC --> RADIO["RadioFacade (client)
data-radio-state: facade → loading → loaded"] + RADIO -- "clique" --> IFRAME["iframe zeno.fm/player/jorge-solla-1313/"] + S21["JinglePlayer (shell S21) · /jingles"] --> GRID +``` + +**Opções consideradas:** A | B | C | D (por decisão, abaixo) +**Recomendação:** extrair o core do player (`JingleCards`) e montar a seção da home com ele + uma facade de rádio sob `src/components/jingles/`; leitura única do listing cacheado na page. +**Rejeitadas:** props de shell no `JinglePlayer` (arrisca `/jingles` sem ganho), grade reimplementada na home (duplica a máquina de estado), iframe direto da rádio (viola o aceite), spec e2e novo (segundo dono das linhas de jingle + entrada de manifest). + +### Decisões de engenharia + +**D1 — Reuso do player (caro: fronteira de módulo)** +Opções: A) props de shell/compacto no `JinglePlayer`; B) extrair `JingleCards` (core client) e deixar `JinglePlayer` como shell do S21; C) montar a grade na home sem reusar. +Recomendação: B — uma única dona da exclusividade (`activeId`/`playback`/refs em `JinglePlayer.tsx:83-131`), `/jingles` visual e contratualmente intacta (seletores pinados `article[data-jingle]`, `data-state`, `button[data-play]`, `audio[preload="none"]`, `download={downloadFilename}`) e nenhum branching novo no componente que o e2e `frontendJingles` pina. +Alternativas rejeitadas: A porque move o branching para o shell pinado e arrisca regressão visual em `/jingles`; C porque duplica a exclusividade, o catch do `play()` abortado e o progresso — drift garantido contra `jinglePlayer.unit.spec.tsx`. + +**D2 — Facade da rádio (caro: componente client + contrato de terceiro)** +Opções: A) `RadioFacade` client em `src/components/jingles/`, sem sandbox, estados `facade/loading/loaded`, iframe oficial só no clique; B) iframe direto (como o YouTube da story); C) só link externo; D) facade com `sandbox` restritivo. +Recomendação: A — entrega o "embed tocável" pedido sem expor GA (`G-2T527NZWVM`) e redes de anúncio da zeno em todo pageview; fica sob o prefixo `src/components/jingles` (manifest `frontendJingles`, sem entrada nova); iframe com `src` do widget, `title` em pt-BR, `allow="autoplay"`, altura reservada; clique monta, `onLoad` promove `loading → loaded`; `aria-busy` no botão de carga; link "Abrir no Zeno" `target="_blank" rel="noopener noreferrer"` sempre visível na facade. +Alternativas rejeitadas: B porque carrega third-party/ads em toda abertura da home e viola o aceite; C porque não entrega o player pedido; D porque sandbox sem contrato do widget pode quebrar o player sem ganho de isolamento (o vendor já vê o que veria) — reavaliar apenas se a zeno documentar requisitos. +Lacuna de design: o artefato só tem `facade`/`loaded` (a cena 03 é "loaded", sem pending). Fase 0 estende o artefato com a cena de loading antes de qualquer markup. + +**D3 — Leitura na home e derivação do rodapé (caro: cache/estático)** +Opções: A) uma chamada `getPublishedJingleItems()` na page e derivar `showJingles` e a seleção; B) manter `hasPublishedJingles()` + chamada extra ao listing; C) mover o slice para dentro de `JingleHomeSection`. +Recomendação: A — o listing cacheado (`unstable_cache` tag `jingles`, `jingleReads.ts:32-35`) já é a fonte do rodapé e o padrão existente é o dono da lista derivar o flag (`/jingles/page.tsx:84-100`, comentário em `CampaignFooter.tsx:21`); uma leitura só evita divergir "tem jingles" de "tem grade"; `slice(0,3)` em memória (limit 0 devolve todos; volume trivial). +Alternativas rejeitadas: B porque `hasPublishedJingles` vira pass-through de `length > 0` quando a lista já está em mãos (depth check); C porque esconde a regra de produto no componente e tira do teste da page a seleção. +`hasPublishedJingles` continua usado por `(home)/cards/page.tsx:27` — não fica órfão (knip ok). O mock de `tests/unit/campaignHome.unit.spec.tsx:41-43` passa a exportar `getPublishedJingleItems`. + +**D4 — Posição e pino de ordem (caro: pin de e2e)** +Opções: A) `data-home-section="sound"` entre `story` e `cards` + atualizar o pino; B) renderizar sem `data-home-section` para não tocar o teste. +Recomendação: A — o pino (`frontend.e2e.spec.ts:1700-1706`) existe para proteger a ordem do bloco de conversão; a seção nova é parte da home e deve aparecer no contrato. Atualizar para `sound === story + 1` e `cards === sound + 1` (e manter `cards === newsletter - 1`). +Alternativa rejeitada: B porque deixa a seção invisível ao teste de ordem e qualquer reordenação futura passa silenciosa. + +**D5 — Estratégia de testes (caro: superfície de spec/manifest)** +Opções: A) estender `frontendJingles.e2e.spec.ts` (serial, dono das linhas de jingle) + update do pino/overflow em `frontend.e2e.spec.ts`; B) criar `frontendJinglesHome.e2e.spec.ts`; C) só unit. +Recomendação: A — o spec serial controla publicação/kill switch e já visita a home (`frontendJingles.e2e.spec.ts:200-201,231-232`), então grade/facade/zero/overflow-com-conteúdo cabem nele; o dono da home mantém ordem e overflow independente-de-conteúdo. Os prefixos atuais do manifest (`src/components/jingles` → `frontendJingles`, `:99-108`; `src/app/(frontend)` → `frontend`, `:77`) já cobrem todos os arquivos tocados — sem mudança no manifest. +Alternativas rejeitadas: B porque exige entrada nova no manifest e cria segundo dono das linhas de jingle (serialização/limpeza duplicadas) sem cobertura extra; C porque não prova o facade sem request de terceiro nem o estado real da home. + +### Componentes / mudanças + +- **`src/app/(frontend)/(home)/page.tsx`** — troca o import de `hasPublishedJingles` (`:9`) por `getPublishedJingleItems`; `const jingles = await getPublishedJingleItems()`; `const homeJingles = jingles.slice(0, 3)`; `showJingles={jingles.length > 0}`; render ` homeJingles.length} />` entre `` (`:241`) e `` (`:242`). +- **`src/components/jingles/JingleCards.tsx`** (novo, `'use client'`) — core extraído de `JinglePlayer.tsx:83-259`: estado de exclusividade, progresso e o grid (`max-w-6xl`, `gap-5 sm:gap-6 md:grid-cols-3`), com todo o markup/contrato dos cards (capa `next/image fill` sizes 33vw, título, `data-play`, `data-state`, progressbar, download com `downloadFilename` e `