← back to Ken
src/app/dashboard/weather/page.js
125 lines
'use client';
import { useState, useEffect } from 'react';
const DEMO_CITIES = ['New York', 'Chicago', 'Los Angeles', 'Miami', 'Denver', 'Seattle'];
export default function WeatherPage() {
const [forecasts, setForecasts] = useState([]);
const [loading, setLoading] = useState(true);
const [fetching, setFetching] = useState(false);
const [selectedCity, setSelectedCity] = useState('New York');
async function loadForecasts() {
try {
const res = await fetch('/api/weather');
const data = await res.json();
setForecasts(data.forecasts || []);
} catch (err) { console.error(err); }
setLoading(false);
}
useEffect(() => { loadForecasts(); }, []);
async function fetchWeather() {
setFetching(true);
try {
const res = await fetch('/api/weather', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'fetch', city: selectedCity })
});
const data = await res.json();
if (data.error) alert('Error: ' + data.error);
else alert(`Fetched ${data.periods || 0} forecast periods for ${selectedCity}`);
loadForecasts();
} catch (err) { alert('Fetch error: ' + err.message); }
setFetching(false);
}
async function fetchAllCities() {
setFetching(true);
try {
const res = await fetch('/api/weather', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'fetch_all' })
});
const data = await res.json();
alert(`Fetched forecasts for ${data.cities_fetched || 0} cities`);
loadForecasts();
} catch (err) { alert('Fetch error: ' + err.message); }
setFetching(false);
}
if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
// Group by location
const byLocation = {};
forecasts.forEach(f => {
const key = f.location_name || f.geom_hash;
if (!byLocation[key]) byLocation[key] = [];
byLocation[key].push(f);
});
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Weather Forecasts</h1>
<p className="text-gray-500 text-sm">NWS forecast data for tracked locations</p>
</div>
<div className="flex items-center gap-2">
<select value={selectedCity} onChange={e => setSelectedCity(e.target.value)}
className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm">
{DEMO_CITIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<button onClick={fetchWeather} disabled={fetching} className="btn btn-primary">
{fetching ? 'Fetching...' : 'Fetch City'}
</button>
<button onClick={fetchAllCities} disabled={fetching} className="btn btn-success">
Fetch All
</button>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Forecast Runs</p><p className="stat-value text-blue-400">{forecasts.length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Locations</p><p className="stat-value text-green-400">{Object.keys(byLocation).length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Latest</p><p className="text-sm text-gray-300 mt-1">{forecasts[0]?.run_ts ? new Date(forecasts[0].run_ts).toLocaleString() : 'None'}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Sources</p><p className="stat-value text-purple-400">{[...new Set(forecasts.map(f => f.source))].length}</p></div>
</div>
{Object.entries(byLocation).map(([location, runs]) => (
<div key={location} className="card">
<h3 className="font-semibold mb-3">{location}</h3>
<table>
<thead>
<tr><th>Source</th><th>Run Time</th><th>Variable</th><th>Lead (h)</th><th>Mean</th><th>Min</th><th>Max</th><th>Spread</th></tr>
</thead>
<tbody>
{runs.slice(0, 20).map((r, i) => (
<tr key={i}>
<td><span className="badge badge-blue">{r.source}</span></td>
<td className="text-gray-400 text-sm">{new Date(r.run_ts).toLocaleString()}</td>
<td className="font-medium">{r.variable}</td>
<td className="font-mono">{r.lead_hours}h</td>
<td className="font-mono text-green-400">{parseFloat(r.value_mean || 0).toFixed(1)}</td>
<td className="font-mono text-blue-400">{parseFloat(r.value_min || 0).toFixed(1)}</td>
<td className="font-mono text-red-400">{parseFloat(r.value_max || 0).toFixed(1)}</td>
<td className="font-mono text-purple-400">{parseFloat(r.ensemble_spread || 0).toFixed(1)}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
{forecasts.length === 0 && (
<div className="card text-center py-12">
<p className="text-gray-500">No forecast data yet. Click "Fetch City" or "Fetch All" to begin.</p>
</div>
)}
</div>
);
}