// site-film-hotpage.jsx — Cinematic hotpage for streaming week // Replaces FilmDetail when festival.streaming.enabled && videoUrl is set const HP_STRAND_COLORS = { Fiction: { color:"#ff5c70", grad:"linear-gradient(150deg,#611f2f 0%,#2d1019 100%)" }, Documentary: { color:"#9b6fc0", grad:"linear-gradient(150deg,#42295f 0%,#241646 100%)" }, Animation: { color:"#ffc436", grad:"linear-gradient(150deg,#634b1a 0%,#2d230c 100%)" }, Experimental: { color:"#5fd0d8", grad:"linear-gradient(150deg,#13444a 0%,#0b2126 100%)" }, }; function hpFallback(sc) { return `radial-gradient(120% 85% at 80% 16%,${sc.color}4d 0%,transparent 50%),radial-gradient(90% 70% at 8% 98%,${sc.color}26 0%,transparent 52%),${sc.grad}`; } // NOTE: film hotpage playback (both the main video and the trailer) is rendered via the // shared, analytics-wired window.VideoPlayer (site-video-player.jsx) — see usages below. // A duplicate local VideoPlayer used to live here, shadowing window.VideoPlayer for the // trailer JSX tag ( resolves to same-file declarations before window.*), // and it only ever called window.trackEvent — a function that is never defined anywhere in // the app (only sketched in STATS_EXPANSION_PLAN.md). That silently dropped 100% of trailer // play/completion analytics. Removed; trailer now explicitly uses window.VideoPlayer too. function FilmHotpage({ filmId, navigate }) { const d = useSite(); const f = d.festival || {}; const str = f.streaming || {}; const ovr = (str.filmOverrides || {})[filmId] || {}; const currentYear = (f.dates?.year) || new Date().getFullYear().toString(); const film = (d.films || []).find(fl => fl.id === filmId); if (!film) { return (
Film not found.
); } const sc = HP_STRAND_COLORS[film.strand] || HP_STRAND_COLORS.Fiction; const still = ovr.stillOverride || film.stillsImages?.[0]?.url || film.bannerUrl || film.poster || ""; const statement = ovr.directorStatement || film.directorBios?.[0]?.bio || ""; const videoUrl = ovr.videoUrl || ""; const trailerUrl = ovr.trailerUrl || ""; // More in strand (excluding this film) const more = (d.films || []).filter(fl => fl.strand === film.strand && fl.id !== filmId && fl.year === currentYear && fl.status === "published" ).slice(0, 6); const goFilm = id => navigate("/film/" + id); // Track impression React.useEffect(() => { if (window.trackEvent) window.trackEvent("film_hotpage_view", { filmId, strand: film.strand }); }, [filmId]); return (
{/* ── Hero ──────────────────────────────────── */}
{/* Background */}
{still && {film.title}} {/* Scrims */}
{/* Back */} {/* Logo */}
LJMU MA
Short Film Festival
{/* Content */}
{film.strand} · Sixth Edition {currentYear}

{film.title}

{film.runtime && {film.runtime}′} {film.directors && dir. {film.directors}} {film.country && <> {film.country} } {film.strand}
{videoUrl && ( )} {trailerUrl && ( )} {(() => { const votingMode = f.voting?.mode || "internal"; const showVote = f.votingOpen && String(film.year) === String(currentYear) && (votingMode === "internal" ? film.voteEnabled : film.voteLink); return showVote && window.VoteButton && React.createElement(window.VoteButton, { film, mode: votingMode, hex: sc.color, textOn: "#141318", size: "lg", style: { background: "transparent", color: "#f0eee9", border: "1px solid rgba(240,238,233,.3)" }, }); })()}
{/* ── Details ────────────────────────────────── */}
{/* Full film player */}
{window.VideoPlayer ? React.createElement(window.VideoPlayer, { videoUrl, posterSrc:still, filmId, autoplay:false }) :
document.getElementById("hp-player-fallback")?.click()} style={{ width:"100%", height:"100%", display:"flex", alignItems:"center", justifyContent:"center", cursor:"pointer", background:sc.grad }}>
}
{/* Poster + Metadata grid */}
{film.poster && (
{film.title
)}
{/* Synopsis */} {film.synopsis && ( <>
Synopsis

{film.synopsis}

)} {/* Director statement */} {statement && ( <>
Director's Statement

"{statement}"

{film.directors}, Director
)}
{/* Credits + selection */}
Credits
{[ ["Director", film.directors], ["Country", film.country], ["Language", film.language], ["Runtime", film.runtime ? film.runtime + "′" : ""], ["Strand", film.strand], ["Format", "Short film · DCP / ProRes"], film.premiere && ["Premiere", film.premiere], ].filter(Boolean).filter(([,v])=>v).map(([role, val]) => (
{role}
{val}
))}
{/* Official selection badge */}
Official Selection
Sixth Edition · {currentYear}
{/* Trailer player */} {trailerUrl && ( <>
Trailer
{window.VideoPlayer && React.createElement(window.VideoPlayer, { videoUrl: trailerUrl, posterSrc: still, filmId: filmId + "-trailer", autoplay:false })}
)}
{/* ── More in strand ─────────────────────────── */} {more.length > 0 && (
More in {film.strand}
{more.map(fl => { const nsc = HP_STRAND_COLORS[fl.strand] || sc; const flStill = (str.filmOverrides?.[fl.id]?.stillOverride) || fl.stillsImages?.[0]?.url || fl.bannerUrl || fl.poster || ""; return (
goFilm(fl.id)} style={{ flex:"none", width:200, cursor:"pointer", transition:"transform .26s cubic-bezier(.2,.7,.3,1)" }} onMouseEnter={e=>e.currentTarget.style.transform="translateY(-6px)"} onMouseLeave={e=>e.currentTarget.style.transform="translateY(0)"}>
{flStill && {fl.title}}
{fl.title}
dir. {fl.directors} {fl.runtime && {fl.runtime}′}
); })}
)}
); } Object.assign(window, { FilmHotpage });