feat: implement multilingual SEO support and enhance map UI with data synchronization updates

This commit is contained in:
David Fencl
2026-06-06 17:24:30 +02:00
parent 66021e001e
commit 6395df1992
30 changed files with 3036 additions and 280 deletions
+63
View File
@@ -0,0 +1,63 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
import fs from 'fs';
import https from 'https';
async function compare() {
const URL = 'https://www.pvl.cz/portal/nadrze/cz/pc/Mereni.aspx?oid=1&id=VLL1';
const agent = new https.Agent({ rejectUnauthorized: false });
const response = await axios.get(URL, { httpsAgent: agent });
const $ = cheerio.load(response.data);
let tblFound = null;
$('table').each((i, tbl) => {
if ($(tbl).text().includes('Datum') && $(tbl).text().includes('Odtok')) {
tblFound = $(tbl);
}
});
const pvlRows = [];
if (tblFound) {
tblFound.find('tr').each((i, row) => {
if (i === 0) return;
const cols = $(row).find('td');
if (cols.length >= 3) {
const rawDate = $(cols[0]).text().trim();
const levelStr = $(cols[1]).text().trim().replace(',', '.');
let flowStr = $(cols[2]).text().trim().replace(',', '.');
if (flowStr === '' && cols.length >= 4) {
flowStr = $(cols[3]).text().trim().replace(',', '.');
}
pvlRows.push({
date: rawDate,
level: parseFloat(levelStr),
flow: parseFloat(flowStr)
});
}
});
}
const localData = JSON.parse(fs.readFileSync('public/data/VLL1.json', 'utf-8'));
// Sort local data descending (newest first) to match PVL which is newest first
const sortedLocal = localData.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
console.log('--- POROVNÁNÍ DAT: LIPNO 1 ---');
console.log(String('PVL.CZ').padEnd(40) + ' | ' + 'NAŠE LOKÁLNÍ DATABÁZE');
console.log('-'.repeat(85));
for (let i = 0; i < Math.min(10, pvlRows.length); i++) {
const p = pvlRows[i];
const l = sortedLocal[i];
// Format our local UTC timestamp back to something readable
const d = new Date(l.timestamp);
const localDateStr = `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth()+1).toString().padStart(2, '0')}.${d.getFullYear()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
const pvlStr = `[${p.date}] H: ${p.level} m, O: ${p.flow} m3/s`.padEnd(40);
const locStr = `[${localDateStr}] H: ${l.level} m, O: ${l.flow} m3/s, P: ${l.inflow} m3/s`;
console.log(`${pvlStr} | ${locStr}`);
}
}
compare().catch(console.error);
+6 -1
View File
@@ -4,7 +4,12 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/jpeg" href="/favicon.jpg" /> <link rel="icon" type="image/jpeg" href="/favicon.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Davis Fencl</title> <title>Hladinátor - Aktuální stav přehrad a nádrží</title>
<meta name="description" content="Sledujte aktuální vodní stav, průtok a vývoj počasí na českých přehradách a nádržích v reálném čase." />
<meta property="og:title" content="Hladinátor - Aktuální stav přehrad a nádrží" />
<meta property="og:description" content="Sledujte aktuální vodní stav, průtok a vývoj počasí na českých přehradách a nádržích v reálném čase." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://hladinator.cz" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+48 -1
View File
@@ -14,6 +14,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-helmet-async": "^3.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
@@ -3936,6 +3937,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -4016,7 +4026,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@@ -4219,6 +4228,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4688,6 +4709,26 @@
"react": "^19.2.0" "react": "^19.2.0"
} }
}, },
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-helmet-async": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-3.0.0.tgz",
"integrity": "sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==",
"license": "Apache-2.0",
"dependencies": {
"invariant": "^2.2.4",
"react-fast-compare": "^3.2.2",
"shallowequal": "^1.1.0"
},
"peerDependencies": {
"react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-icons": { "node_modules/react-icons": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
@@ -4957,6 +4998,12 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+1
View File
@@ -24,6 +24,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-helmet-async": "^3.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
+242 -1
View File
@@ -226,8 +226,249 @@
{ {
"timestamp": "2026-06-06T10:30:00.000Z", "timestamp": "2026-06-06T10:30:00.000Z",
"level": 467.74, "level": 467.74,
"flow": 0, "flow": 0.7,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 467.74,
"flow": 0,
"inflow": 2.24,
"volume": 26.53,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 467.74,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 467.74,
"flow": 0,
"inflow": 2.24,
"volume": 26.54,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 467.75,
"flow": 0,
"inflow": 2.24,
"volume": 26.54,
"temperature": 21.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 21.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 467.75,
"flow": 0,
"inflow": 2.24,
"volume": 26.54,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 467.75,
"flow": 0,
"inflow": 2.24,
"volume": 26.54,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 467.75,
"flow": 0.7,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 467.75,
"flow": 0,
"inflow": 2.24,
"volume": 26.54,
"temperature": 22,
"precipitation": 0
} }
] ]
+250
View File
@@ -229,5 +229,255 @@
"flow": 2.52, "flow": 2.52,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 352.84,
"flow": 0,
"inflow": 1.47,
"volume": 32.3,
"temperature": 20.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 20.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 20.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 20.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 20.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 20.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 1.47,
"volume": 32.31,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 352.83,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 21.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 352.84,
"flow": 0,
"inflow": 1.47,
"volume": 32.28,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 352.84,
"flow": 2.53,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 352.83,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 352.84,
"flow": 0,
"inflow": 1.47,
"volume": 32.3,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 352.83,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 1.47,
"volume": 32.29,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 352.84,
"flow": 2.52,
"inflow": 0,
"volume": 0,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 352.83,
"flow": 2.52,
"inflow": 1.47,
"volume": 32.31,
"temperature": 22.7,
"precipitation": 0
} }
] ]
+250
View File
@@ -229,5 +229,255 @@
"flow": 2.5, "flow": 2.5,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 20.4,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 20.4,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 369.84,
"flow": 2.5,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 369.84,
"flow": 8.79,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 20.4,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 369.84,
"flow": 14.24,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 369.83,
"flow": 14.22,
"inflow": 0,
"volume": 20.4,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 369.83,
"flow": 14.22,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 369.83,
"flow": 14.22,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 369.83,
"flow": 14.22,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 369.83,
"flow": 14.22,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 369.83,
"flow": 14.22,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 369.83,
"flow": 14.22,
"inflow": 0,
"volume": 20.37,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 369.83,
"flow": 14.23,
"inflow": 0,
"volume": 0,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 369.83,
"flow": 14.23,
"inflow": 0,
"volume": 20.37,
"temperature": 22.6,
"precipitation": 0
} }
] ]
+250
View File
@@ -229,5 +229,255 @@
"flow": 19.05, "flow": 19.05,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 352.5,
"flow": 19.05,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 352.49,
"flow": 19.05,
"inflow": 13.43,
"volume": 2.77,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 352.48,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 352.48,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 352.47,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 352.47,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 352.47,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 352.46,
"flow": 19.05,
"inflow": 13.43,
"volume": 2.76,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 352.46,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 352.45,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 352.45,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 13.43,
"volume": 2.75,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 352.43,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 352.43,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 13.43,
"volume": 2.74,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 352.43,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 352.43,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 13.43,
"volume": 2.74,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 352.44,
"flow": 19.05,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 352.43,
"flow": 19.05,
"inflow": 13.43,
"volume": 2.74,
"temperature": 21.9,
"precipitation": 0
} }
] ]
+250
View File
@@ -229,5 +229,255 @@
"flow": 1.51, "flow": 1.51,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 723.09,
"flow": 0,
"inflow": 9.25,
"volume": 199.67,
"temperature": 19.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 19.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 19.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 19.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 19.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 19.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 9.25,
"volume": 199.67,
"temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 723.09,
"flow": 0,
"inflow": 9.25,
"volume": 199.67,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 9.25,
"volume": 199.67,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 723.09,
"flow": 0,
"inflow": 9.25,
"volume": 199.67,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 723.09,
"flow": 1.51,
"inflow": 0,
"volume": 0,
"temperature": 20.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 723.09,
"flow": 0,
"inflow": 9.25,
"volume": 199.67,
"temperature": 20.7,
"precipitation": 0
} }
] ]
+252 -2
View File
@@ -212,14 +212,14 @@
{ {
"timestamp": "2026-06-06T10:10:00.000Z", "timestamp": "2026-06-06T10:10:00.000Z",
"level": 558.94, "level": 558.94,
"flow": 0, "flow": 7.18,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
}, },
{ {
"timestamp": "2026-06-06T10:20:00.000Z", "timestamp": "2026-06-06T10:20:00.000Z",
"level": 558.93, "level": 558.93,
"flow": 0, "flow": 7.18,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
}, },
@@ -229,5 +229,255 @@
"flow": 0, "flow": 0,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 558.9,
"flow": 0,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 558.88,
"flow": 0,
"inflow": 5.37,
"volume": 0.45,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 558.87,
"flow": 7.22,
"inflow": 0,
"volume": 0,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 558.86,
"flow": 7.24,
"inflow": 0,
"volume": 0,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 558.84,
"flow": 7.27,
"inflow": 0,
"volume": 0,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 558.82,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 558.81,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 20.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 558.79,
"flow": 0,
"inflow": 5.37,
"volume": 0.43,
"temperature": 21.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 558.77,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 558.75,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 558.74,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 558.72,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 558.71,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.3,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 558.68,
"flow": 0,
"inflow": 5.37,
"volume": 0.41,
"temperature": 21.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 558.67,
"flow": 7.53,
"inflow": 0,
"volume": 0,
"temperature": 21.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 558.65,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 558.63,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 558.62,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 558.6,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 558.58,
"flow": 0,
"inflow": 5.37,
"volume": 0.39,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 558.56,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 558.54,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 558.52,
"flow": 7.51,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 558.5,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 558.49,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 558.47,
"flow": 0,
"inflow": 5.37,
"volume": 0.37,
"temperature": 21.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 558.44,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.8,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 558.43,
"flow": 0,
"inflow": 5.37,
"volume": 0.35,
"temperature": 21.7,
"precipitation": 0
} }
] ]
+250
View File
@@ -229,5 +229,255 @@
"flow": 0, "flow": 0,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 345.27,
"flow": 0,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 345.27,
"flow": 0,
"inflow": 24.39,
"volume": 522.32,
"temperature": 21,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 24.39,
"volume": 522.52,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 24.39,
"volume": 522.52,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 345.28,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 24.39,
"volume": 522.52,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.1,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 24.39,
"volume": 522.72,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 345.29,
"flow": 0,
"inflow": 24.39,
"volume": 522.72,
"temperature": 22.6,
"precipitation": 0
} }
] ]
+250
View File
@@ -229,5 +229,255 @@
"flow": 0, "flow": 0,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 81.06,
"volume": 261.23,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 269.87,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 81.06,
"volume": 261.01,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.7,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 81.06,
"volume": 261.17,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.9,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 269.89,
"flow": 0,
"inflow": 81.06,
"volume": 261.08,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 81.06,
"volume": 261.14,
"temperature": 23.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 269.88,
"flow": 0,
"inflow": 81.06,
"volume": 261.1,
"temperature": 23.5,
"precipitation": 0
} }
] ]
+250
View File
@@ -229,5 +229,255 @@
"flow": 0, "flow": 0,
"inflow": 0, "inflow": 0,
"volume": 0 "volume": 0
},
{
"timestamp": "2026-06-06T10:40:00.000Z",
"level": 217.04,
"flow": 0,
"inflow": 0,
"volume": 0
},
{
"timestamp": "2026-06-06T10:50:00.000Z",
"level": 217.05,
"flow": 0,
"inflow": 48.25,
"volume": 8.23,
"temperature": 21.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:00:00.000Z",
"level": 217.01,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:10:00.000Z",
"level": 217.02,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:20:00.000Z",
"level": 217.03,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:30:00.000Z",
"level": 217.01,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:40:00.000Z",
"level": 217.01,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 21.4,
"precipitation": 0
},
{
"timestamp": "2026-06-06T11:50:00.000Z",
"level": 217.02,
"flow": 0,
"inflow": 48.25,
"volume": 8.19,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:00:00.000Z",
"level": 217.01,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:10:00.000Z",
"level": 217,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:20:00.000Z",
"level": 217.01,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:30:00.000Z",
"level": 216.98,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:40:00.000Z",
"level": 216.99,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.6,
"precipitation": 0
},
{
"timestamp": "2026-06-06T12:50:00.000Z",
"level": 217,
"flow": 0,
"inflow": 48.25,
"volume": 8.19,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:00:00.000Z",
"level": 216.96,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:10:00.000Z",
"level": 216.98,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:20:00.000Z",
"level": 217,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:30:00.000Z",
"level": 216.97,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:40:00.000Z",
"level": 216.99,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 22.5,
"precipitation": 0
},
{
"timestamp": "2026-06-06T13:50:00.000Z",
"level": 216.98,
"flow": 0,
"inflow": 48.25,
"volume": 8.14,
"temperature": 23,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:00:00.000Z",
"level": 216.95,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:10:00.000Z",
"level": 216.98,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:20:00.000Z",
"level": 216.97,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:30:00.000Z",
"level": 216.94,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:40:00.000Z",
"level": 216.98,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23,
"precipitation": 0
},
{
"timestamp": "2026-06-06T14:50:00.000Z",
"level": 216.97,
"flow": 0,
"inflow": 48.25,
"volume": 8.14,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:00:00.000Z",
"level": 216.95,
"flow": 0,
"inflow": 0,
"volume": 0,
"temperature": 23.2,
"precipitation": 0
},
{
"timestamp": "2026-06-06T15:10:00.000Z",
"level": 216.98,
"flow": 0,
"inflow": 48.25,
"volume": 8.14,
"temperature": 23.2,
"precipitation": 0
} }
] ]
+122 -122
View File
@@ -5,11 +5,11 @@
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "723.09", "level": "723.09",
"capacity": 76.3, "capacity": 65.3,
"storageDiff": -1.81, "storageDiff": -1.81,
"inflow": "0.0", "inflow": "9.3",
"outflow": "1.5", "outflow": "0.0",
"volume": 233.5, "volume": 199.67,
"maxVolume": 306, "maxVolume": 306,
"lat": 48.6322, "lat": 48.6322,
"lng": 14.2215, "lng": 14.2215,
@@ -33,28 +33,28 @@
"name": "Lipno II", "name": "Lipno II",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "558.92", "level": "558.43",
"capacity": 34.9, "capacity": 23.3,
"storageDiff": -1.58, "storageDiff": -2.07,
"inflow": "0.0", "inflow": "5.4",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.5, "volume": 0.35,
"maxVolume": 1.5, "maxVolume": 1.5,
"lat": 48.625, "lat": 48.625,
"lng": 14.318, "lng": 14.318,
"sparkline": [ "sparkline": [
559.59, 558.63,
559.52, 558.62,
559.44, 558.6,
559.37, 558.58,
559.29, 558.56,
559.21, 558.54,
559.13, 558.52,
559.05, 558.5,
558.96, 558.49,
558.94, 558.47,
558.93, 558.44,
558.92 558.43
] ]
}, },
{ {
@@ -62,28 +62,28 @@
"name": "Hněvkovice", "name": "Hněvkovice",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "369.84", "level": "369.83",
"capacity": 88, "capacity": 96.5,
"storageDiff": -0.26, "storageDiff": -0.27,
"inflow": "0.0", "inflow": "0.0",
"outflow": "2.5", "outflow": "14.2",
"volume": 18.6, "volume": 20.37,
"maxVolume": 21.1, "maxVolume": 21.1,
"lat": 49.183, "lat": 49.183,
"lng": 14.444, "lng": 14.444,
"sparkline": [ "sparkline": [
369.84,
369.84,
369.84,
369.83, 369.83,
369.84,
369.85,
369.84,
369.84,
369.84,
369.81,
369.83, 369.83,
369.84, 369.83,
369.84, 369.83,
369.84, 369.83,
369.84 369.83,
369.83,
369.83,
369.83
] ]
}, },
{ {
@@ -91,28 +91,28 @@
"name": "Kořensko", "name": "Kořensko",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "352.50", "level": "352.43",
"capacity": 33.3, "capacity": 97.9,
"storageDiff": -0.1, "storageDiff": -0.17,
"inflow": "0.0", "inflow": "13.4",
"outflow": "19.1", "outflow": "19.1",
"volume": 0.9, "volume": 2.74,
"maxVolume": 2.8, "maxVolume": 2.8,
"lat": 49.255, "lat": 49.255,
"lng": 14.398, "lng": 14.398,
"sparkline": [ "sparkline": [
352.47, 352.44,
352.48, 352.43,
352.49, 352.43,
352.52, 352.44,
352.56, 352.44,
352.57, 352.44,
352.56, 352.44,
352.55, 352.43,
352.52, 352.43,
352.51, 352.44,
352.51, 352.44,
352.5 352.43
] ]
}, },
{ {
@@ -120,28 +120,28 @@
"name": "Orlík", "name": "Orlík",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "345.27", "level": "345.29",
"capacity": 63.6, "capacity": 73,
"storageDiff": -4.63, "storageDiff": -4.61,
"inflow": "0.0", "inflow": "24.4",
"outflow": "0.0", "outflow": "0.0",
"volume": 455.7, "volume": 522.72,
"maxVolume": 716.5, "maxVolume": 716.5,
"lat": 49.606, "lat": 49.606,
"lng": 14.17, "lng": 14.17,
"sparkline": [ "sparkline": [
345.25, 345.29,
345.26, 345.29,
345.26, 345.29,
345.25, 345.29,
345.26, 345.29,
345.26, 345.29,
345.26, 345.29,
345.27, 345.29,
345.27, 345.29,
345.27, 345.29,
345.27, 345.29,
345.27 345.29
] ]
}, },
{ {
@@ -150,25 +150,25 @@
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "269.88", "level": "269.88",
"capacity": 78.2, "capacity": 97,
"storageDiff": -0.72, "storageDiff": -0.72,
"inflow": "0.0", "inflow": "81.1",
"outflow": "0.0", "outflow": "0.0",
"volume": 210.6, "volume": 261.1,
"maxVolume": 269.3, "maxVolume": 269.3,
"lat": 49.822, "lat": 49.822,
"lng": 14.436, "lng": 14.436,
"sparkline": [ "sparkline": [
269.87,
269.86,
269.89,
269.86,
269.89,
269.88,
269.89,
269.88, 269.88,
269.89, 269.89,
269.89, 269.89,
269.89,
269.88,
269.88,
269.88,
269.88,
269.88,
269.88,
269.88, 269.88,
269.88 269.88
] ]
@@ -178,28 +178,28 @@
"name": "Štěchovice", "name": "Štěchovice",
"river": "Vltava", "river": "Vltava",
"priority": true, "priority": true,
"level": "217.04", "level": "216.98",
"capacity": 1.6, "capacity": 72.7,
"storageDiff": -2.36, "storageDiff": -2.42,
"inflow": "0.0", "inflow": "48.3",
"outflow": "0.0", "outflow": "0.0",
"volume": 0.2, "volume": 8.14,
"maxVolume": 11.2, "maxVolume": 11.2,
"lat": 49.845, "lat": 49.845,
"lng": 14.412, "lng": 14.412,
"sparkline": [ "sparkline": [
218.33, 217,
218.25, 216.97,
218.1, 216.99,
217.82, 216.98,
217.57, 216.95,
217.32, 216.98,
217.19, 216.97,
217.1, 216.94,
217.05, 216.98,
217.04, 216.97,
217.06, 216.95,
217.04 216.98
] ]
}, },
{ {
@@ -207,28 +207,28 @@
"name": "Římov", "name": "Římov",
"river": "Malše", "river": "Malše",
"priority": true, "priority": true,
"level": "467.74", "level": "467.75",
"capacity": 74.9, "capacity": 78.5,
"storageDiff": -2.91, "storageDiff": -2.9,
"inflow": "0.0", "inflow": "2.2",
"outflow": "0.0", "outflow": "0.0",
"volume": 25.3, "volume": 26.54,
"maxVolume": 33.8, "maxVolume": 33.8,
"lat": 48.847, "lat": 48.847,
"lng": 14.487, "lng": 14.487,
"sparkline": [ "sparkline": [
467.73, 467.75,
467.73, 467.75,
467.73, 467.75,
467.73, 467.75,
467.73, 467.75,
467.73, 467.75,
467.74, 467.75,
467.74, 467.75,
467.74, 467.75,
467.74, 467.75,
467.74, 467.75,
467.74 467.75
] ]
}, },
{ {
@@ -236,28 +236,28 @@
"name": "Hracholusky", "name": "Hracholusky",
"river": "Mže", "river": "Mže",
"priority": true, "priority": true,
"level": "352.84", "level": "352.83",
"capacity": 11.4, "capacity": 57,
"storageDiff": -16.66, "storageDiff": -16.67,
"inflow": "0.0", "inflow": "1.5",
"outflow": "2.5", "outflow": "2.5",
"volume": 6.5, "volume": 32.31,
"maxVolume": 56.7, "maxVolume": 56.7,
"lat": 49.789, "lat": 49.789,
"lng": 13.155, "lng": 13.155,
"sparkline": [ "sparkline": [
352.84, 352.84,
352.84, 352.84,
352.83,
352.84, 352.84,
352.84, 352.84,
352.84, 352.84,
352.84, 352.84,
352.83,
352.84, 352.84,
352.84, 352.84,
352.84, 352.84,
352.84, 352.83
352.84,
352.84
] ]
} }
] ]
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://hladinator.cz/sitemap.xml
+28 -61
View File
@@ -78,70 +78,37 @@ async function scrapeLake(lakeId: string, oid: string, internalId: string) {
}); });
const records: DataRecord[] = []; const records: DataRecord[] = [];
let dataTable = null;
$('table').each((i, tbl) => {
if ($(tbl).text().includes('Datum') && $(tbl).text().includes('Odtok')) {
dataTable = $(tbl);
}
});
const parseTable = (htmlContent: string) => { if (dataTable) {
const _$ = cheerio.load(htmlContent); dataTable.find('tr').each((i, row) => {
let dataTable = null; if (i === 0) return; // skip header
_$('table').each((i, tbl) => { const cols = $(row).find('td');
if (_$(tbl).text().includes('Datum') && _$(tbl).text().includes('Odtok')) { if (cols.length >= 3) {
dataTable = _$(tbl); const rawDate = $(cols[0]).text().trim();
const levelStr = $(cols[1]).text().trim().replace(',', '.');
let flowStr = $(cols[2]).text().trim().replace(',', '.');
if (flowStr === '' && cols.length >= 4) {
flowStr = $(cols[3]).text().trim().replace(',', '.');
}
const parsedDateStr = parseDateString(rawDate);
if (parsedDateStr) {
records.push({
timestamp: parsedDateStr,
level: parseFloat(levelStr) || 0,
flow: parseFloat(flowStr) || 0,
inflow: 0,
volume: 0
});
}
} }
}); });
if (dataTable) {
dataTable.find('tr').each((i, row) => {
if (i === 0) return; // skip header
const cols = _$(row).find('td');
if (cols.length >= 3) {
const rawDate = _$(cols[0]).text().trim();
const levelStr = _$(cols[1]).text().trim().replace(',', '.');
let flowStr = _$(cols[2]).text().trim().replace(',', '.');
if (flowStr === '' && cols.length >= 4) {
flowStr = _$(cols[3]).text().trim().replace(',', '.');
}
const parsedDateStr = parseDateString(rawDate);
if (parsedDateStr) {
records.push({
timestamp: parsedDateStr,
level: parseFloat(levelStr) || 0,
flow: parseFloat(flowStr) || 0,
inflow: 0,
volume: 0
});
}
}
});
}
};
// 1. Zpracování týdenních dat (GET)
parseTable(response.data);
// 2. Získání a zpracování měsíčních dat (POST)
try {
const viewstate = $('#__VIEWSTATE').val();
const viewstategenerator = $('#__VIEWSTATEGENERATOR').val();
const eventvalidation = $('#__EVENTVALIDATION').val();
if (viewstate && viewstategenerator && eventvalidation) {
const postData = new URLSearchParams();
postData.append('__EVENTTARGET', 'ctl00$ObsahCPH$PrechodNaBilancniData');
postData.append('__EVENTARGUMENT', '');
postData.append('__VIEWSTATE', viewstate as string);
postData.append('__VIEWSTATEGENERATOR', viewstategenerator as string);
postData.append('__EVENTVALIDATION', eventvalidation as string);
const postRes = await axios.post(URL, postData.toString(), {
httpsAgent: agent,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 10000
});
parseTable(postRes.data);
}
} catch (err: any) {
console.warn(`Failed to fetch monthly data for ${internalId}:`, err.message);
} }
if (records.length > 0) { if (records.length > 0) {
+13 -4
View File
@@ -12,13 +12,13 @@ import { lakesConfig } from '../scripts/lakesConfig';
import { slugify } from './utils/slugify'; import { slugify } from './utils/slugify';
import './App.css'; import './App.css';
const LakeDetailWrapper = ({ language }: { language: Language }) => { const LakeDetailWrapper = ({ language, windUnit }: { language: Language, windUnit: 'kmh' | 'ms' }) => {
const { slug } = useParams(); const { slug } = useParams();
const lake = lakesConfig.find(l => slugify(l.text) === slug); const lake = lakesConfig.find(l => slugify(l.text) === slug);
if (!lake) return <Navigate to="/" replace />; if (!lake) return <Navigate to="/" replace />;
return <LakeDetail language={language} lakeId={lake.id} />; return <LakeDetail language={language} lakeId={lake.id} windUnit={windUnit} />;
}; };
function App() { function App() {
@@ -28,6 +28,9 @@ function App() {
const [theme, setTheme] = useState<'dark' | 'light'>(() => { const [theme, setTheme] = useState<'dark' | 'light'>(() => {
return (localStorage.getItem('hladinator_theme') as 'dark' | 'light') || 'dark'; return (localStorage.getItem('hladinator_theme') as 'dark' | 'light') || 'dark';
}); });
const [windUnit, setWindUnit] = useState<'kmh' | 'ms'>(() => {
return (localStorage.getItem('hladinator_windUnit') as 'kmh' | 'ms') || 'kmh';
});
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@@ -49,6 +52,10 @@ function App() {
localStorage.setItem('hladinator_lang', language); localStorage.setItem('hladinator_lang', language);
}, [language]); }, [language]);
useEffect(() => {
localStorage.setItem('hladinator_windUnit', windUnit);
}, [windUnit]);
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
{/* Mobile overlay */} {/* Mobile overlay */}
@@ -73,7 +80,7 @@ function App() {
<Route path="/" element={<LakesOverview language={language} />} /> <Route path="/" element={<LakesOverview language={language} />} />
<Route path="/favorites" element={<FavoritesOverview language={language} />} /> <Route path="/favorites" element={<FavoritesOverview language={language} />} />
<Route path="/map" element={<LakeMap language={language} />} /> <Route path="/map" element={<LakeMap language={language} />} />
<Route path="/:slug" element={<LakeDetailWrapper language={language} />} /> <Route path="/:slug" element={<LakeDetailWrapper language={language} windUnit={windUnit} />} />
</Routes> </Routes>
</div> </div>
<footer style={{ <footer style={{
@@ -85,7 +92,7 @@ function App() {
color: 'var(--text-muted)', color: 'var(--text-muted)',
marginTop: 'auto' marginTop: 'auto'
}}> }}>
<span>Zdroje dat: pvl.cz, open-meteo.com</span> <span>{t[language].chart.dataSources} pvl.cz, open-meteo.com</span>
<span>{t[language].chart.createdIn}</span> <span>{t[language].chart.createdIn}</span>
</footer> </footer>
</div> </div>
@@ -96,6 +103,8 @@ function App() {
setLanguage={setLanguage} setLanguage={setLanguage}
theme={theme} theme={theme}
setTheme={setTheme} setTheme={setTheme}
windUnit={windUnit}
setWindUnit={setWindUnit}
onClose={() => setIsSettingsOpen(false)} onClose={() => setIsSettingsOpen(false)}
/> />
)} )}
+12 -5
View File
@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { FiStar } from 'react-icons/fi'; import { FiStar } from 'react-icons/fi';
import { type Language } from '../translations'; import { type Language, t } from '../translations';
import { useFavorites } from '../hooks/useFavorites'; import { useFavorites } from '../hooks/useFavorites';
import { Helmet } from 'react-helmet-async';
import { CircularProgress } from './CircularProgress'; import { CircularProgress } from './CircularProgress';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { slugify } from '../utils/slugify'; import { slugify } from '../utils/slugify';
@@ -42,6 +43,12 @@ const FavoritesOverview = ({ language }: Props) => {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<Helmet>
<title>{t[language].seo.favoritesTitle}</title>
<meta name="description" content={t[language].seo.favoritesDesc} />
<meta property="og:title" content={t[language].seo.favoritesTitle} />
<meta property="og:description" content={t[language].seo.favoritesDesc} />
</Helmet>
<div> <div>
<h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0 0 0.5rem 0', display: 'flex', alignItems: 'center', gap: '0.6rem' }}> <h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0 0 0.5rem 0', display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<FiStar size={24} fill="#f59e0b" color="#f59e0b" /> <FiStar size={24} fill="#f59e0b" color="#f59e0b" />
@@ -84,7 +91,7 @@ const FavoritesOverview = ({ language }: Props) => {
{/* Unpin button */} {/* Unpin button */}
<button <button
onClick={(e) => { e.stopPropagation(); toggleFavorite(lake.id); }} onClick={(e) => { e.stopPropagation(); toggleFavorite(lake.id); }}
title="Odepnout" title={language === 'cs' ? 'Odepnout' : 'Unpin'}
style={{ style={{
position: 'absolute', top: '1rem', right: '1rem', position: 'absolute', top: '1rem', right: '1rem',
background: 'none', border: 'none', cursor: 'pointer', background: 'none', border: 'none', cursor: 'pointer',
@@ -104,7 +111,7 @@ const FavoritesOverview = ({ language }: Props) => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<div> <div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Water level</div> <div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{t[language].kpi.level}</div>
<div style={{ fontSize: '2rem', fontWeight: 'bold' }}>{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)' }}>m n.m.</span></div> <div style={{ fontSize: '2rem', fontWeight: 'bold' }}>{lake.level} <span style={{ fontSize: '1rem', fontWeight: 'normal', color: 'var(--text-muted)' }}>m n.m.</span></div>
</div> </div>
</div> </div>
@@ -123,11 +130,11 @@ const FavoritesOverview = ({ language }: Props) => {
<div style={{ display: 'flex', gap: '1rem', fontSize: '0.85rem' }}> <div style={{ display: 'flex', gap: '1rem', fontSize: '0.85rem' }}>
<div style={{ display: 'flex', gap: '0.5rem' }}> <div style={{ display: 'flex', gap: '0.5rem' }}>
<FiTrendingUp color="var(--color-green)" /> <FiTrendingUp color="var(--color-green)" />
<span style={{ color: 'var(--text-muted)' }}>Inflow <span style={{ color: 'var(--color-green)' }}>{lake.inflow} m³/s</span></span> <span style={{ color: 'var(--text-muted)' }}>{t[language].kpi.inflow} <span style={{ color: 'var(--color-green)' }}>{lake.inflow} m³/s</span></span>
</div> </div>
<div style={{ display: 'flex', gap: '0.5rem' }}> <div style={{ display: 'flex', gap: '0.5rem' }}>
<FiTrendingDown color="var(--color-red)" /> <FiTrendingDown color="var(--color-red)" />
<span style={{ color: 'var(--text-muted)' }}>Outflow <span style={{ color: 'var(--color-red)' }}>{lake.outflow} m³/s</span></span> <span style={{ color: 'var(--text-muted)' }}>{t[language].kpi.outflow} <span style={{ color: 'var(--color-red)' }}>{lake.outflow} m³/s</span></span>
</div> </div>
</div> </div>
+2 -2
View File
@@ -36,7 +36,7 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
}, [showTooltip]); }, [showTooltip]);
return ( return (
<div className="kpi-grid-container"> <>
{/* CARD 1: HLADINA */} {/* CARD 1: HLADINA */}
<div className="kpi-card"> <div className="kpi-card">
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}> <div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
@@ -146,7 +146,7 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
</div> </div>
</div> </div>
</div> </div>
</div> </>
); );
}; };
+72 -8
View File
@@ -1,10 +1,14 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, ComposedChart, ReferenceLine, Bar } from 'recharts'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, ComposedChart, ReferenceLine, Bar } from 'recharts';
import { Helmet } from 'react-helmet-async';
import { type Language, t } from '../translations'; import { type Language, t } from '../translations';
import KpiCards from './KpiCards'; import KpiCards from './KpiCards';
import { WeatherWidget } from './WeatherWidget'; import { WeatherWidget } from './WeatherWidget';
import { WindChart } from './WindChart';
import { NAVIGATION_LIMITS } from '../utils/navigationLimits'; import { NAVIGATION_LIMITS } from '../utils/navigationLimits';
import { FiAlertCircle } from 'react-icons/fi'; import { lakesConfig } from '../../scripts/lakesConfig';
import { FiAlertCircle, FiStar } from 'react-icons/fi';
import { useFavorites } from '../hooks/useFavorites';
interface LipnoData { interface LipnoData {
timestamp: string; timestamp: string;
@@ -21,6 +25,7 @@ interface LipnoData {
interface Props { interface Props {
language: Language; language: Language;
lakeId: string | null; lakeId: string | null;
windUnit?: 'kmh' | 'ms';
} }
const CustomTooltip = ({ active, payload, label, language, isWeather }: any) => { const CustomTooltip = ({ active, payload, label, language, isWeather }: any) => {
@@ -34,7 +39,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
const isTemp = entry.name === 'temperature' || entry.dataKey === 'temperature'; const isTemp = entry.name === 'temperature' || entry.dataKey === 'temperature';
return ( return (
<p key={index} style={{ margin: 0, color: 'var(--text-main)' }}> <p key={index} style={{ margin: 0, color: 'var(--text-main)' }}>
{isTemp ? 'Teplota' : 'Srážky'}: <span style={{ fontWeight: 'bold' }}>{entry.value !== undefined && entry.value !== null ? entry.value.toFixed(1) : 'N/A'} {isTemp ? '°C' : 'mm'}</span> {isTemp ? (language === 'cs' ? 'Teplota' : 'Temperature') : (language === 'cs' ? 'Srážky' : 'Precipitation')}: <span style={{ fontWeight: 'bold' }}>{entry.value !== undefined && entry.value !== null ? entry.value.toFixed(1) : 'N/A'} {isTemp ? '°C' : 'mm'}</span>
</p> </p>
); );
})} })}
@@ -74,7 +79,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
return null; return null;
}; };
const LakeDetail = ({ language, lakeId }: Props) => { const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
const [data, setData] = useState<LipnoData[]>([]); const [data, setData] = useState<LipnoData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [lakeInfo, setLakeInfo] = useState<any>(null); const [lakeInfo, setLakeInfo] = useState<any>(null);
@@ -174,6 +179,9 @@ const LakeDetail = ({ language, lakeId }: Props) => {
const targetMs7d = nowMs - 7 * 24 * 60 * 60 * 1000; const targetMs7d = nowMs - 7 * 24 * 60 * 60 * 1000;
const targetMs30d = nowMs - 30 * 24 * 60 * 60 * 1000; const targetMs30d = nowMs - 30 * 24 * 60 * 60 * 1000;
const { isFavorite, toggleFavorite } = useFavorites();
const isFav = lakeId ? isFavorite(lakeId) : false;
let level24hAgo = latestData.level; let level24hAgo = latestData.level;
let level7dAgo = latestData.level; let level7dAgo = latestData.level;
let level30dAgo = latestData.level; let level30dAgo = latestData.level;
@@ -221,19 +229,64 @@ const LakeDetail = ({ language, lakeId }: Props) => {
}; };
const limits = lakeInfo ? NAVIGATION_LIMITS[lakeInfo.id] : undefined; 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;
if (limits) limits.forEach(l => { if (l.level < min) min = l.level; });
return min - 0.5;
},
(dataMax: number) => {
let max = dataMax;
if (staticConfig?.maxLevel && staticConfig.maxLevel > max) max = staticConfig.maxLevel;
if (staticConfig?.storageLevel && staticConfig.storageLevel > max) max = staticConfig.storageLevel;
return max + 0.5;
}
];
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', width: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', width: '100%' }}>
{lakeInfo && (
<>
<Helmet>
<title>{t[language].seo.lakeTitle.replace('{name}', lakeInfo.name)}</title>
<meta name="description" content={t[language].seo.lakeDesc.replace('{name}', lakeInfo.name)} />
<meta property="og:title" content={t[language].seo.lakeTitle.replace('{name}', lakeInfo.name)} />
<meta property="og:description" content={t[language].seo.lakeDesc.replace('{name}', lakeInfo.name)} />
</Helmet>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', margin: '0 0 0.5rem 0' }}>
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', margin: 0, color: 'var(--text-main)' }}>
{lakeInfo.name}
</h1>
<button
onClick={(e) => { e.preventDefault(); if (lakeId) toggleFavorite(lakeId); }}
style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', padding: '0.25rem' }}
title={isFav ? (language === 'cs' ? "Odebrat z oblíbených" : "Remove from favorites") : (language === 'cs' ? "Přidat do oblíbených" : "Add to favorites")}
>
<FiStar size={24} fill={isFav ? '#f59e0b' : 'none'} color={isFav ? '#f59e0b' : 'var(--text-muted)'} />
</button>
</div>
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.95rem', lineHeight: '1.5' }}>
{t[language].seo.lakeDesc.replace('{name}', lakeInfo.name)}
</p>
</div>
</>
)}
<div style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginTop: '-0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <div style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginTop: '-0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span>{topbarDict.updated} {new Date().toLocaleDateString(language === 'cs' ? 'cs-CZ' : 'en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}, {new Date().toLocaleTimeString(language === 'cs' ? 'cs-CZ' : 'en-GB', { hour: '2-digit', minute: '2-digit' })} UTC</span> <span>{topbarDict.updated} {new Date().toLocaleDateString(language === 'cs' ? 'cs-CZ' : 'en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}, {new Date().toLocaleTimeString(language === 'cs' ? 'cs-CZ' : 'en-GB', { hour: '2-digit', minute: '2-digit' })} UTC</span>
<div className="status-dot"></div> <div className="status-dot"></div>
</div> </div>
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} /> <div className="kpi-grid-container">
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} />
{lakeInfo && lakeInfo.lat && lakeInfo.lng && ( {lakeInfo && lakeInfo.lat && lakeInfo.lng && (
<WeatherWidget lat={lakeInfo.lat} lng={lakeInfo.lng} language={language} sensorTemp={latestData.temperature} /> <WeatherWidget lat={lakeInfo.lat} lng={lakeInfo.lng} language={language} windUnit={windUnit} sensorTemp={latestData.temperature} />
)} )}
</div>
{limits && limits.map((limit, idx) => { {limits && limits.map((limit, idx) => {
const diff = latestData.level - limit.level; const diff = latestData.level - limit.level;
@@ -279,7 +332,7 @@ const LakeDetail = ({ language, lakeId }: Props) => {
</linearGradient> </linearGradient>
</defs> </defs>
<XAxis dataKey="date" stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} minTickGap={50} /> <XAxis dataKey="date" stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} minTickGap={50} />
<YAxis yAxisId="left" domain={['dataMin - 0.5', 'dataMax + 0.5']} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} tickFormatter={(v) => v.toFixed(2)} /> <YAxis yAxisId="left" domain={leftYAxisDomain as any} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} tickFormatter={(v) => v.toFixed(2)} />
<YAxis yAxisId="right" orientation="right" domain={[0, (dataMax: number) => Math.max(dataMax, 1)]} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} /> <YAxis yAxisId="right" orientation="right" domain={[0, (dataMax: number) => Math.max(dataMax, 1)]} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} />
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} /> <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
@@ -289,6 +342,12 @@ const LakeDetail = ({ language, lakeId }: Props) => {
{limits && limits.map((limit, idx) => ( {limits && limits.map((limit, idx) => (
<ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomRight', value: language === 'cs' ? limit.labelCs : limit.labelEn, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 12 }} /> <ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomRight', value: language === 'cs' ? limit.labelCs : limit.labelEn, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 12 }} />
))} ))}
{staticConfig && staticConfig.maxLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.maxLevel} stroke="var(--color-red)" strokeDasharray="3 3" label={{ position: 'insideTopLeft', value: language === 'cs' ? `Maximální retenční hladina (${staticConfig.maxLevel.toFixed(2)})` : `Max retention level (${staticConfig.maxLevel.toFixed(2)})`, fill: 'var(--color-red)', fontSize: 12 }} />
)}
{staticConfig && staticConfig.storageLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.storageLevel} stroke="var(--color-green)" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Hladina zásobního prostoru (${staticConfig.storageLevel.toFixed(2)})` : `Storage space level (${staticConfig.storageLevel.toFixed(2)})`, fill: 'var(--color-green)', fontSize: 12 }} />
)}
<Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" isAnimationActive={animate} /> <Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" isAnimationActive={animate} />
<Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-orange)" strokeWidth={2} dot={false} isAnimationActive={animate} /> <Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-orange)" strokeWidth={2} dot={false} isAnimationActive={animate} />
<Line yAxisId="right" type={curveType} dataKey="inflow" stroke="#8b5cf6" strokeWidth={2} dot={false} isAnimationActive={animate} /> <Line yAxisId="right" type={curveType} dataKey="inflow" stroke="#8b5cf6" strokeWidth={2} dot={false} isAnimationActive={animate} />
@@ -329,6 +388,11 @@ const LakeDetail = ({ language, lakeId }: Props) => {
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '12px', backgroundColor: 'var(--color-cyan)', opacity: 0.6 }}></div> {language === 'cs' ? 'Srážky' : 'Precipitation'} [mm]</span> <span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '12px', backgroundColor: 'var(--color-cyan)', opacity: 0.6 }}></div> {language === 'cs' ? 'Srážky' : 'Precipitation'} [mm]</span>
</div> </div>
{/* Wind Chart placed inside the main card below the weather graph */}
{lakeInfo && lakeInfo.lat && lakeInfo.lng && (
<WindChart lat={lakeInfo.lat} lng={lakeInfo.lng} language={language} timeRange={timeRange} windUnit={windUnit} />
)}
{/* Smoothed Toggle Control */} {/* Smoothed Toggle Control */}
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '3rem', marginBottom: '1rem' }}> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '3rem', marginBottom: '1rem' }}>
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{dict.view}</span> <span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{dict.view}</span>
+15 -7
View File
@@ -6,6 +6,7 @@ import { FiX, FiSearch, FiDroplet } from 'react-icons/fi';
import { type Language, t } from '../translations'; import { type Language, t } from '../translations';
import { slugify } from '../utils/slugify'; import { slugify } from '../utils/slugify';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
interface LakeData { interface LakeData {
id: string; id: string;
@@ -61,6 +62,13 @@ const LakeMap = ({ language }: Props) => {
return ( return (
<div className="map-view-container"> <div className="map-view-container">
<Helmet>
<title>{t[language].seo.mapTitle}</title>
<meta name="description" content={t[language].seo.mapDesc} />
<meta property="og:title" content={t[language].seo.mapTitle} />
<meta property="og:description" content={t[language].seo.mapDesc} />
</Helmet>
{/* Leaflet Map */} {/* Leaflet Map */}
<MapContainer <MapContainer
center={[49.8, 15.5]} center={[49.8, 15.5]}
@@ -95,18 +103,18 @@ const LakeMap = ({ language }: Props) => {
<div className="map-overlay-panel"> <div className="map-overlay-panel">
<div className="map-overlay-header"> <div className="map-overlay-header">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
<h3 style={{ margin: 0, fontSize: '1.1rem' }}>Seznam Jezer (Lakes List)</h3> <h3 style={{ margin: 0, fontSize: '1.1rem' }}>{language === 'cs' ? 'Seznam jezer a nádrží' : 'Lakes and Reservoirs List'}</h3>
<FiX style={{ cursor: 'pointer', color: 'var(--text-muted)' }} onClick={() => setIsPanelVisible(false)} /> <FiX style={{ cursor: 'pointer', color: 'var(--text-muted)' }} onClick={() => setIsPanelVisible(false)} />
</div> </div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '1rem' }}> <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
Nalezeno: {filteredLakes.length} Jezer {language === 'cs' ? `Nalezeno: ${filteredLakes.length} záznamů` : `Found: ${filteredLakes.length} records`}
</div> </div>
<div className="search-bar" style={{ width: '100%', padding: '0.5rem', backgroundColor: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)' }}> <div className="search-bar" style={{ width: '100%', padding: '0.5rem', backgroundColor: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)' }}>
<FiSearch /> <FiSearch />
<input <input
type="text" type="text"
placeholder="Find a lake..." placeholder={language === 'cs' ? 'Najít jezero...' : 'Find a lake...'}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
style={{ width: '100%', background: 'transparent', border: 'none', color: 'white', outline: 'none' }} style={{ width: '100%', background: 'transparent', border: 'none', color: 'white', outline: 'none' }}
@@ -117,14 +125,14 @@ const LakeMap = ({ language }: Props) => {
<div className="map-overlay-list"> <div className="map-overlay-list">
{filteredLakes.map((lake, index) => ( {filteredLakes.map((lake, index) => (
<div key={lake.id} className="map-lake-card" onClick={() => navigate(`/${slugify(lake.name)}`)}> <div key={lake.id} className="map-lake-card" onClick={() => navigate(`/${slugify(lake.name)}`)}>
<div style={{ fontWeight: 'bold', marginBottom: '0.5rem' }}>{index + 1}. Jezero {lake.name}</div> <div style={{ fontWeight: 'bold', marginBottom: '0.5rem' }}>{index + 1}. {lake.name}</div>
<div className="map-lake-stats"> <div className="map-lake-stats">
<div> <div>
<span style={{ color: 'var(--text-muted)', display: 'block' }}>Area</span> <span style={{ color: 'var(--text-muted)', display: 'block' }}>{language === 'cs' ? 'Rozloha' : 'Area'}</span>
<span style={{ color: 'var(--text-main)' }}>{(Math.random() * 50 + 10).toFixed(1)} km²</span> <span style={{ color: 'var(--text-main)' }}>{(Math.random() * 50 + 10).toFixed(1)} km²</span>
</div> </div>
<div> <div>
<span style={{ color: 'var(--text-muted)', display: 'block' }}>Depth</span> <span style={{ color: 'var(--text-muted)', display: 'block' }}>{language === 'cs' ? 'Hloubka' : 'Depth'}</span>
<span style={{ color: 'var(--text-main)' }}>{(Math.random() * 30 + 5).toFixed(1)}m</span> <span style={{ color: 'var(--text-main)' }}>{(Math.random() * 30 + 5).toFixed(1)}m</span>
</div> </div>
</div> </div>
@@ -139,7 +147,7 @@ const LakeMap = ({ language }: Props) => {
style={{ position: 'absolute', top: 10, right: 10, zIndex: 1000, background: 'var(--bg-card)', border: '1px solid var(--border-color)', color: 'white', padding: '0.5rem 1rem', borderRadius: '8px', cursor: 'pointer' }} style={{ position: 'absolute', top: 10, right: 10, zIndex: 1000, background: 'var(--bg-card)', border: '1px solid var(--border-color)', color: 'white', padding: '0.5rem 1rem', borderRadius: '8px', cursor: 'pointer' }}
onClick={() => setIsPanelVisible(true)} onClick={() => setIsPanelVisible(true)}
> >
Show List {language === 'cs' ? 'Zobrazit seznam' : 'Show List'}
</button> </button>
)} )}
</div> </div>
+17 -29
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import { FiTrendingUp, FiTrendingDown, FiStar } from 'react-icons/fi'; import { FiTrendingUp, FiTrendingDown, FiStar } from 'react-icons/fi';
import { type Language, t } from '../translations'; import { type Language, t } from '../translations';
import { AreaChart, Area, ResponsiveContainer, YAxis } from 'recharts'; import { AreaChart, Area, ResponsiveContainer, YAxis } from 'recharts';
@@ -45,7 +46,7 @@ const LakeCard = ({ lake, language, isFav, onToggleFav }: { lake: Lake, language
{/* Star / Favorite button */} {/* Star / Favorite button */}
<button <button
onClick={(e) => { e.stopPropagation(); onToggleFav(lake.id); }} onClick={(e) => { e.stopPropagation(); onToggleFav(lake.id); }}
title={isFav ? 'Odepnout' : 'Připnout jako oblíbené'} title={isFav ? (language === 'cs' ? 'Odepnout' : 'Unpin') : (language === 'cs' ? 'Připnout jako oblíbené' : 'Pin to favorites')}
style={{ style={{
position: 'absolute', top: '1rem', right: '1rem', position: 'absolute', top: '1rem', right: '1rem',
background: 'none', border: 'none', cursor: 'pointer', background: 'none', border: 'none', cursor: 'pointer',
@@ -131,7 +132,7 @@ const SmallLakeCard = ({ lake, isFav, onToggleFav }: { lake: Lake, isFav: boolea
{/* Star button */} {/* Star button */}
<button <button
onClick={(e) => { e.stopPropagation(); onToggleFav(lake.id); }} onClick={(e) => { e.stopPropagation(); onToggleFav(lake.id); }}
title={isFav ? 'Odepnout' : 'Připnout jako oblíbené'} title={isFav ? (language === 'cs' ? 'Odepnout' : 'Unpin') : (language === 'cs' ? 'Připnout jako oblíbené' : 'Pin to favorites')}
style={{ style={{
position: 'absolute', top: '0.6rem', right: '0.6rem', position: 'absolute', top: '0.6rem', right: '0.6rem',
background: 'none', border: 'none', cursor: 'pointer', background: 'none', border: 'none', cursor: 'pointer',
@@ -166,7 +167,6 @@ const SmallLakeCard = ({ lake, isFav, onToggleFav }: { lake: Lake, isFav: boolea
const LakesOverview = ({ language }: Props) => { const LakesOverview = ({ language }: Props) => {
const [lakes, setLakes] = useState<Lake[]>([]); const [lakes, setLakes] = useState<Lake[]>([]);
const [sortBy, setSortBy] = useState<'name' | 'level' | 'capacity' | 'inflow'>('name');
const { isFavorite, toggleFavorite, favorites } = useFavorites(); const { isFavorite, toggleFavorite, favorites } = useFavorites();
useEffect(() => { useEffect(() => {
@@ -186,41 +186,29 @@ const LakesOverview = ({ language }: Props) => {
const priorityLakes = lakes.filter(l => l.priority && !isFavorite(l.id)); const priorityLakes = lakes.filter(l => l.priority && !isFavorite(l.id));
const otherLakes = lakes.filter(l => !l.priority && !isFavorite(l.id)); const otherLakes = lakes.filter(l => !l.priority && !isFavorite(l.id));
otherLakes.sort((a, b) => { otherLakes.sort((a, b) => a.name.localeCompare(b.name));
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'level') return b.level - a.level;
if (sortBy === 'capacity') return b.capacity - a.capacity;
if (sortBy === 'inflow') return b.inflow - a.inflow;
return 0;
});
const sortButtonStyle = (type: string) => ({
background: 'none', border: 'none',
color: sortBy === type ? 'var(--text-main)' : 'var(--text-muted)',
cursor: 'pointer', fontSize: '0.85rem'
});
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}> <Helmet>
<div> <title>{t[language].seo.homeTitle}</title>
<h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0 0 0.5rem 0' }}>Overview: Lakes ({lakes.length})</h1> <meta name="description" content={t[language].seo.homeDesc} />
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.9rem' }}>Monitoring {lakes.length} reservoirs across the Czech Republic</p> <meta property="og:title" content={t[language].seo.homeTitle} />
</div> <meta property="og:description" content={t[language].seo.homeDesc} />
<div style={{ display: 'flex', gap: '0.5rem', color: 'var(--text-muted)', fontSize: '0.85rem' }}> </Helmet>
<span>Sort by:</span>
<button style={sortButtonStyle('name')} onClick={() => setSortBy('name')}>Name (A-Z)</button> | <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<button style={sortButtonStyle('level')} onClick={() => setSortBy('level')}>Level</button> | <h1 style={{ fontSize: '1.75rem', fontWeight: 'bold', margin: '0', color: 'var(--text-main)' }}>{t[language].sidebar.lakes} ({lakes.length})</h1>
<button style={sortButtonStyle('capacity')} onClick={() => setSortBy('capacity')}>Capacity</button> | <p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.95rem', lineHeight: '1.5' }}>
<button style={sortButtonStyle('inflow')} onClick={() => setSortBy('inflow')}>Flow In</button> {t[language].seo.homeDesc}
</div> </p>
</div> </div>
{/* Favorites section */} {/* Favorites section */}
{favoriteLakes.length > 0 && ( {favoriteLakes.length > 0 && (
<section> <section>
<h2 style={{ fontSize: '1.1rem', fontWeight: 'bold', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <h2 style={{ fontSize: '1.1rem', fontWeight: 'bold', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<FiStar size={16} fill="#f59e0b" color="#f59e0b" /> Oblíbená ({favoriteLakes.length}) <FiStar size={16} fill="#f59e0b" color="#f59e0b" /> {language === 'cs' ? 'Oblíbená' : 'Favorites'} ({favoriteLakes.length})
</h2> </h2>
<div style={{ <div style={{
display: 'grid', display: 'grid',
+41 -4
View File
@@ -1,4 +1,4 @@
import { FiX, FiMoon, FiSun, FiGlobe, FiCoffee } from 'react-icons/fi'; import { FiX, FiMoon, FiSun, FiGlobe, FiCoffee, FiWind } from 'react-icons/fi';
import { type Language, t } from '../translations'; import { type Language, t } from '../translations';
interface Props { interface Props {
@@ -6,10 +6,12 @@ interface Props {
setLanguage: (lang: Language) => void; setLanguage: (lang: Language) => void;
theme: 'dark' | 'light'; theme: 'dark' | 'light';
setTheme: (theme: 'dark' | 'light') => void; setTheme: (theme: 'dark' | 'light') => void;
windUnit: 'kmh' | 'ms';
setWindUnit: (unit: 'kmh' | 'ms') => void;
onClose: () => void; onClose: () => void;
} }
const SettingsModal = ({ language, setLanguage, theme, setTheme, onClose }: Props) => { const SettingsModal = ({ language, setLanguage, theme, setTheme, windUnit, setWindUnit, onClose }: Props) => {
const dict = t[language].settings; const dict = t[language].settings;
return ( return (
@@ -96,7 +98,7 @@ const SettingsModal = ({ language, setLanguage, theme, setTheme, onClose }: Prop
cursor: 'pointer', transition: 'all 0.2s' cursor: 'pointer', transition: 'all 0.2s'
}} }}
> >
<FiGlobe /> {dict.english} <span style={{ fontSize: '1.2rem', lineHeight: 1 }}>🇬🇧</span> {dict.english}
</button> </button>
<button <button
onClick={() => setLanguage('cs')} onClick={() => setLanguage('cs')}
@@ -109,7 +111,42 @@ const SettingsModal = ({ language, setLanguage, theme, setTheme, onClose }: Prop
cursor: 'pointer', transition: 'all 0.2s' cursor: 'pointer', transition: 'all 0.2s'
}} }}
> >
<FiGlobe /> {dict.czech} <span style={{ fontSize: '1.2rem', lineHeight: 1 }}>🇨🇿</span> {dict.czech}
</button>
</div>
</div>
{/* Wind Units Setting */}
<div style={{ marginBottom: '2rem' }}>
<label style={{ display: 'block', marginBottom: '0.75rem', color: 'var(--text-muted)', fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '1px' }}>
{dict.windUnits}
</label>
<div style={{ display: 'flex', gap: '1rem' }}>
<button
onClick={() => setWindUnit('kmh')}
style={{
flex: 1, padding: '0.75rem', borderRadius: '0.5rem',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem',
border: windUnit === 'kmh' ? '1px solid var(--color-cyan)' : '1px solid var(--border-color)',
backgroundColor: windUnit === 'kmh' ? 'rgba(6, 182, 212, 0.1)' : 'transparent',
color: windUnit === 'kmh' ? 'var(--color-cyan)' : 'var(--text-main)',
cursor: 'pointer', transition: 'all 0.2s'
}}
>
<FiWind /> {dict.windUnitKmh}
</button>
<button
onClick={() => setWindUnit('ms')}
style={{
flex: 1, padding: '0.75rem', borderRadius: '0.5rem',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem',
border: windUnit === 'ms' ? '1px solid var(--color-cyan)' : '1px solid var(--border-color)',
backgroundColor: windUnit === 'ms' ? 'rgba(6, 182, 212, 0.1)' : 'transparent',
color: windUnit === 'ms' ? 'var(--color-cyan)' : 'var(--text-main)',
cursor: 'pointer', transition: 'all 0.2s'
}}
>
<FiWind /> {dict.windUnitMs}
</button> </button>
</div> </div>
</div> </div>
+24 -19
View File
@@ -53,26 +53,31 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
<div className="nav-links"> <div className="nav-links">
{/* Favourites */} {/* Favourites */}
<div className={`nav-item ${isFavoritesPage ? 'active' : ''}`} onClick={() => handleNavigate('/favorites')} style={{ position: 'relative' }}> <div className={`nav-item ${isFavoritesPage ? 'active' : ''}`} onClick={() => handleNavigate('/favorites')} style={{ position: 'relative' }}>
<FiStar fill={favorites.length > 0 ? '#f59e0b' : 'none'} color={favorites.length > 0 ? '#f59e0b' : 'currentColor'} /> <div style={{ position: 'relative', display: 'flex' }}>
<FiStar fill={favorites.length > 0 ? '#f59e0b' : 'none'} color={favorites.length > 0 ? '#f59e0b' : 'currentColor'} />
{favorites.length > 0 && (
<span style={{
position: 'absolute',
top: '-8px',
right: '-12px',
backgroundColor: '#f59e0b',
color: '#000',
borderRadius: '999px',
fontSize: '0.65rem',
fontWeight: 'bold',
minWidth: '16px',
height: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 4px',
border: '2px solid var(--bg-card)'
}}>
{favorites.length}
</span>
)}
</div>
<span className="sidebar-text">{dict.favorites}</span> <span className="sidebar-text">{dict.favorites}</span>
{favorites.length > 0 && (
<span style={{
marginLeft: 'auto',
backgroundColor: '#f59e0b',
color: '#000',
borderRadius: '999px',
fontSize: '0.7rem',
fontWeight: 'bold',
minWidth: '18px',
height: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 5px',
}}>
{favorites.length}
</span>
)}
</div> </div>
{/* Lakes & Reservoirs */} {/* Lakes & Reservoirs */}
+7 -6
View File
@@ -6,6 +6,7 @@ interface WeatherProps {
lng: number; lng: number;
language: 'cs' | 'en'; language: 'cs' | 'en';
sensorTemp?: number; sensorTemp?: number;
windUnit?: 'kmh' | 'ms';
} }
interface WeatherData { interface WeatherData {
@@ -31,7 +32,7 @@ const formatTime = (isoString: string) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}; };
export const WeatherWidget = ({ lat, lng, language, sensorTemp }: WeatherProps) => { export const WeatherWidget = ({ lat, lng, language, sensorTemp, windUnit = 'kmh' }: WeatherProps) => {
const [data, setData] = useState<WeatherData | null>(null); const [data, setData] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
@@ -46,7 +47,7 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp }: WeatherProps)
const fetchWeather = async () => { const fetchWeather = async () => {
try { try {
setLoading(true); 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`); 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=${windUnit}`);
if (!res.ok) throw new Error('Failed to fetch weather'); if (!res.ok) throw new Error('Failed to fetch weather');
const json = await res.json(); const json = await res.json();
@@ -98,7 +99,7 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp }: WeatherProps)
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}> <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> <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' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.5rem', alignItems: 'center' }}>
{/* Left Column: Wind */} {/* Left Column: Wind */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}> <div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
@@ -113,16 +114,16 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp }: WeatherProps)
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', lineHeight: 1.1, color: 'var(--text-main)' }}> <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> {data.windSpeed.toFixed(1)} <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
</div> </div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginTop: '4px' }}> <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> {getCompassDirection(data.windDir, language)} {dict.gusts}: <span style={{ color: data.windGusts > (windUnit === 'kmh' ? 50 : 13.8) ? 'var(--color-red)' : 'var(--text-main)' }}>{data.windGusts.toFixed(1)} {windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
</div> </div>
</div> </div>
</div> </div>
{/* Right Column: Other Info */} {/* 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', flexDirection: 'column', gap: '0.4rem', borderLeft: '1px solid var(--border-color)', paddingLeft: '1rem' }}>
<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'}> <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)" /> <FiThermometer color="var(--color-orange)" />
<span style={{ fontWeight: 'bold' }}>{sensorTemp !== undefined ? sensorTemp.toFixed(1) : data.temp.toFixed(1)} °C</span> <span style={{ fontWeight: 'bold' }}>{sensorTemp !== undefined ? sensorTemp.toFixed(1) : data.temp.toFixed(1)} °C</span>
+273
View File
@@ -0,0 +1,273 @@
import { useState, useEffect } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, ComposedChart } from 'recharts';
import { FiWind } from 'react-icons/fi';
import { type Language } from '../translations';
interface WindChartProps {
lat: number;
lng: number;
language: Language;
timeRange?: '24h' | '7d' | '30d' | '1y' | 'all';
windUnit?: 'kmh' | 'ms';
}
interface WindDataPoint {
time: string;
speed: number;
gusts: number;
dir: number;
dirStr: 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 CustomWindTooltip = ({ active, payload, label, language, windUnit = 'kmh' }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
const date = new Date(label);
const dateStr = date.toLocaleDateString(language === 'cs' ? 'cs-CZ' : 'en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
const timeStr = date.toLocaleTimeString(language === 'cs' ? 'cs-CZ' : 'en-GB', { hour: '2-digit', minute: '2-digit' });
return (
<div style={{ backgroundColor: 'rgba(30, 41, 59, 0.95)', border: '1px solid var(--border-color)', borderRadius: '8px', padding: '12px', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.5)', color: 'var(--text-main)', fontSize: '0.9rem', zIndex: 100 }}>
<div style={{ fontWeight: 'bold', marginBottom: '8px', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '4px' }}>
{dateStr} {timeStr}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ color: 'var(--color-cyan)', fontSize: '1.2rem' }}></span>
<span>{language === 'cs' ? 'Rychlost větru' : 'Wind Speed'}: <strong>{data.speed} {windUnit === 'kmh' ? 'km/h' : 'm/s'}</strong></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ color: 'var(--color-purple)', fontSize: '1.2rem' }}></span>
<span>{language === 'cs' ? 'Nárazy větru' : 'Wind Gusts'}: <strong>{data.gusts} {windUnit === 'kmh' ? 'km/h' : 'm/s'}</strong></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '4px', color: 'var(--text-muted)' }}>
<FiWind />
<span>{language === 'cs' ? 'Směr' : 'Direction'}: <strong>{data.dirStr} ({data.dir}°)</strong></span>
</div>
</div>
</div>
);
}
return null;
};
const CustomWindDot = (props: any) => {
const { cx, cy, payload } = props;
if (!cx || !cy || payload.dir === undefined) return null;
return (
<g transform={`translate(${cx},${cy}) rotate(${payload.dir}) scale(1.5)`}>
<path
d="M0,-6 L-4,4 L0,2 L4,4 Z"
fill="var(--color-cyan)"
stroke="#1e293b"
strokeWidth={1}
/>
</g>
);
};
export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'kmh' }: WindChartProps) => {
const [data, setData] = useState<WindDataPoint[]>([]);
const [loading, setLoading] = useState(true);
const [currentSpeed, setCurrentSpeed] = useState(0);
const [maxGust, setMaxGust] = useState(0);
useEffect(() => {
const fetchWind = async () => {
try {
setLoading(true);
let url = '';
let isDaily = false;
if (timeRange === '1y' || timeRange === 'all') {
isDaily = true;
const end = new Date();
end.setDate(end.getDate() - 1);
const endStr = end.toISOString().split('T')[0];
const start = new Date();
if (timeRange === '1y') {
start.setDate(start.getDate() - 365);
} else {
start.setFullYear(2020);
}
const startStr = start.toISOString().split('T')[0];
url = `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lng}&start_date=${startStr}&end_date=${endStr}&daily=wind_speed_10m_max,wind_gusts_10m_max,wind_direction_10m_dominant&wind_speed_unit=${windUnit}&timezone=auto`;
} else {
let pastDays = 7;
if (timeRange === '24h') pastDays = 1;
if (timeRange === '30d') pastDays = 30;
url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&hourly=wind_speed_10m,wind_gusts_10m,wind_direction_10m&past_days=${pastDays}&forecast_days=1&wind_speed_unit=${windUnit}&timezone=auto`;
}
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch wind data');
const json = await res.json();
const times = isDaily ? json.daily.time : json.hourly.time;
const speeds = isDaily ? json.daily.wind_speed_10m_max : json.hourly.wind_speed_10m;
const gusts = isDaily ? json.daily.wind_gusts_10m_max : json.hourly.wind_gusts_10m;
const dirs = isDaily ? json.daily.wind_direction_10m_dominant : json.hourly.wind_direction_10m;
const chartData: WindDataPoint[] = [];
let maxG = 0;
const now = new Date();
let closestIdx = 0;
let minDiff = Infinity;
for (let i = 0; i < times.length; i++) {
const t = new Date(times[i]);
const diff = Math.abs(t.getTime() - now.getTime());
if (diff < minDiff) {
minDiff = diff;
closestIdx = i;
}
if (t.getTime() <= now.getTime() || isDaily) {
if (gusts[i] > maxG) maxG = gusts[i];
chartData.push({
time: times[i],
speed: speeds[i] || 0,
gusts: gusts[i] || 0,
dir: dirs[i] || 0,
dirStr: getCompassDirection(dirs[i] || 0, language)
});
}
}
let downsampleFactor = 1;
if (timeRange === '7d') downsampleFactor = 3;
if (timeRange === '30d') downsampleFactor = 12;
if (timeRange === '1y') downsampleFactor = 3;
if (timeRange === 'all') downsampleFactor = 14;
const downsampled = chartData.filter((_, i) => i % downsampleFactor === 0 || i === chartData.length - 1);
setData(downsampled);
setMaxGust(maxG);
setCurrentSpeed(speeds[closestIdx] || speeds[speeds.length - 1] || 0);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
if (lat && lng) {
fetchWind();
}
}, [lat, lng, language, timeRange]);
if (loading) {
return (
<div style={{ marginTop: '2rem', display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px', color: 'var(--text-muted)' }}>
{language === 'cs' ? 'Načítám data o větru...' : 'Loading wind data...'}
</div>
);
}
if (data.length === 0) return null;
return (
<div style={{ marginTop: '3rem', paddingTop: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '1rem' }}>
<h3 style={{ margin: 0, fontSize: '1.1rem', color: 'var(--text-main)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<FiWind style={{ color: 'var(--color-cyan)' }} />
{language === 'cs' ? `Aktivita větru (${timeRange === '1y' || timeRange === 'all' ? 'denní maxima' : timeRange})` : `Wind Activity (${timeRange === '1y' || timeRange === 'all' ? 'daily max' : timeRange})`}
</h3>
<div style={{ display: 'flex', gap: '1.5rem' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{language === 'cs' ? 'Aktuální rychlost' : 'Current Speed'}</span>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.25rem' }}>
<span style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--text-main)' }}>{currentSpeed.toFixed(1)}</span>
<span style={{ fontSize: '0.9rem', color: 'var(--color-cyan)' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{language === 'cs' ? 'Max. nárazy' : 'Peak Gusts'}</span>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.25rem' }}>
<span style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--text-main)' }}>{maxGust.toFixed(1)}</span>
<span style={{ fontSize: '0.9rem', color: 'var(--color-purple)' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
</div>
</div>
</div>
</div>
<div style={{ flex: 1, minHeight: '280px', width: '100%', marginTop: '0.5rem' }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={data} margin={{ top: 20, right: 0, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="colorWind" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-cyan)" stopOpacity={0.4}/>
<stop offset="95%" stopColor="var(--color-cyan)" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis
dataKey="time"
stroke="var(--text-muted)"
tick={{fill: 'var(--text-muted)', fontSize: 11}}
minTickGap={60}
tickFormatter={(v) => {
const d = new Date(v);
return `${d.getDate()}.${d.getMonth()+1}.`;
}}
/>
<YAxis
stroke="var(--text-muted)"
tick={{fill: 'var(--text-muted)', fontSize: 11}}
/>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<Tooltip content={<CustomWindTooltip language={language} windUnit={windUnit} />} />
<Area
type="monotone"
dataKey="speed"
stroke="var(--color-cyan)"
strokeWidth={2}
fillOpacity={1}
fill="url(#colorWind)"
isAnimationActive={true}
dot={<CustomWindDot />}
activeDot={{ r: 6, fill: 'var(--color-cyan)', stroke: '#1e293b', strokeWidth: 2 }}
/>
<Line
type="monotone"
dataKey="gusts"
stroke="var(--color-purple)"
strokeWidth={2}
strokeDasharray="5 5"
dot={false}
isAnimationActive={true}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: '1.5rem', fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ display: 'inline-block', width: '12px', height: '3px', backgroundColor: 'var(--color-cyan)' }}></span>
<span>{language === 'cs' ? 'Rychlost větru' : 'Wind Speed'}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ display: 'inline-block', width: '12px', height: '3px', borderTop: '2px dashed var(--color-purple)' }}></span>
<span>{language === 'cs' ? 'Nárazy větru' : 'Wind Gusts'}</span>
</div>
</div>
</div>
);
};
+8 -1
View File
@@ -10,10 +10,11 @@
--color-green: #22c55e; /* Přítok / Positive trend */ --color-green: #22c55e; /* Přítok / Positive trend */
--color-red: #ef4444; /* Odtok / Negative trend */ --color-red: #ef4444; /* Odtok / Negative trend */
--color-orange: #f97316; /* Odtok line chart color */ --color-orange: #f97316; /* Odtok line chart color */
--color-purple: #a855f7; /* Wind gusts line color */
.kpi-grid-container { .kpi-grid-container {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 1.5rem; gap: 1.5rem;
width: 100%; width: 100%;
} }
@@ -161,6 +162,12 @@
border-top: 8px solid white; border-top: 8px solid white;
} }
@media (max-width: 1024px) {
.kpi-grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.map-overlay-panel { .map-overlay-panel {
top: auto; top: auto;
+8 -5
View File
@@ -2,15 +2,18 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { FavoritesProvider } from './hooks/useFavorites' import { FavoritesProvider } from './hooks/useFavorites'
import { HelmetProvider } from 'react-helmet-async'
import './index.css' import './index.css'
import App from './App.tsx' import App from './App.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<BrowserRouter> <HelmetProvider>
<FavoritesProvider> <BrowserRouter>
<App /> <FavoritesProvider>
</FavoritesProvider> <App />
</BrowserRouter> </FavoritesProvider>
</BrowserRouter>
</HelmetProvider>
</StrictMode>, </StrictMode>,
) )
+26
View File
@@ -12,6 +12,16 @@ export const t = {
search: 'Search river or reservoir (e.g. Lipno)...', search: 'Search river or reservoir (e.g. Lipno)...',
updated: 'Last updated:' updated: 'Last updated:'
}, },
seo: {
homeTitle: 'Hladinátor - Water levels and flow rates of reservoirs',
homeDesc: 'Track current water levels, flow rates, inflow, and weather development on major Czech dams and reservoirs in real time. Data sourced from official river basin authorities.',
lakeTitle: '{name} - Water level and flow | Hladinátor',
lakeDesc: 'Current water level and statistics for the {name} reservoir. Track water level, flow rate, wind strength, and storage capacity in real time.',
favoritesTitle: 'Favorites | Hladinátor',
favoritesDesc: 'Your pinned lakes and reservoirs. Track water level, flow rate, and weather development on major Czech dams and reservoirs.',
mapTitle: 'Map | Hladinátor',
mapDesc: 'Interactive map of all monitored lakes and reservoirs in the Czech Republic.'
},
kpi: { kpi: {
level: 'WATER LEVEL', level: 'WATER LEVEL',
flow: 'FLOW RATE', flow: 'FLOW RATE',
@@ -46,6 +56,9 @@ export const t = {
language: 'Language', language: 'Language',
english: 'English', english: 'English',
czech: 'Čeština', czech: 'Čeština',
windUnits: 'Wind units',
windUnitKmh: 'km/h',
windUnitMs: 'm/s',
contact: 'Contact', contact: 'Contact',
contactPlaceholder: 'Your email address', contactPlaceholder: 'Your email address',
buyCoffee: 'Buy Me a Coffee' buyCoffee: 'Buy Me a Coffee'
@@ -62,6 +75,16 @@ export const t = {
search: 'Hledat tok nebo nádrž (např. Lipno)...', search: 'Hledat tok nebo nádrž (např. Lipno)...',
updated: 'Aktualizováno:' updated: 'Aktualizováno:'
}, },
seo: {
homeTitle: 'Hladinátor - Aktuální stav přehrad a nádrží',
homeDesc: 'Sledujte aktuální vodní stav, průtok, přítok a vývoj počasí na nejvýznamnějších českých přehradách v reálném čase. Oficiální data z povodí.',
lakeTitle: '{name} - Stav hladiny a průtok | Hladinátor',
lakeDesc: 'Aktuální vodní stav a statistiky pro vodní dílo {name}. Sledujte vývoj hladiny, sílu větru a kapacitu zásobního prostoru v reálném čase.',
favoritesTitle: 'Oblíbené | Hladinátor',
favoritesDesc: 'Vaše připnuté přehrady a nádrže. Sledujte aktuální vodní stav, průtok a vývoj počasí na vybraných českých přehradách.',
mapTitle: 'Mapa | Hladinátor',
mapDesc: 'Interaktivní mapa všech sledovaných přehrad a nádrží v České republice.'
},
kpi: { kpi: {
level: 'HLADINA', level: 'HLADINA',
flow: 'PRŮTOK', flow: 'PRŮTOK',
@@ -96,6 +119,9 @@ export const t = {
language: 'Jazyk', language: 'Jazyk',
english: 'English', english: 'English',
czech: 'Čeština', czech: 'Čeština',
windUnits: 'Jednotky větru',
windUnitKmh: 'km/h',
windUnitMs: 'm/s',
contact: 'Kontakt', contact: 'Kontakt',
contactPlaceholder: 'Vaše e-mailová adresa', contactPlaceholder: 'Vaše e-mailová adresa',
buyCoffee: 'Kup mi kávu' buyCoffee: 'Kup mi kávu'
+1 -1
View File
@@ -7,7 +7,7 @@ export interface NavigationLimit {
export const NAVIGATION_LIMITS: Record<string, NavigationLimit[]> = { export const NAVIGATION_LIMITS: Record<string, NavigationLimit[]> = {
// Orlík // Orlík
'VLOR|1': [ 'VLOR|2': [
{ {
level: 342.50, level: 342.50,
labelCs: 'Minimální hladina pro lodní výtah Orlík', labelCs: 'Minimální hladina pro lodní výtah Orlík',