feat: update lake index, sync scraping scripts, and prune unused data files
This commit is contained in:
+83
-52
@@ -1,5 +1,6 @@
|
||||
import { FiArrowUp, FiArrowDown } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface KpiData {
|
||||
level: number;
|
||||
@@ -16,73 +17,103 @@ interface Props {
|
||||
}
|
||||
|
||||
const KpiCards = ({ data, language, lakeName = 'Lipno 1' }: Props) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const dict = t[language].kpi;
|
||||
const flowDiff = data.inflow - data.outflow;
|
||||
|
||||
useEffect(() => {
|
||||
if (showTooltip) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowTooltip(false);
|
||||
}, 3500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [showTooltip]);
|
||||
|
||||
return (
|
||||
<div className="kpi-container-mobile">
|
||||
<div className="kpi-grid-container">
|
||||
{/* CARD 1: HLADINA */}
|
||||
<div className="kpi-card-full">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.level} {lakeName}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.level} {lakeName}
|
||||
</div>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, marginBottom: '0.5rem' }}>
|
||||
{data.level.toFixed(2)} <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>m n. m.</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--color-green)' }}>
|
||||
(+0.02 m / 24h)
|
||||
</div>
|
||||
|
||||
{/* Decorative Circle for Level */}
|
||||
<div style={{ position: 'absolute', right: '1.5rem', top: '1.5rem', width: '40px', height: '40px', borderRadius: '50%', border: '4px solid rgba(0, 195, 255, 0.2)', borderTopColor: 'var(--color-cyan)', transform: 'rotate(45deg)' }}></div>
|
||||
</div>
|
||||
|
||||
{/* CARD 2: PRŮTOK */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.flow}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem' }}>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
|
||||
{dict.inflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.inflow.toFixed(1)} m³/s</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1 }}>
|
||||
{data.level.toFixed(2)} <span style={{ fontSize: '1.2rem', fontWeight: 'normal', color: 'var(--text-main)' }}>m n. m.</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.9rem', color: 'var(--color-green)', marginTop: '0.5rem' }}>
|
||||
(+0.02 m / 24h)
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
{dict.outflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.outflow.toFixed(1)} m³/s</span>
|
||||
{flowDiff > 0 ? <FiArrowUp color="var(--color-green)" /> : flowDiff < 0 ? <FiArrowDown color="var(--color-red)" /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Circle */}
|
||||
<div style={{ width: '60px', height: '60px', position: 'relative' }}>
|
||||
<svg width="60" height="60" viewBox="0 0 60 60">
|
||||
<circle cx="30" cy="30" r="26" fill="transparent" stroke="rgba(255,255,255,0.05)" strokeWidth="6" />
|
||||
<circle cx="30" cy="30" r="26" fill="transparent" stroke="var(--color-cyan)" strokeWidth="6" strokeDasharray="163" strokeDashoffset="40" strokeLinecap="round" transform="rotate(-90 30 30)" />
|
||||
</svg>
|
||||
{/* Flow Circle */}
|
||||
<div style={{ width: '40px', height: '40px', borderRadius: '50%', border: '4px solid rgba(0, 195, 255, 0.2)', borderTopColor: 'var(--color-cyan)', borderRightColor: 'var(--color-cyan)', display: 'flex', alignItems: 'center', justifyContent: 'center', transform: 'rotate(-45deg)' }}>
|
||||
<span style={{ fontSize: '0.65rem', transform: 'rotate(45deg)', color: 'var(--text-main)', fontWeight: 'bold' }}>
|
||||
<div style={{ lineHeight: 1 }}>{data.outflow.toFixed(1)}</div>
|
||||
<div style={{ fontSize: '0.45rem', opacity: 0.7 }}>m³/s</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="kpi-row-half">
|
||||
{/* CARD 2: PRŮTOK */}
|
||||
<div className="kpi-card-half">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.flow}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem' }}>
|
||||
<span style={{ color: 'var(--text-main)' }}>{dict.inflow}: <span style={{ fontWeight: 'bold' }}>{data.inflow.toFixed(1)} m³/s</span></span>
|
||||
<span style={{ color: 'var(--text-main)' }}>{dict.outflow}: <span style={{ fontWeight: 'bold' }}>{data.outflow.toFixed(1)} m³/s</span> <FiArrowDown color="var(--color-red)" /></span>
|
||||
{/* CARD 3: NAPLNĚNOST */}
|
||||
<div className="kpi-card">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
|
||||
{dict.fullness}
|
||||
<span
|
||||
onClick={() => setShowTooltip(!showTooltip)}
|
||||
style={{ cursor: 'pointer', fontSize: '0.85rem', opacity: 0.6, padding: '0 4px' }}
|
||||
>
|
||||
ⓘ
|
||||
</span>
|
||||
{showTooltip && (
|
||||
<div
|
||||
onClick={() => setShowTooltip(false)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: 'var(--bg-card)',
|
||||
border: '1px solid var(--border-color)',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '8px',
|
||||
width: '250px',
|
||||
zIndex: 100,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
color: 'var(--text-main)',
|
||||
fontSize: '0.85rem',
|
||||
lineHeight: 1.4,
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
{language === 'cs' ? "Odhad vypočítaný z aktuální výšky hladiny (mezi min. a max. kótou)." : "Estimate calculated from current water level (between min and max levels)."}
|
||||
</div>
|
||||
|
||||
{/* Flow Circle */}
|
||||
<div style={{ width: '50px', height: '50px', position: 'relative' }}>
|
||||
<svg width="50" height="50" viewBox="0 0 50 50">
|
||||
<circle cx="25" cy="25" r="22" fill="transparent" stroke="rgba(255,255,255,0.05)" strokeWidth="4" />
|
||||
<circle cx="25" cy="25" r="22" fill="transparent" stroke="var(--color-cyan)" strokeWidth="4" strokeDasharray="138" strokeDashoffset="40" strokeLinecap="round" transform="rotate(-90 25 25)" />
|
||||
</svg>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<span style={{ fontSize: '0.8rem', fontWeight: 'bold', lineHeight: 1 }}>{Math.max(data.inflow, data.outflow).toFixed(1)}</span>
|
||||
<span style={{ fontSize: '0.5rem', color: 'var(--text-muted)' }}>m³/s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* CARD 3: NAPLNĚNOST */}
|
||||
<div className="kpi-card-half">
|
||||
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
|
||||
{dict.fullness}
|
||||
</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem' }}>
|
||||
{data.fullness.toFixed(1)}%
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
|
||||
{dict.volume}: {data.volume.toFixed(1)} mil. m³
|
||||
</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem' }}>
|
||||
{data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A'}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
|
||||
{dict.volume}: {data.volume.toFixed(1)} mil. m³
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,7 @@ const CustomTooltip = ({ active, payload, label, language }: any) => {
|
||||
<div style={{ backgroundColor: 'var(--bg-card)', padding: '1rem', border: '1px solid var(--border-color)', borderRadius: '0.5rem', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)' }}>
|
||||
<p style={{ margin: '0 0 0.5rem 0', fontWeight: 'bold', color: 'var(--text-main)' }}>{label}</p>
|
||||
<p style={{ margin: 0, color: 'var(--text-main)' }}>{dict.level}: <span style={{ fontWeight: 'bold' }}>{payload[0].value.toFixed(2)} m n. m.</span></p>
|
||||
<p style={{ margin: 0, color: 'var(--text-main)' }}>{dict.inflow}: <span style={{ fontWeight: 'bold' }}>{payload[1].value.toFixed(1)} m³/s</span></p>
|
||||
<p style={{ margin: 0, color: 'var(--text-main)' }}>{dict.outflow}: <span style={{ fontWeight: 'bold' }}>{payload[2].value.toFixed(1)} m³/s</span></p>
|
||||
<p style={{ margin: 0, color: 'var(--text-main)' }}>{dict.outflow}: <span style={{ fontWeight: 'bold' }}>{payload[1].value.toFixed(1)} m³/s</span></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,7 +56,6 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
.then(json => {
|
||||
const formattedData = json.map((item: any) => {
|
||||
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
|
||||
const inflow = outflow; // No random inflow anymore, PVL only gives us one flow value
|
||||
|
||||
return {
|
||||
timestamp: item.timestamp,
|
||||
@@ -67,8 +65,8 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
}),
|
||||
level: item.level === null || isNaN(item.level) ? 0 : item.level,
|
||||
outflow: outflow,
|
||||
inflow: inflow,
|
||||
volume: 0, // PVL doesn't provide this here
|
||||
inflow: item.inflow || 0,
|
||||
volume: item.volume || 0,
|
||||
fullness: 0
|
||||
};
|
||||
});
|
||||
@@ -110,22 +108,19 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
<div className="status-dot"></div>
|
||||
</div>
|
||||
|
||||
<div className="top-time-controls">
|
||||
<button className="active">24h</button>
|
||||
<button>7d</button>
|
||||
<button>30d</button>
|
||||
<button>{dict.year}</button>
|
||||
<button>{dict.all}</button>
|
||||
</div>
|
||||
|
||||
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} />
|
||||
|
||||
{/* CHART SECTION */}
|
||||
<div className="chart-card">
|
||||
<div className="chart-header" style={{ borderBottom: 'none', paddingBottom: '0' }}>
|
||||
<span className="chart-title">
|
||||
{dict.title} {lakeInfo ? `${lakeInfo.name} ${lakeInfo.river ? `- ${lakeInfo.river}` : ''}` : 'Lipno 1 - Vltava'}
|
||||
</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '1.1rem', color: 'var(--text-main)' }}>{dict.title} {lakeInfo ? `${lakeInfo.name} ${lakeInfo.river ? `- ${lakeInfo.river}` : ''}` : 'Lipno 1 - Vltava'}</h3>
|
||||
<div className="top-time-controls" style={{ margin: 0 }}>
|
||||
<button className="active">24h</button>
|
||||
<button>7d</button>
|
||||
<button>30d</button>
|
||||
<button>{dict.year}</button>
|
||||
<button>{dict.all}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minHeight: '300px', width: '100%', marginTop: '1rem' }}>
|
||||
@@ -146,7 +141,6 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
|
||||
{/* Data Series */}
|
||||
<Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" />
|
||||
<Line yAxisId="right" type={curveType} dataKey="inflow" stroke="var(--color-green)" strokeWidth={2} dot={false} />
|
||||
<Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-orange)" strokeWidth={2} dot={false} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -155,7 +149,6 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
{/* Chart Legend */}
|
||||
<div className="chart-legend-container" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '1rem', marginTop: '1rem', fontSize: '0.85rem', color: 'var(--text-main)' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-cyan)' }}></div> {dict.level}</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-green)' }}></div> {dict.inflow}</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-orange)' }}></div> {dict.outflow}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number
|
||||
/>
|
||||
</svg>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: size * 0.25, fontWeight: 'bold' }}>
|
||||
{value}%
|
||||
{value > 0 ? `${value}%` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -80,7 +80,7 @@ const PriorityCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: s
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<CircularProgress value={lake.capacity} size={70} strokeWidth={6} />
|
||||
<div>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 'bold' }}>{lake.capacity}% / <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>{lake.volume} mil. m³</span></div>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 'bold' }}>{lake.capacity > 0 ? `${lake.capacity}%` : 'N/A'} / <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>{lake.volume} mil. m³</span></div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Volume</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+26
-3
@@ -11,10 +11,29 @@
|
||||
--color-red: #ef4444; /* Odtok / Negative trend */
|
||||
--color-orange: #f97316; /* Odtok line chart color */
|
||||
|
||||
.kpi-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Hide old styles but keep them in case they are used elsewhere */
|
||||
.kpi-container-mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -151,6 +170,9 @@
|
||||
max-height: 50%;
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
.kpi-grid-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Time controls pill layout */
|
||||
@@ -160,7 +182,7 @@
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.top-time-controls button {
|
||||
@@ -169,10 +191,11 @@
|
||||
border: none;
|
||||
border-right: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
padding: 0.75rem 0;
|
||||
padding: 0.4rem 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.top-time-controls button:last-child {
|
||||
border-right: none;
|
||||
|
||||
Reference in New Issue
Block a user