import React, { useState, useCallback, useRef, useEffect, memo, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { MapContainer, TileLayer, Polygon, Polyline, CircleMarker, Circle, Tooltip, useMap, useMapEvents, } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { BarChart, Bar, XAxis, YAxis, Tooltip as RechartsTooltip, ResponsiveContainer, CartesianGrid, Cell, } from 'recharts'; import { CloudLightning, Wind, Zap, Droplets, Filter, X, ChevronRight, Home, AlertTriangle, TrendingUp, Users, Calendar, MapPin, Flame, Snowflake, Clock, ArrowRight, Eye, UserPlus, Loader2, DollarSign, Target, BarChart2, Map, Pencil, Sun, Cloud, CloudRain, CloudDrizzle, CheckCircle, XCircle, CalendarRange, Search, Crosshair, CloudSnow, CloudHail, Navigation, } from 'lucide-react'; import { toast } from 'sonner'; import { useTheme } from '../context/ThemeContext'; import { useMockStore } from '../data/mockStore'; import { useStormEvents, conversionWindow } from '../hooks/useStormEvents'; import { useStormAttribution } from '../hooks/useStormAttribution'; import { useStormForecast } from '../hooks/useStormForecast'; import ZoneAssignmentModal from '../components/storm/ZoneAssignmentModal'; import { useAuth } from '../context/AuthContext'; // ─── Constants ─────────────────────────────────────────────────────────────── const PLANO_CENTER = [33.055, -96.752]; const DEFAULT_ZOOM = 11; const SEVERITY_STYLES = { severe: { fill: '#DC2626', stroke: '#991B1B', fillOpacity: 0.28, weight: 2.5, text: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-500/10', border: 'border-red-200 dark:border-red-500/20', label: 'Severe', dot: '#DC2626' }, significant: { fill: '#EF4444', stroke: '#DC2626', fillOpacity: 0.22, weight: 2, text: 'text-red-500 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-500/10', border: 'border-red-200 dark:border-red-500/20', label: 'Significant', dot: '#EF4444' }, moderate: { fill: '#F97316', stroke: '#EA580C', fillOpacity: 0.20, weight: 2, text: 'text-orange-500 dark:text-orange-400', bg: 'bg-orange-50 dark:bg-orange-500/10', border: 'border-orange-200 dark:border-orange-500/20', label: 'Moderate', dot: '#F97316' }, trace: { fill: '#EAB308', stroke: '#CA8A04', fillOpacity: 0.15, weight: 1.5, text: 'text-yellow-600 dark:text-yellow-400', bg: 'bg-yellow-50 dark:bg-yellow-500/10', border: 'border-yellow-200 dark:border-yellow-500/20', label: 'Trace', dot: '#EAB308' }, }; const TYPE_ICONS = { hail: CloudHail, wind: Wind, tornado: Zap, flood: Droplets, rain: CloudRain, snow: CloudSnow, ice: CloudSnow, thunderstorm: CloudLightning, }; const TYPE_LABELS = { hail: 'Hail', wind: 'Wind', tornado: 'Tornado', flood: 'Flood', rain: 'Rain', snow: 'Snow', ice: 'Ice Storm', thunderstorm: 'Thunderstorm', }; const FORECAST_ICONS = { 1000: Sun, 1100: Sun, 1001: Cloud, 1101: Cloud, 1102: Cloud, 4000: CloudDrizzle, 4200: CloudDrizzle, 4001: CloudRain, 4201: CloudRain, 8000: CloudLightning, 7000: Snowflake, 7101: Snowflake, 7102: Snowflake, 5000: Snowflake, 5100: Snowflake, 5101: Snowflake, }; function scoreColor(score) { if (score >= 65) return '#DC2626'; if (score >= 40) return '#F97316'; if (score >= 20) return '#EAB308'; return '#10B981'; } function haversine(lat1, lon1, lat2, lon2) { const R = 3958.8; const toRad = d => d * Math.PI / 180; const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function polygonCentroid(polygon) { const n = polygon.length; return [ polygon.reduce((s, p) => s + p[0], 0) / n, polygon.reduce((s, p) => s + p[1], 0) / n, ]; } const MAP_STYLES = ` .storm-intel-map .leaflet-container { background: #e8e0d8; font-family: system-ui, sans-serif; } .storm-tooltip { background: white !important; border: 1px solid rgba(0,0,0,0.08) !important; border-radius: 10px !important; box-shadow: 0 4px 14px rgba(0,0,0,0.12) !important; padding: 8px 10px !important; pointer-events: none !important; } .storm-tooltip::before { display: none !important; } .draw-cursor .leaflet-container { cursor: crosshair !important; } `; // ─── Map sub-components ─────────────────────────────────────────────────────── const MapFlyTo = ({ storm, flyCount }) => { const map = useMap(); const prevCount = useRef(0); useEffect(() => { const invalidate = () => map.invalidateSize({ animate: false }); const t1 = setTimeout(invalidate, 60); const t2 = setTimeout(invalidate, 340); window.addEventListener('resize', invalidate); return () => { clearTimeout(t1); clearTimeout(t2); window.removeEventListener('resize', invalidate); }; }, [map]); useEffect(() => { if (!storm?.polygon?.length || flyCount === prevCount.current) return; prevCount.current = flyCount; try { const bounds = L.latLngBounds(storm.polygon); map.fitBounds(bounds, { padding: [60, 60], maxZoom: 14, animate: true, duration: 0.7 }); } catch { /* malformed polygon */ } }, [storm, flyCount, map]); return null; }; const ZoneLayer = ({ zones }) => { const map = useMap(); if (!zones?.length) return null; const flyToZone = (polygon) => { try { const bounds = L.latLngBounds(polygon); map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15, animate: true, duration: 0.7 }); } catch { /* malformed polygon */ } }; return zones.map(zone => { if (!zone.polygon?.length) return null; const centroid = zone.polygon.reduce( (acc, pt) => [acc[0] + pt[0] / zone.polygon.length, acc[1] + pt[1] / zone.polygon.length], [0, 0] ); return ( flyToZone(zone.polygon) }} >

{zone.name}

{zone.assignedAgentName} · {zone.status === 'completed' ? 'Completed' : 'Active'}

{/* Agent initials circle at centroid — also clickable */} flyToZone(zone.polygon) }} >

{zone.name}

); }); }; const ZoneDrawingLayer = ({ draftPolygon, onAddPoint, onFinish }) => { const map = useMap(); useEffect(() => { map.doubleClickZoom.disable(); map.getContainer().classList.add('draw-cursor'); return () => { map.doubleClickZoom.enable(); map.getContainer().classList.remove('draw-cursor'); }; }, [map]); useMapEvents({ click(e) { onAddPoint([e.latlng.lat, e.latlng.lng]); }, dblclick() { if (draftPolygon.length >= 3) onFinish(); }, }); if (draftPolygon.length === 0) return null; const closedPath = draftPolygon.length > 1 ? [...draftPolygon, draftPolygon[0]] : draftPolygon; return ( <> {/* Fill preview */} {draftPolygon.length >= 3 && ( )} {/* Vertex dots */} {draftPolygon.map((pt, i) => ( ))} ); }; const PinDropLayer = ({ onDrop }) => { useMapEvents({ click: e => onDrop([e.latlng.lat, e.latlng.lng]) }); return null; }; const PIN_RADIUS_M = 8047; // 5 miles in metres const StormMap = memo(({ events, selectedId, pinnedIds, onSelect, theme, flyCount, selectedStorm, stormZones, isDrawing, draftPolygon, onAddPoint, onFinishPolygon, pinDropMode, pins, onDropPin, }) => { return (
{/* Storm polygons — only selected + pinned (or all if none active) */} {events.map(storm => { const sty = SEVERITY_STYLES[storm.severity] ?? SEVERITY_STYLES.trace; const isSelected = storm.id === selectedId; const isPinned = pinnedIds?.has(storm.id); return ( !isDrawing && !pinDropMode && onSelect(storm), mouseover: (e) => { e.target.setStyle({ fillOpacity: Math.min(sty.fillOpacity + 0.1, 0.4), weight: sty.weight + 1 }); }, mouseout: (e) => { if (storm.id === selectedId) return; e.target.setStyle({ fillOpacity: isPinned ? Math.min(sty.fillOpacity + 0.06, 0.36) : sty.fillOpacity, weight: isPinned ? sty.weight + 0.5 : sty.weight, }); }, }} >
{sty.label} Hail {storm.maxHailSize && ( {storm.maxHailSize}" )}

{storm.areaName}

{Math.floor((Date.now() - new Date(storm.date).getTime()) / 86400000)}d ago · ~{(storm.estimatedHomes ?? '?').toLocaleString()} homes

); })} {/* Canvasser zones layer */} {/* Pin drop circles */} {pins?.map((pin, i) => (

Pin {i + 1} · 5 mi radius

))} {/* Pin drop capture layer */} {pinDropMode && } {/* Drawing layer (active only when isDrawing) */} {isDrawing && ( )}
{/* Legend */}

Severity

{Object.entries(SEVERITY_STYLES).map(([key, sty]) => (
{sty.label}
))}
{/* Storm count badge */}
{events.length} storm{events.length !== 1 ? 's' : ''}
{/* Drawing instructions */} {isDrawing && (
{draftPolygon.length < 3 ? `Click to place vertices (${draftPolygon.length}/3 min)` : `Double-click to close polygon (${draftPolygon.length} pts)` }
)} {/* Pin drop instruction */} {pinDropMode && (
Click map to drop a pin (5 mi radius)
)}
); }); StormMap.displayName = 'StormMap'; // ─── Forecast strip ─────────────────────────────────────────────────────────── const ForecastStrip = ({ forecast, loading, alertDay, hasStormAlert }) => { if (loading) { return (
{Array.from({ length: 7 }).map((_, i) => (
))}
); } if (!forecast.length) return null; return (
{/* Storm alert banner */} {hasStormAlert && alertDay && (
High-Risk Storm Day {new Date(alertDay.date).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} {' — '}{alertDay.weatherLabel} · {alertDay.windGust} mph gusts · {alertDay.precipProb}% rain
Score {alertDay.stormScore}
)} {/* 7-day forecast row */}
7-Day {forecast.map((day, i) => { const FIcon = FORECAST_ICONS[day.weatherCode] ?? Cloud; const isAlert = day.stormScore >= 65; const isWarn = day.stormScore >= 40 && !isAlert; const dayLabel = i === 0 ? 'Today' : new Date(day.date).toLocaleDateString('en-US', { weekday: 'short' }); return (
{/* Day label — high contrast on alert cards */} {dayLabel} {/* Temp — white in dark mode on alert so it pops against the colored bg */} {day.tempF}° {/* Storm score bar */}
{day.precipProb}% rain
); })}
); }; // ─── Storm card ─────────────────────────────────────────────────────────────── const StormCard = ({ storm, isSelected, isPinned, onSelect, onTogglePin, distanceMiles }) => { const sty = SEVERITY_STYLES[storm.severity] ?? SEVERITY_STYLES.trace; const conv = conversionWindow(storm.date); const TypeIcon = TYPE_ICONS[storm.type] ?? CloudLightning; const days = Math.floor((Date.now() - new Date(storm.date).getTime()) / 86400000); const dateLabel = new Date(storm.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); return (

{dateLabel}

{sty.label} {storm.maxHailSize && ( {storm.maxHailSize}" )} {conv.label}
{storm.estimatedHomes && (
~{storm.estimatedHomes.toLocaleString()} homes affected
)}
); }; // ─── Detail drawer ──────────────────────────────────────────────────────────── const StormDetailDrawer = ({ storm, open, onClose, onFlyTo, onAssignZone, canManageZones }) => { const navigate = useNavigate(); const { theme } = useTheme(); if (!storm) return null; const sty = SEVERITY_STYLES[storm.severity] ?? SEVERITY_STYLES.trace; const conv = conversionWindow(storm.date); const TypeIcon = TYPE_ICONS[storm.type] ?? CloudLightning; const days = Math.floor((Date.now() - new Date(storm.date).getTime()) / 86400000); const dateLabel = new Date(storm.date).toLocaleDateString('en-US', { weekday: 'short', month: 'long', day: 'numeric', year: 'numeric' }); const handleCreateLead = () => { navigate('/emp/fa/leads/new', { state: { stormSource: { id: storm.id, date: storm.date, areaName: storm.areaName, maxHailSize: storm.maxHailSize, severity: storm.severity, }, }, }); }; return ( <> {open && ( )} {open && (

{sty.label} {storm.type === 'hail' ? 'Hail' : storm.type}

{dateLabel}

{storm.areaName}

{storm.county} County, TX

{conv.label === 'Hot Zone' && } {conv.label === 'Warm' && } {conv.label === 'Cold' && } {conv.label === 'Too Fresh' && }

{conv.label}

{conv.label === 'Hot Zone' && 'Peak canvassing window — highest conversion likelihood'} {conv.label === 'Warm' && 'Still worth canvassing — moderate conversion rate'} {conv.label === 'Cold' && 'Low conversion — storm damage assessment mostly done'} {conv.label === 'Too Fresh' && 'Too recent — wait for claims process to begin'}

{storm.maxHailSize && (

Max Hail

{storm.maxHailSize}"

{sty.label} category

)} {storm.estimatedHomes && (

Est. Homes

{storm.estimatedHomes.toLocaleString()}

in impact zone

)}

Days Ago

{days}

since impact

Source

{storm.source}

data origin

Intel

{storm.description}

{canManageZones ? ( ) : ( )}
)} ); }; // ─── Attribution view ──────────────────────────────────────────────────────── const SEVERITY_DOT = { severe: '#DC2626', significant: '#EF4444', moderate: '#F97316', trace: '#EAB308' }; const CustomTooltip = ({ active, payload, label }) => { if (!active || !payload?.length) return null; return (

{label}

{payload.map(p => (
{p.name}: {p.value}
))}
); }; const AttributionView = ({ events, sessionLeads }) => { const { theme } = useTheme(); const { summary, perStorm } = useStormAttribution(events, sessionLeads); const isDark = theme === 'dark'; const maxRevenue = Math.max(...perStorm.map(p => p.revenue), 1); const chartData = perStorm .filter(p => p.leadsCount > 0) .map(p => ({ name: (p.storm.areaName ?? 'Unknown').split('/')[0].trim().replace('Plano', '').trim() || p.storm.areaName, Leads: p.leadsCount, Closed: p.closedCount, stormId: p.storm.id, severity: p.storm.severity, })); const statCards = [ { label: 'Storm Leads', value: summary.totalLeads, icon: Users, color: '#3B82F6' }, { label: 'Closed', value: summary.closedLeads, icon: Target, color: '#10B981' }, { label: 'Conv. Rate', value: `${summary.conversionRate}%`, icon: TrendingUp, color: '#F59E0B' }, { label: 'Revenue', value: `$${(summary.totalRevenue / 1000).toFixed(0)}k`, icon: DollarSign, color: '#8B5CF6' }, { label: 'Pipeline', value: `$${(summary.pipelineValue / 1000).toFixed(0)}k`, icon: BarChart2, color: '#F97316' }, { label: 'Avg Deal', value: `$${(summary.avgDealSize / 1000).toFixed(1)}k`, icon: CloudLightning, color: '#EC4899' }, ]; return (
{statCards.map(({ label, value, icon: Icon, color }) => (

{label}

{value}

))}

Leads Generated Per Storm

} cursor={{ fill: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)' }} /> {chartData.map((entry, i) => ( ))}
Generated (by severity)
Closed

Storm Revenue Breakdown

{perStorm.map((item, idx) => { const sty = SEVERITY_STYLES[item.storm.severity] ?? SEVERITY_STYLES.trace; const revenuePercent = item.revenue > 0 ? Math.round((item.revenue / maxRevenue) * 100) : 0; const stormDate = item.storm.date ? new Date(item.storm.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'; return (
#{idx + 1}

{item.storm.areaName ?? 'Unknown Zone'}

{sty.label}

{stormDate}{item.storm.maxHailSize ? ` · ${item.storm.maxHailSize}"` : ''}

{item.revenue > 0 ? `$${item.revenue.toLocaleString()}` : '—'}

revenue

{[ { label: 'Leads', value: item.leadsCount, color: null }, { label: 'Closed', value: item.closedCount, color: '#10B981' }, { label: 'Appointed', value: item.appointedCount, color: '#F59E0B' }, { label: 'Conv %', value: `${item.conversionRate}%`, color: null }, { label: 'Pipeline', value: item.pipeline > 0 ? `$${(item.pipeline / 1000).toFixed(0)}k` : '—', color: '#F97316' }, ].map(({ label, value, color }) => (

{value}

{label}

))}
{item.revenue > 0 && (
)}
); })}
{perStorm.length > 0 && perStorm[0].revenue > 0 && (

Top Performing Zone

{perStorm[0].storm.areaName} generated ${perStorm[0].revenue.toLocaleString()} from {perStorm[0].closedCount} closed deal{perStorm[0].closedCount !== 1 ? 's' : ''} ({perStorm[0].conversionRate}% conversion). {perStorm[0].pipeline > 0 && ` $${(perStorm[0].pipeline / 1000).toFixed(0)}k more in pipeline.`}

)}
); }; // ─── Filter chip ────────────────────────────────────────────────────────────── const FilterChip = ({ active, onClick, children }) => ( ); // ─── Main page ──────────────────────────────────────────────────────────────── const StormIntelPage = () => { const { theme } = useTheme(); const { user } = useAuth(); const { leads: storeLeads = [], stormZones, addStormZone } = useMockStore(); const canManageZones = ['ADMIN', 'OWNER'].includes(user?.role); const [activeTab, setActiveTab] = useState('map'); const [dateRange, setDateRange] = useState('12m'); const [customStart, setCustomStart] = useState(''); const [customEnd, setCustomEnd] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [severityFilter, setSeverityFilter] = useState('all'); const [sortBy, setSortBy] = useState('date'); const [searchQuery, setSearchQuery] = useState(''); const [selectedStorm, setSelectedStorm] = useState(null); const [pinnedIds, setPinnedIds] = useState(new Set()); const [drawerOpen, setDrawerOpen] = useState(false); const [flyCount, setFlyCount] = useState(0); const [showFilters, setShowFilters] = useState(false); // Pin drop — up to 5 pins const [pinDropMode, setPinDropMode] = useState(false); const [pins, setPins] = useState([]); // array of [lat,lng] const MAX_PINS = 5; // Zone drawing state const [isDrawing, setIsDrawing] = useState(false); const [draftPolygon, setDraftPolygon] = useState([]); const [drawingLinkedStormId, setDrawingLinkedStormId] = useState(null); const [showZoneModal, setShowZoneModal] = useState(false); const { events, loading, error, totalHomes } = useStormEvents({ dateRange, typeFilter, severityFilter, sortBy, customStart, customEnd }); const { events: allEvents } = useStormEvents({ dateRange: '36m' }); const { forecast, loading: forecastLoading, alertDay, hasStormAlert } = useStormForecast(); // Text search filter const displayEvents = useMemo(() => { if (!searchQuery.trim()) return events; const q = searchQuery.toLowerCase(); return events.filter(e => e.areaName?.toLowerCase().includes(q) || e.type?.toLowerCase().includes(q) || e.severity?.toLowerCase().includes(q) || e.description?.toLowerCase().includes(q) ); }, [events, searchQuery]); // Events within 5 miles of ANY dropped pin const nearbyEvents = useMemo(() => { if (!pins.length) return []; const seen = new Set(); const results = []; for (const event of displayEvents) { if (!event.polygon?.length) continue; const [cLat, cLon] = polygonCentroid(event.polygon); const minDist = Math.min(...pins.map(p => haversine(p[0], p[1], cLat, cLon))); if (minDist <= 5 && !seen.has(event.id)) { seen.add(event.id); results.push({ ...event, _distanceMiles: minDist }); } } return results.sort((a, b) => a._distanceMiles - b._distanceMiles); }, [displayEvents, pins]); // Sidebar shows nearby events when pins exist, otherwise full filtered list const sidebarEvents = pins.length > 0 ? nearbyEvents : displayEvents; // Map shows: when pins exist → all nearby; else → selected+pinned only (or none until pins placed) const mapEvents = useMemo(() => { if (pins.length > 0) return nearbyEvents; if (!selectedStorm && pinnedIds.size === 0) return []; // nothing until pin placed return displayEvents.filter(e => pinnedIds.has(e.id) || e.id === selectedStorm?.id); }, [displayEvents, nearbyEvents, pins, selectedStorm, pinnedIds]); const handleDropPin = useCallback((latlng) => { setPins(prev => { if (prev.length >= MAX_PINS) return prev; // cap at 5 return [...prev, latlng]; }); setPinDropMode(false); }, []); const handleRemovePin = useCallback((idx) => { setPins(prev => prev.filter((_, i) => i !== idx)); }, []); const handleClearPins = useCallback(() => { setPins([]); setPinDropMode(false); }, []); const handleTogglePin = useCallback((id) => { setPinnedIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); // Map shows only selected + pinned events; if nothing active, show all const mapEvents = useMemo(() => { if (!selectedStorm && pinnedIds.size === 0) return events; return events.filter(e => pinnedIds.has(e.id) || e.id === selectedStorm?.id); }, [events, selectedStorm, pinnedIds]); const handleSelect = useCallback((storm) => { setSelectedStorm(storm); setDrawerOpen(true); setFlyCount(c => c + 1); }, []); const handleFlyTo = useCallback(() => { setFlyCount(c => c + 1); setDrawerOpen(false); }, []); const handleClose = useCallback(() => { setDrawerOpen(false); }, []); const handleStartDrawing = useCallback((linkedStormId = null) => { setIsDrawing(true); setDraftPolygon([]); setDrawingLinkedStormId(linkedStormId); setDrawerOpen(false); }, []); const handleAddPoint = useCallback((pt) => { setDraftPolygon(prev => [...prev, pt]); }, []); const handleFinishPolygon = useCallback(() => { if (draftPolygon.length < 3) { toast.error('Need at least 3 points to create a zone'); return; } setIsDrawing(false); setShowZoneModal(true); }, [draftPolygon]); const handleCancelDrawing = useCallback(() => { setIsDrawing(false); setDraftPolygon([]); setDrawingLinkedStormId(null); }, []); const handleSaveZone = useCallback((zoneData) => { addStormZone({ ...zoneData, polygon: draftPolygon }); setShowZoneModal(false); setDraftPolygon([]); setDrawingLinkedStormId(null); toast.success(`Zone "${zoneData.name}" saved and assigned to ${zoneData.assignedAgentName}`); }, [addStormZone, draftPolygon]); // Responsive map height const [mapHeight, setMapHeight] = useState('320px'); useEffect(() => { const update = () => { const w = window.innerWidth; setMapHeight(w < 640 ? '280px' : w < 768 ? '340px' : w < 1024 ? '400px' : '100%'); }; update(); window.addEventListener('resize', update); return () => window.removeEventListener('resize', update); }, []); return (
{/* ── Header ── */}

Storm Intel

Plano TX · Collin County

{/* Summary stats */}

{events.length}

Storms

{(totalHomes / 1000).toFixed(1)}k

Homes

{events.filter(e => conversionWindow(e.date).label === 'Hot Zone').length}

Hot Zones

{stormZones.length > 0 && ( <>

{stormZones.filter(z => z.status === 'active').length}

Active Zones

)}
{/* Draw Zone button — Admin/Owner only */} {canManageZones && activeTab === 'map' && !isDrawing && ( )} {/* Cancel drawing */} {isDrawing && ( )} {/* Tab switcher */}
{activeTab === 'map' && ( )}
{/* ── Filter bar — map tab only ── */} {activeTab === 'map' && (showFilters || true) && ( = 1024 ? '' : 'hidden lg:flex'}`} >
Range {[['1m', '1 Mo'], ['3m', '3 Mo'], ['6m', '6 Mo'], ['12m', '12 Mo'], ['18m', '18 Mo'], ['24m', '24 Mo'], ['36m', '36 Mo']].map(([val, label]) => ( setDateRange(val)}>{label} ))} setDateRange('custom')}> Custom
{/* Custom date inputs */} {dateRange === 'custom' && (
setCustomStart(e.target.value)} className="px-2.5 py-1.5 rounded-lg border border-zinc-200 dark:border-white/[0.08] bg-white dark:bg-zinc-800 text-[12px] text-zinc-800 dark:text-zinc-200 outline-none focus:ring-2 focus:ring-amber-500/40 focus:border-amber-400 transition-all" /> to setCustomEnd(e.target.value)} className="px-2.5 py-1.5 rounded-lg border border-zinc-200 dark:border-white/[0.08] bg-white dark:bg-zinc-800 text-[12px] text-zinc-800 dark:text-zinc-200 outline-none focus:ring-2 focus:ring-amber-500/40 focus:border-amber-400 transition-all" />
)}
Type {[['all','All'],['hail','Hail'],['tornado','Tornado'],['flood','Flood'],['wind','Wind'],['snow','Snow'],['rain','Rain']].map(([val, label]) => ( setTypeFilter(val)}>{label} ))}
Severity {[['all', 'All'], ['severe', 'Severe'], ['significant', 'Significant'], ['moderate', 'Moderate'], ['trace', 'Trace']].map(([val, label]) => ( setSeverityFilter(val)}>{label} ))}
Sort setSortBy('date')}>Newest setSortBy('severity')}>Severity
)} {/* ── Forecast strip — map tab only ── */} {activeTab === 'map' && ( )} {/* ── Attribution tab ── */} {activeTab === 'attribution' && ( )} {/* ── Map tab body ── */}
{/* Map */}
{loading ? (

Loading storm data...

) : error ? (

Failed to load storm data

{error}

) : pins.length === 0 ? ( /* Show base map with pin prompt overlay */ ) : mapEvents.length === 0 ? (

No storms within 5 miles of your pins

) : ( )}
{/* Storm list */}
{/* Sidebar header */}

{pins.length > 0 ? <>{nearbyEvents.length} Nearby{pinnedIds.size > 0 && · {pinnedIds.size} pinned} : <>{displayEvents.length} Event{displayEvents.length !== 1 ? 's' : ''}{pinnedIds.size > 0 && · {pinnedIds.size} pinned} }

{/* Pin drop toggle */} {pins.length > 0 && ( )} {pinnedIds.size > 0 && ( )} {selectedStorm && ( )}
{/* Search */}
setSearchQuery(e.target.value)} placeholder="Search events..." className="w-full pl-7 pr-7 py-1.5 text-[12px] rounded-lg border border-zinc-200 dark:border-white/[0.07] bg-zinc-50 dark:bg-zinc-800/60 text-zinc-800 dark:text-zinc-200 placeholder-zinc-400 outline-none focus:ring-2 focus:ring-amber-500/30 focus:border-amber-400 transition-all" /> {searchQuery && ( )}
{/* Active pins strip */} {pins.length > 0 && (
{pins.map((pin, i) => (
Pin {i+1}
))} {pins.length < MAX_PINS && ( )}
)} {/* Zone summary strip */} {stormZones.length > 0 && (

Canvasser Zones

{canManageZones && ( )}
{stormZones.slice(0, 4).map(zone => (
{zone.name} {zone.status === 'completed' && }
))} {stormZones.length > 4 && (
+{stormZones.length - 4}
)}
)}
{loading ? ( Array.from({ length: 4 }).map((_, i) => (
)) ) : pins.length === 0 ? ( /* No pins yet — prompt user to drop one */

Drop a pin to explore

Click the crosshair button and place a pin on the map to find storm events within 5 miles.

) : sidebarEvents.length === 0 ? (

No events within 5 miles of your pins

) : ( sidebarEvents.map(storm => ( )) )}
{/* Detail drawer */} {/* Zone assignment modal */} { setShowZoneModal(false); setDraftPolygon([]); setDrawingLinkedStormId(null); }} onSave={handleSaveZone} polygon={draftPolygon} linkedStormId={drawingLinkedStormId} events={allEvents} />
); }; export default StormIntelPage;