import React, { useRef, useCallback, useState, useEffect, useMemo } from 'react' import Map, { Source, Layer, NavigationControl, Popup } from 'react-map-gl/maplibre' import 'maplibre-gl/dist/maplibre-gl.css' const MAP_STYLE = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" const ENTITY_COLORS = { startup: '#3B82F6', sme: '#10B981', college_ecell: '#FBBF24', incubator: '#A855F7', accelerator: '#EC4899', coworking: '#14B8A6', investor: '#EF4444', } const ENTITY_LABELS = { startup: 'πŸš€ Startup', sme: '🏒 SME', college_ecell: 'πŸŽ“ E-Cell', incubator: 'πŸ§ͺ Incubator', accelerator: '⚑ Accelerator', } // ─── Safe social icons (React elements, not raw HTML) ────────────────────────── const SafeSocialIcon = ({ icon }) => { const icons = { linkedin: πŸ’Ό, twitter: 𝕏, instagram: πŸ“Έ, facebook: πŸ“˜, website: 🌐, google: πŸ”, } return icons[icon] || πŸ”— } // ─── URL validation helper ─────────────────────────────────────────────────── function isValidHttpUrl(url) { if (!url || typeof url !== 'string') return false try { const u = new URL(url) return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false } } function sanitizeUrl(url) { if (!url) return '' // Remove javascript: and data: prefixes const lower = url.trim().toLowerCase() if (lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('vbscript:')) { return '' } return url } function SocialLinks({ entity }) { const links = [] const name = entity.name if (entity.linkedin_url && isValidHttpUrl(entity.linkedin_url)) links.push({ name: 'LinkedIn', url: sanitizeUrl(entity.linkedin_url), icon: 'linkedin', color: '#0A66C2' }) if (entity.twitter_url && isValidHttpUrl(entity.twitter_url)) links.push({ name: 'X (Twitter)', url: sanitizeUrl(entity.twitter_url), icon: 'twitter', color: '#000000' }) if (entity.instagram_url && isValidHttpUrl(entity.instagram_url)) links.push({ name: 'Instagram', url: sanitizeUrl(entity.instagram_url), icon: 'instagram', color: '#E4405F' }) if (entity.facebook_url && isValidHttpUrl(entity.facebook_url)) links.push({ name: 'Facebook', url: sanitizeUrl(entity.facebook_url), icon: 'facebook', color: '#1877F2' }) if (entity.website && isValidHttpUrl(entity.website)) links.push({ name: 'Website', url: sanitizeUrl(entity.website), icon: 'website', color: '#6366F1' }) // Always add search links even if no stored URL const encName = encodeURIComponent(name) const slug = name.toLowerCase().replace(/\s+/g, '-').replace('.', '') if (!entity.linkedin_url) links.push({ name: 'LinkedIn', url: `https://www.linkedin.com/search/results/companies/?keywords=${encName}`, icon: 'linkedin', color: '#0A66C2', search: true }) if (!entity.twitter_url) links.push({ name: 'X', url: `https://x.com/search?q=${encName}&src=typed_query`, icon: 'twitter', color: '#000000', search: true }) if (!entity.instagram_url) links.push({ name: 'Instagram', url: `https://www.instagram.com/${slug}`, icon: 'instagram', color: '#E4405F', search: true }) if (!entity.website) links.push({ name: 'Google', url: `https://www.google.com/search?q=${encName}+company`, icon: 'google', color: '#4285F4', search: true }) links.push({ name: 'Crunchbase', url: `https://www.crunchbase.com/organization/${slug}`, icon: 'crunchbase', color: '#0288D1', search: true }) links.push({ name: 'Tracxn', url: `https://tracxn.com/d/companies/${slug}/`, icon: 'tracxn', color: '#FF6F00', search: true }) return (
{links.filter(l => isValidHttpUrl(l.url)).map(l => ( {l.name} {l.search && β†—} ))}
) } function PopupContent({ properties, onDetailClick }) { const p = properties const color = ENTITY_COLORS[p.entity_type] || '#64748B' const label = ENTITY_LABELS[p.entity_type] || p.entity_type let sectors = p.sectors if (typeof sectors === 'string') { try { sectors = JSON.parse(sectors) } catch { sectors = [] } } return (

{p.name}

πŸ“ {p.city}, {p.state}

{label}
{p.description &&

{p.description}

}
{p.dpiit_recognized && βœ… Verified} {p.unicorn_status === 'unicorn' && πŸ¦„ Unicorn} {p.is_women_led && πŸ‘© Women-led} {p.nsa_winner && πŸ† NSA}
{(Array.isArray(sectors) ? sectors : []).slice(0, 4).map(s => ( {s} ))}
{p.founded_year && πŸ“… {p.founded_year}} {p.linkedin_team_size && πŸ‘₯ {p.linkedin_team_size}} {p.funding_display && p.funding_display !== 'Bootstrapped' && πŸ’° {p.funding_display}}
{/* Social Media Links */} {/* AI Analysis button */}
πŸ€– AI Analysis πŸ”— All Profiles
) } export default function StartupMap({ geojson, onViewportChange, onEntityClick, loading, mapMode, zoom: currentZoom, flyTo, onFlyToConsumed }) { const mapRef = useRef(null) const [popup, setPopup] = useState(null) useEffect(() => { if (flyTo && mapRef.current) { mapRef.current.flyTo({ center: [flyTo.lng, flyTo.lat], zoom: flyTo.zoom || 8, duration: 1800, essential: true, curve: 1.42, }) onFlyToConsumed?.() } }, [flyTo, onFlyToConsumed]) const onMoveEnd = useCallback((evt) => { const map = evt.target const bounds = map.getBounds() const z = map.getZoom() onViewportChange({ min_lng: bounds.getWest(), max_lng: bounds.getEast(), min_lat: bounds.getSouth(), max_lat: bounds.getNorth(), }, z) }, [onViewportChange]) const isClusterMode = useMemo(() => geojson?.cluster_mode === true, [geojson]) const onMapClick = useCallback((evt) => { const pf = evt.target.queryRenderedFeatures(evt.point, { layers: ['entity-points', 'entity-icons'] }) if (pf.length > 0) { const feature = pf[0] const props = feature.properties let sectors = props.sectors if (typeof sectors === 'string') { try { sectors = JSON.parse(sectors) } catch { sectors = [] } } setPopup({ longitude: feature.geometry.coordinates[0], latitude: feature.geometry.coordinates[1], properties: { ...props, sectors }, isCluster: false, }) return } const cf = evt.target.queryRenderedFeatures(evt.point, { layers: ['server-clusters'] }) if (cf.length > 0) { const cluster = cf[0] const targetZoom = Math.min((cluster.properties.expansion_zoom || currentZoom + 2), 18) evt.target.flyTo({ center: cluster.geometry.coordinates, zoom: targetZoom, duration: 800 }) return } const ccf = evt.target.queryRenderedFeatures(evt.point, { layers: ['client-clusters'] }) if (ccf.length > 0) { const src = evt.target.getSource('entities') if (src?.getClusterExpansionZoom) { src.getClusterExpansionZoom(ccf[0].properties.cluster_id, (err, z) => { if (!err) evt.target.flyTo({ center: ccf[0].geometry.coordinates, zoom: z + 1, duration: 800 }) }) } } }, [currentZoom]) const onMouseEnter = useCallback(() => { if (mapRef.current) mapRef.current.getCanvas().style.cursor = 'pointer' }, []) const onMouseLeave = useCallback(() => { if (mapRef.current) mapRef.current.getCanvas().style.cursor = '' }, []) const showHeatmap = mapMode === 'heatmap' const showClusters = mapMode === 'clusters' const showPoints = mapMode === 'points' const { clusterFeatures, pointFeatures } = useMemo(() => { if (!geojson?.features) return { clusterFeatures: null, pointFeatures: null } if (isClusterMode) return { clusterFeatures: { type: 'FeatureCollection', features: geojson.features }, pointFeatures: null } return { clusterFeatures: null, pointFeatures: geojson } }, [geojson, isClusterMode]) const interactiveLayers = useMemo(() => { const l = ['entity-points', 'entity-icons'] if (isClusterMode) l.push('server-clusters') if (showPoints) l.push('client-clusters') return l }, [isClusterMode, showPoints]) return ( {showClusters && clusterFeatures && ( )} {pointFeatures && (showClusters || showPoints || showHeatmap) && ( {showPoints && (<> )} {showHeatmap && ( )} =', ['zoom'], 10]]} layout={{ 'text-field': ['match', ['get', 'entity_type'], 'startup', 'πŸš€', 'sme', '🏒', 'college_ecell', 'πŸŽ“', 'incubator', 'πŸ§ͺ', 'accelerator', '⚑', 'πŸ“'], 'text-size': ['interpolate', ['linear'], ['zoom'], 10, 12, 14, 18, 18, 24], 'text-allow-overlap': false, 'text-ignore-placement': false, 'symbol-sort-key': ['case', ['==', ['get', 'unicorn_status'], 'unicorn'], 0, ['==', ['get', 'entity_type'], 'incubator'], 1, 5], }} paint={{ 'text-opacity': ['interpolate', ['linear'], ['zoom'], 10, 0, 11, 0.8, 13, 1], }} /> =', ['zoom'], 13]]} layout={{ 'text-field': ['get', 'name'], 'text-font': ['Open Sans Semibold'], 'text-size': 11, 'text-offset': [0, 1.6], 'text-anchor': 'top', 'text-max-width': 10, }} paint={{ 'text-color': '#E2E8F0', 'text-halo-color': '#0F172A', 'text-halo-width': 2 }} /> )} {popup && !popup.isCluster && ( setPopup(null)} closeOnClick={false} maxWidth="400px" offset={15}> { onEntityClick(popup.properties.slug); setPopup(null) }} /> )} {loading && (
Loading…
)} {!geojson && loading && (
{[...Array(3)].map((_, i) => (
))}

Loading startup data…

)} ) }