| 1 | /** |
| 2 | * <SessionMedia> — renders one entry from web/lib/media-manifest.ts. |
| 3 | * |
| 4 | * Two states, both honest: |
| 5 | * |
| 6 | * pending — a plainly labeled panel: "Recording pending release |
| 7 | * candidate". No mock terminal, no staged screenshot, no |
| 8 | * recycled clip. The panel exists so the absence of footage is |
| 9 | * visible and explained, not hidden. |
| 10 | * |
| 11 | * published — a <video> with the declared poster, per-locale WebVTT |
| 12 | * captions, a transcript link, and a GIF fallback link. The |
| 13 | * reduced-motion contract is structural (REDUCED_MOTION_POLICY |
| 14 | * = "static-poster-no-autoplay"): preload="none", no autoPlay, |
| 15 | * user-initiated playback only. The optional GIF is a link, |
| 16 | * not an autoplaying reduced-motion substitute. |
| 17 | * |
| 18 | * Server component: no client JS, SSG-safe. |
| 19 | */ |
| 20 | |
| 21 | import { REDUCED_MOTION_POLICY, type MediaAsset } from "@/lib/media-manifest"; |
| 22 | import { getDocsShell, pickText } from "@/lib/i18n/dictionaries"; |
| 23 | import { StatusBadge } from "./status-badge"; |
| 24 | |
| 25 | const MEDIA_PLAN_DOC = |
| 26 | "https://github.com/codewhale-hq/CodeWhale/blob/main/docs/releases/v0.9.2-media-plan.md"; |
| 27 | const REPO_BLOB_BASE = "https://github.com/codewhale-hq/CodeWhale/blob/main"; |
| 28 | |
| 29 | export function SessionMedia({ asset, locale = "en" }: { asset: MediaAsset; locale?: string }) { |
| 30 | const t = getDocsShell(locale); |
| 31 | |
| 32 | if (asset.status === "pending") { |
| 33 | return ( |
| 34 | <figure |
| 35 | className="session-media session-media-pending" |
| 36 | data-media-id={asset.id} |
| 37 | data-media-status={asset.status} |
| 38 | data-reduced-motion-policy={REDUCED_MOTION_POLICY} |
| 39 | > |
| 40 | <div className="session-media-stage"> |
| 41 | <StatusBadge kind="pending" locale={locale} label={asset.pendingLabel} /> |
| 42 | <p className="session-media-pending-note"> |
| 43 | {t.mediaPendingNote} |
| 44 | </p> |
| 45 | </div> |
| 46 | <figcaption className="session-media-caption"> |
| 47 | <strong>{pickText(asset.title, locale)}</strong> |
| 48 | <span>{pickText(asset.description, locale)}</span> |
| 49 | <a href={MEDIA_PLAN_DOC} target="_blank" rel="noreferrer"> |
| 50 | {t.mediaPlanLink} |
| 51 | </a> |
| 52 | </figcaption> |
| 53 | </figure> |
| 54 | ); |
| 55 | } |
| 56 | |
| 57 | // status === "published": manifest tests require every field and verify |
| 58 | // file presence/byte budgets plus poster dimensions. The human recording |
| 59 | // checklist remains authoritative for actual video duration/dimensions. |
| 60 | const poster = asset.poster!; |
| 61 | const video = asset.video!; |
| 62 | const captions = asset.captions ?? []; |
| 63 | // The track matching the page's language is the default; English only |
| 64 | // when the page's language has no captions. |
| 65 | const defaultCaption = captions.some((track) => track.srclang === locale) ? locale : "en"; |
| 66 | |
| 67 | return ( |
| 68 | <figure |
| 69 | className="session-media" |
| 70 | data-media-id={asset.id} |
| 71 | data-media-status={asset.status} |
| 72 | data-reduced-motion-policy={REDUCED_MOTION_POLICY} |
| 73 | > |
| 74 | <video |
| 75 | controls |
| 76 | preload="none" |
| 77 | poster={`/${poster.src}`} |
| 78 | width={video.width} |
| 79 | height={video.height} |
| 80 | aria-label={pickText(poster.alt, locale)} |
| 81 | > |
| 82 | <source src={`/${video.src}`} type="video/mp4" /> |
| 83 | {captions.map((track) => ( |
| 84 | <track |
| 85 | key={track.srclang} |
| 86 | kind="captions" |
| 87 | src={`/${track.src}`} |
| 88 | srcLang={track.srclang} |
| 89 | label={track.label} |
| 90 | default={track.srclang === defaultCaption} |
| 91 | /> |
| 92 | ))} |
| 93 | </video> |
| 94 | <figcaption className="session-media-caption"> |
| 95 | <strong>{pickText(asset.title, locale)}</strong> |
| 96 | <span>{pickText(asset.description, locale)}</span> |
| 97 | {asset.gifFallback && ( |
| 98 | <a href={`/${asset.gifFallback.src}`}> |
| 99 | {t.mediaGifFallback} |
| 100 | </a> |
| 101 | )} |
| 102 | {asset.transcript && ( |
| 103 | <a href={`${REPO_BLOB_BASE}/${asset.transcript}`} target="_blank" rel="noreferrer"> |
| 104 | {t.mediaTranscript} |
| 105 | </a> |
| 106 | )} |
| 107 | </figcaption> |
| 108 | </figure> |
| 109 | ); |
| 110 | } |
| 111 |