// ============================================================ // Tribeca Lisboa — PROGRAMME page (main website) // One layout, three entry points (festival | mtf | all) via ?id=. // Search + filter drawer + chronological grouping + date rail. // Reuses SiteHeader / RFooter / CardLabel / IconPin + design system. // ============================================================ const { useState: useStatePG, useEffect: useEffectPG, useRef: useRefPG, useMemo: useMemoPG } = React; // ---- bilingual date helpers --------------------------------- const PG_MONTHS_PT = ['janeiro','fevereiro','março','abril','maio','junho','julho','agosto','setembro','outubro','novembro','dezembro']; const PG_MONTHS_EN = ['January','February','March','April','May','June','July','August','September','October','November','December']; const PG_WD_PT = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb']; const PG_WD_EN = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; function pgParts(iso) { const [y, m, d] = iso.split('-').map(Number); const wd = new Date(Date.UTC(y, m - 1, d)).getUTCDay(); return { y, m: m - 1, d, wd }; } function pgDayLabel(iso, lang) { const { y, m, d, wd } = pgParts(iso); const wdN = (lang === 'EN' ? PG_WD_EN : PG_WD_PT)[wd]; const moN = (lang === 'EN' ? PG_MONTHS_EN : PG_MONTHS_PT)[m]; return lang === 'EN' ? `${wdN} ${d} ${moN}` : `${wdN}, ${d} ${moN}`; } function pgMonthLabel(key, lang) { const [y, m] = key.split('-').map(Number); const moN = (lang === 'EN' ? PG_MONTHS_EN : PG_MONTHS_PT)[m - 1]; return `${moN} ${y}`; } const PG_PILLARS = ['film', 'podcasts', 'vibes', 'nightlive', 'immersive'].filter((k) => !(window.HIDDEN_CATS || []).includes(k)); const PG_VENUES = ['Cinema São Jorge', 'Cineteatro Capitólio', 'Teatro Tivoli BBVA']; const PG_DAYS = ['2026-12-10', '2026-12-11', '2026-12-12', '2026-12-13']; const pgArea = (ev) => (ev.venueArea || '').split(',')[0].trim(); // ---- status + access semantics ------------------------------ function pgStatus(ev, lang) { if (ev.avail === 'soldout') return { key: 'soldout', label: tt(lang, 'Sold Out', 'Esgotado'), cls: 'is-soldout' }; if (ev.avail === 'soon') return { key: 'soon', label: tt(lang, 'Coming Soon', 'Em breve'), cls: 'is-soon' }; return { key: 'available', label: tt(lang, 'Available', 'Disponível'), cls: 'is-available' }; } function pgAccess(access, lang) { if (access === 'paid') return tt(lang, 'Paid', 'Pago'); if (access === 'reg') return tt(lang, 'Free with Registration', 'Gratuito com inscrição'); return tt(lang, 'Free', 'Gratuito'); } // CTA only for available events; free + non-available → none function pgCta(ev, lang) { if (ev.avail !== 'available') return null; if (ev.access === 'paid') return tt(lang, 'Buy Ticket', 'Comprar bilhete'); if (ev.access === 'reg') return tt(lang, 'Register', 'Inscrever'); return null; } // ---- entry-point context ------------------------------------ const PG_TABS = [ { key: 'festival', label: (l) => tt(l, 'Festival', 'Festival') }, { key: 'mtf', label: () => FIX('More than a festival') }, { key: 'all', label: (l) => tt(l, 'All', 'Tudo') }, ]; function pgContext(type, lang) { if (type === 'festival') return { h: tt(lang, 'Festival', 'Festival'), d: tt(lang, 'Events taking place during Tribeca Festival Lisboa.', 'Eventos que decorrem durante o Tribeca Festival Lisboa.') }; if (type === 'mtf') return { h: FIX('More than a festival'), d: tt(lang, 'Events taking place throughout the year.', 'Eventos que decorrem ao longo de todo o ano.') }; return { h: tt(lang, 'Full Program', 'Programa completo'), d: tt(lang, 'All Tribeca Festival Lisboa events.', 'Todos os eventos do Tribeca Festival Lisboa.') }; } // ---- event row card (existing card language, list-optimised) - function PgCard({ ev }) { const lang = useCtx(LangContext); const c = CAT[ev.cat] || CAT.film; const st = pgStatus(ev, lang); const cta = pgCta(ev, lang); const [time] = (ev.date || '').split(' • '); return (
{st.label}

{evTitle(lang, ev)}

{ev.dayDate ? tt(lang, ev.dayDateEn, ev.dayDate) : ev.date} • {ev.time}
{ev.venue} · {pgArea(ev)}
{pgAccess(ev.access, lang)} {cta ? {cta} ↗ : (st.key === 'available' ? null : {st.label})}
); } // ---- filter drawer ------------------------------------------ function PgChips({ options, selected, onToggle }) { return (
{options.map((o) => ( ))}
); } function PgDrawer({ open, onClose, lang, type, setType, facets, filters, setFilters, resultCount }) { const toggle = (group, value) => setFilters((f) => { const next = new Set(f[group]); next.has(value) ? next.delete(value) : next.add(value); return { ...f, [group]: next }; }); const clearAll = () => setFilters({ pillars: new Set(), locations: new Set(), access: new Set(), months: new Set() }); return (

{tt(lang, 'Filters', 'Filtros')}

{tt(lang, 'Pillars', 'Pilares')}
({ value: p, label: (CAT[p] || {}).label || p }))} selected={filters.pillars} onToggle={(v) => toggle('pillars', v)} />
{tt(lang, 'Venues', 'Salas')}
({ value: v, label: v }))} selected={filters.locations} onToggle={(v) => toggle('locations', v)} />
{tt(lang, 'Date', 'Data')}
({ value: d, label: tt(lang, `${+d.slice(8)} December`, `${+d.slice(8)} de dezembro`) }))} selected={filters.months} onToggle={(v) => toggle('months', v)} />
); } // ---- page --------------------------------------------------- function ProgrammeInner() { const lang = useCtx(LangContext); const initType = ['festival', 'mtf', 'all'].includes(getParam('id')) ? getParam('id') : 'all'; const [type, setTypeState] = useStatePG(initType); const [query, setQuery] = useStatePG(''); const [filters, setFilters] = useStatePG({ pillars: new Set(), locations: new Set(), access: new Set(), months: new Set() }); const [drawer, setDrawer] = useStatePG(false); const [menu, setMenu] = useStatePG(false); const setType = (t) => { setTypeState(t); setFilters({ pillars: new Set(), locations: new Set(), access: new Set(), months: new Set() }); try { history.replaceState(null, '', urlFor('program', t)); } catch (e) {} }; // base set for the active entry point const base = useMemoPG(() => { if (type === 'festival') return ALL_EVENTS.filter((e) => e.type === 'festival'); if (type === 'mtf') return ALL_EVENTS.filter((e) => e.type === 'mtf'); return ALL_EVENTS; }, [type]); // facet options derived from the base set const facets = useMemoPG(() => { const pillars = [...new Set(base.map((e) => e.cat))]; const locations = [...new Set(base.map(pgArea))].sort(); const months = [...new Set(base.map((e) => e.iso.slice(0, 7)))].sort(); return { pillars, locations, months }; }, [base]); // apply search + filters const filtered = useMemoPG(() => { const q = query.trim().toLowerCase(); return base.filter((e) => { if (q && !(`${e.title} ${e.titleEn}`.toLowerCase().includes(q))) return false; if (filters.pillars.size && !filters.pillars.has(e.cat)) return false; if (filters.locations.size && !filters.locations.has(e.venue)) return false; if (filters.months.size && !filters.months.has(e.iso)) return false; return true; }).sort((a, b) => (a.iso + a.time).localeCompare(b.iso + b.time)); }, [base, query, filters]); // group: festival → by day; mtf/all → by month const groups = useMemoPG(() => { const byDay = type === 'festival'; const map = new Map(); filtered.forEach((e) => { const key = byDay ? e.iso : e.iso.slice(0, 7); if (!map.has(key)) map.set(key, []); map.get(key).push(e); }); return [...map.entries()].map(([key, items]) => ({ key, items, label: byDay ? pgDayLabel(key, lang) : pgMonthLabel(key, lang), })); }, [filtered, type, lang]); const activeFilterCount = filters.pillars.size + filters.locations.size + filters.months.size; const ctx = pgContext(type, lang); const jumpTo = (key) => { const el = document.getElementById('pg-g-' + key); if (el) { const hdr = 150; window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - hdr, behavior: 'smooth' }); } }; return (
setMenu(true)} /> {/* contextual header */}
{tt(lang, 'Programme', 'Programa')}

{ctx.h}

{ctx.d}

setFilters((f) => ({ ...f, pillars: p === 'all' ? new Set() : new Set([p]) }))} />
{/* sticky controls: search + filters + date rail */}
setQuery(e.target.value)} placeholder={tt(lang, 'Search events by title', 'Procurar eventos por título')} aria-label={tt(lang, 'Search events', 'Procurar eventos')} /> {query && }
{/* list */}
{groups.length === 0 && (
{tt(lang, 'No events found', 'Nenhum evento encontrado')}

{tt(lang, 'Try adjusting your search or filters.', 'Tente ajustar a pesquisa ou os filtros.')}

)} {groups.map((g) => (

{g.label}

{g.items.length} {tt(lang, g.items.length === 1 ? 'event' : 'events', g.items.length === 1 ? 'evento' : 'eventos')}
{g.items.map((ev) => )}
))}
setDrawer(false)} lang={lang} type={type} setType={setType} facets={facets} filters={filters} setFilters={setFilters} resultCount={filtered.length} /> setMenu(false)} />
); } function PgSearchIcon() { return ; } function PgFilterIcon() { return ; } function Programme() { const [lang] = useLang(); return ( ); } ReactDOM.createRoot(document.getElementById('root')).render();