feat: add automatic data polling, conditional search visibility, and extended scraper functionality for monthly lake records

This commit is contained in:
David Fencl
2026-06-06 12:34:20 +02:00
parent dbb22e7972
commit db1aadcc8d
18 changed files with 2731 additions and 152 deletions
+40 -39
View File
@@ -84,44 +84,50 @@ const LakeDetail = ({ language, lakeId }: Props) => {
const topbarDict = t[language].topbar;
useEffect(() => {
fetch('/data/lakes_index.json')
.then(res => res.json())
.then(indexData => {
const found = indexData.find((l: any) => l.id === lakeId);
setLakeInfo(found || { name: 'Lipno 1', river: 'Vltava' });
})
.catch(err => console.error(err));
const loadData = () => {
fetch(`/data/lakes_index.json?t=${Date.now()}`)
.then(res => res.json())
.then(indexData => {
const found = indexData.find((l: any) => l.id === lakeId);
setLakeInfo(found || { name: 'Lipno 1', river: 'Vltava' });
})
.catch(err => console.error(err));
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
fetch(`/data/${internalId}.json`)
.then(res => res.json())
.then(json => {
const formattedData = json.map((item: any) => {
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
fetch(`/data/${internalId}.json?t=${Date.now()}`)
.then(res => res.json())
.then(json => {
const formattedData = json.map((item: any) => {
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
return {
timestamp: item.timestamp,
date: new Date(item.timestamp).toLocaleString(language === 'cs' ? 'cs-CZ' : 'en-GB', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
}),
level: item.level === null || isNaN(item.level) ? 0 : item.level,
outflow: outflow,
inflow: item.inflow || 0,
volume: item.volume || 0,
fullness: 0,
temperature: item.temperature,
precipitation: item.precipitation
};
return {
timestamp: item.timestamp,
date: new Date(item.timestamp).toLocaleString(language === 'cs' ? 'cs-CZ' : 'en-GB', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
}),
level: item.level === null || isNaN(item.level) ? 0 : item.level,
outflow: outflow,
inflow: item.inflow || 0,
volume: item.volume || 0,
fullness: 0,
temperature: item.temperature,
precipitation: item.precipitation
};
});
setData(formattedData);
setLoading(false);
})
.catch(err => {
console.error('Failed to load data', err);
setLoading(false);
});
setData(formattedData);
setLoading(false);
})
.catch(err => {
console.error('Failed to load data', err);
setLoading(false);
});
};
loadData();
const intervalId = setInterval(loadData, 60 * 1000); // Poll every 1 minute
return () => clearInterval(intervalId);
}, [language, lakeId]);
if (loading) {
@@ -336,11 +342,6 @@ const LakeDetail = ({ language, lakeId }: Props) => {
</div>
</div>
</div>
<div className="dashboard-footer" style={{ marginTop: '0' }}>
<span>{dict.dataSources} <a href="https://www.chmi.cz/" target="_blank" rel="noreferrer">ČHMÚ</a>, <a href="https://www.pvl.cz/" target="_blank" rel="noreferrer">pvl.cz</a></span>
<span>{dict.createdIn}</span>
</div>
</div>
);
};
+10 -4
View File
@@ -170,10 +170,16 @@ const LakesOverview = ({ language }: Props) => {
const { isFavorite, toggleFavorite, favorites } = useFavorites();
useEffect(() => {
fetch(`/data/lakes_index.json?t=${Date.now()}`)
.then(res => res.json())
.then(data => setLakes(data))
.catch(err => console.error(err));
const loadData = () => {
fetch(`/data/lakes_index.json?t=${Date.now()}`)
.then(res => res.json())
.then(data => setLakes(data))
.catch(err => console.error(err));
};
loadData();
const intervalId = setInterval(loadData, 60 * 1000); // Poll every 1 minute
return () => clearInterval(intervalId);
}, []);
const favoriteLakes = lakes.filter(l => isFavorite(l.id));
+9 -4
View File
@@ -1,5 +1,6 @@
import { FiSearch, FiMenu, FiDroplet } from 'react-icons/fi';
import { type Language, t } from '../translations';
import { useLocation } from 'react-router-dom';
interface Props {
language: Language;
@@ -8,6 +9,8 @@ interface Props {
const Topbar = ({ language, onToggleMobileMenu }: Props) => {
const dict = t[language].topbar;
const location = useLocation();
const showSearch = location.pathname === '/' || location.pathname === '/favorites';
return (
<div className="topbar">
@@ -19,10 +22,12 @@ const Topbar = ({ language, onToggleMobileMenu }: Props) => {
<span>Hladinator</span>
</div>
<div className="search-bar">
<FiSearch />
<input type="text" placeholder={dict.search} />
</div>
{showSearch && (
<div className="search-bar">
<FiSearch />
<input type="text" placeholder={dict.search} />
</div>
)}
</div>
</div>
);