fix(storm-intel): forecast day contrast + range filter upgrade to 36mo + custom date range
- Alert/warn forecast cards: day label, temp, and rain% now use high-contrast red/orange text instead of muted gray — readable in both light and dark mode - Alert card background slightly more opaque (bg-red-100/dark:bg-red-500/15) for better definition - Range chips: 12 Mo / 18 Mo / 24 Mo / 36 Mo replacing old 1m/3m/6m/1y options - Custom range: CalendarRange chip reveals two date inputs (from/to) with amber focus ring - useStormEvents: DATE_RANGE_DAYS updated for new keys; custom range filters by customStart/customEnd; allEvents now uses 36m baseline
This commit is contained in:
+29
-16
@@ -3,9 +3,6 @@
|
||||
*
|
||||
* USE_MOCK = true → returns MOCK_STORM_EVENTS from mockStore (local dev)
|
||||
* USE_MOCK = false → calls /api/storm-events which proxies NWS/NOAA
|
||||
*
|
||||
* Flip to false once the Vercel deployment is live (no extra env vars needed —
|
||||
* NWS api.weather.gov is a free, unauthenticated public endpoint).
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
@@ -13,14 +10,21 @@ import { MOCK_STORM_EVENTS } from '../data/mockStore';
|
||||
|
||||
const USE_MOCK = true;
|
||||
|
||||
const DATE_RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365 };
|
||||
const DATE_RANGE_DAYS = { '12m': 365, '18m': 548, '24m': 730, '36m': 1095 };
|
||||
|
||||
const SEVERITY_ORDER = { severe: 4, significant: 3, moderate: 2, trace: 1 };
|
||||
|
||||
export function useStormEvents({ dateRange = '6m', typeFilter = 'all', severityFilter = 'all', sortBy = 'date' } = {}) {
|
||||
export function useStormEvents({
|
||||
dateRange = '12m',
|
||||
typeFilter = 'all',
|
||||
severityFilter = 'all',
|
||||
sortBy = 'date',
|
||||
customStart = '',
|
||||
customEnd = '',
|
||||
} = {}) {
|
||||
const [allEvents, setAllEvents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -34,7 +38,7 @@ export function useStormEvents({ dateRange = '6m', typeFilter = 'all', severityF
|
||||
await new Promise(r => setTimeout(r, 450));
|
||||
data = MOCK_STORM_EVENTS;
|
||||
} else {
|
||||
const days = DATE_RANGE_DAYS[dateRange] || 180;
|
||||
const days = DATE_RANGE_DAYS[dateRange] ?? 365;
|
||||
const res = await fetch(`/api/storm-events?days=${days}&county=Collin`);
|
||||
if (!res.ok) throw new Error(`Storm events error: ${res.status}`);
|
||||
data = await res.json();
|
||||
@@ -52,10 +56,19 @@ export function useStormEvents({ dateRange = '6m', typeFilter = 'all', severityF
|
||||
}, [dateRange]);
|
||||
|
||||
const events = useMemo(() => {
|
||||
const cutoffMs = Date.now() - (DATE_RANGE_DAYS[dateRange] || 180) * 86400000;
|
||||
|
||||
let filtered = allEvents.filter(e => {
|
||||
if (new Date(e.date).getTime() < cutoffMs) return false;
|
||||
const d = new Date(e.date);
|
||||
|
||||
// Custom date range
|
||||
if (dateRange === 'custom') {
|
||||
const start = customStart ? new Date(customStart) : new Date(0);
|
||||
const end = customEnd ? new Date(customEnd + 'T23:59:59') : new Date();
|
||||
if (d < start || d > end) return false;
|
||||
} else {
|
||||
const days = DATE_RANGE_DAYS[dateRange] ?? 365;
|
||||
if (d.getTime() < Date.now() - days * 86400000) return false;
|
||||
}
|
||||
|
||||
if (typeFilter !== 'all' && e.type !== typeFilter) return false;
|
||||
if (severityFilter !== 'all' && e.severity !== severityFilter) return false;
|
||||
return true;
|
||||
@@ -68,7 +81,7 @@ export function useStormEvents({ dateRange = '6m', typeFilter = 'all', severityF
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [allEvents, dateRange, typeFilter, severityFilter, sortBy]);
|
||||
}, [allEvents, dateRange, typeFilter, severityFilter, sortBy, customStart, customEnd]);
|
||||
|
||||
const totalHomes = useMemo(() => events.reduce((s, e) => s + (e.estimatedHomes ?? 0), 0), [events]);
|
||||
|
||||
@@ -78,8 +91,8 @@ export function useStormEvents({ dateRange = '6m', typeFilter = 'all', severityF
|
||||
// Returns a label + color class for how "hot" a storm zone is (conversion likelihood)
|
||||
export function conversionWindow(dateStr) {
|
||||
const days = Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000);
|
||||
if (days < 7) return { label: 'Too Fresh', color: 'text-sky-500', bg: 'bg-sky-50 dark:bg-sky-500/10', border: 'border-sky-200 dark:border-sky-500/20' };
|
||||
if (days <= 60) return { label: 'Hot Zone', color: 'text-red-500', bg: 'bg-red-50 dark:bg-red-500/10', border: 'border-red-200 dark:border-red-500/20' };
|
||||
if (days <= 120) return { label: 'Warm', color: 'text-orange-500', bg: 'bg-orange-50 dark:bg-orange-500/10', border: 'border-orange-200 dark:border-orange-500/20' };
|
||||
return { label: 'Cold', color: 'text-zinc-400', bg: 'bg-zinc-100 dark:bg-zinc-800', border: 'border-zinc-200 dark:border-white/10' };
|
||||
if (days < 7) return { label: 'Too Fresh', color: 'text-sky-500', bg: 'bg-sky-50 dark:bg-sky-500/10', border: 'border-sky-200 dark:border-sky-500/20' };
|
||||
if (days <= 60) return { label: 'Hot Zone', color: 'text-red-500', bg: 'bg-red-50 dark:bg-red-500/10', border: 'border-red-200 dark:border-red-500/20' };
|
||||
if (days <= 120) return { label: 'Warm', color: 'text-orange-500', bg: 'bg-orange-50 dark:bg-orange-500/10', border: 'border-orange-200 dark:border-orange-500/20' };
|
||||
return { label: 'Cold', color: 'text-zinc-400', bg: 'bg-zinc-100 dark:bg-zinc-800', border: 'border-zinc-200 dark:border-white/10' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user