mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
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:
parent
f5f412a529
commit
a06344cbaf
7 changed files with 58 additions and 31 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue