teqo-backup/src/lib/contentPieceShare.ts
Francisco Solla dda55adc27 feat(S27): Central de Conteúdos pública — Peça voto pra Solla 1313
/conteudos + /conteudos/<slug>: catálogo filtrável (Tipo · Cidade · Região ·
Tema · Instituição + termo na URL canônica), play sob demanda (Vídeo/Áudio só
carregam no toque; Foto com preview leve), Baixar e Compartilhar do próprio
card com a mensagem de voto por tipo (wa.me sem destinatário, editável; "A
mídia" usa a folha nativa e cai em download onde o aparelho não suportar) e o
card personalizado como peça sintética do catálogo → /cards.

- leitura pública nova (contentPieceReads) cacheada na tag contentPieces com o
  gate where status=publicado + contentPieceIsPublic; a rota GET
  /conteudos/<slug>/midia reusa buildPrivateMediaResponse (range, disposition,
  private no-store, 404 uniforme) com download jorge-solla-1313-<slug>.<ext> e
  a allowlist inline do dono estendida (webp/gif/avif + áudios das peças)
- contrato público puro do catálogo (filtros/VM/download) + mensagens de voto
  (contentPieceShare); OG com fallback do global; conteudos entra nos slugs
  reservados; kill switch por peça vale na hora (lista, página, rodapé/selo);
  descoberta condicional no CampaignFooter e no header local
- UI portada classe-a-classe do design aprovado (cenas 01–08, com as cenas
  06–08 estendidas no trigger a); sem migration, sem Consent e sem access novo;
  contentMedia segue privada (sem segunda cópia de mídia)
- testes: 2 unit specs novos, 18 casos int (leitura pública + rota de mídia) e
  e2e frontendConteudos (5 casos); manifest e curado atualizados
- follow-up de engenharia: #1276 (docs/plans/escala-dry-pos-s27.md)

Closes #1255
2026-09-23 11:13:57 -03:00

54 lines
2.3 KiB
TypeScript

/**
* S27 — pure share contract of the Central de Conteúdos: the vote message per
* piece type (the literals of the gate, always editable by the voter) and the
* link each piece shares. The system never sends anything: the sheet opens the
* sender's own WhatsApp with the text prefilled (`wa.me`, no recipient).
*
* The S4 home vocabulary (`src/lib/contentShare.ts`) stays its own owner — a
* different message family with its own pinned contract.
*/
import type { ContentPieceType } from '@/lib/contentPiece'
import { contentPiecePublicPath } from '@/lib/contentPieceCatalog'
import { buildContentShareLink } from '@/lib/contentShare'
import { buildWhatsAppTextShareUrl } from '@/lib/phone'
/** Link-piece sources share the platform URL; an archived piece shares its page. */
export type ContentPieceShareSource = {
slug: string
isLink: boolean
sourceUrl: string | null
}
/**
* Vote message per piece type (gate literals, "assumidas — validar com a
* comunicação"): the voter reviews the text in the sheet before opening
* WhatsApp, so the template is a starting point, never an automatic send.
*/
export const contentPieceVoteMessage = (
type: ContentPieceType,
title: string,
link: string,
): string => {
if (type === 'video') {
return `Estamos na reta final e o Jorge Solla 1313 precisa do seu voto. Olha esse vídeo: ${title} — ${link}. Peça voto pra Solla 1313 pra quem você conhece.`
}
if (type === 'audio') {
return `Ouça e mande pro grupo: ${title} — ${link}. Peça voto pra Solla 1313.`
}
return `Fiz/achei esse material do Solla 1313: ${title} — ${link}. Peça voto pra Solla 1313 também.`
}
/**
* The link a piece shares: the canonical platform URL for a link piece (never
* a second copy), the public piece page for an archived one. Absolute against
* the visitor's origin so the message carries a clickable URL (the S4
* fail-soft resolver is the single owner of that absolutization).
*/
export const contentPieceShareLink = (piece: ContentPieceShareSource, origin: string): string =>
piece.isLink && piece.sourceUrl
? piece.sourceUrl
: buildContentShareLink(contentPiecePublicPath(piece.slug), origin)
/** `wa.me/?text=` with the prefilled message — no recipient, no automatic send. */
export const buildContentPieceWhatsAppUrl = (message: string): string =>
buildWhatsAppTextShareUrl(message)