feat: implement weather radar component and update water resource data records. before river

This commit is contained in:
David Fencl
2026-06-06 21:04:19 +02:00
parent 231961da19
commit ec540e056d
49 changed files with 1038 additions and 198 deletions
+1
View File
@@ -29,6 +29,7 @@ interface Lake {
interface Props {
language: Language;
windUnit?: 'kmh' | 'ms';
}
const FavoritesOverview = ({ language }: Props) => {
+1
View File
@@ -28,6 +28,7 @@ interface Lake {
interface Props {
language: Language;
windUnit?: 'kmh' | 'ms';
}
const LakeCard = ({ lake, language, isFav, onToggleFav }: { lake: Lake, language: Language, isFav: boolean, onToggleFav: (id: string) => void }) => {
+17 -10
View File
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { FiDroplet, FiStar, FiMap, FiSettings, FiChevronLeft, FiChevronRight, FiDatabase } from 'react-icons/fi';
import { FiDroplet, FiStar, FiMap, FiSettings, FiChevronLeft, FiChevronRight, FiDatabase, FiCloudRain } from 'react-icons/fi';
import { type Language, t } from '../translations';
import { useFavorites } from '../hooks/useFavorites';
@@ -21,6 +21,7 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
const isOverview = location.pathname === '/';
const isFavoritesPage = location.pathname === '/favorites';
const isMap = location.pathname === '/map';
const isRadar = location.pathname === '/radar';
const handleNavigate = (path: string) => {
navigate(path);
@@ -29,17 +30,17 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
return (
<div className={`sidebar ${isCollapsed ? 'collapsed' : ''} ${isMobileMenuOpen ? 'mobile-open' : ''}`}>
<div className="sidebar-logo">
<FiDroplet size={28} color="var(--color-cyan)" />
<div className="sidebar-text">
<span style={{ fontWeight: 'bold', letterSpacing: '0.5px', fontSize: '1.1rem' }}>HLADINATOR</span>
<small>v1.0</small>
<div className="sidebar-logo" style={{ alignItems: 'center', gap: '0.4rem' }}>
<FiDroplet size={34} color="var(--color-cyan)" style={{ marginLeft: '-4px', flexShrink: 0 }} />
<div className="sidebar-text" style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<span style={{ fontWeight: 'bold', letterSpacing: '0.5px', fontSize: '1.15rem', lineHeight: 1 }}>HLADINATOR</span>
<small style={{ position: 'absolute', top: '100%', left: '2px', marginTop: '6px', lineHeight: 1, fontSize: '0.75rem', fontWeight: 'bold', color: 'var(--text-muted)' }}>v1.0</small>
</div>
</div>
{/* Toggle Button */}
<div style={{ display: 'flex', justifyContent: isCollapsed ? 'center' : 'flex-end', marginBottom: '1.5rem', marginTop: isCollapsed ? '1rem' : '-0.5rem' }}>
<button
<div style={{ display: 'flex', justifyContent: isCollapsed ? 'center' : 'flex-end', marginBottom: '0.5rem', marginTop: isCollapsed ? '0.5rem' : '-1.5rem' }}>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
style={{
background: 'var(--bg-card)', border: '1px solid var(--border-color)', color: 'var(--text-main)',
@@ -50,7 +51,7 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
{isCollapsed ? <FiChevronRight size={18} /> : <FiChevronLeft size={18} />}
</button>
</div>
<div className="nav-links">
{/* Favourites */}
<div className={`nav-item ${isFavoritesPage ? 'active' : ''}`} onClick={() => handleNavigate('/favorites')} style={{ position: 'relative' }}>
@@ -92,6 +93,12 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
<FiMap />
<span className="sidebar-text">{dict.map}</span>
</div>
{/* Radar */}
<div className={`nav-item ${isRadar ? 'active' : ''}`} onClick={() => handleNavigate('/radar')}>
<FiCloudRain />
<span className="sidebar-text">{dict.radar}</span>
</div>
</div>
<div className="sidebar-footer">
+66 -20
View File
@@ -8,6 +8,11 @@ interface Props {
export const Tooltip = ({ content, children }: Props) => {
const [show, setShow] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const [positionStyle, setPositionStyle] = useState<React.CSSProperties>({
left: '50%',
transform: 'translateX(-50%)',
});
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
@@ -19,6 +24,45 @@ export const Tooltip = ({ content, children }: Props) => {
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
useEffect(() => {
if (!show) {
setPositionStyle({
left: '50%',
transform: 'translateX(-50%)',
});
return;
}
// Measure and adjust positioning to prevent overflow
const adjustPosition = () => {
if (tooltipRef.current) {
const rect = tooltipRef.current.getBoundingClientRect();
const windowWidth = window.innerWidth;
const padding = 12; // safety margin from screen edges
if (rect.right > windowWidth - padding) {
setPositionStyle({
right: '0px',
left: 'auto',
transform: 'none',
});
} else if (rect.left < padding) {
setPositionStyle({
left: '0px',
transform: 'none',
});
}
}
};
// Run adjustment immediately
adjustPosition();
// Also adjust on resize
window.addEventListener('resize', adjustPosition);
return () => window.removeEventListener('resize', adjustPosition);
}, [show]);
return (
<div
ref={containerRef}
@@ -32,26 +76,28 @@ export const Tooltip = ({ content, children }: Props) => {
>
{children}
{show && (
<div style={{
position: 'absolute',
bottom: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginBottom: '8px',
backgroundColor: 'var(--bg-card)',
border: '1px solid var(--border-color)',
padding: '0.5rem 0.75rem',
borderRadius: '8px',
width: 'max-content',
maxWidth: '220px',
zIndex: 9999,
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
color: 'var(--text-main)',
fontSize: '0.8rem',
lineHeight: 1.4,
textAlign: 'center',
pointerEvents: 'none'
}}>
<div
ref={tooltipRef}
style={{
position: 'absolute',
bottom: '100%',
marginBottom: '8px',
backgroundColor: 'var(--bg-card)',
border: '1px solid var(--border-color)',
padding: '0.5rem 0.75rem',
borderRadius: '8px',
width: 'max-content',
maxWidth: '220px',
zIndex: 9999,
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
color: 'var(--text-main)',
fontSize: '0.8rem',
lineHeight: 1.4,
textAlign: 'center',
pointerEvents: 'none',
...positionStyle
}}
>
{content}
</div>
)}
+39
View File
@@ -0,0 +1,39 @@
import { Helmet } from 'react-helmet-async';
import { type Language, t } from '../translations';
interface Props {
language: Language;
}
const WeatherRadar = ({ language }: Props) => {
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Helmet>
<title>{t[language].sidebar.radar} | Hladinátor</title>
</Helmet>
<div style={{ padding: '0 1.5rem 1rem' }}>
<h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0 0 0.5rem 0', color: 'var(--text-main)' }}>
{t[language].sidebar.radar}
</h1>
<p style={{ margin: 0, color: 'var(--text-muted)' }}>
{language === 'cs' ? 'Aktuální srážkový radar a předpověď počasí pro celou ČR.' : 'Current precipitation radar and weather forecast for the Czech Republic.'}
</p>
</div>
<div style={{ flex: 1, minHeight: '600px', borderRadius: '12px', overflow: 'hidden', margin: '0 1.5rem 1.5rem 1.5rem', border: '1px solid var(--border-color)', backgroundColor: 'var(--bg-card)' }}>
<iframe
width="100%"
height="100%"
src={`https://embed.windy.com/embed.html?type=map&location=coordinates&metricRain=mm&metricTemp=%C2%B0C&metricWind=km/h&zoom=7&overlay=radar&product=radar&level=surface&lat=49.8&lon=15.5&message=true&lang=${language}`}
frameBorder="0"
title="Windy Weather Radar"
style={{ display: 'block' }}
allowFullScreen
></iframe>
</div>
</div>
);
};
export default WeatherRadar;