Spaces:
Sleeping
Sleeping
| import { useState, useEffect } from 'react' | |
| import { Sun, Cloud, CloudRain, CloudSnow, CloudDrizzle, CloudLightning, Wind } from 'lucide-react' | |
| import { fetchWeather } from '../../services/weatherQueue' | |
| import { useSettingsStore } from '../../store/settingsStore' | |
| const WEATHER_ICON_MAP = { | |
| Clear: Sun, | |
| Clouds: Cloud, | |
| Rain: CloudRain, | |
| Drizzle: CloudDrizzle, | |
| Thunderstorm: CloudLightning, | |
| Snow: CloudSnow, | |
| Mist: Wind, | |
| Fog: Wind, | |
| Haze: Wind, | |
| } | |
| interface WeatherIconProps { | |
| main: string | |
| size?: number | |
| } | |
| function WeatherIcon({ main, size = 13 }: WeatherIconProps) { | |
| const Icon = WEATHER_ICON_MAP[main] || Cloud | |
| return <Icon size={size} strokeWidth={1.8} /> | |
| } | |
| function getWeatherCache(key) { | |
| try { | |
| const raw = sessionStorage.getItem(key) | |
| if (raw === null) return undefined | |
| return JSON.parse(raw) | |
| } catch { return undefined } | |
| } | |
| function setWeatherCache(key, value) { | |
| try { sessionStorage.setItem(key, JSON.stringify(value)) } catch {} | |
| } | |
| interface WeatherWidgetProps { | |
| lat: number | null | |
| lng: number | null | |
| date: string | |
| compact?: boolean | |
| /** Vertical icon-over-temp layout that inherits its color (for the day badge). */ | |
| stacked?: boolean | |
| } | |
| export default function WeatherWidget({ lat, lng, date, compact = false, stacked = false }: WeatherWidgetProps) { | |
| const [weather, setWeather] = useState(null) | |
| const [loading, setLoading] = useState(false) | |
| const [failed, setFailed] = useState(false) | |
| const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit' | |
| useEffect(() => { | |
| if (!lat || !lng || !date) return | |
| const rLat = Math.round(lat * 100) / 100 | |
| const rLng = Math.round(lng * 100) / 100 | |
| const cacheKey = `weather_${rLat}_${rLng}_${date}` | |
| const cached = getWeatherCache(cacheKey) | |
| if (cached !== undefined) { | |
| if (cached === null) setFailed(true) | |
| // Climate data: use from cache but re-fetch in background to upgrade to forecast | |
| else if (cached.type === 'climate') { | |
| setWeather(cached) | |
| fetchWeather(lat, lng, date) | |
| .then(data => { | |
| if (!data.error && data.temp !== undefined && data.type === 'forecast') { | |
| setWeatherCache(cacheKey, data) | |
| setWeather(data) | |
| } | |
| }) | |
| .catch(() => {}) | |
| return | |
| } else { | |
| setWeather(cached) | |
| return | |
| } | |
| return | |
| } | |
| setLoading(true) | |
| fetchWeather(lat, lng, date) | |
| .then(data => { | |
| if (data.error || data.temp === undefined) { | |
| setFailed(true) | |
| } else { | |
| setWeatherCache(cacheKey, data) | |
| setWeather(data) | |
| } | |
| }) | |
| .catch(() => { setFailed(true) }) | |
| .finally(() => setLoading(false)) | |
| }, [lat, lng, date]) | |
| if (!lat || !lng) return null | |
| const fontStyle = { fontFamily: "var(--font-system)" } | |
| if (loading) { | |
| return ( | |
| <span style={{ fontSize: 11, color: '#d1d5db', ...fontStyle }}>…</span> | |
| ) | |
| } | |
| if (failed || !weather) { | |
| return ( | |
| <span style={{ fontSize: 11, color: '#9ca3af', ...fontStyle }}>—</span> | |
| ) | |
| } | |
| const rawTemp = weather.temp | |
| const temp = rawTemp !== undefined ? Math.round(isFahrenheit ? rawTemp * 9/5 + 32 : rawTemp) : null | |
| const unit = isFahrenheit ? '°F' : '°C' | |
| const isClimate = weather.type === 'climate' | |
| if (stacked) { | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, fontSize: 9.5, fontWeight: 600, lineHeight: 1, color: 'inherit', ...fontStyle }}> | |
| <WeatherIcon main={weather.main} size={13} /> | |
| {temp !== null && <span>{isClimate ? 'Ø' : ''}{temp}°</span>} | |
| </div> | |
| ) | |
| } | |
| if (compact) { | |
| return ( | |
| <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 11, color: isClimate ? '#a1a1aa' : '#6b7280', ...fontStyle }}> | |
| <WeatherIcon main={weather.main} size={12} /> | |
| {temp !== null && <span>{isClimate ? 'Ø ' : ''}{temp}{unit}</span>} | |
| </span> | |
| ) | |
| } | |
| return ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: isClimate ? '#71717a' : '#374151', background: 'rgba(0,0,0,0.04)', borderRadius: 8, padding: '5px 10px', ...fontStyle }}> | |
| <WeatherIcon main={weather.main} size={15} /> | |
| {temp !== null && <span style={{ fontWeight: 500 }}>{isClimate ? 'Ø ' : ''}{temp}{unit}</span>} | |
| {weather.description && <span style={{ fontSize: 11, color: '#9ca3af', textTransform: 'capitalize' }}>{weather.description}</span>} | |
| </div> | |
| ) | |
| } | |