← 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>
  );
}