import { useEffect, useState } from "react"; import { Cloud, CloudRain, Sun, CloudSnow, Wind, Thermometer, } from "lucide-react"; import { registerWidget } from "./WidgetRegistry"; import type { WeatherConfig, WidgetComponentProps, } from "@/types/homepage-types"; import { GRID_SIZE } from "@/types/homepage-types"; import { WidgetTitle } from "./WidgetTitle"; import { runVisibleInterval } from "../use-visible-interval"; interface WttrCurrent { temp_C: string; FeelsLikeC: string; weatherDesc: { value: string }[]; weatherCode: string; } interface WttrDay { mintempC: string; maxtempC: string; hourly: { weatherDesc: { value: string }[]; weatherCode: string }[]; } interface WttrResponse { nearest_area: { areaName: { value: string }[]; country: { value: string }[]; }[]; current_condition: WttrCurrent[]; weather: WttrDay[]; } function weatherIcon(code: string, size = 20) { const n = Number(code); if (n === 113) return ; if (n <= 119) return ; if (n <= 143) return ; if (n <= 266) return ; if (n <= 335) return ; if (n <= 389) return ; return ; } function celsiusToF(c: number) { return Math.round((c * 9) / 5 + 32); } function WeatherWidget({ widget, config, }: WidgetComponentProps) { const { location, unit, showForecast } = config; const [data, setData] = useState(null); const [error, setError] = useState(false); useEffect(() => { if (!location) return; let cancelled = false; const fetchWeather = async () => { try { setError(false); const res = await fetch( `https://wttr.in/${encodeURIComponent(location)}?format=j1`, ); if (!res.ok) throw new Error("bad response"); const json: WttrResponse = await res.json(); if (!cancelled) setData(json); } catch { if (!cancelled) setError(true); } }; fetchWeather(); const stop = runVisibleInterval( () => { void fetchWeather(); }, 10 * 60 * 1000, ); return () => { cancelled = true; stop(); }; }, [location]); if (!location) { return (
Configure a location in widget settings
); } if (error) { return (
Could not load weather for "{location}"
); } if (!data) { return (
Loading...
); } const current = data.current_condition[0]; const area = data.nearest_area[0]; const cityName = area?.areaName[0]?.value ?? location; const tempC = Number(current.temp_C); const feelsC = Number(current.FeelsLikeC); const displayTemp = unit === "F" ? `${celsiusToF(tempC)}°F` : `${tempC}°C`; const displayFeels = unit === "F" ? `${celsiusToF(feelsC)}°F` : `${feelsC}°C`; const desc = current.weatherDesc[0]?.value ?? ""; const code = current.weatherCode; return (
} />
{/* Current conditions */}
{weatherIcon(code, 32)}
{displayTemp} {desc} Feels like {displayFeels}
{cityName}
{/* 3-day forecast */} {showForecast && data.weather.length >= 3 && (
{data.weather.slice(0, 3).map((day, i) => { const hourly = day.hourly[4] ?? day.hourly[0]; const minC = Number(day.mintempC); const maxC = Number(day.maxtempC); const lo = unit === "F" ? `${celsiusToF(minC)}°` : `${minC}°`; const hi = unit === "F" ? `${celsiusToF(maxC)}°` : `${maxC}°`; const dayLabel = i === 0 ? "Today" : i === 1 ? "Tmrw" : "+2"; return (
{dayLabel} {weatherIcon(hourly?.weatherCode ?? "113", 12)} {hi} {lo}
); })}
)}
); } registerWidget({ id: "weather", name: "Weather", description: "Live weather for any location", category: "info", icon: , defaultConfig: { location: "", unit: "C", showForecast: true }, defaultSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 7 }, minSize: { w: GRID_SIZE * 2, h: GRID_SIZE * 2 }, component: WeatherWidget, }); export { WeatherWidget };