feat: enable weather widget for Otavalo using Open-Meteo API (free, no key required)

This commit is contained in:
2026-06-28 23:23:50 -05:00
parent a1d88af78b
commit a892e9e132
+38 -63
View File
@@ -6,61 +6,31 @@ export default function WeatherWidget() {
const [weather, setWeather] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [config, setConfig] = useState<any>(null);
// Load weather configuration
useEffect(() => {
const loadConfig = async () => {
try {
const res = await fetch('/api/site-assets?key=weather_config');
if (res.ok) {
const data = await res.json();
if (data.data) {
const parsedConfig = JSON.parse(data.data);
setConfig(parsedConfig);
}
}
} catch (err) {
console.error('Failed to load weather config:', err);
}
};
loadConfig();
}, []);
// Fetch weather data when config is loaded and enabled
useEffect(() => {
if (!config || !config.enabled || config.cities.length === 0) {
setLoading(false);
return;
}
const fetchWeather = async () => {
try {
setLoading(true);
setError(null);
// Use the first city in the list for now
const city = config.cities[0];
const res = await fetch(`/api/weather?lat=${city.lat}&lon=${city.lon}`);
const res = await fetch('/api/weather');
if (res.ok) {
const data = await res.json();
setWeather(data);
setWeather(data.data);
} else {
throw new Error('Failed to fetch weather data');
}
} catch (err) {
console.error('Weather API error:', err);
setError('Failed to load weather data');
setError('Failed to load weather');
} finally {
setLoading(false);
}
};
fetchWeather();
}, [config]);
if (!config?.enabled) return null;
}, []);
if (loading) {
return (
@@ -78,49 +48,54 @@ export default function WeatherWidget() {
}
if (error || !weather) {
return (
<div style={{
background: 'rgba(255, 255, 255, 0.9)',
borderRadius: '12px',
padding: '12px 16px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.2)'
}}>
<div style={{ fontSize: '14px', color: '#666' }}>Weather unavailable</div>
</div>
);
return null;
}
const current = weather.current;
const temp = current.celsius;
const icon = '🌤️';
const today = weather.today;
const weatherIcons: Record<number, string> = {
0: '☀️', 1: '🌤️', 2: '⛅', 3: '☁️',
45: '🌫️', 48: '🌫️', 51: '🌦️', 53: '🌦️', 55: '🌦️',
61: '🌧️', 63: '🌧️', 65: '🌧️', 71: '🌨️', 73: '🌨️',
75: '🌨️', 80: '🌦️', 81: '🌦️', 82: '🌧️',
95: '⛈️', 96: '⛈️', 99: '⛈️'
};
const icon = weatherIcons[current.weathercode] || '🌤️';
return (
<div style={{
background: 'rgba(255, 255, 255, 0.9)',
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: '12px',
padding: '12px 16px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.2)',
minWidth: '180px'
border: '1px solid rgba(255,255,255,0.3)',
minWidth: '160px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
<span style={{ fontSize: '20px' }}>{icon}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style={{ fontSize: '28px' }}>{icon}</span>
<div>
<div style={{ fontSize: '18px', fontWeight: '600', color: '#333' }}>
{temp}°C
<div style={{ fontSize: '22px', fontWeight: '600', color: '#1a1a2e' }}>
{current.celsius}°C
</div>
{config.cities[0] && (
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
{config.cities[0].name}
</div>
)}
<div style={{ fontSize: '11px', color: '#666', marginTop: '2px' }}>
{current.summary}
</div>
</div>
<div style={{ fontSize: '11px', color: '#888', marginTop: '4px' }}>
Updated: {new Date(current.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '8px',
paddingTop: '8px',
borderTop: '1px solid rgba(0,0,0,0.1)',
fontSize: '11px',
color: '#555'
}}>
<span>📍 Otavalo</span>
<span>{today.maxC}° {today.minC}°</span>
</div>
</div>
);