1.5
Code refactoring Profile fixes Table fixes Animation additions
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user