// site-programme-hotpage.jsx // Replaces the normal Programme page when festival.streaming.enabled === true. // Toggle OFF → normal loads instead (routing handled in site-shell.jsx). const PH_STRANDS = { Fiction: { color:"#ff5c70", dark:"#2d1019", desc:"Scripted, acted, directed — stories imagined and put on screen. From intimate drama to dark comedy, the oldest tradition in cinema still finding new ground." }, Documentary: { color:"#9b6fc0", dark:"#241646", desc:"Films rooted in reality. Observational, investigative, personal — work that bears witness to the world as it is, whether intimate or expansive." }, Animation: { color:"#ffc436", dark:"#2d230c", desc:"Hand-drawn, painted, and crafted frame by frame. Animation as a form in its own right — every second a deliberate work of craft." }, Experimental: { color:"#5fd0d8", dark:"#0b2126", desc:"Films that question what cinema can be. Structural, essayistic, formally adventurous — work that doesn't fit neatly anywhere else." }, }; function phFallback(sc) { return `linear-gradient(135deg,${sc.dark} 0%,#16151a 100%)`; } function phStill(fl, ovr) { return (ovr && ovr.stillOverride) || fl.stillsImages?.[0]?.url || fl.bannerUrl || fl.poster || ""; } function PHFilmCard({ fl, sc, ovr, navigate }) { const still = phStill(fl, ovr); const hasVideo = !!(ovr && ovr.videoUrl); const [hov, setHov] = React.useState(false); function fmt(mins, secs) { const m = parseInt(mins)||0, s = parseInt(secs)||0; if (!m && !s) return ""; return s ? m + "'" + String(s).padStart(2,"0") + "\"" : m + "'"; } const runtime = fmt(fl.runtime, fl.runtimeSecs); return (
navigate("/film/" + fl.id)} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} style={{ flex:"none", width:"clamp(220px,20vw,290px)", cursor:"pointer", scrollSnapAlign:"start", transform: hov ? "translateY(-7px)" : "translateY(0)", transition:"transform .26s cubic-bezier(.2,.7,.3,1)" }}> {/* 16:9 thumbnail */}
{/* Fallback gradient */}
{/* Still */} {still && ( {fl.title} )} {/* Scrim */}
{/* Strand colour tick */}
{/* Watch badge */} {hasVideo && (
Watch
)} {/* Play hover */}
{/* Title */}
{fl.title}
{/* Caption */}
dir. {fl.directors} {runtime && ( {runtime} )}
); } function PHStrandSection({ strand, films, overrides, navigate, defaultOpen }) { const sc = PH_STRANDS[strand] || PH_STRANDS.Fiction; const trackRef = React.useRef(null); const [canPrev, setCanPrev] = React.useState(false); const [canNext, setCanNext] = React.useState(true); const updateArrows = () => { const t = trackRef.current; if (!t) return; setCanPrev(t.scrollLeft > 4); setCanNext(t.scrollLeft < t.scrollWidth - t.clientWidth - 4); }; const scroll = dir => { if (trackRef.current) trackRef.current.scrollBy({ left: dir * 600, behavior:"smooth" }); }; return (
{/* Strand header */}
{strand}
{sc.desc}
{/* Count + arrows */}
{films.length} film{films.length !== 1 ? "s" : ""}
{[-1,1].map(dir => ( ))}
{/* Rail — edge fade via separate overlay layers (opacity-transitionable) */}
{films.map(fl => ( ))}
{/* Divider */}
); } function ProgrammeHotpage({ navigate, strand: activeStrand }) { const d = useSite(); const f = d.festival || {}; const str = f.streaming || {}; const overrides = str.filmOverrides || {}; const year = String(f.year || new Date().getFullYear()); const films = (d.films || []).filter(fl => String(fl.year || f.year) === year ); const totalFilms = films.length; const withVideo = Object.values(overrides).filter(o => o.videoUrl).length; // Group by strand — order follows the CMS-configurable strand order (Programme → Strand settings) const phOrder = window.getStrandOrder(f); const byStrand = {}; phOrder.forEach(s => { byStrand[s] = films.filter(fl => fl.strand === s); }); // Auto-scroll to strand if navigated with #/programme/Fiction etc. const strandRefs = React.useRef({}); React.useEffect(() => { if (!activeStrand) return; const el = strandRefs.current[activeStrand]; if (el) { const top = el.getBoundingClientRect().top + window.scrollY - 90; window.scrollTo({ top, behavior:"smooth" }); } }, [activeStrand]); return (
{/* ── Hero ── */}
{/* Live badge */}
Streaming Now · Sixth Edition {year}

The Programme

{totalFilms} films across four strands — free to stream during the festival week. {withVideo > 0 && ` ${withVideo} film${withVideo !== 1 ? "s" : ""} ready to watch now.`}

{/* Strand quick-links */}
{phOrder.filter(s => byStrand[s].length > 0).map(s => { const sc = PH_STRANDS[s]; return ( ); })}
{/* close wrap */}
{/* ── Strand sections ── */}
{phOrder.filter(s => byStrand[s].length > 0).map(s => (
strandRefs.current[s] = el}>
))}
{/* ── Footer note ── */}

Films available to stream during the festival week only. All rights remain with the filmmakers.

{/* close wrap */}
); } Object.assign(window, { ProgrammeHotpage });