feat: enable weather widget for Otavalo using Open-Meteo API (free, no key required)
This commit is contained in:
@@ -6,61 +6,31 @@ export default function WeatherWidget() {
|
|||||||
const [weather, setWeather] = useState<any>(null);
|
const [weather, setWeather] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [config, setConfig] = useState<any>(null);
|
|
||||||
|
|
||||||
// Load weather configuration
|
|
||||||
useEffect(() => {
|
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 () => {
|
const fetchWeather = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Use the first city in the list for now
|
const res = await fetch('/api/weather');
|
||||||
const city = config.cities[0];
|
|
||||||
const res = await fetch(`/api/weather?lat=${city.lat}&lon=${city.lon}`);
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setWeather(data);
|
setWeather(data.data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to fetch weather data');
|
throw new Error('Failed to fetch weather data');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Weather API error:', err);
|
console.error('Weather API error:', err);
|
||||||
setError('Failed to load weather data');
|
setError('Failed to load weather');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchWeather();
|
fetchWeather();
|
||||||
}, [config]);
|
}, []);
|
||||||
|
|
||||||
if (!config?.enabled) return null;
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -78,49 +48,54 @@ export default function WeatherWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error || !weather) {
|
if (error || !weather) {
|
||||||
return (
|
return null;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = weather.current;
|
const current = weather.current;
|
||||||
const temp = current.celsius;
|
const today = weather.today;
|
||||||
const icon = '🌤️';
|
|
||||||
|
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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'rgba(255, 255, 255, 0.9)',
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '12px 16px',
|
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)',
|
backdropFilter: 'blur(10px)',
|
||||||
border: '1px solid rgba(255,255,255,0.2)',
|
border: '1px solid rgba(255,255,255,0.3)',
|
||||||
minWidth: '180px'
|
minWidth: '160px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
<span style={{ fontSize: '20px' }}>{icon}</span>
|
<span style={{ fontSize: '28px' }}>{icon}</span>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '18px', fontWeight: '600', color: '#333' }}>
|
<div style={{ fontSize: '22px', fontWeight: '600', color: '#1a1a2e' }}>
|
||||||
{temp}°C
|
{current.celsius}°C
|
||||||
</div>
|
</div>
|
||||||
{config.cities[0] && (
|
<div style={{ fontSize: '11px', color: '#666', marginTop: '2px' }}>
|
||||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
|
{current.summary}
|
||||||
{config.cities[0].name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '11px', color: '#888', marginTop: '4px' }}>
|
</div>
|
||||||
Updated: {new Date(current.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user