| 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 { pickText } from "@/lib/i18n/dictionaries"; |
| 23 | import { StatusBadge } from "./status-badge"; |
| 24 | |
| 25 | const MEDIA_PLAN_DOC = |
| 26 | "https://github.com/Hmbown/CodeWhale/blob/main/docs/releases/v0.9.2-media-plan.md"; |
| 27 | const REPO_BLOB_BASE = "https://github.com/Hmbown/CodeWhale/blob/main"; |
| 28 | |
| 29 | export function SessionMedia({ asset, locale = "en" }: { asset: MediaAsset; locale?: string }) { |
| 30 | const isZh = locale === "zh"; |
| 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 | {isZh |
| 44 | ? "还没有录像。录好之后会放在这里,附字幕、文字稿和可选的 GIF 下载。" |
| 45 | : "There is no recording yet. When there is, it goes here with captions, a transcript, and an optional GIF download."} |
| 46 | </p> |
| 47 | </div> |
| 48 | <figcaption className="session-media-caption"> |
| 49 | <strong>{pickText(asset.title, locale)}</strong> |
| 50 | <span>{pickText(asset.description, locale)}</span> |
| 51 | <a href={MEDIA_PLAN_DOC} target="_blank" rel="noreferrer"> |
| 52 | {isZh ? "录制计划与验收清单 ↗" : "Recording plan and acceptance checklist ↗"} |
| 53 | </a> |
| 54 | </figcaption> |
| 55 | </figure> |
| 56 | ); |
| 57 | } |
| 58 | |
| 59 | // status === "published": manifest tests require every field and verify |
| 60 | // file presence/byte budgets plus poster dimensions. The human recording |
| 61 | // checklist remains authoritative for actual video duration/dimensions. |
| 62 | const poster = asset.poster!; |
| 63 | const video = asset.video!; |
| 64 | const captions = asset.captions ?? []; |
| 65 | |
| 66 | return ( |
| 67 | <figure |
| 68 | className="session-media" |
| 69 | data-media-id={asset.id} |
| 70 | data-media-status={asset.status} |
| 71 | data-reduced-motion-policy={REDUCED_MOTION_POLICY} |
| 72 | > |
| 73 | <video |
| 74 | controls |
| 75 | preload="none" |
| 76 | poster={`/${poster.src}`} |
| 77 | width={video.width} |
| 78 | height={video.height} |
| 79 | aria-label={pickText(poster.alt, locale)} |
| 80 | > |
| 81 | <source src={`/${video.src}`} type="video/mp4" /> |
| 82 | {captions.map((track) => ( |
| 83 | <track |
| 84 | key={track.srclang} |
| 85 | kind="captions" |
| 86 | src={`/${track.src}`} |
| 87 | srcLang={track.srclang} |
| 88 | label={track.label} |
| 89 | default={track.srclang === "en"} |
| 90 | /> |
| 91 | ))} |
| 92 | </video> |
| 93 | <figcaption className="session-media-caption"> |
| 94 | <strong>{pickText(asset.title, locale)}</strong> |
| 95 | <span>{pickText(asset.description, locale)}</span> |
| 96 | {asset.gifFallback && ( |
| 97 | <a href={`/${asset.gifFallback.src}`}> |
| 98 | {isZh ? "GIF 下载回退(无视频环境)" : "GIF fallback download (no-video environments)"} |
| 99 | </a> |
| 100 | )} |
| 101 | {asset.transcript && ( |
| 102 | <a href={`${REPO_BLOB_BASE}/${asset.transcript}`} target="_blank" rel="noreferrer"> |
| 103 | {isZh ? "文字稿 ↗" : "Transcript ↗"} |
| 104 | </a> |
| 105 | )} |
| 106 | </figcaption> |
| 107 | </figure> |
| 108 | ); |
| 109 | } |
| 110 |