// ============================================================ // Land of Festival — main App // ============================================================ /* global React, ReactDOM, LOF_Parsers, IndiaMap, Header, SidePanel, Timeline, MW, MONTHS_FULL, MONTHS_SHORT */ const { useState, useEffect, useMemo, useRef } = React; function App() { const [festivals, setFestivals] = useState(null); const [topo, setTopo] = useState(null); const [loadError, setLoadError] = useState(null); useEffect(() => { Promise.all([ fetch('data/festivals.json').then((r) => r.json()), fetch('data/india_states.topo.json').then((r) => r.json()), ]) .then(([f, t]) => { setFestivals(f); setTopo(t); }) .catch((e) => { console.error(e); setLoadError(e.message); }); }, []); const processed = useMemo(() => { if (!festivals) return null; return festivals.map((f, i) => { const parsed = LOF_Parsers.parseTime(f.Typical_Time_of_Year); const { pan, list } = LOF_Parsers.splitStates(f.Primary_State_or_UT_or_Region); return { id: 'f' + i, name: f.Festival_Name, level: f.Festival_Level, states: list, pan, zone: f.Zone, type: f.Festival_Type, timeRaw: f.Typical_Time_of_Year, timeLabel: parsed.label, lunar: parsed.lunar, weeks: parsed.weeks, description: f.Cultural_Scientific_Description, }; }); }, [festivals]); const festivalsByWeek = useMemo(() => { if (!processed) return {}; const counts = {}; processed.forEach((f) => { f.weeks.forEach((w) => { counts[w] = (counts[w] || 0) + 1; }); }); return counts; }, [processed]); const [week, setWeek] = useState(() => { try { const v = +localStorage.getItem('lof_week'); if (v >= 1 && v <= 52) return v; } catch (e) {} return 1; }); useEffect(() => { try { localStorage.setItem('lof_week', String(week)); } catch (e) {} }, [week]); const [hoveredPin, setHoveredPin] = useState(null); const [selectedPin, setSelectedPin] = useState(null); const prevWeek = useRef(week); useEffect(() => { if (prevWeek.current !== week) { setSelectedPin(null); prevWeek.current = week; } }, [week]); const weekFestivals = useMemo(() => { if (!processed) return []; return processed.filter((f) => f.weeks.has(week)); }, [processed, week]); const pins = useMemo(() => { const out = []; weekFestivals.forEach((f) => { if (f.pan) { out.push({ id: f.id, festival: f, pan: true }); } else { f.states.forEach((st) => { out.push({ id: f.id, state: st, festival: f }); }); } }); return out; }, [weekFestivals]); const fallbackFacts = useMemo(() => [ 'India has more public-facing festivals than calendar days. The "official holidays" you grew up with are a tiny island in a much wider sea.', 'Almost every village in rural Maharashtra holds an annual jatra — a fair for the local deity that doubles as a market, social gathering, and matchmaking event.', 'Many festivals tracked agricultural science long before modern almanacs: when to sow, when to harvest, when to let the soil rest, when the rivers would rise.', 'Tribal festivals across the Northeast — Wangala, Sekrenyi, Hornbill, Ali-Ai-Ligang — preserve agrarian knowledge that has survived for centuries outside the mainstream calendar.', 'A "school holiday" is a poor proxy for cultural memory. The festivals that stop being celebrated are usually the ones that stop being remembered.', ], []); const factPool = useMemo(() => { if (!processed) return fallbackFacts; const local = weekFestivals .map((f) => `${f.name} — ${f.description}`) .filter(Boolean); return local.length > 0 ? local : fallbackFacts; }, [weekFestivals, processed, fallbackFacts]); const [factIdx, setFactIdx] = useState(0); useEffect(() => { setFactIdx(0); }, [week]); useEffect(() => { if (factPool.length <= 1) return; const id = setInterval(() => setFactIdx((i) => (i + 1) % factPool.length), 7000); return () => clearInterval(id); }, [factPool]); const didYouKnow = factPool[factIdx % factPool.length] || ''; const mapHostRef = useRef(null); const [mapBox, setMapBox] = useState({ w: 600, h: 500 }); useEffect(() => { function measure() { const el = mapHostRef.current; if (!el) return; const r = el.getBoundingClientRect(); const w = Math.max(280, r.width); const h = Math.max(280, r.height); setMapBox({ w, h }); } measure(); const t = setTimeout(measure, 80); window.addEventListener('resize', measure); return () => { window.removeEventListener('resize', measure); clearTimeout(t); }; }, [processed, topo]); if (loadError) { return (

Could not load data: {loadError}

); } if (!processed || !topo) { return ; } return (
); } function MapDecor({ tricolor, week }) { return ( <> {[[0,0],[100,0],[0,100],[100,100]].map(([x,y], i) => ( ))} {tricolor && (
{week === 4 ? 'Republic Day — 26 January' : 'Independence Day — 15 August'}
)} ); } function MapLegend({ weekFestivals, tricolor }) { const stateSet = new Set(); let pan = false; weekFestivals.forEach((f) => { if (f.pan) pan = true; f.states.forEach((s) => stateSet.add(s)); }); if (tricolor) return null; return (
Lit this week
{stateSet.size}{pan ? '+' : ''} {stateSet.size === 1 ? 'state' : 'states'}{pan ? ', and the whole country' : ''}
); } function LoadingScreen() { return (
Land of Festival
उत्सवों की भूमि …
); } ReactDOM.createRoot(document.getElementById('root')).render();