feat: add automatic data polling, conditional search visibility, and extended scraper functionality for monthly lake records
This commit is contained in:
+22
-8
@@ -7,7 +7,7 @@ import FavoritesOverview from './components/FavoritesOverview';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Topbar from './components/Topbar';
|
||||
import SettingsModal from './components/SettingsModal';
|
||||
import { type Language } from './translations';
|
||||
import { type Language, t } from './translations';
|
||||
import { lakesConfig } from '../scripts/lakesConfig';
|
||||
import { slugify } from './utils/slugify';
|
||||
import './App.css';
|
||||
@@ -66,14 +66,28 @@ function App() {
|
||||
onCloseMobileMenu={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
|
||||
<div className="main-content">
|
||||
<div className="main-content" style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<Topbar language={language} onToggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)} />
|
||||
<Routes>
|
||||
<Route path="/" element={<LakesOverview language={language} />} />
|
||||
<Route path="/favorites" element={<FavoritesOverview language={language} />} />
|
||||
<Route path="/map" element={<LakeMap language={language} />} />
|
||||
<Route path="/:slug" element={<LakeDetailWrapper language={language} />} />
|
||||
</Routes>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<LakesOverview language={language} />} />
|
||||
<Route path="/favorites" element={<FavoritesOverview language={language} />} />
|
||||
<Route path="/map" element={<LakeMap language={language} />} />
|
||||
<Route path="/:slug" element={<LakeDetailWrapper language={language} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
<footer style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '1.5rem',
|
||||
fontSize: '0.8rem',
|
||||
color: 'var(--text-muted)',
|
||||
marginTop: 'auto'
|
||||
}}>
|
||||
<span>Zdroje dat: pvl.cz, open-meteo.com</span>
|
||||
<span>{t[language].chart.createdIn}</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{isSettingsOpen && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user