// ============================================================
// 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}
{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 */}
{/* sticky controls: search + filters + date rail */}
{/* 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')}
))}
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();