feat: implement multilingual SEO support and enhance map UI with data synchronization updates
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
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 KpiCards from './KpiCards';
|
||||
import { WeatherWidget } from './WeatherWidget';
|
||||
import { WindChart } from './WindChart';
|
||||
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 {
|
||||
timestamp: string;
|
||||
@@ -21,6 +25,7 @@ interface LipnoData {
|
||||
interface Props {
|
||||
language: Language;
|
||||
lakeId: string | null;
|
||||
windUnit?: 'kmh' | 'ms';
|
||||
}
|
||||
|
||||
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';
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
@@ -74,7 +79,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather }: any) =>
|
||||
return null;
|
||||
};
|
||||
|
||||
const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
const [data, setData] = useState<LipnoData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lakeInfo, setLakeInfo] = useState<any>(null);
|
||||
@@ -174,6 +179,9 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
const targetMs7d = nowMs - 7 * 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 level7dAgo = latestData.level;
|
||||
let level30dAgo = latestData.level;
|
||||
@@ -221,19 +229,64 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
};
|
||||
|
||||
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 (
|
||||
<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' }}>
|
||||
<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>
|
||||
|
||||
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} />
|
||||
|
||||
{lakeInfo && lakeInfo.lat && lakeInfo.lng && (
|
||||
<WeatherWidget lat={lakeInfo.lat} lng={lakeInfo.lng} language={language} sensorTemp={latestData.temperature} />
|
||||
)}
|
||||
<div className="kpi-grid-container">
|
||||
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} />
|
||||
|
||||
{lakeInfo && lakeInfo.lat && lakeInfo.lng && (
|
||||
<WeatherWidget lat={lakeInfo.lat} lng={lakeInfo.lng} language={language} windUnit={windUnit} sensorTemp={latestData.temperature} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{limits && limits.map((limit, idx) => {
|
||||
const diff = latestData.level - limit.level;
|
||||
@@ -279,7 +332,7 @@ const LakeDetail = ({ language, lakeId }: Props) => {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<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}} />
|
||||
|
||||
<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) => (
|
||||
<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} />
|
||||
<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} />
|
||||
@@ -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>
|
||||
</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 */}
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user