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:
@@ -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 => (
|
||||
|
||||
Reference in New Issue
Block a user