feat(storm-intel): configurable pin radius, fix blank map, 6mo default, remove mock polygons

- Map always renders (removed blank overlay when no nearby events)
- Pin radius is now user-configurable (default 5 mi, input shown in pin strip)
- Default date range changed to 6 months for faster initial load
- IEM fetch window narrowed to 6 months to match default
- MOCK_STORM_EVENTS cleared (IEM provides real NWS polygon data)
This commit is contained in:
Satyam-Rastogi
2026-05-19 00:47:43 +05:30
parent 1b7af0699d
commit a6d3997d07
3 changed files with 50 additions and 200 deletions
+48 -54
View File
@@ -242,12 +242,10 @@ const PinDropLayer = ({ onDrop }) => {
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,
pinDropMode, pins, onDropPin, pinRadius = 5,
}) => {
return (
<div className="relative w-full h-full storm-intel-map">
@@ -334,7 +332,7 @@ const StormMap = memo(({
<React.Fragment key={i}>
<Circle
center={pin}
radius={PIN_RADIUS_M}
radius={pinRadius * 1609.34}
pathOptions={{ color: '#8B5CF6', fillColor: '#8B5CF6', fillOpacity: 0.06, weight: 1.5, dashArray: '5 5' }}
/>
<CircleMarker
@@ -343,7 +341,7 @@ const StormMap = memo(({
pathOptions={{ color: '#fff', fillColor: '#8B5CF6', fillOpacity: 1, weight: 2 }}
>
<Tooltip direction="top" opacity={1} className="storm-tooltip" permanent={false}>
<p style={{ fontSize: 11, fontWeight: 700, color: '#18181b' }}>Pin {i + 1} · 5 mi radius</p>
<p style={{ fontSize: 11, fontWeight: 700, color: '#18181b' }}>Pin {i + 1} · {pinRadius} mi radius</p>
</Tooltip>
</CircleMarker>
</React.Fragment>
@@ -978,7 +976,7 @@ const StormIntelPage = () => {
const canManageZones = ['ADMIN', 'OWNER'].includes(user?.role);
const [activeTab, setActiveTab] = useState('map');
const [dateRange, setDateRange] = useState('12m');
const [dateRange, setDateRange] = useState('6m');
const [customStart, setCustomStart] = useState('');
const [customEnd, setCustomEnd] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
@@ -993,6 +991,7 @@ const StormIntelPage = () => {
// Pin drop — up to 5 pins
const [pinDropMode, setPinDropMode] = useState(false);
const [pins, setPins] = useState([]); // array of [lat,lng]
const [pinRadius, setPinRadius] = useState(5); // miles
const MAX_PINS = 5;
// Zone drawing state
@@ -1026,13 +1025,13 @@ const StormIntelPage = () => {
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)) {
if (minDist <= pinRadius && !seen.has(event.id)) {
seen.add(event.id);
results.push({ ...event, _distanceMiles: minDist });
}
}
return results.sort((a, b) => a._distanceMiles - b._distanceMiles);
}, [displayEvents, pins]);
}, [displayEvents, pins, pinRadius]);
// Sidebar shows nearby events when pins exist, otherwise full filtered list
const sidebarEvents = pins.length > 0 ? nearbyEvents : displayEvents;
@@ -1335,33 +1334,9 @@ const StormIntelPage = () => {
<p className="text-[13px] text-zinc-500 font-medium">Failed to load storm data</p>
<p className="text-[11px] text-zinc-400">{error}</p>
</div>
) : pins.length === 0 ? (
/* Show base map with pin prompt overlay */
<StormMap
events={[]}
selectedId={null}
pinnedIds={pinnedIds}
selectedStorm={null}
onSelect={handleSelect}
theme={theme}
flyCount={flyCount}
stormZones={stormZones}
isDrawing={false}
draftPolygon={[]}
onAddPoint={handleAddPoint}
onFinishPolygon={handleFinishPolygon}
pinDropMode={pinDropMode}
pins={pins}
onDropPin={handleDropPin}
/>
) : mapEvents.length === 0 ? (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-100 dark:bg-zinc-900 gap-2">
<CloudLightning size={28} className="text-zinc-300" />
<p className="text-[13px] text-zinc-400 font-medium">No storms within 5 miles of your pins</p>
</div>
) : (
<StormMap
events={mapEvents}
events={pins.length === 0 ? [] : mapEvents}
selectedId={selectedStorm?.id}
pinnedIds={pinnedIds}
selectedStorm={selectedStorm}
@@ -1376,6 +1351,7 @@ const StormIntelPage = () => {
pinDropMode={pinDropMode}
pins={pins}
onDropPin={handleDropPin}
pinRadius={pinRadius}
/>
)}
</div>
@@ -1386,7 +1362,7 @@ const StormIntelPage = () => {
<div className="px-4 py-3 border-b border-zinc-100 dark:border-white/[0.06] shrink-0 flex items-center justify-between gap-2">
<p className="text-[12px] font-black uppercase tracking-wider text-zinc-500 dark:text-zinc-400 min-w-0 truncate">
{pins.length > 0
? <>{nearbyEvents.length} Nearby{pinnedIds.size > 0 && <span className="ml-1 text-amber-500">· {pinnedIds.size} pinned</span>}</>
? <>{nearbyEvents.length} within {pinRadius} mi{pinnedIds.size > 0 && <span className="ml-1 text-amber-500">· {pinnedIds.size} pinned</span>}</>
: <>{displayEvents.length} Event{displayEvents.length !== 1 ? 's' : ''}{pinnedIds.size > 0 && <span className="ml-1 text-amber-500">· {pinnedIds.size} pinned</span>}</>
}
</p>
@@ -1395,7 +1371,7 @@ const StormIntelPage = () => {
<button
onClick={() => setPinDropMode(m => !m)}
disabled={pins.length >= MAX_PINS}
title={pins.length >= MAX_PINS ? 'Max 5 pins' : pinDropMode ? 'Cancel pin drop' : 'Drop a pin (5 mi radius)'}
title={pins.length >= MAX_PINS ? 'Max 5 pins' : pinDropMode ? 'Cancel pin drop' : `Drop a pin (${pinRadius} mi radius)`}
className={`p-1.5 rounded-lg border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
pinDropMode
? 'bg-violet-600 border-violet-700 text-white'
@@ -1446,24 +1422,42 @@ const StormIntelPage = () => {
{/* Active pins strip */}
{pins.length > 0 && (
<div className="px-3 py-2 border-b border-violet-100 dark:border-violet-500/15 bg-violet-50/60 dark:bg-violet-500/5 shrink-0 flex flex-wrap gap-1.5">
{pins.map((pin, i) => (
<div key={i} className="flex items-center gap-1 pl-2 pr-1 py-0.5 rounded-full bg-violet-100 dark:bg-violet-500/15 border border-violet-200 dark:border-violet-500/25">
<div className="w-2 h-2 rounded-full bg-violet-500 shrink-0" />
<span className="text-[10px] font-bold text-violet-700 dark:text-violet-300">Pin {i+1}</span>
<button onClick={() => handleRemovePin(i)} className="text-violet-400 hover:text-violet-600 ml-0.5">
<X size={10} />
<div className="px-3 py-2 border-b border-violet-100 dark:border-violet-500/15 bg-violet-50/60 dark:bg-violet-500/5 shrink-0 space-y-1.5">
<div className="flex flex-wrap gap-1.5">
{pins.map((pin, i) => (
<div key={i} className="flex items-center gap-1 pl-2 pr-1 py-0.5 rounded-full bg-violet-100 dark:bg-violet-500/15 border border-violet-200 dark:border-violet-500/25">
<div className="w-2 h-2 rounded-full bg-violet-500 shrink-0" />
<span className="text-[10px] font-bold text-violet-700 dark:text-violet-300">Pin {i+1}</span>
<button onClick={() => handleRemovePin(i)} className="text-violet-400 hover:text-violet-600 ml-0.5">
<X size={10} />
</button>
</div>
))}
{pins.length < MAX_PINS && (
<button
onClick={() => setPinDropMode(true)}
className="flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed border-violet-300 dark:border-violet-500/40 text-[10px] font-bold text-violet-500 hover:bg-violet-100 dark:hover:bg-violet-500/15 transition-colors"
>
+ Add pin
</button>
</div>
))}
{pins.length < MAX_PINS && (
<button
onClick={() => setPinDropMode(true)}
className="flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed border-violet-300 dark:border-violet-500/40 text-[10px] font-bold text-violet-500 hover:bg-violet-100 dark:hover:bg-violet-500/15 transition-colors"
>
+ Add pin
</button>
)}
)}
</div>
{/* Radius control */}
<div className="flex items-center gap-1.5">
<span className="text-[10px] font-bold text-violet-500 dark:text-violet-400 uppercase tracking-wider">Radius</span>
<input
type="number"
min="1"
max="100"
value={pinRadius}
onChange={e => {
const v = parseInt(e.target.value, 10);
if (!isNaN(v) && v >= 1) setPinRadius(Math.min(v, 100));
}}
className="w-14 text-center text-[11px] font-bold rounded-md border border-violet-200 dark:border-violet-500/30 bg-white dark:bg-violet-500/10 text-violet-700 dark:text-violet-300 outline-none focus:ring-1 focus:ring-violet-400 px-1 py-0.5"
/>
<span className="text-[10px] text-violet-400">mi</span>
</div>
</div>
)}
@@ -1510,7 +1504,7 @@ const StormIntelPage = () => {
<Crosshair size={22} className="text-violet-500" />
</div>
<p className="text-[13px] font-bold text-zinc-700 dark:text-zinc-200 mb-1">Drop a pin to explore</p>
<p className="text-[11px] text-zinc-400 mb-3 leading-relaxed">Click the crosshair button and place a pin on the map to find storm events within 5 miles.</p>
<p className="text-[11px] text-zinc-400 mb-3 leading-relaxed">Click the crosshair button and place a pin on the map to find storm events within your chosen radius.</p>
<button
onClick={() => setPinDropMode(true)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-violet-600 text-white text-[12px] font-bold hover:bg-violet-700 transition-colors"
@@ -1521,7 +1515,7 @@ const StormIntelPage = () => {
) : sidebarEvents.length === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-center">
<CloudLightning size={22} className="text-zinc-300 mb-2" />
<p className="text-[12px] text-zinc-400">No events within 5 miles of your pins</p>
<p className="text-[12px] text-zinc-400">No events within {pinRadius} mi of your pins</p>
</div>
) : (
sidebarEvents.map(storm => (