// ============================================================
// Tribeca Lisboa — TALENTOS listing + detail (one app, two pages)
// Data is derived from existing SPEAKERS + event guests + news
// (getTalents in data.jsx). No new data model.
// ============================================================
const TL_CATS = [
{ key: 'all', en: 'All', pt: 'Todos' },
{ key: 'guest', en: 'Confirmed guests', pt: 'Confirmed guests' },
{ key: 'director', en: 'Directors', pt: 'Realizadores' },
{ key: 'host', en: 'Hosts & podcasts', pt: 'Hosts & podcasts' },
{ key: 'orchestra', en: 'Orchestras', pt: 'Orquestras' },
];
function tlGroup(t) {
if (t.news) return 'guest';
const r = (t.roles[0] || {}).role || '';
if (/Orquestra/i.test(r)) return 'orchestra';
if (/Realizador/i.test(r)) return 'director';
return 'host';
}
const tlEvTitle = (lang, ev) => tt(lang, ev.titleEn || ev.title, ev.title);
function TlCard({ t }) {
const lang = useCtx(LangContext);
const ev = t.events[0];
return (
{talentCategory(t, lang)}
{t.name}
{ev && {tlEvTitle(lang, ev)}{t.events.length > 1 ? ` + ${t.events.length - 1}` : ''}
}
{!t.news && {isPendingImg(t.img)
? tt(lang, 'Bio and headshot awaiting approval.', 'Bio e headshot a aguardar aprovação.')
: tt(lang, 'Bio awaiting approval.', 'Bio a aguardar aprovação.')}
}
);
}
function TalentsListInner() {
const lang = useCtx(LangContext);
const [menu, setMenu] = React.useState(false);
const [cat, setCat] = React.useState('all');
const all = React.useMemo(() => {
const order = { guest: 0, director: 1, host: 2, orchestra: 3 };
return getTalents().filter((t) => t.news).sort((a, b) => order[tlGroup(a)] - order[tlGroup(b)]);
}, []);
const list = cat === 'all' ? all : all.filter((t) => tlGroup(t) === cat);
return (
setMenu(true)} />
Tribeca Festival Lisboa 2026
{tt(lang, 'Talent', 'Talentos')}
{tt(lang, 'Guests, directors, hosts and artists taking part in the festival.', 'Convidados, realizadores, hosts e artistas que participam no festival.')}
setMenu(false)} />
);
}
function TalentDetailInner() {
const lang = useCtx(LangContext);
const [menu, setMenu] = React.useState(false);
const all = React.useMemo(() => getTalents().filter((x) => x.news), []);
const t = all.find((x) => x.id === getParam('id')) || all[0];
const n = t.news;
const roleFor = (ev) => t.roles.filter((r) => r.eventId === ev.id).map((r) => tt(lang, r.roleEn || r.role, r.role)).join(' · ');
return (
setMenu(true)} />
← {tt(lang, 'Talent', 'Talentos')}
{talentCategory(t, lang)}
{t.name}
{t.events.length > 0 &&
{t.events.map((ev) =>
{roleFor(ev)} — {tlEvTitle(lang, ev)}
)}
}
{n &&
{tt(lang, n.dekEn, n.dek)}
}
{isPendingImg(t.img) &&
{tt(lang, 'Headshot awaiting approval.', 'Headshot a aguardar aprovação.')}
}
{n ? (
{n.body.map((b, i) => b.t === 'h2'
?
{tt(lang, b.xEn, b.x)}
:
{tt(lang, b.xEn, b.x)}
)}
) : (
{isPendingImg(t.img)
? tt(lang, 'Bio and headshot awaiting approval.', 'Bio e headshot a aguardar aprovação.')
: tt(lang, 'Bio awaiting approval.', 'Bio a aguardar aprovação.')}
)}
{t.events.length > 0 && (
{tt(lang, 'At the festival', 'No festival')}
)}
setMenu(false)} />
);
}
function TalentsRoot() {
const [lang] = useLang();
const Page = window.TL_PAGE === 'detail' ? TalentDetailInner : TalentsListInner;
return ;
}
ReactDOM.createRoot(document.getElementById('root')).render();