/* WeatherMate — app root */ const DEFAULT_CITY = { id: "thunderbay", name: "Thunder Bay", admin: "Ontario", country: "Canada", lat: 48.3809, lon: -89.2477 }; const COND_BY_LABEL = { Clear: "clear", "Partly Cloudy": "partly", Cloudy: "cloudy", Fog: "fog", Rain: "rain", Snow: "snow", Thunderstorm: "thunder" }; const CODE_BY_COND = { clear: 0, partly: 2, cloudy: 3, fog: 45, drizzle: 53, rain: 63, snow: 73, thunder: 95 }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "style": "classic", "units": "°C", "animations": true, "timeOfDay": "Auto", "forceWeather": "Auto" }/*EDITMODE-END*/; /* ---- pull to refresh hook (touch) ---- */ function usePullToRefresh(onRefresh) { const [height, setHeight] = React.useState(0); const [spinning, setSpinning] = React.useState(false); const startY = React.useRef(null); const hRef = React.useRef(0); const THRESH = 62; hRef.current = height; const bind = (scrollRef) => ({ onTouchStart: (e) => { if (!spinning && scrollRef.current && scrollRef.current.scrollTop <= 0) startY.current = e.touches[0].clientY; }, onTouchMove: (e) => { if (startY.current == null) return; const dy = e.touches[0].clientY - startY.current; if (dy > 0 && scrollRef.current && scrollRef.current.scrollTop <= 0) { setHeight(Math.min(96, dy * 0.5)); } else if (dy <= 0) { setHeight(0); } }, onTouchEnd: async () => { if (startY.current == null) return; startY.current = null; if (hRef.current >= THRESH && !spinning) { setSpinning(true); setHeight(54); try { await onRefresh(); } catch (e) {} setSpinning(false); setHeight(0); } else { setHeight(0); } }, }); return { height, spinning, bind, zoneRef: React.useRef(null) }; } function Loader({ theme }) { return (
Fetching live weather…
); } function UnitToggle({ unit, onToggle }) { return ( ); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const unit = t.units === "°F" ? "f" : "c"; const [saved, setSaved] = React.useState(() => { try { const s = JSON.parse(localStorage.getItem("wm_cities")); if (s && s.length) return s; } catch (e) {} return [DEFAULT_CITY]; }); const [activeId, setActiveId] = React.useState(() => { try { return localStorage.getItem("wm_active") || saved[0].id; } catch (e) { return saved[0].id; } }); const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [sample, setSample] = React.useState(false); const [view, setView] = React.useState("home"); const [forecastMode, setForecastMode] = React.useState("hourly"); const [clock, setClock] = React.useState(""); const activeCity = saved.find(c => c.id === activeId) || saved[0]; React.useEffect(() => { try { localStorage.setItem("wm_cities", JSON.stringify(saved)); } catch (e) {} }, [saved]); React.useEffect(() => { try { localStorage.setItem("wm_active", activeId); } catch (e) {} }, [activeId]); const loadWeather = React.useCallback(async (city) => { setLoading(true); try { const d = await WX.fetchWeather(city.lat, city.lon); setData(d); setSample(false); } catch (e) { setData(WX.sampleData()); setSample(true); } setLoading(false); }, []); React.useEffect(() => { if (activeCity) loadWeather(activeCity); }, [activeId]); // clock at city local time React.useEffect(() => { if (!data) return; const tick = () => { const now = new Date(); const utc = now.getTime() + now.getTimezoneOffset() * 60000; const ct = new Date(utc + (data.utcOffsetSec || 0) * 1000); let h = ct.getHours(); const m = ct.getMinutes(); const ap = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; setClock(`${h}:${String(m).padStart(2, "0")}`); }; tick(); const id = setInterval(tick, 10000); return () => clearInterval(id); }, [data]); const ptr = usePullToRefresh(() => loadWeather(activeCity)); // ---- effective display (apply tweaks overrides) ---- const baseCond = data ? WX.conditionOf(data.current.code) : "clear"; const forcedCond = t.forceWeather === "Auto" ? null : COND_BY_LABEL[t.forceWeather]; const cond = forcedCond || baseCond; const isDay = !data ? false : (t.timeOfDay === "Auto" ? data.current.isDay : t.timeOfDay === "Day"); const theme = WX.themeFor(cond, isDay); const animate = t.animations; const displayData = React.useMemo(() => { if (!data) return null; if (!forcedCond && t.timeOfDay === "Auto") return data; const code = forcedCond ? CODE_BY_COND[forcedCond] : data.current.code; const dayv = t.timeOfDay === "Auto" ? data.current.isDay : t.timeOfDay === "Day"; return { ...data, current: { ...data.current, code, isDay: dayv } }; }, [data, forcedCond, t.timeOfDay]); // ---- city management ---- const addCity = (r) => { const city = { id: String(r.id), name: r.name, admin: r.admin, country: r.country, lat: r.lat, lon: r.lon }; setSaved(prev => prev.find(c => c.id === city.id) ? prev : [...prev, city]); setActiveId(city.id); setView("home"); }; const selectCity = (c) => { setActiveId(c.id); setView("home"); }; const removeCity = (c) => { setSaved(prev => { const next = prev.filter(x => x.id !== c.id); if (c.id === activeId && next.length) setActiveId(next[0].id); return next.length ? next : [DEFAULT_CITY]; }); }; return (
{sample &&
Demo data · live feed unavailable
} {view === "home" && (
{!displayData ? : ( )} setTweak("units", unit === "c" ? "°F" : "°C")} /> setView("search")} onList={() => setForecastMode(m => m === "hourly" ? "weekly" : "hourly")} onHome={() => setView("home")} onRadar={() => setView("radar")} onPrefs={() => setView("prefs")} />
)} {view === "home" && loading && displayData && (
)} {view === "search" && ( setView("home")} /> )} {view === "radar" && ( setView("home")} /> )} setTweak("style", v)} /> setTweak("units", v)} /> setTweak("animations", v)} /> setTweak("timeOfDay", v)} /> setTweak("forceWeather", v)} />
); } ReactDOM.createRoot(document.getElementById("root")).render();