| 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 { StatusBadge } from "./status-badge"; |
| 23 | |
| 24 | const MEDIA_PLAN_DOC = |
| 25 | "https://github.com/Hmbown/CodeWhale/blob/main/docs/releases/v0.9.2-media-plan.md"; |
| 26 | const REPO_BLOB_BASE = "https://github.com/Hmbown/CodeWhale/blob/main"; |
| 27 | |
| 28 | export function SessionMedia({ asset, locale = "en" }: { asset: MediaAsset; locale?: string }) { |
| 29 | const isZh = locale === "zh"; |
| 30 | |
| 31 | if (asset.status === "pending") { |
| 32 | return ( |
| 33 | <figure |
| 34 | className="session-media session-media-pending" |
| 35 | data-media-id={asset.id} |
| 36 | data-media-status={asset.status} |
| 37 | data-reduced-motion-policy={REDUCED_MOTION_POLICY} |
| 38 | > |
| 39 | <div className="session-media-stage"> |
| 40 | <StatusBadge kind="pending" locale={locale} label={asset.pendingLabel} /> |
| 41 | <p className="session-media-pending-note"> |
| 42 | {isZh |
| 43 | ? "此处不展示占位或摆拍影像。录制完成后,这段真实会话将带海报帧、双语字幕、文字稿和可选的 GIF 回退下载一同发布。" |
| 44 | : "No placeholder or staged footage is shown here. When the recording exists, the real session ships with a poster frame, bilingual captions, a transcript, and an optional GIF fallback download."} |
| 45 | </p> |
| 46 | </div> |
| 47 | <figcaption className="session-media-caption"> |
| 48 | <strong>{isZh ? asset.title.zh : asset.title.en}</strong> |
| 49 | <span>{isZh ? asset.description.zh : asset.description.en}</span> |
| 50 | <a href={MEDIA_PLAN_DOC} target="_blank" rel="noreferrer"> |
| 51 | {isZh ? "录制计划与验收清单 ↗" : "Recording plan and acceptance checklist ↗"} |
| 52 | </a> |
| 53 | </figcaption> |
| 54 | </figure> |
| 55 | ); |
| 56 | } |
| 57 | |
| 58 | // status === "published": manifest tests require every field and verify |
| 59 | // file presence/byte budgets plus poster dimensions. The human recording |
| 60 | // checklist remains authoritative for actual video duration/dimensions. |
| 61 | const poster = asset.poster!; |
| 62 | const video = asset.video!; |
| 63 | const captions = asset.captions ?? []; |
| 64 | |
| 65 | return ( |
| 66 | <figure |
| 67 | className="session-media" |
| 68 | data-media-id={asset.id} |
| 69 | data-media-status={asset.status} |
| 70 | data-reduced-motion-policy={REDUCED_MOTION_POLICY} |
| 71 | > |
| 72 | <video |
| 73 | controls |
| 74 | preload="none" |
| 75 | poster={`/${poster.src}`} |
| 76 | width={video.width} |
| 77 | height={video.height} |
| 78 | aria-label={isZh ? poster.alt.zh : poster.alt.en} |
| 79 | > |
| 80 | <source src={`/${video.src}`} type="video/mp4" /> |
| 81 | {captions.map((track) => ( |
| 82 | <track |
| 83 | key={track.srclang} |
| 84 | kind="captions" |
| 85 | src={`/${track.src}`} |
| 86 | srcLang={track.srclang} |
| 87 | label={track.label} |
| 88 | default={track.srclang === "en"} |
| 89 | /> |
| 90 | ))} |
| 91 | </video> |
| 92 | <figcaption className="session-media-caption"> |
| 93 | <strong>{isZh ? asset.title.zh : asset.title.en}</strong> |
| 94 | <span>{isZh ? asset.description.zh : asset.description.en}</span> |
| 95 | {asset.gifFallback && ( |
| 96 | <a href={`/${asset.gifFallback.src}`}> |
| 97 | {isZh ? "GIF 下载回退(无视频环境)" : "GIF fallback download (no-video environments)"} |
| 98 | </a> |
| 99 | )} |
| 100 | {asset.transcript && ( |
| 101 | <a href={`${REPO_BLOB_BASE}/${asset.transcript}`} target="_blank" rel="noreferrer"> |
| 102 | {isZh ? "文字稿 ↗" : "Transcript ↗"} |
| 103 | </a> |
| 104 | )} |
| 105 | </figcaption> |
| 106 | </figure> |
| 107 | ); |
| 108 | } |
| 109 |