diff --git a/public/data/MARI.json b/public/data/MARI.json index 58d1d96..f2b4b10 100644 --- a/public/data/MARI.json +++ b/public/data/MARI.json @@ -465,10 +465,37 @@ { "timestamp": "2026-06-06T15:10:00.000Z", "level": 467.75, + "flow": 0.7, + "inflow": 0, + "volume": 0, + "temperature": 22.2, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 467.75, + "flow": 0.7, + "inflow": 0, + "volume": 0, + "temperature": 22.2, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 467.75, + "flow": 0.7, + "inflow": 0, + "volume": 0, + "temperature": 22.2, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 467.75, "flow": 0, "inflow": 2.24, "volume": 26.54, - "temperature": 22, + "temperature": 21.8, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/MZHR.json b/public/data/MZHR.json index 4673920..9781ba7 100644 --- a/public/data/MZHR.json +++ b/public/data/MZHR.json @@ -475,9 +475,36 @@ "timestamp": "2026-06-06T15:10:00.000Z", "level": 352.83, "flow": 2.52, + "inflow": 0, + "volume": 0, + "temperature": 22.9, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 352.83, + "flow": 2.52, + "inflow": 0, + "volume": 0, + "temperature": 22.9, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 352.84, + "flow": 2.52, + "inflow": 0, + "volume": 0, + "temperature": 22.9, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 352.84, + "flow": 0, "inflow": 1.47, "volume": 32.31, - "temperature": 22.7, + "temperature": 22.5, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLHN.json b/public/data/VLHN.json index b197b0b..2bd7e1a 100644 --- a/public/data/VLHN.json +++ b/public/data/VLHN.json @@ -476,6 +476,33 @@ "level": 369.83, "flow": 14.23, "inflow": 0, + "volume": 0, + "temperature": 22.5, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 369.83, + "flow": 14.23, + "inflow": 0, + "volume": 0, + "temperature": 22.5, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 369.83, + "flow": 14.23, + "inflow": 0, + "volume": 0, + "temperature": 22.5, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 369.83, + "flow": 14.23, + "inflow": 0, "volume": 20.37, "temperature": 22.6, "precipitation": 0 diff --git a/public/data/VLKO.json b/public/data/VLKO.json index 38aa456..c571598 100644 --- a/public/data/VLKO.json +++ b/public/data/VLKO.json @@ -475,9 +475,36 @@ "timestamp": "2026-06-06T15:10:00.000Z", "level": 352.43, "flow": 19.05, + "inflow": 0, + "volume": 0, + "temperature": 21.9, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 352.43, + "flow": 19.05, + "inflow": 0, + "volume": 0, + "temperature": 21.9, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 352.43, + "flow": 19.05, + "inflow": 0, + "volume": 0, + "temperature": 21.9, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 352.43, + "flow": 19.05, "inflow": 13.43, "volume": 2.74, - "temperature": 21.9, + "temperature": 22.1, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLL1.json b/public/data/VLL1.json index f35ac81..5b8bcb6 100644 --- a/public/data/VLL1.json +++ b/public/data/VLL1.json @@ -474,10 +474,37 @@ { "timestamp": "2026-06-06T15:10:00.000Z", "level": 723.09, + "flow": 1.51, + "inflow": 0, + "volume": 0, + "temperature": 20.8, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 723.09, + "flow": 1.51, + "inflow": 0, + "volume": 0, + "temperature": 20.8, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 723.09, + "flow": 1.51, + "inflow": 0, + "volume": 0, + "temperature": 20.8, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 723.09, "flow": 0, "inflow": 9.25, "volume": 199.67, - "temperature": 20.7, + "temperature": 20.6, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLL2.json b/public/data/VLL2.json index 7be54d7..0d99a8b 100644 --- a/public/data/VLL2.json +++ b/public/data/VLL2.json @@ -465,7 +465,7 @@ { "timestamp": "2026-06-06T15:00:00.000Z", "level": 558.44, - "flow": 0, + "flow": 7.51, "inflow": 0, "volume": 0, "temperature": 21.8, @@ -474,10 +474,37 @@ { "timestamp": "2026-06-06T15:10:00.000Z", "level": 558.43, + "flow": 7.51, + "inflow": 0, + "volume": 0, + "temperature": 21.8, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 558.41, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 21.8, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 558.38, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 21.8, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 558.37, "flow": 0, "inflow": 5.37, "volume": 0.35, - "temperature": 21.7, + "temperature": 21.6, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLOR.json b/public/data/VLOR.json index 4857002..f77d133 100644 --- a/public/data/VLOR.json +++ b/public/data/VLOR.json @@ -475,9 +475,36 @@ "timestamp": "2026-06-06T15:10:00.000Z", "level": 345.29, "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 22.6, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 345.29, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 22.6, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 345.29, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 22.6, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 345.29, + "flow": 0, "inflow": 24.39, "volume": 522.72, - "temperature": 22.6, + "temperature": 22.3, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLSL.json b/public/data/VLSL.json index f80c887..e847fd3 100644 --- a/public/data/VLSL.json +++ b/public/data/VLSL.json @@ -475,9 +475,36 @@ "timestamp": "2026-06-06T15:10:00.000Z", "level": 269.88, "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 23.5, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 269.88, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 23.5, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 269.88, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 23.5, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 269.89, + "flow": 0, "inflow": 81.06, "volume": 261.1, - "temperature": 23.5, + "temperature": 23.3, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLST.json b/public/data/VLST.json index cbb28e3..5bbf55b 100644 --- a/public/data/VLST.json +++ b/public/data/VLST.json @@ -475,9 +475,36 @@ "timestamp": "2026-06-06T15:10:00.000Z", "level": 216.98, "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 23.2, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:20:00.000Z", + "level": 216.96, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 23.2, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:30:00.000Z", + "level": 216.94, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 23.2, + "precipitation": 0 + }, + { + "timestamp": "2026-06-06T15:40:00.000Z", + "level": 216.95, + "flow": 0, "inflow": 48.25, "volume": 8.14, - "temperature": 23.2, + "temperature": 22.9, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/lakes_index.json b/public/data/lakes_index.json index 413904f..9eee985 100644 --- a/public/data/lakes_index.json +++ b/public/data/lakes_index.json @@ -33,9 +33,9 @@ "name": "Lipno II", "river": "Vltava", "priority": true, - "level": "558.43", + "level": "558.37", "capacity": 23.3, - "storageDiff": -2.07, + "storageDiff": -2.13, "inflow": "5.4", "outflow": "0.0", "volume": 0.35, @@ -43,9 +43,6 @@ "lat": 48.625, "lng": 14.318, "sparkline": [ - 558.63, - 558.62, - 558.6, 558.58, 558.56, 558.54, @@ -54,7 +51,10 @@ 558.49, 558.47, 558.44, - 558.43 + 558.43, + 558.41, + 558.38, + 558.37 ] }, { @@ -72,9 +72,9 @@ "lat": 49.183, "lng": 14.444, "sparkline": [ - 369.84, - 369.84, - 369.84, + 369.83, + 369.83, + 369.83, 369.83, 369.83, 369.83, @@ -101,9 +101,6 @@ "lat": 49.255, "lng": 14.398, "sparkline": [ - 352.44, - 352.43, - 352.43, 352.44, 352.44, 352.44, @@ -112,6 +109,9 @@ 352.43, 352.44, 352.44, + 352.43, + 352.43, + 352.43, 352.43 ] }, @@ -149,9 +149,9 @@ "name": "Slapy", "river": "Vltava", "priority": true, - "level": "269.88", + "level": "269.89", "capacity": 97, - "storageDiff": -0.72, + "storageDiff": -0.71, "inflow": "81.1", "outflow": "0.0", "volume": 261.1, @@ -159,9 +159,6 @@ "lat": 49.822, "lng": 14.436, "sparkline": [ - 269.88, - 269.89, - 269.89, 269.89, 269.88, 269.88, @@ -170,7 +167,10 @@ 269.88, 269.88, 269.88, - 269.88 + 269.88, + 269.88, + 269.88, + 269.89 ] }, { @@ -178,9 +178,9 @@ "name": "Štěchovice", "river": "Vltava", "priority": true, - "level": "216.98", + "level": "216.95", "capacity": 72.7, - "storageDiff": -2.42, + "storageDiff": -2.45, "inflow": "48.3", "outflow": "0.0", "volume": 8.14, @@ -188,9 +188,6 @@ "lat": 49.845, "lng": 14.412, "sparkline": [ - 217, - 216.97, - 216.99, 216.98, 216.95, 216.98, @@ -199,7 +196,10 @@ 216.98, 216.97, 216.95, - 216.98 + 216.98, + 216.96, + 216.94, + 216.95 ] }, { @@ -236,19 +236,16 @@ "name": "Hracholusky", "river": "Mže", "priority": true, - "level": "352.83", + "level": "352.84", "capacity": 57, - "storageDiff": -16.67, + "storageDiff": -16.66, "inflow": "1.5", - "outflow": "2.5", + "outflow": "0.0", "volume": 32.31, "maxVolume": 56.7, "lat": 49.789, "lng": 13.155, "sparkline": [ - 352.84, - 352.84, - 352.83, 352.84, 352.84, 352.84, @@ -257,7 +254,10 @@ 352.84, 352.84, 352.84, - 352.83 + 352.83, + 352.83, + 352.84, + 352.84 ] } ] \ No newline at end of file diff --git a/scripts/watchData.ts b/scripts/watchData.ts index a2f98c3..7bac7a2 100644 --- a/scripts/watchData.ts +++ b/scripts/watchData.ts @@ -1,25 +1,52 @@ import { execSync } from 'child_process'; -const args = process.argv.slice(2); -const minutes = parseInt(args[0], 10) || 10; -const intervalMs = minutes * 60 * 1000; +// How many minutes after the 10-minute mark should we run the scraper? +// The basin authority (PVL) generates data at HH:00, HH:10, HH:20... but it takes time to publish. +// 5 minutes (HH:05, HH:15...) is a safe buffer to avoid fetching outdated data. +const offsetMinutes = 5; console.log(`\n⏱️ HLADINATOR Watcher spuštěn!`); -console.log(`Budu automaticky stahovat nová data každých ${minutes} minut.\n`); +console.log(`Budu automaticky stahovat nová data vždy v časech končících na ${offsetMinutes} (např. 10:05, 10:15, 10:25...).\nTo zajistí, že má Povodí dostatek času data vygenerovat a nahrát.\n`); function runUpdate() { const now = new Date().toLocaleTimeString('cs-CZ'); console.log(`[${now}] 🔄 Spouštím npm run data:update...`); try { execSync('npm run data:update', { stdio: 'inherit' }); - console.log(`[${new Date().toLocaleTimeString('cs-CZ')}] ✅ Úspěšně hotovo. Další kontrola za ${minutes} minut...\n`); + console.log(`[${new Date().toLocaleTimeString('cs-CZ')}] ✅ Úspěšně hotovo.\n`); } catch (error: any) { console.error(`[${new Date().toLocaleTimeString('cs-CZ')}] ❌ Chyba při aktualizaci:`, error.message); } + scheduleNextRun(); } -// Spustit ihned po zapnutí -runUpdate(); +function scheduleNextRun() { + const now = new Date(); + const currentMinute = now.getMinutes(); + + // Find the next target minute (ending in 5) + // E.g. if it's 12, next will be 15. If it's 26, next will be 35. + let nextMinute = Math.floor(currentMinute / 10) * 10 + offsetMinutes; + if (nextMinute <= currentMinute) { + nextMinute += 10; + } + + const targetTime = new Date(now); + if (nextMinute >= 60) { + targetTime.setHours(targetTime.getHours() + 1); + targetTime.setMinutes(nextMinute % 60); + } else { + targetTime.setMinutes(nextMinute); + } + targetTime.setSeconds(0); + targetTime.setMilliseconds(0); + + const waitMs = targetTime.getTime() - now.getTime(); + + console.log(`[${new Date().toLocaleTimeString('cs-CZ')}] ⏳ Další stahování naplánováno na: ${targetTime.toLocaleTimeString('cs-CZ')} (za ${(waitMs / 60000).toFixed(1)} minut)\n`); + + setTimeout(runUpdate, waitMs); +} -// A pak periodicky v zadaném intervalu -setInterval(runUpdate, intervalMs); +// Run update immediately on first launch and then set the timer +runUpdate(); diff --git a/src/App.css b/src/App.css index 4fc42ae..98d74a9 100644 --- a/src/App.css +++ b/src/App.css @@ -7,12 +7,12 @@ } .sidebar { - width: 250px; + width: 190px; background-color: var(--bg-card); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; - padding: 1.5rem 1rem; + padding: 1.5rem 0.75rem; transition: width 0.3s ease; overflow: hidden; } @@ -75,8 +75,8 @@ .nav-item { display: flex; align-items: center; - gap: 1rem; - padding: 0.75rem 1rem; + gap: 0.75rem; + padding: 0.75rem 0.5rem; border-radius: 0.5rem; color: var(--text-muted); font-size: 0.95rem; @@ -86,6 +86,11 @@ white-space: nowrap; } +.nav-item svg { + flex-shrink: 0; + min-width: 20px; +} + .nav-item:hover { background-color: rgba(255, 255, 255, 0.03); color: var(--text-main); diff --git a/src/App.tsx b/src/App.tsx index d18b697..2516c81 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Routes, Route, useParams, useLocation, useNavigate, Navigate } from 'react-router-dom'; +import { Routes, Route, useParams, Navigate } from 'react-router-dom'; import LakeDetail from './components/LakeDetail'; import LakesOverview from './components/LakesOverview'; import LakeMap from './components/LakeMap'; diff --git a/src/components/FavoritesOverview.tsx b/src/components/FavoritesOverview.tsx index 8143c12..44de3f9 100644 --- a/src/components/FavoritesOverview.tsx +++ b/src/components/FavoritesOverview.tsx @@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet-async'; import { CircularProgress } from './CircularProgress'; import { useNavigate } from 'react-router-dom'; import { slugify } from '../utils/slugify'; -import { AreaChart, Area, ResponsiveContainer } from 'recharts'; +import { AreaChart, Area, ResponsiveContainer, YAxis } from 'recharts'; import { FiTrendingUp, FiTrendingDown } from 'react-icons/fi'; interface Lake { @@ -80,7 +80,21 @@ const FavoritesOverview = ({ language }: Props) => {
{favoriteLakes.map(lake => { const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val })); - const isFav = isFavorite(lake.id); + + const minVal = Math.min(...lake.sparkline); + const maxVal = Math.max(...lake.sparkline); + const diff = maxVal - minVal; + const padding = diff === 0 ? 0.1 : diff * 0.1; + const yDomain = [minVal - padding, maxVal + padding]; + + const firstVal = lake.sparkline[0] || 0; + const lastVal = lake.sparkline[lake.sparkline.length - 1] || 0; + const trendDiff = lastVal - firstVal; + + let trendColor = 'var(--color-cyan)'; + if (trendDiff > 0.01) trendColor = 'var(--color-green)'; + else if (trendDiff < -0.01) trendColor = 'var(--color-red)'; + return (
{
-
-
- - {t[language].kpi.inflow} {lake.inflow} m³/s +
+
+ + + + + + + + + + + +
-
- - {t[language].kpi.outflow} {lake.outflow} m³/s + +
+
+ + {t[language].kpi.inflow} {lake.inflow} m³/s +
+
+ + {t[language].kpi.outflow} {lake.outflow} m³/s +
diff --git a/src/components/KpiCards.tsx b/src/components/KpiCards.tsx index 4e096da..0b442dc 100644 --- a/src/components/KpiCards.tsx +++ b/src/components/KpiCards.tsx @@ -13,6 +13,9 @@ interface KpiData { volume: number; fullness: number; storageDiff?: number; + minDiff?: number; + avgInflow24h?: number; + avgOutflow24h?: number; } interface Props { @@ -37,43 +40,62 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => { return ( <> - {/* CARD 1: HLADINA */} + {/* CARD 1: WATER LEVEL */}
{dict.level} {lakeName}
-
+
{data.level.toFixed(2)} m n. m.
-
-
= 0 ? 'var(--color-green)' : 'var(--color-red)' }}> - ({(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{((data.levelDiff24h ?? 0) * 100).toFixed(1)} cm / 24h) +
+
+ 1D + = 0 ? 'var(--color-green)' : 'var(--color-red)' }}> + {(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{((data.levelDiff24h ?? 0) * 100).toFixed(1)} cm +
-
= 0 ? 'var(--color-green)' : 'var(--color-red)' }}> - ({(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{((data.levelDiff7d ?? 0) * 100).toFixed(1)} cm / 7d) +
+ 7D + = 0 ? 'var(--color-green)' : 'var(--color-red)' }}> + {(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{((data.levelDiff7d ?? 0) * 100).toFixed(1)} cm +
-
= 0 ? 'var(--color-green)' : 'var(--color-red)' }}> - ({(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{((data.levelDiff30d ?? 0) * 100).toFixed(1)} cm / 30d) +
+ 30D + = 0 ? 'var(--color-green)' : 'var(--color-red)' }}> + {(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{((data.levelDiff30d ?? 0) * 100).toFixed(1)} cm +
- {/* CARD 2: PRŮTOK */} + {/* CARD 2: FLOW */}
{dict.flow}
-
- +
+ {dict.inflow}: {data.inflow.toFixed(1)} m³/s
-
- + {data.avgInflow24h !== undefined && ( +
+ Ø 24h: {data.avgInflow24h.toFixed(1)} m³/s +
+ )} +
+ {dict.outflow}: {data.outflow.toFixed(1)} m³/s {flowDiff > 0 ? : flowDiff < 0 ? : null}
+ {data.avgOutflow24h !== undefined && ( +
+ Ø 24h: {data.avgOutflow24h.toFixed(1)} m³/s +
+ )}
{/* Flow Circle */} @@ -98,7 +120,7 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
- {/* CARD 3: NAPLNĚNOST */} + {/* CARD 3: CAPACITY */}
{dict.fullness} @@ -133,16 +155,23 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
)}
-
- - -
-
+
+
+
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)} m` : `${data.storageDiff.toFixed(2)} m`) : (data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A')}
-
- {dict.volume}: {data.volume.toFixed(1)} mil. m³ +
+ {dict.volume}: {data.volume.toFixed(1)} mil. m³
+ {data.minDiff !== undefined && ( +
+ {language === 'cs' ? 'K minimu:' : 'To min:'} {data.minDiff.toFixed(2)} m +
+ )} +
+ +
+
diff --git a/src/components/LakeDetail.tsx b/src/components/LakeDetail.tsx index 9c92144..605fe17 100644 --- a/src/components/LakeDetail.tsx +++ b/src/components/LakeDetail.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, ComposedChart, ReferenceLine, Bar } from 'recharts'; +import { ComposedChart, Area, Line, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; import { Helmet } from 'react-helmet-async'; import { type Language, t } from '../translations'; import KpiCards from './KpiCards'; @@ -30,7 +30,7 @@ interface Props { const CustomTooltip = ({ active, payload, label, language, isWeather }: any) => { if (active && payload && payload.length) { - const dict = t[language].chart; + const dict = t[language as Language].chart; if (isWeather) { return (
@@ -118,7 +118,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { volume: item.volume || 0, fullness: 0, temperature: item.temperature, - precipitation: item.precipitation + precipitation: item.precipitation === null ? undefined : item.precipitation }; }); setData(formattedData); @@ -190,6 +190,10 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { let minDiff7d = Infinity; let minDiff30d = Infinity; + let inflowSum24h = 0; + let outflowSum24h = 0; + let flowCount24h = 0; + for (const d of data) { const t = new Date(d.timestamp).getTime(); @@ -210,11 +214,23 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { minDiff30d = diff30d; level30dAgo = d.level; } + + if (t >= targetMs24h && d.inflow !== undefined && d.outflow !== undefined) { + inflowSum24h += d.inflow; + outflowSum24h += d.outflow; + flowCount24h++; + } } const levelDiff24h = latestData.level - level24hAgo; const levelDiff7d = latestData.level - level7dAgo; const levelDiff30d = latestData.level - level30dAgo; + + const avgInflow24h = flowCount24h > 0 ? inflowSum24h / flowCount24h : undefined; + const avgOutflow24h = flowCount24h > 0 ? outflowSum24h / flowCount24h : undefined; + + const limits = lakeInfo ? NAVIGATION_LIMITS[lakeInfo.id] : undefined; + const staticConfig = lakeInfo ? lakesConfig.find(l => l.id === lakeInfo.id) : undefined; const kpiData = { level: latestData.level, @@ -225,12 +241,12 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { outflow: lastValidFlowData.outflow, volume: lakeInfo?.volume || 0, fullness: lakeInfo?.capacity || 0, - storageDiff: lakeInfo?.storageDiff + storageDiff: lakeInfo?.storageDiff, + minDiff: staticConfig?.minLevel ? latestData.level - staticConfig.minLevel : undefined, + avgInflow24h, + avgOutflow24h }; - const limits = lakeInfo ? NAVIGATION_LIMITS[lakeInfo.id] : undefined; - const staticConfig = lakeInfo ? lakesConfig.find(l => l.id === lakeInfo.id) : undefined; - const leftYAxisDomain = [ (dataMin: number) => { let min = dataMin; @@ -284,7 +300,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { {lakeInfo && lakeInfo.lat && lakeInfo.lng && ( - + )}
@@ -343,14 +359,14 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { ))} {staticConfig && staticConfig.maxLevel && ( - + )} {staticConfig && staticConfig.storageLevel && ( - + )} - - + +
@@ -358,8 +374,8 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => { {/* Chart Legend */}
{dict.level}
-
{dict.outflow}
-
{dict.inflow}
+
{dict.outflow}
+
{dict.inflow}
{/* WEATHER CHART SECTION */} diff --git a/src/components/LakeMap.tsx b/src/components/LakeMap.tsx index 280a54e..2f4e473 100644 --- a/src/components/LakeMap.tsx +++ b/src/components/LakeMap.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; 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 { FiX, FiSearch } from 'react-icons/fi'; import { type Language, t } from '../translations'; import { slugify } from '../utils/slugify'; import { useNavigate } from 'react-router-dom'; diff --git a/src/components/LakesOverview.tsx b/src/components/LakesOverview.tsx index 3356480..1d995a5 100644 --- a/src/components/LakesOverview.tsx +++ b/src/components/LakesOverview.tsx @@ -33,9 +33,17 @@ const LakeCard = ({ lake, language, isFav, onToggleFav }: { lake: Lake, language const minVal = Math.min(...lake.sparkline); const maxVal = Math.max(...lake.sparkline); const diff = maxVal - minVal; - // Enforce a minimum visual span of 0.5 meters so tiny fluctuations don't look like mountains - const padding = diff < 0.5 ? (0.5 - diff) / 2 : 0; + const padding = diff === 0 ? 0.1 : diff * 0.1; // dynamic 10% padding const yDomain = [minVal - padding, maxVal + padding]; + + const firstVal = lake.sparkline[0] || 0; + const lastVal = lake.sparkline[lake.sparkline.length - 1] || 0; + const trendDiff = lastVal - firstVal; + + // Dynamic color based on trend direction: stable=cyan, rising=green, falling=red + let trendColor = 'var(--color-cyan)'; + if (trendDiff > 0.01) trendColor = 'var(--color-green)'; + else if (trendDiff < -0.01) trendColor = 'var(--color-red)'; return (
- - - + + + - +
@@ -120,54 +128,9 @@ const LakeCard = ({ lake, language, isFav, onToggleFav }: { lake: Lake, language ); }; -const SmallLakeCard = ({ lake, isFav, onToggleFav }: { lake: Lake, isFav: boolean, onToggleFav: (id: string) => void }) => { - const navigate = useNavigate(); - - return ( -
navigate(`/${slugify(lake.name)}`)} - style={{ cursor: 'pointer', padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', position: 'relative' }} - > - {/* Star button */} - - -
{lake.name}
-
{lake.level} m n.m.
-
- = 80 ? 'var(--color-green)' : lake.capacity < 40 ? 'var(--color-red)' : 'var(--text-muted)', fontWeight: 600 }}> - {lake.capacity > 0 ? `${lake.capacity}%` : 'N/A'} - - {lake.storageDiff !== undefined && ( - = 0 ? 'var(--color-green)' : 'var(--color-red)', marginLeft: '4px' }}> - ({lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m) - - )} -
-
- ); -}; - const LakesOverview = ({ language }: Props) => { const [lakes, setLakes] = useState([]); - const { isFavorite, toggleFavorite, favorites } = useFavorites(); + const { isFavorite, toggleFavorite } = useFavorites(); useEffect(() => { const loadData = () => { @@ -234,6 +197,19 @@ const LakesOverview = ({ language }: Props) => {
)} + + {otherLakes.length > 0 && ( +
+

{language === 'cs' ? 'Ostatní' : 'Other'}

+
+ {otherLakes.map(lake => )} +
+
+ )}
); }; diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index bf26f87..1cc11d2 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,4 +1,4 @@ -import { FiX, FiMoon, FiSun, FiGlobe, FiCoffee, FiWind } from 'react-icons/fi'; +import { FiX, FiMoon, FiSun, FiCoffee, FiWind } from 'react-icons/fi'; import { type Language, t } from '../translations'; interface Props { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index ccb6783..61661ff 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -29,18 +29,19 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu return (
-
- +
+
- HLADINATOR + HLADINATOR v1.0
- - {/* Toggle Button */} +
+ + {/* Toggle Button */} +