import { useCallback, useRef, useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; import { MapPin, Settings } from 'lucide-react'; import { Widget } from '../Widget'; import { getWeatherInfo } from './weather-codes'; type WeatherLocation = { latitude: number; longitude: number; name: string; }; type GeocodingResult = { id: number; name: string; country: string; admin1?: string; latitude: number; longitude: number; }; type CurrentWeather = { temperature_2m: number; apparent_temperature: number; weather_code: number; wind_speed_10m: number; relative_humidity_2m: number; is_day: number; }; type DailyWeather = { time: string[]; temperature_2m_max: number[]; temperature_2m_min: number[]; weather_code: number[]; }; type ForecastResponse = { current: CurrentWeather; daily: DailyWeather; }; const USER_STATE_KEY = ['USER_STATE']; const LOCATION_STATE_KEY = 'weather-location'; function useWeatherLocation() { const client = useClient(); const { isAuthenticated } = useAuth(); const queryClient = useQueryClient(); const clientRef = useRef(client); clientRef.current = client; const { data: state = {} } = useQuery>({ queryKey: USER_STATE_KEY, enabled: isAuthenticated, queryFn: () => client.get('/user/state'), staleTime: Infinity, }); const location = (state[LOCATION_STATE_KEY] as WeatherLocation | undefined) ?? null; const setLocation = useCallback( (loc: WeatherLocation | null) => { const currentState = queryClient.getQueryData>(USER_STATE_KEY) ?? {}; queryClient.setQueryData(USER_STATE_KEY, { ...currentState, [LOCATION_STATE_KEY]: loc }); clientRef.current.patch('/user/state', { [LOCATION_STATE_KEY]: loc }).catch(() => {}); }, [queryClient], ); return [location, setLocation] as const; } const LocationSetup = ({ onSelect }: { onSelect: (loc: WeatherLocation) => void }) => { const [query, setQuery] = useState(''); const [geoLoading, setGeoLoading] = useState(false); const { data: results = [] } = useQuery({ queryKey: ['geocoding', query], enabled: query.length >= 2, queryFn: async () => { const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=5`); const data = await res.json(); return data.results ?? []; }, staleTime: 60_000, }); const useGeolocation = () => { if (!navigator.geolocation) return; setGeoLoading(true); navigator.geolocation.getCurrentPosition( async (pos) => { const { latitude, longitude } = pos.coords; try { const res = await fetch( `https://geocoding-api.open-meteo.com/v1/search?name=${latitude.toFixed(2)},${longitude.toFixed(2)}&count=1`, ); const data = await res.json(); const name = data.results?.[0]?.name ?? `${latitude.toFixed(2)}, ${longitude.toFixed(2)}`; onSelect({ latitude, longitude, name }); } catch { onSelect({ latitude, longitude, name: `${latitude.toFixed(2)}, ${longitude.toFixed(2)}` }); } finally { setGeoLoading(false); } }, () => setGeoLoading(false), ); }; return (
setQuery(e.target.value)} className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring" /> {results.length > 0 && (
{results.map((r) => ( ))}
)}
); }; const dayName = (dateStr: string) => { const d = new Date(dateStr + 'T00:00:00'); return d.toLocaleDateString(undefined, { weekday: 'short' }); }; const WeatherDisplay = ({ location, onReset }: { location: WeatherLocation; onReset: () => void }) => { const { data, isLoading } = useQuery({ queryKey: ['weather', location.latitude, location.longitude], queryFn: async () => { const params = new URLSearchParams({ latitude: String(location.latitude), longitude: String(location.longitude), current: 'temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m,is_day', daily: 'temperature_2m_max,temperature_2m_min,weather_code', timezone: 'auto', forecast_days: '5', }); const res = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`); return res.json(); }, staleTime: 15 * 60_000, }); if (isLoading || !data) { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); } const { current, daily } = data; const info = getWeatherInfo(current.weather_code, current.is_day === 1); return (
{info.icon}
{Math.round(current.temperature_2m)}°C
{info.label}
Feels {Math.round(current.apparent_temperature)}°C 💧 {current.relative_humidity_2m}% 💨 {Math.round(current.wind_speed_10m)} km/h
{location.name}
{daily.time.map((date, i) => { const dayInfo = getWeatherInfo(daily.weather_code[i]!, true); return (
{dayName(date)} {dayInfo.icon} {Math.round(daily.temperature_2m_max[i]!)}° / {Math.round(daily.temperature_2m_min[i]!)}°
); })}
); }; export const Weather = () => { const [location, setLocation] = useWeatherLocation(); const [showSetup, setShowSetup] = useState(false); if (!location || showSetup) { return ( { setLocation(loc); setShowSetup(false); }} /> ); } return ( setShowSetup(true)} /> ); };