fix(C194): port fiel ao artefato após crítica do designer (#1153)

- player mobile 226px (CTA do detalhe acima da dobra), 330px a partir de lg
- painel de downloads no mobile: CTA primário primeiro, 'Outros arquivos' depois
- cards mobile só com título/funcionalidade; data e 'Abrir reel' a partir de lg
- despublicado: aviso + publicação antes da mídia bloqueada; 'Sobre o reel' depois
- feature sem '(#cards)' na UI e data de publicação só com o dia
- 'Abrir reel' aria-hidden (o card inteiro é o link)

Design tier: openai/gpt-5.6-sol — crítica final certificada (v3, 10 capturas).
This commit is contained in:
Francisco Solla 2026-09-18 21:34:29 -03:00
parent 448f3845a2
commit 7e164b637d
7 changed files with 58 additions and 31 deletions

View file

@ -59,6 +59,7 @@ export default async function ReelDetailPage({ params }: ReelDetailPageProps) {
if (error instanceof ReelNotFoundError) notFound()
throw error
})
const blocked = !reel.canServeMedia
return (
<CampaignPageShell>
@ -85,22 +86,25 @@ export default async function ReelDetailPage({ params }: ReelDetailPageProps) {
</div>
</div>
{blocked ? (
<div className="flex flex-col gap-4">
<div className="rounded-lg bg-muted p-3">
<p className="text-sm font-medium">Fora da lista</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
Este reel não aparece na biblioteca. O registro e os arquivos continuam guardados,
mas nenhuma mídia é servida enquanto ele não estiver publicado.
</p>
</div>
<ReelPublicationPanel reelId={reel.id} status={reel.status} />
</div>
) : null}
<div className="grid gap-6 lg:grid-cols-[360px_minmax(0,1fr)_280px]">
<div>
<ReelPlayer reel={reel} />
</div>
<div className="min-w-0">
{reel.canServeMedia ? null : (
<div className="mb-4 rounded-lg bg-muted p-3">
<p className="text-sm font-medium">Fora da lista</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
Este reel não aparece na biblioteca. O registro e os arquivos continuam guardados,
mas nenhuma mídia é servida enquanto ele não estiver publicado.
</p>
</div>
)}
<ReelDownloadPanel reel={reel} />
{reel.transcript ? (
@ -125,7 +129,7 @@ export default async function ReelDetailPage({ params }: ReelDetailPageProps) {
</dl>
</section>
<ReelPublicationPanel reelId={reel.id} status={reel.status} />
{blocked ? null : <ReelPublicationPanel reelId={reel.id} status={reel.status} />}
<p className="rounded-lg bg-muted px-3 py-2 text-xs leading-5 text-muted-foreground">
Nada é publicado no Instagram por esta tela.

View file

@ -27,20 +27,30 @@ export const ReelDownloadPanel = ({ reel }: { reel: ReelDetailViewModel }) => {
const secondary = reel.downloads.filter((item) => !item.primary)
return (
<section className="rounded-xl border border-border p-4">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-sm font-semibold">Downloads</h2>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
Arquivos deste reel para finalizar e publicar fora do Teqo.
</p>
<section className="flex flex-col rounded-xl border border-border p-4">
<div className="order-2 mt-4 lg:order-1 lg:mt-0">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="hidden text-sm font-semibold lg:block">Downloads</h2>
<h2 className="text-sm font-semibold lg:hidden">Outros arquivos</h2>
<p className="mt-1 hidden text-xs leading-5 text-muted-foreground lg:block">
Arquivos deste reel para finalizar e publicar fora do Teqo.
</p>
</div>
<DownloadIcon
className="hidden size-5 shrink-0 text-muted-foreground lg:block"
aria-hidden="true"
/>
</div>
<DownloadIcon className="size-5 shrink-0 text-muted-foreground" aria-hidden="true" />
</div>
{primary ? <PrimaryDownload item={primary} /> : null}
{primary ? (
<div className="order-1 lg:order-2">
<PrimaryDownload item={primary} />
</div>
) : null}
<div className="mt-3 divide-y divide-border rounded-lg border border-border">
<div className="order-3 mt-3 divide-y divide-border rounded-lg border border-border">
{secondary.map((item) => (
<DownloadRow key={item.kind} item={item} />
))}

View file

@ -32,9 +32,14 @@ export const ReelLibraryCard = ({ reel }: { reel: ReelLibraryItemViewModel }) =>
<h3 className="text-sm leading-5 font-medium">{reel.title}</h3>
<p className="mt-1.5 text-xs text-muted-foreground">{reel.featureLabel}</p>
{reel.publishedAtLabel ? (
<p className="mt-1 text-xs text-muted-foreground">Publicado em {reel.publishedAtLabel}</p>
<p className="mt-1 text-xs text-muted-foreground max-lg:hidden">
Publicado em {reel.publishedAtLabel}
</p>
) : null}
<span className="mt-3 inline-flex h-8 items-center text-xs font-medium underline underline-offset-4 group-hover:text-primary">
<span
aria-hidden="true"
className="mt-3 hidden h-8 items-center text-xs font-medium underline underline-offset-4 group-hover:text-primary lg:inline-flex"
>
Abrir reel
</span>
</div>

View file

@ -9,7 +9,7 @@ import type { ReelDetailViewModel } from '@/lib/reel'
export const ReelPlayer = ({ reel }: { reel: ReelDetailViewModel }) => {
if (!reel.videoSourceUrl) {
return (
<div className="mx-auto flex aspect-[9/16] w-full max-w-[330px] flex-col items-center justify-center gap-1 rounded-xl border border-dashed bg-muted/40 p-4 text-center">
<div className="mx-auto flex aspect-[9/16] w-full max-w-[226px] flex-col items-center justify-center gap-1 rounded-xl border border-dashed bg-muted/40 p-4 text-center lg:max-w-[330px]">
<span className="text-sm font-medium">Player indisponível</span>
<p className="text-xs text-muted-foreground">{reel.mediaBlockedLabel}</p>
</div>
@ -23,7 +23,7 @@ export const ReelPlayer = ({ reel }: { reel: ReelDetailViewModel }) => {
preload="metadata"
poster={reel.videoPosterUrl}
src={reel.videoSourceUrl}
className="mx-auto aspect-[9/16] w-full max-w-[330px] rounded-xl border bg-black"
className="mx-auto aspect-[9/16] w-full max-w-[226px] rounded-xl border bg-black lg:max-w-[330px]"
/>
)
}

View file

@ -6,7 +6,7 @@
*/
import { campaignReelDetailHref } from '@/lib/campaignPaths'
import { formatBahiaDateTimeLabel } from '@/lib/campaignTime'
import { formatBahiaCivilDate } from '@/lib/campaignTime'
/** Upload collection of the artifacts; also its local disk directory in dev. */
export const REEL_MEDIA_SLUG = 'reelMedia'
@ -33,10 +33,16 @@ export const isReelStatus = (value: unknown): value is ReelStatus =>
export const REEL_FEATURES = ['cards'] as const
export type ReelFeature = (typeof REEL_FEATURES)[number]
/** Admin/select wording: keeps the public anchor the tutorial teaches. */
export const reelFeatureLabels: Record<ReelFeature, string> = {
cards: 'Cards de apoio (#cards)',
}
/** Library UI wording: the anchor belongs to the admin context, not the card. */
const reelFeatureDisplayLabels: Record<ReelFeature, string> = {
cards: 'Cards de apoio',
}
/**
* The artifacts a reel can carry, each mapping to exactly one upload field.
* `video` is the primary file (silent); the others are optional variants.
@ -179,11 +185,13 @@ const mediaAlt = (value: ReelMediaRef, fallback: string): string => {
return alt ? alt : fallback
}
/** Date-only (`dd/mm/aaaa`) publication label — the library never shows the hour. */
const publishedAtLabel = (iso: string | null | undefined): string | null => {
if (!iso) return null
const instant = new Date(iso)
if (Number.isNaN(instant.getTime())) return null
return formatBahiaDateTimeLabel(iso)
const [year, month, day] = formatBahiaCivilDate(instant).split('-')
return `${day}/${month}/${year}`
}
const reelCover = (record: ReelRecordForView): { coverUrl: string; coverAlt: string } => ({
@ -199,7 +207,7 @@ export const toReelLibraryItemViewModel = (record: ReelRecordForView): ReelLibra
id: record.id,
title: record.title,
featureLabel: isReelFeature(record.feature)
? reelFeatureLabels[record.feature]
? reelFeatureDisplayLabels[record.feature]
: record.feature,
status,
statusLabel: reelStatusLabels[status],

View file

@ -153,7 +153,7 @@ describe('reel library data (C194)', () => {
const newest = data.rows.find((row) => row.id === newer.id)
expect(newest).toMatchObject({
title: 'Reel novo',
featureLabel: 'Cards de apoio (#cards)',
featureLabel: 'Cards de apoio',
status: 'published',
statusLabel: 'Publicado',
coverUrl: `/campanha/comunicacao/reels/${newer.id}/media/cover`,

View file

@ -159,12 +159,12 @@ describe('reel view models (C194)', () => {
expect(item).toEqual({
id: 42,
title: 'Como criar seu card de apoio',
featureLabel: 'Cards de apoio (#cards)',
featureLabel: 'Cards de apoio',
status: 'published',
statusLabel: 'Publicado',
coverUrl: '/campanha/comunicacao/reels/42/media/cover',
coverAlt: 'Capa do tutorial',
publishedAtLabel: '18/09/2026 às 14:00',
publishedAtLabel: '18/09/2026',
detailHref: '/campanha/comunicacao/reels/42',
})
})