// ════════════════════════════════════════════════════════════════════════ // useForecast — fetches the weather forecast and air quality for the // given location and keeps them fresh. // // Responsibilities: // 1. Fetch /v1/forecast on location change. // 2. Fetch /v1/air-quality on location change, with a 6-hour // localStorage cache (AQI / pollen update slowly). // 3. Auto-refresh both every 5 minutes so the displayed data stays // current as time passes. Air quality only refetches if cache is // stale. // // Inputs: // location — { lat, lon, name, country } // // Outputs: // forecast — raw /v1/forecast response, or null // airQuality — raw /v1/air-quality response, or null // loading — true while the forecast fetch is in flight // error — fetch error message, or null // now — Date that ticks every 5 minutes (drives the "current row") // ════════════════════════════════════════════════════════════════════════ import { useState, useEffect } from '../../vendor/preact-hooks.js'; const SIX_HOURS_MS = 6 * 60 * 60 * 1000; const FIVE_MIN = 5 * 60 * 1000; function buildForecastUrl(loc) { return `https://api.open-meteo.com/v1/forecast` + `?latitude=${loc.lat}&longitude=${loc.lon}` + `&hourly=temperature_2m,relative_humidity_2m,dew_point_2m,` + `wind_speed_10m,wind_direction_10m,wind_gusts_10m,` + `direct_radiation,diffuse_radiation,shortwave_radiation,` + `cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high,` + `uv_index,precipitation,snowfall,visibility,` + `soil_temperature_0cm,soil_temperature_6cm,soil_moisture_0_to_1cm` + `&wind_speed_unit=ms&timezone=auto&forecast_days=14`; } function buildAirQualityUrl(loc) { return `https://air-quality-api.open-meteo.com/v1/air-quality` + `?latitude=${loc.lat}&longitude=${loc.lon}` + `&hourly=european_aqi,` + `grass_pollen,birch_pollen,alder_pollen,` + `mugwort_pollen,olive_pollen,ragweed_pollen` + `&timezone=auto&forecast_days=5`; } export function useForecast(location) { const [forecast, setForecast] = useState(null); const [airQuality, setAirQuality] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [now, setNow] = useState(new Date()); // Air quality loader — also used by the 5-minute refresh below. async function loadAirQuality(loc) { const cacheKey = `sunscope_aq_${loc.lat.toFixed(4)}_${loc.lon.toFixed(4)}`; try { const cached = localStorage.getItem(cacheKey); if (cached) { const { ts, data } = JSON.parse(cached); if (Date.now() - ts < SIX_HOURS_MS) { setAirQuality(data); return; } } } catch (e) { /* ignore bad cache */ } try { const r = await fetch(buildAirQualityUrl(loc)); if (!r.ok) return; // silently fail — these columns just show '—' const data = await r.json(); setAirQuality(data); try { localStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), data })); } catch (e) { /* ignore storage errors */ } } catch (_) { /* silently ignore */ } } // ─── INITIAL FETCH on location change ───────────────────────────── useEffect(() => { async function load() { setLoading(true); setError(null); try { const r = await fetch(buildForecastUrl(location)); if (!r.ok) throw new Error(`Open-Meteo HTTP ${r.status}`); setForecast(await r.json()); } catch (e) { setError(e.message); } finally { setLoading(false); } } load(); loadAirQuality(location); }, [location]); // ─── AUTO-REFRESH every 5 minutes ───────────────────────────────── // Updates `now` (drives the "current hour" highlight) and refetches // the forecast so fresh API data comes in automatically. Air quality // only refetches if its 6-hour cache has expired. useEffect(() => { const id = setInterval(() => { setNow(new Date()); async function refresh() { try { const r = await fetch(buildForecastUrl(location)); if (r.ok) setForecast(await r.json()); } catch (_) { /* silently ignore refresh errors */ } const cacheKey = `sunscope_aq_${location.lat.toFixed(4)}_${location.lon.toFixed(4)}`; try { const cached = localStorage.getItem(cacheKey); if (cached) { const { ts } = JSON.parse(cached); if (Date.now() - ts < SIX_HOURS_MS) return; // still fresh } } catch (e) { /* ignore */ } loadAirQuality(location); } refresh(); }, FIVE_MIN); return () => clearInterval(id); }, [location]); return { forecast, airQuality, loading, error, now }; }