- (+0.02 m / 24h)
+
+
= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
+ ({(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{((data.levelDiff24h ?? 0) * 100).toFixed(1)} cm / 24h)
+
+
= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
+ ({(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{((data.levelDiff7d ?? 0) * 100).toFixed(1)} cm / 7d)
+
+
= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
+ ({(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{((data.levelDiff30d ?? 0) * 100).toFixed(1)} cm / 30d)
+
{/* Decorative Circle for Level */}
diff --git a/src/components/LakeDetail.tsx b/src/components/LakeDetail.tsx
index 4c205df..3e00328 100644
--- a/src/components/LakeDetail.tsx
+++ b/src/components/LakeDetail.tsx
@@ -42,7 +42,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
{label}
{[...payload].sort((a: any, b: any) => {
- const order = ['level', 'inflow', 'outflow'];
+ const order = ['level', 'inflow', 'outflow', 'temperature', 'precipitation'];
const indexA = order.indexOf(a.dataKey);
const indexB = order.indexOf(b.dataKey);
return (indexA === -1 ? 99 : indexA) - (indexB === -1 ? 99 : indexB);
@@ -53,6 +53,8 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
if (entry.dataKey === 'level') { labelStr = dict.level; unit = 'm n. m.'; color = 'var(--color-cyan)'; }
else if (entry.dataKey === 'outflow') { labelStr = dict.outflow; unit = 'm³/s'; color = 'var(--color-orange)'; }
else if (entry.dataKey === 'inflow') { labelStr = dict.inflow; unit = 'm³/s'; color = '#8b5cf6'; }
+ else if (entry.dataKey === 'temperature') { labelStr = language === 'cs' ? 'Teplota' : 'Temperature'; unit = '°C'; color = 'var(--color-red)'; }
+ else if (entry.dataKey === 'precipitation') { labelStr = language === 'cs' ? 'Srážky' : 'Precipitation'; unit = 'mm'; color = 'var(--color-cyan)'; }
if (!labelStr || entry.value === null || entry.value === undefined) return null;
@@ -157,8 +159,51 @@ const LakeDetail = ({ language, lakeId }: Props) => {
const animate = chartData.length < 150;
+ // Find record from 24h, 7d, 30d ago
+ const nowMs = new Date(latestData.timestamp).getTime();
+ const targetMs24h = nowMs - 24 * 60 * 60 * 1000;
+ const targetMs7d = nowMs - 7 * 24 * 60 * 60 * 1000;
+ const targetMs30d = nowMs - 30 * 24 * 60 * 60 * 1000;
+
+ let level24hAgo = latestData.level;
+ let level7dAgo = latestData.level;
+ let level30dAgo = latestData.level;
+
+ let minDiff24h = Infinity;
+ let minDiff7d = Infinity;
+ let minDiff30d = Infinity;
+
+ for (const d of data) {
+ const t = new Date(d.timestamp).getTime();
+
+ const diff24h = Math.abs(t - targetMs24h);
+ if (diff24h < minDiff24h) {
+ minDiff24h = diff24h;
+ level24hAgo = d.level;
+ }
+
+ const diff7d = Math.abs(t - targetMs7d);
+ if (diff7d < minDiff7d) {
+ minDiff7d = diff7d;
+ level7dAgo = d.level;
+ }
+
+ const diff30d = Math.abs(t - targetMs30d);
+ if (diff30d < minDiff30d) {
+ minDiff30d = diff30d;
+ level30dAgo = d.level;
+ }
+ }
+
+ const levelDiff24h = latestData.level - level24hAgo;
+ const levelDiff7d = latestData.level - level7dAgo;
+ const levelDiff30d = latestData.level - level30dAgo;
+
const kpiData = {
level: latestData.level,
+ levelDiff24h,
+ levelDiff7d,
+ levelDiff30d,
inflow: lastValidFlowData.inflow,
outflow: lastValidFlowData.outflow,
volume: lakeInfo?.volume || 0,
@@ -219,29 +264,14 @@ const LakeDetail = ({ language, lakeId }: Props) => {
{dict.inflow}
- {/* Smoothed Toggle Control */}
-
-
{dict.view}
-
-
{dict.raw}
-
setIsSmoothed(!isSmoothed)}
- >
-
{dict.smoothed}
-
-
-
-
- {/* WEATHER CHART SECTION */}
-
-
-
Počasí (Teplota a Srážky)
+ {/* WEATHER CHART SECTION */}
+
+
{language === 'cs' ? 'Počasí (Teplota a Srážky)' : 'Weather (Temperature & Precipitation)'}
-
+
-
+
v.toFixed(1)} />
@@ -256,8 +286,21 @@ const LakeDetail = ({ language, lakeId }: Props) => {
-
Teplota vzduchu [°C]
-
Srážky (24h) [mm]
+
{language === 'cs' ? 'Teplota vzduchu' : 'Temperature'} [°C]
+
{language === 'cs' ? 'Srážky' : 'Precipitation'} [mm]
+
+
+ {/* Smoothed Toggle Control */}
+
+
{dict.view}
+
+
{dict.raw}
+
setIsSmoothed(!isSmoothed)}
+ >
+
{dict.smoothed}
+
diff --git a/src/components/LakeMap.tsx b/src/components/LakeMap.tsx
index 2bcd99d..a584f12 100644
--- a/src/components/LakeMap.tsx
+++ b/src/components/LakeMap.tsx
@@ -3,7 +3,9 @@ import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { FiX, FiSearch, FiDroplet } from 'react-icons/fi';
-import { type Language } from '../translations';
+import { type Language, t } from '../translations';
+import { slugify } from '../utils/slugify';
+import { useNavigate } from 'react-router-dom';
interface LakeData {
id: string;
@@ -21,7 +23,6 @@ interface LakeData {
interface Props {
language: Language;
- onSelectLake: (id: string) => void;
}
// Create custom icon
@@ -39,8 +40,9 @@ const createCustomIcon = () => {
});
};
-const LakeMap = ({ language, onSelectLake }: Props) => {
+const LakeMap = ({ language }: Props) => {
const [lakes, setLakes] = useState
([]);
+ const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState('');
const [isPanelVisible, setIsPanelVisible] = useState(true);
@@ -77,7 +79,7 @@ const LakeMap = ({ language, onSelectLake }: Props) => {
position={[lake.lat, lake.lng]}
icon={customIcon}
eventHandlers={{
- click: () => onSelectLake(lake.id)
+ click: () => navigate(`/${slugify(lake.name)}`)
}}
>
@@ -114,7 +116,7 @@ const LakeMap = ({ language, onSelectLake }: Props) => {
{filteredLakes.map((lake, index) => (
-
onSelectLake(lake.id)}>
+
navigate(`/${slugify(lake.name)}`)}>
{index + 1}. Jezero {lake.name}
diff --git a/src/components/LakesOverview.tsx b/src/components/LakesOverview.tsx
index ff589b4..1b2532b 100644
--- a/src/components/LakesOverview.tsx
+++ b/src/components/LakesOverview.tsx
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react';
import { FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
import { type Language, t } from '../translations';
-import Topbar from './Topbar';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
+import { useNavigate } from 'react-router-dom';
+import { slugify } from '../utils/slugify';
interface Lake {
id: string;
@@ -19,7 +20,6 @@ interface Lake {
interface Props {
language: Language;
- onSelectLake: (id: string) => void;
}
const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number, size?: number, strokeWidth?: number }) => {
@@ -57,11 +57,16 @@ const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number
);
};
-const PriorityCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: string) => void }) => {
+const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => {
+ const navigate = useNavigate();
const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val }));
return (
-
+
navigate(`/${slugify(lake.name)}`)}
+ style={{ cursor: 'pointer', flex: 1, padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative' }}
+ >
{lake.name} {lake.river ? `- ${lake.river}` : ''}
@@ -73,7 +78,6 @@ const PriorityCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: s
Water level
{lake.level} m n.m.
-
Depth
@@ -105,72 +109,18 @@ const PriorityCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: s
Inflow {lake.inflow} m³/s
- / Outflow {lake.outflow} m³/s
- Inflow {lake.inflow} m³/s
- / Outflow {lake.outflow} m³/s
+ Outflow {lake.outflow} m³/s
-
-
-
- );
-};
-
-const SmallCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: string) => void }) => {
- const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val }));
-
- return (
-
onSelectLake(lake.id)}
- style={{ padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', cursor: 'pointer', transition: 'transform 0.2s', minHeight: '120px' }}
- onMouseOver={e => e.currentTarget.style.transform = 'translateY(-2px)'}
- onMouseOut={e => e.currentTarget.style.transform = 'translateY(0)'}
- >
-
-
-
- {lake.name}
-
-
{lake.level}
-
-
-
-
-
);
};
-const LakesOverview = ({ language, onSelectLake }: Props) => {
+const LakesOverview = ({ language }: Props) => {
const [lakes, setLakes] = useState
([]);
const [sortBy, setSortBy] = useState<'name' | 'level' | 'capacity' | 'inflow'>('name');
@@ -184,7 +134,6 @@ const LakesOverview = ({ language, onSelectLake }: Props) => {
const priorityLakes = lakes.filter(l => l.priority);
const otherLakes = lakes.filter(l => !l.priority);
- // Sorting
otherLakes.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'level') return b.level - a.level;
@@ -223,7 +172,7 @@ const LakesOverview = ({ language, onSelectLake }: Props) => {
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
gap: '1.5rem'
}}>
- {priorityLakes.map(lake => )}
+ {priorityLakes.map(lake => )}
)}
@@ -235,7 +184,7 @@ const LakesOverview = ({ language, onSelectLake }: Props) => {
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '1rem'
}}>
- {otherLakes.map(lake =>
)}
+ {otherLakes.map(lake =>
)}
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index 0ebfd9f..6176a5b 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -117,7 +117,7 @@ const SettingsModal = ({ language, setLanguage, theme, setTheme, onClose }: Prop
{/* Buy me a coffee */}
void;
- activeView: 'overview' | 'detail' | 'map';
- onNavigate: (view: 'overview' | 'detail' | 'map') => void;
isMobileMenuOpen?: boolean;
onCloseMobileMenu?: () => void;
}
-const Sidebar = ({ language, onOpenSettings, activeView, onNavigate, isMobileMenuOpen, onCloseMobileMenu }: Props) => {
+const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu }: Props) => {
const [isCollapsed, setIsCollapsed] = useState(false);
+ const navigate = useNavigate();
+ const location = useLocation();
const dict = t[language].sidebar;
+ const isOverview = location.pathname === '/';
+ const isMap = location.pathname === '/map';
+ const isDetail = !isOverview && !isMap;
+
+ const handleNavigate = (path: string) => {
+ navigate(path);
+ if (onCloseMobileMenu) onCloseMobileMenu();
+ };
+
return (
@@ -39,15 +49,15 @@ const Sidebar = ({ language, onOpenSettings, activeView, onNavigate, isMobileMen
-
onNavigate('detail')}>
+
handleNavigate('/lipno-1')}>
{dict.favorites}
-
onNavigate('overview')}>
+
handleNavigate('/')}>
{dict.lakes}
-
onNavigate('map')}>
+
handleNavigate('/map')}>
{dict.map}
diff --git a/src/main.tsx b/src/main.tsx
index bef5202..ade9d64 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
-
+
+
+
,
)
diff --git a/src/utils/slugify.ts b/src/utils/slugify.ts
new file mode 100644
index 0000000..68e6c9a
--- /dev/null
+++ b/src/utils/slugify.ts
@@ -0,0 +1,9 @@
+export const slugify = (text: string) => {
+ return text
+ .split(' - ')[0] // "VD Lipno 1 - Vltava" -> "VD Lipno 1"
+ .replace(/^VD\s+/i, '') // Remove "VD " prefix -> "Lipno 1"
+ .normalize('NFD') // Decompose diacritics
+ .replace(/[\u0300-\u036f]/g, '') // Remove diacritics
+ .toLowerCase()
+ .replace(/\s+/g, '-'); // Replace spaces with dashes -> "lipno-1"
+};