refactor: remove coverage report and add weather widget and navigation utility files

This commit is contained in:
David Fencl
2026-06-06 11:41:13 +02:00
parent a3b3d40769
commit 6d77c20c84
34 changed files with 819 additions and 3002 deletions
+143
View File
@@ -0,0 +1,143 @@
import { useState, useEffect } from 'react';
import { FiWind, FiSunrise, FiSunset, FiThermometer, FiAlertCircle } from 'react-icons/fi';
interface WeatherProps {
lat: number;
lng: number;
language: 'cs' | 'en';
sensorTemp?: number;
}
interface WeatherData {
temp: number;
windSpeed: number; // m/s
windGusts: number; // m/s
windDir: number; // degrees
sunrise: string;
sunset: string;
}
const getCompassDirection = (degrees: number, language: 'cs' | 'en') => {
const directionsEn = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const directionsCs = ['S', 'SV', 'V', 'JV', 'J', 'JZ', 'Z', 'SZ'];
const directions = language === 'cs' ? directionsCs : directionsEn;
const index = Math.round(((degrees %= 360) < 0 ? degrees + 360 : degrees) / 45) % 8;
return directions[index];
};
const formatTime = (isoString: string) => {
if (!isoString) return '--:--';
const date = new Date(isoString);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
export const WeatherWidget = ({ lat, lng, language, sensorTemp }: WeatherProps) => {
const [data, setData] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
if (!lat || !lng) {
setLoading(false);
setError(true);
return;
}
const fetchWeather = async () => {
try {
setLoading(true);
const res = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current=temperature_2m,wind_speed_10m,wind_direction_10m,wind_gusts_10m&daily=sunrise,sunset&timezone=auto&wind_speed_unit=ms`);
if (!res.ok) throw new Error('Failed to fetch weather');
const json = await res.json();
setData({
temp: json.current.temperature_2m,
windSpeed: json.current.wind_speed_10m,
windGusts: json.current.wind_gusts_10m,
windDir: json.current.wind_direction_10m,
sunrise: json.daily.sunrise[0],
sunset: json.daily.sunset[0]
});
setError(false);
} catch (err) {
console.error('Weather fetch error:', err);
setError(true);
} finally {
setLoading(false);
}
};
fetchWeather();
// Refresh weather every 15 minutes
const interval = setInterval(fetchWeather, 15 * 60 * 1000);
return () => clearInterval(interval);
}, [lat, lng]);
const dict = {
cs: { title: 'Počasí a Vítr (Aktuálně)', error: 'Data nedostupná', wind: 'Vítr', gusts: 'Nárazy', temp: 'Teplota' },
en: { title: 'Weather & Wind (Current)', error: 'Data unavailable', wind: 'Wind', gusts: 'Gusts', temp: 'Temp' }
}[language];
if (loading) {
return <div className="kpi-card" style={{ minHeight: '120px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)' }}>Loading weather...</div>;
}
if (error || !data) {
return (
<div className="kpi-card" style={{ opacity: 0.7 }}>
<h3 style={{ fontSize: '1rem', color: 'var(--text-muted)', margin: '0 0 0.5rem 0' }}>{dict.title}</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--color-red)' }}>
<FiAlertCircle /> {dict.error}
</div>
</div>
);
}
return (
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<h3 style={{ fontSize: '1rem', color: 'var(--text-muted)', margin: '0 0 1rem 0' }}>{dict.title}</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
{/* Left Column: Wind */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
<div style={{
width: '40px', height: '40px', borderRadius: '50%', backgroundColor: 'rgba(0, 195, 255, 0.1)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--color-cyan)', fontSize: '1.2rem',
transform: `rotate(${data.windDir}deg)`
}} title={`Wind direction: ${data.windDir}°`}>
<FiWind style={{ transform: 'rotate(-90deg)' }} /> {/* Assume icon points UP by default, wind from south (180) should point UP. Arrow should point where wind is GOING. */}
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', lineHeight: 1.1, color: 'var(--text-main)' }}>
{data.windSpeed.toFixed(1)} <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>m/s</span>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginTop: '4px' }}>
{getCompassDirection(data.windDir, language)} {dict.gusts}: <span style={{ color: data.windGusts > 10 ? 'var(--color-red)' : 'var(--text-main)' }}>{data.windGusts.toFixed(1)} m/s</span>
</div>
</div>
</div>
{/* Right Column: Other Info */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', borderLeft: '1px solid var(--border-color)', paddingLeft: '1.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem' }} title={sensorTemp !== undefined ? (language === 'cs' ? 'Měřeno přímo senzorem na hrázi' : 'Measured by sensor at the dam') : 'OpenMeteo API'}>
<FiThermometer color="var(--color-orange)" />
<span style={{ fontWeight: 'bold' }}>{sensorTemp !== undefined ? sensorTemp.toFixed(1) : data.temp.toFixed(1)} °C</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
<FiSunrise color="#f59e0b" />
<span>{formatTime(data.sunrise)}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
<FiSunset color="#f59e0b" />
<span>{formatTime(data.sunset)}</span>
</div>
</div>
</div>
</div>
);
};