feat: implement Open-Meteo weather integration with backfill scripts and updated lake data models.
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/push Build encountered an error
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
|
||||
import { type Language, t } from '../translations';
|
||||
import Topbar from './Topbar';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { slugify } from '../utils/slugify';
|
||||
|
||||
interface Lake {
|
||||
id: string;
|
||||
@@ -19,7 +20,6 @@ interface Lake {
|
||||
|
||||
interface Props {
|
||||
language: Language;
|
||||
onSelectLake: (id: string) => void;
|
||||
}
|
||||
|
||||
const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number, size?: number, strokeWidth?: number }) => {
|
||||
@@ -57,11 +57,16 @@ const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number
|
||||
);
|
||||
};
|
||||
|
||||
const PriorityCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: string) => void }) => {
|
||||
const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => {
|
||||
const navigate = useNavigate();
|
||||
const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val }));
|
||||
|
||||
return (
|
||||
<div className="kpi-card priority-lake-card" style={{ flex: 1, padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative' }}>
|
||||
<div
|
||||
className="kpi-card priority-lake-card"
|
||||
onClick={() => navigate(`/${slugify(lake.name)}`)}
|
||||
style={{ cursor: 'pointer', flex: 1, padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative' }}
|
||||
>
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0 }}>{lake.name} {lake.river ? `- ${lake.river}` : ''}</h3>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
@@ -73,7 +78,6 @@ const PriorityCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: s
|
||||
<div>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Water 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: '0.8rem', color: 'var(--text-muted)' }}>Depth</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,72 +109,18 @@ const PriorityCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: s
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<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)' }}>/ Outflow <span style={{ color: 'var(--color-red)' }}>{lake.outflow} m³/s</span></span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<FiTrendingDown color="var(--color-red)" />
|
||||
<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)' }}>/ Outflow <span style={{ color: 'var(--color-red)' }}>{lake.outflow} m³/s</span></span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Outflow <span style={{ color: 'var(--color-red)' }}>{lake.outflow} m³/s</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onSelectLake(lake.id)}
|
||||
style={{
|
||||
width: '100%', padding: '0.75rem', borderRadius: '0.5rem',
|
||||
backgroundColor: 'var(--color-cyan)', color: 'white',
|
||||
border: 'none', fontWeight: 'bold', cursor: 'pointer',
|
||||
marginTop: 'auto', transition: 'background-color 0.2s'
|
||||
}}
|
||||
onMouseOver={e => e.currentTarget.style.backgroundColor = '#0284c7'}
|
||||
onMouseOut={e => e.currentTarget.style.backgroundColor = 'var(--color-cyan)'}
|
||||
>
|
||||
View Full Details
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SmallCard = ({ lake, onSelectLake }: { lake: Lake, onSelectLake: (id: string) => void }) => {
|
||||
const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val }));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="kpi-card"
|
||||
onClick={() => onSelectLake(lake.id)}
|
||||
style={{ padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', cursor: 'pointer', transition: 'transform 0.2s', minHeight: '120px' }}
|
||||
onMouseOver={e => e.currentTarget.style.transform = 'translateY(-2px)'}
|
||||
onMouseOut={e => e.currentTarget.style.transform = 'translateY(0)'}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.9rem', color: 'var(--text-muted)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '120px' }}>
|
||||
{lake.name}
|
||||
</div>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 'bold' }}>{lake.level}</div>
|
||||
</div>
|
||||
<CircularProgress value={lake.capacity} size={36} strokeWidth={3} />
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minHeight: '30px', marginTop: 'auto' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id={`spark-${lake.id}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-cyan)" stopOpacity={0.5}/>
|
||||
<stop offset="95%" stopColor="var(--color-cyan)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey="value" stroke="var(--color-cyan)" strokeWidth={1.5} fillOpacity={1} fill={`url(#spark-${lake.id})`} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LakesOverview = ({ language, onSelectLake }: Props) => {
|
||||
const LakesOverview = ({ language }: Props) => {
|
||||
const [lakes, setLakes] = useState<Lake[]>([]);
|
||||
const [sortBy, setSortBy] = useState<'name' | 'level' | 'capacity' | 'inflow'>('name');
|
||||
|
||||
@@ -184,7 +134,6 @@ const LakesOverview = ({ language, onSelectLake }: Props) => {
|
||||
const priorityLakes = lakes.filter(l => l.priority);
|
||||
const otherLakes = lakes.filter(l => !l.priority);
|
||||
|
||||
// Sorting
|
||||
otherLakes.sort((a, b) => {
|
||||
if (sortBy === 'name') return a.name.localeCompare(b.name);
|
||||
if (sortBy === 'level') return b.level - a.level;
|
||||
@@ -223,7 +172,7 @@ const LakesOverview = ({ language, onSelectLake }: Props) => {
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
{priorityLakes.map(lake => <PriorityCard key={lake.id} lake={lake} onSelectLake={onSelectLake} />)}
|
||||
{priorityLakes.map(lake => <LakeCard key={lake.id} lake={lake} language={language} />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -235,7 +184,7 @@ const LakesOverview = ({ language, onSelectLake }: Props) => {
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
{otherLakes.map(lake => <SmallCard key={lake.id} lake={lake} onSelectLake={onSelectLake} />)}
|
||||
{otherLakes.map(lake => <LakeCard key={lake.id} lake={lake} language={language} />)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user