feat: implement weather radar component and update water resource data records. before river
This commit is contained in:
+33
-29
@@ -210,8 +210,13 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kpi-trend.positive { color: var(--color-green); }
|
||||
.kpi-trend.negative { color: var(--color-red); }
|
||||
.kpi-trend.positive {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.kpi-trend.negative {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background-color: var(--bg-card);
|
||||
@@ -330,19 +335,19 @@
|
||||
.mobile-only {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
.dashboard-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.sidebar.mobile-open {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
@@ -351,108 +356,107 @@
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.topbar-mobile-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.search-bar input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.search-bar {
|
||||
width: auto;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.search-bar svg {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
|
||||
.kpi-container {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.kpi-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.kpi-value {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
|
||||
.kpi-subtitle {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
|
||||
.chart-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.chart-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.chart-title {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
|
||||
.chart-controls {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.control-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.button-group {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.control-btn {
|
||||
padding: 0.5rem;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.chart-legend-container {
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.chart-legend-container > span {
|
||||
|
||||
.chart-legend-container>span {
|
||||
flex: 0 0 calc(50% - 0.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+4
-2
@@ -4,6 +4,7 @@ import LakeDetail from './components/LakeDetail';
|
||||
import LakesOverview from './components/LakesOverview';
|
||||
import LakeMap from './components/LakeMap';
|
||||
import FavoritesOverview from './components/FavoritesOverview';
|
||||
import WeatherRadar from './components/WeatherRadar';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Topbar from './components/Topbar';
|
||||
import SettingsModal from './components/SettingsModal';
|
||||
@@ -79,9 +80,10 @@ function App() {
|
||||
<Topbar language={language} onToggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)} />
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<LakesOverview language={language} />} />
|
||||
<Route path="/favorites" element={<FavoritesOverview language={language} />} />
|
||||
<Route path="/" element={<LakesOverview language={language} windUnit={windUnit} />} />
|
||||
<Route path="/favorites" element={<FavoritesOverview language={language} windUnit={windUnit} />} />
|
||||
<Route path="/map" element={<LakeMap language={language} />} />
|
||||
<Route path="/radar" element={<WeatherRadar language={language} />} />
|
||||
<Route path="/:slug" element={<LakeDetailWrapper language={language} windUnit={windUnit} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Lake {
|
||||
|
||||
interface Props {
|
||||
language: Language;
|
||||
windUnit?: 'kmh' | 'ms';
|
||||
}
|
||||
|
||||
const FavoritesOverview = ({ language }: Props) => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
@@ -6,6 +6,7 @@ export const t = {
|
||||
favorites: 'Favorites',
|
||||
lakes: 'Lakes & Reservoirs',
|
||||
map: 'Map',
|
||||
radar: 'Weather Radar',
|
||||
settings: 'Settings'
|
||||
},
|
||||
topbar: {
|
||||
@@ -78,6 +79,7 @@ export const t = {
|
||||
favorites: 'Oblíbené',
|
||||
lakes: 'Jezera a nádrže',
|
||||
map: 'Mapa',
|
||||
radar: 'Meteoradar',
|
||||
settings: 'Nastavení'
|
||||
},
|
||||
topbar: {
|
||||
|
||||
Reference in New Issue
Block a user