import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react' import StartupMap from './components/StartupMap' import Sidebar from './components/Sidebar' import EntityDetail from './components/EntityDetail' import StatsBar from './components/StatsBar' import SearchBar from './components/SearchBar' import AnalyticsPanel from './components/AnalyticsPanel' import MLInsightsPanel from './components/MLInsightsPanel' import ChatWidget from './components/ChatWidget' import ErrorBoundary from './components/ErrorBoundary' const INITIAL_FILTERS = { entity_type: [], sector: [], stage: [], search: '', dpiit_only: false, dpiit_category: [], business_model: [], unicorn_status: [], is_women_led: false, is_rural_impact: false, is_campus_startup: false, nsa_winner: false, state: '', city: '', } const STATE_COORDS = { 'Karnataka': { lng: 75.7, lat: 15.3, zoom: 7 }, 'Maharashtra': { lng: 75.7, lat: 19.7, zoom: 6.5 }, 'Delhi': { lng: 77.1, lat: 28.6, zoom: 10 }, 'Tamil Nadu': { lng: 78.6, lat: 11.1, zoom: 7 }, 'Telangana': { lng: 79.0, lat: 17.4, zoom: 8 }, 'Gujarat': { lng: 72.0, lat: 22.3, zoom: 7 }, 'Kerala': { lng: 76.3, lat: 10.8, zoom: 8 }, 'Rajasthan': { lng: 74.2, lat: 27.0, zoom: 6.5 }, 'Uttar Pradesh': { lng: 80.9, lat: 26.8, zoom: 6.5 }, 'West Bengal': { lng: 87.9, lat: 22.9, zoom: 7 }, 'Haryana': { lng: 76.1, lat: 29.0, zoom: 8 }, 'Punjab': { lng: 75.3, lat: 31.1, zoom: 8 }, 'Madhya Pradesh': { lng: 78.6, lat: 23.5, zoom: 6.5 }, 'Bihar': { lng: 85.3, lat: 25.6, zoom: 7 }, 'Odisha': { lng: 84.0, lat: 20.5, zoom: 7 }, 'Assam': { lng: 92.9, lat: 26.2, zoom: 7 }, 'Goa': { lng: 74.1, lat: 15.4, zoom: 10 }, 'Andhra Pradesh': { lng: 79.7, lat: 15.9, zoom: 7 }, } const ARRAY_KEYS = ['entity_type', 'sector', 'stage', 'dpiit_category', 'business_model', 'unicorn_status'] const BOOL_KEYS = ['dpiit_only', 'is_women_led', 'is_rural_impact', 'is_campus_startup', 'nsa_winner'] const STRING_KEYS = ['search', 'state', 'city'] function filtersFromURL() { const params = new URLSearchParams(window.location.search) const f = { ...INITIAL_FILTERS } ARRAY_KEYS.forEach(k => { const v = params.get(k); if (v) f[k] = v.split(',').filter(Boolean) }) BOOL_KEYS.forEach(k => { if (params.get(k) === 'true') f[k] = true }) STRING_KEYS.forEach(k => { const v = params.get(k); if (v) f[k] = v }) return f } function filtersToURL(filters) { const params = new URLSearchParams() ARRAY_KEYS.forEach(k => { if (filters[k]?.length > 0) params.set(k, filters[k].join(',')) }) BOOL_KEYS.forEach(k => { if (filters[k]) params.set(k, 'true') }) STRING_KEYS.forEach(k => { if (filters[k]) params.set(k, filters[k]) }) const qs = params.toString() const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname window.history.replaceState(null, '', url) } function useIsMobile() { const [isMobile, setIsMobile] = useState(false) useEffect(() => { const check = () => setIsMobile(window.innerWidth < 768) check() window.addEventListener('resize', check) return () => window.removeEventListener('resize', check) }, []) return isMobile } function useMapData(filters, viewport, zoom, mapMode) { const [geojson, setGeojson] = useState(null) const [loading, setLoading] = useState(true) const [totalInViewport, setTotalInViewport] = useState(0) const abortRef = useRef(null) const debounceRef = useRef(null) const buildFilterParams = useCallback((params) => { if (filters.entity_type.length > 0) params.set('entity_type', filters.entity_type.join(',')) if (filters.sector.length > 0) params.set('sector', filters.sector.join(',')) if (filters.stage.length > 0) params.set('stage', filters.stage.join(',')) if (filters.dpiit_only) params.set('dpiit_only', 'true') if (filters.dpiit_category.length > 0) params.set('dpiit_category', filters.dpiit_category.join(',')) if (filters.business_model.length > 0) params.set('business_model', filters.business_model.join(',')) if (filters.unicorn_status.length > 0) params.set('unicorn_status', filters.unicorn_status.join(',')) if (filters.is_women_led) params.set('is_women_led', 'true') if (filters.is_rural_impact) params.set('is_rural_impact', 'true') if (filters.is_campus_startup) params.set('is_campus_startup', 'true') if (filters.nsa_winner) params.set('nsa_winner', 'true') if (filters.search) params.set('search', filters.search) if (filters.state) params.set('state', filters.state) return params }, [filters]) const fetchData = useCallback(async () => { if (abortRef.current) abortRef.current.abort() const controller = new AbortController() abortRef.current = controller setLoading(true) try { const params = new URLSearchParams() params.set('min_lng', viewport.min_lng) params.set('max_lng', viewport.max_lng) params.set('min_lat', viewport.min_lat) params.set('max_lat', viewport.max_lat) params.set('zoom', zoom) buildFilterParams(params) const endpoint = mapMode === 'heatmap' ? `/api/entities/geojson?${params}&max_features=5000` : `/api/entities/clusters?${params}` const resp = await fetch(endpoint, { signal: controller.signal }) const data = await resp.json() setGeojson(data) setTotalInViewport(data.total_count || data.features?.length || 0) } catch (err) { if (err.name !== 'AbortError') console.error('Failed to fetch data:', err) } setLoading(false) }, [viewport, zoom, filters, mapMode, buildFilterParams]) useEffect(() => { clearTimeout(debounceRef.current) debounceRef.current = setTimeout(() => { fetchData() }, 200) return () => clearTimeout(debounceRef.current) }, [fetchData]) return { geojson, loading, totalInViewport, fetchData } } function useViewportSummary(viewport, filters) { const [viewportSummary, setViewportSummary] = useState(null) const fetchViewportSummary = useCallback(async () => { try { const params = new URLSearchParams() params.set('min_lng', viewport.min_lng) params.set('max_lng', viewport.max_lng) params.set('min_lat', viewport.min_lat) params.set('max_lat', viewport.max_lat) if (filters.entity_type.length > 0) params.set('entity_type', filters.entity_type.join(',')) const resp = await fetch(`/api/entities/viewport/summary?${params}`) const data = await resp.json() setViewportSummary(data) } catch (err) { console.error('Viewport summary failed:', err) } }, [viewport, filters.entity_type]) useEffect(() => { fetchViewportSummary() }, [fetchViewportSummary]) return viewportSummary } function useFacets(filters) { const [facets, setFacets] = useState(null) const fetchFacets = useCallback(async () => { try { const params = new URLSearchParams() if (filters.entity_type.length > 0) params.set('entity_type', filters.entity_type.join(',')) if (filters.sector.length > 0) params.set('sector', filters.sector.join(',')) if (filters.search) params.set('search', filters.search) const resp = await fetch(`/api/entities/facets?${params}`) const data = await resp.json() setFacets(data) } catch (err) { console.error('Failed to fetch facets:', err) } }, [filters]) useEffect(() => { fetchFacets() }, [fetchFacets]) return facets } export default function App() { const [filters, setFilters] = useState(() => filtersFromURL()) const [viewport, setViewport] = useState({ min_lng: 68, max_lng: 97, min_lat: 6, max_lat: 37 }) const [zoom, setZoom] = useState(4.5) const [mapMode, setMapMode] = useState('clusters') const [selectedEntity, setSelectedEntity] = useState(null) const [sidebarOpen, setSidebarOpen] = useState(false) const [analyticsOpen, setAnalyticsOpen] = useState(false) const [mlInsightsOpen, setMlInsightsOpen] = useState(false) const [chatOpen, setChatOpen] = useState(false) const [flyTo, setFlyTo] = useState(null) const [nearMeLoading, setNearMeLoading] = useState(false) const [exporting, setExporting] = useState(false) const [overview, setOverview] = useState(null) const isMobile = useIsMobile() const { geojson, loading, totalInViewport } = useMapData(filters, viewport, zoom, mapMode) const viewportSummary = useViewportSummary(viewport, filters) const facets = useFacets(filters) useEffect(() => { if (!isMobile) setSidebarOpen(true) }, [isMobile]) useEffect(() => { filtersToURL(filters) }, [filters]) useEffect(() => { fetch('/api/entities/analytics/overview') .then(r => r.json()) .then(setOverview) .catch(console.error) }, []) const handleFilterChange = useCallback((key, value) => { setFilters(prev => ({ ...prev, [key]: value })) if (key === 'state' && value && STATE_COORDS[value]) { const c = STATE_COORDS[value] setFlyTo({ lng: c.lng, lat: c.lat, zoom: c.zoom }) } else if (key === 'state' && !value) { setFlyTo({ lng: 78.5, lat: 22.0, zoom: 4.5 }) } }, []) const handleResetFilters = useCallback(() => { setFilters(INITIAL_FILTERS) setFlyTo({ lng: 78.5, lat: 22.0, zoom: 4.5 }) }, []) const handleEntityClick = useCallback(async (slug) => { try { const resp = await fetch(`/api/entities/detail/${slug}`) const data = await resp.json() setSelectedEntity(data) if (data.latitude && data.longitude) { setFlyTo({ lng: data.longitude, lat: data.latitude, zoom: 14 }) } if (window.innerWidth < 768) setSidebarOpen(false) } catch (err) { console.error('Failed to fetch entity:', err) } }, []) const handleViewportChange = useCallback((vp, z) => { setViewport(vp) setZoom(z || 4.5) }, []) const handleFlyToConsumed = useCallback(() => setFlyTo(null), []) const handleNearMe = useCallback(() => { if (!navigator.geolocation) { alert('Geolocation not supported by your browser.'); return } setNearMeLoading(true) navigator.geolocation.getCurrentPosition( (pos) => { setFlyTo({ lng: pos.coords.longitude, lat: pos.coords.latitude, zoom: 12 }) setNearMeLoading(false) }, () => { alert('Could not get your location. Please allow location access.') setNearMeLoading(false) }, { enableHighAccuracy: false, timeout: 8000 } ) }, []) const handleExport = useCallback(async (format = 'csv') => { setExporting(true) try { const params = new URLSearchParams() params.set('min_lng', viewport.min_lng); params.set('max_lng', viewport.max_lng) params.set('min_lat', viewport.min_lat); params.set('max_lat', viewport.max_lat) ARRAY_KEYS.forEach(k => { if (filters[k]?.length > 0) params.set(k, filters[k].join(',')) }) BOOL_KEYS.forEach(k => { if (filters[k]) params.set(k, 'true') }) STRING_KEYS.forEach(k => { if (filters[k]) params.set(k, filters[k]) }) params.set('format', format) const resp = await fetch(`/api/entities/export?${params}`) if (!resp.ok) throw new Error('Export failed') const blob = await resp.blob() const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url; a.download = `bharat-tech-atlas-export.${format}` document.body.appendChild(a); a.click(); document.body.removeChild(a) URL.revokeObjectURL(url) } catch (err) { console.error('Export failed:', err) alert('Export failed. Try a smaller viewport or fewer filters.') } setExporting(false) }, [viewport, filters]) const handleShareLink = useCallback(() => { navigator.clipboard.writeText(window.location.href) .then(() => alert('Link copied to clipboard!')) .catch(() => alert(window.location.href)) }, []) const featureCount = geojson?.features?.length || 0 const displayCount = viewportSummary?.count || totalInViewport || featureCount return (
{/* Top Bar */}
handleFilterChange('search', v)} onEntitySelect={(entity) => handleEntityClick(entity.slug)} />
{[{id:'clusters',label:'β­•',title:'Clusters'},{id:'points',label:'πŸ“',title:'Points'},{id:'heatmap',label:'πŸ”₯',title:'Heatmap'}].map(mode => ( ))}
{/* Data Disclaimer */} {overview && (

Curated dataset of {overview.total_entities?.toLocaleString()} mapped entities. India has 2.23 lakh+ DPIIT-registered startups and 1.02 lakh+ women-led ventures. Β· Source: DPIIT, Tracxn, Crunchbase

)} {sidebarOpen && ( <> {isMobile &&
setSidebarOpen(false)} />} setSidebarOpen(false)} isMobile={isMobile} /> )} {selectedEntity && ( setSelectedEntity(null)} onEntityClick={handleEntityClick} isMobile={isMobile} /> )} {analyticsOpen && setAnalyticsOpen(false)} />} {mlInsightsOpen && setMlInsightsOpen(false)} onEntityClick={handleEntityClick} />} {chatOpen && setChatOpen(false)} />} {/* Mobile: floating buttons */} {isMobile && !analyticsOpen && !mlInsightsOpen && !chatOpen && !sidebarOpen && !selectedEntity && (
)}
) }