// TutorialOverlay - a lightweight guided tour. Dims the page, spotlights a // target element (located via a [data-tour="..."] selector), and shows a popover // with Back / Next / Close. Steps can request a view switch so the user sees // the real dashboard behind the overlay as each one is explained. const TOUR_STEPS = [ { target: null, title: 'Welcome to the Global Initiatives Atlas', body: 'This short tour walks you through the views, the search, and how to read each map. Use Next and Back to move around, or close the tour at any time — you can reopen it from the “Take a tour” button in the top bar.', }, { target: '[data-tour="tabs"]', placement: 'bottom', title: 'Switch between views', body: 'These tabs are the heart of the Atlas. The first three cover global initiatives; after the divider you’ll find India-specific maps and, last, the methodology. We’ll look at each in turn.', }, { view: 'value-chain', target: '[data-tour="tab-value-chain"]', placement: 'bottom', title: '01 · Energy Ecosystem Infographic', body: 'A schematic of the whole energy ecosystem. Nodes are colour-coded by pillar — click any node for a description, or use the legend cards to highlight pillar-specific segments.', }, { view: 'matrix', target: '[data-tour="tab-matrix"]', placement: 'bottom', title: '02 · Global Initiatives Heat Map', body: 'A table of which organizations work on each value-chain segment, by region — or by barrier (toggle the y-axis at the top). Use the \'heat-map\' view to see where programs and initiatives are focused.', }, { view: 'matrix', target: '[data-tour="matrix-toolbar"]', placement: 'bottom', title: 'Filter & highlight the heat map', body: 'Use “Show as heatmap” to colour each cell by how many initiatives it holds - thin and crowded areas stand out at a glance, and the colours respond to whatever filters are active. Just below, filter the columns by pillar (Energy access / Grid modernization) or by organisation type.', }, { view: 'matrix', target: '[data-tour="search"]', placement: 'bottom', title: 'Search organizations', body: 'Type any organization, topic, or region here. Matches appear in a panel on the right — click a result to highlight it in the current view, or double-click to open its full profile. Search works across the heat map, the networks, and the India views.', }, { view: 'network', target: '[data-tour="tab-network"]', placement: 'bottom', title: '03 · Collaboration Network', body: 'Who works with whom. The graph reveals well-connected actors, isolated initiatives, and partnership gaps across the system. Hover a node to see its partners; click for full details.', }, { view: 'india-value-chain', target: '[data-tour="tab-india-value-chain"]', placement: 'bottom', title: 'India-specific views', body: 'After the divider, the same lenses are scoped to India: a regional ecosystem map, players & initiatives, and a regional collaboration network', }, { view: 'methodology', target: '[data-tour="tab-methodology"]', placement: 'bottom', title: 'Methodology', body: 'How organizations are categorised across the Atlas, what the data does and does not cover, and the biases to keep in mind. The \'?\' bubbles throughout the tool link back to the relevant section in the Methodology page.', }, { target: '[data-tour="feedback"]', placement: 'top', title: 'Share feedback', body: 'Spotted something off, or have a suggestion? This opens a short form - your input helps keep the Atlas accurate and useful.', }, { target: null, title: 'You’re all set', body: 'That’s the tour. Explore the views, search for the organizations you care about, and reopen this guide any time from “Take a tour”.', }, ]; const POPOVER_WIDTH = 340; function TutorialOverlay({ activeView, setActiveView, onClose }) { const { useState, useEffect, useCallback } = React; const [stepIndex, setStepIndex] = useState(0); const [rect, setRect] = useState(null); const step = TOUR_STEPS[stepIndex]; const isFirst = stepIndex === 0; const isLast = stepIndex === TOUR_STEPS.length - 1; const goNext = useCallback(() => { setStepIndex((i) => (i < TOUR_STEPS.length - 1 ? i + 1 : i)); }, []); const goBack = useCallback(() => setStepIndex((i) => Math.max(0, i - 1)), []); // Switch to the view this step wants to show (so the real dashboard sits // behind the overlay). View change clears the measured rect until re-found. useEffect(() => { if (step.view && step.view !== activeView) { setRect(null); setActiveView(step.view); } }, [stepIndex]); // eslint-disable-line // Locate + measure the target. Polls a few frames because a view switch // remounts the workspace and the element may not be in the DOM yet. useEffect(() => { if (!step.target) { setRect(null); return; } let raf, tries = 0, cancelled = false; const measure = () => { const el = document.querySelector(step.target); if (el) { const r = el.getBoundingClientRect(); if (r.width || r.height) { el.scrollIntoView({ block: 'nearest', inline: 'nearest' }); if (!cancelled) setRect(el.getBoundingClientRect()); return; } } if (!cancelled && tries++ < 90) raf = requestAnimationFrame(measure); }; measure(); return () => { cancelled = true; if (raf) cancelAnimationFrame(raf); }; }, [stepIndex, activeView]); // eslint-disable-line // Keep the spotlight aligned if the user resizes or the layout scrolls. useEffect(() => { if (!step.target) return; const update = () => { const el = document.querySelector(step.target); if (el) setRect(el.getBoundingClientRect()); }; window.addEventListener('resize', update); window.addEventListener('scroll', update, true); return () => { window.removeEventListener('resize', update); window.removeEventListener('scroll', update, true); }; }, [stepIndex]); // eslint-disable-line // Keyboard navigation. useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); else if (e.key === 'ArrowRight') goNext(); else if (e.key === 'ArrowLeft') goBack(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose, goNext, goBack]); const hasSpotlight = !!(step.target && rect); // Popover position. Anchors by edge (top/bottom) so we don't need to know // the popover's own height. Centered when there's no target. let popStyle, popClass = 'tour-pop'; if (hasSpotlight) { const vw = window.innerWidth; const left = Math.min(Math.max(12, rect.left), Math.max(12, vw - POPOVER_WIDTH - 12)); if (step.placement === 'top') { popStyle = { bottom: (window.innerHeight - rect.top) + 14, left }; } else { popStyle = { top: rect.bottom + 14, left }; } } else { popClass += ' tour-pop--center'; popStyle = {}; } const spotStyle = hasSpotlight ? { top: rect.top - 6, left: rect.left - 6, width: rect.width + 12, height: rect.height + 12, } : null; return (
{step.body}