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:
@@ -16,7 +16,7 @@ import {
|
||||
Home, AlertTriangle, TrendingUp, Users, Calendar, MapPin,
|
||||
Flame, Snowflake, Clock, ArrowRight, Eye, UserPlus, Loader2,
|
||||
DollarSign, Target, BarChart2, Map, Pencil, Sun, Cloud,
|
||||
CloudRain, CloudDrizzle, CheckCircle, XCircle,
|
||||
CloudRain, CloudDrizzle, CheckCircle, XCircle, CalendarRange,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
@@ -385,20 +385,36 @@ const ForecastStrip = ({ forecast, loading, alertDay, hasStormAlert }) => {
|
||||
key={day.date}
|
||||
className={`flex-none w-[70px] rounded-xl border px-2 py-1.5 flex flex-col items-center gap-0.5 ${
|
||||
isAlert
|
||||
? 'border-red-200 dark:border-red-500/20 bg-red-50/60 dark:bg-red-500/8'
|
||||
? 'border-red-300 dark:border-red-500/30 bg-red-100 dark:bg-red-500/15'
|
||||
: isWarn
|
||||
? 'border-orange-200 dark:border-orange-500/20 bg-orange-50/60 dark:bg-orange-500/8'
|
||||
? 'border-orange-200 dark:border-orange-500/20 bg-orange-50/60 dark:bg-orange-500/10'
|
||||
: 'border-zinc-100 dark:border-white/[0.06] bg-zinc-50/60 dark:bg-zinc-800/30'
|
||||
}`}
|
||||
>
|
||||
<span className={`text-[10px] font-black uppercase tracking-wide ${i === 0 ? 'text-amber-600 dark:text-amber-400' : 'text-zinc-500 dark:text-zinc-400'}`}>
|
||||
{/* Day label — high contrast on alert cards */}
|
||||
<span className={`text-[10px] font-black uppercase tracking-wide ${
|
||||
i === 0
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: isAlert
|
||||
? 'text-red-700 dark:text-red-300'
|
||||
: isWarn
|
||||
? 'text-orange-700 dark:text-orange-300'
|
||||
: 'text-zinc-500 dark:text-zinc-400'
|
||||
}`}>
|
||||
{dayLabel}
|
||||
</span>
|
||||
<FIcon
|
||||
size={16}
|
||||
style={{ color: isAlert ? '#DC2626' : isWarn ? '#F97316' : '#71717a' }}
|
||||
/>
|
||||
<span className={`text-[11px] font-black ${isAlert ? 'text-red-600 dark:text-red-400' : 'text-zinc-700 dark:text-zinc-200'}`}>
|
||||
{/* Temp — white in dark mode on alert so it pops against the colored bg */}
|
||||
<span className={`text-[11px] font-black ${
|
||||
isAlert
|
||||
? 'text-red-700 dark:text-white'
|
||||
: isWarn
|
||||
? 'text-orange-700 dark:text-white'
|
||||
: 'text-zinc-700 dark:text-zinc-200'
|
||||
}`}>
|
||||
{day.tempF}°
|
||||
</span>
|
||||
{/* Storm score bar */}
|
||||
@@ -408,7 +424,9 @@ const ForecastStrip = ({ forecast, loading, alertDay, hasStormAlert }) => {
|
||||
style={{ width: `${day.stormScore}%`, backgroundColor: scoreColor(day.stormScore) }}
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-[9px] font-bold ${isAlert ? 'text-red-500' : isWarn ? 'text-orange-500' : 'text-zinc-400'}`}>
|
||||
<span className={`text-[9px] font-bold ${
|
||||
isAlert ? 'text-red-700 dark:text-red-300' : isWarn ? 'text-orange-600 dark:text-orange-300' : 'text-zinc-400'
|
||||
}`}>
|
||||
{day.precipProb}% rain
|
||||
</span>
|
||||
</div>
|
||||
@@ -864,7 +882,9 @@ const StormIntelPage = () => {
|
||||
const canManageZones = ['ADMIN', 'OWNER'].includes(user?.role);
|
||||
|
||||
const [activeTab, setActiveTab] = useState('map');
|
||||
const [dateRange, setDateRange] = useState('6m');
|
||||
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');
|
||||
@@ -879,8 +899,8 @@ const StormIntelPage = () => {
|
||||
const [drawingLinkedStormId, setDrawingLinkedStormId] = useState(null);
|
||||
const [showZoneModal, setShowZoneModal] = useState(false);
|
||||
|
||||
const { events, loading, error, totalHomes } = useStormEvents({ dateRange, typeFilter, severityFilter, sortBy });
|
||||
const { events: allEvents } = useStormEvents({ dateRange: '1y' });
|
||||
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();
|
||||
|
||||
const handleSelect = useCallback((storm) => {
|
||||
@@ -1062,12 +1082,33 @@ const StormIntelPage = () => {
|
||||
className={`shrink-0 border-b border-zinc-200 dark:border-white/[0.06] bg-white dark:bg-zinc-900/60 ${showFilters || window.innerWidth >= 1024 ? '' : 'hidden lg:flex'}`}
|
||||
>
|
||||
<div className="px-4 sm:px-6 py-3 flex flex-wrap gap-x-6 gap-y-2 items-center">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[11px] font-bold text-zinc-400 uppercase tracking-wider mr-1">Range</span>
|
||||
{[['1m', '1 Month'], ['3m', '3 Months'], ['6m', '6 Months'], ['1y', '1 Year']].map(([val, label]) => (
|
||||
{[['12m', '12 Mo'], ['18m', '18 Mo'], ['24m', '24 Mo'], ['36m', '36 Mo']].map(([val, label]) => (
|
||||
<FilterChip key={val} active={dateRange === val} onClick={() => setDateRange(val)}>{label}</FilterChip>
|
||||
))}
|
||||
<FilterChip active={dateRange === 'custom'} onClick={() => setDateRange('custom')}>
|
||||
<span className="flex items-center gap-1"><CalendarRange size={11} />Custom</span>
|
||||
</FilterChip>
|
||||
</div>
|
||||
{/* Custom date inputs */}
|
||||
{dateRange === 'custom' && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<input
|
||||
type="date"
|
||||
value={customStart}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<span className="text-[11px] text-zinc-400 font-medium">to</span>
|
||||
<input
|
||||
type="date"
|
||||
value={customEnd}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-px h-5 bg-zinc-200 dark:bg-white/10 hidden sm:block" />
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[11px] font-bold text-zinc-400 uppercase tracking-wider mr-1">Severity</span>
|
||||
|
||||
Reference in New Issue
Block a user