diff --git a/public/data/MARI.json b/public/data/MARI.json index 299fa81..c843395 100644 --- a/public/data/MARI.json +++ b/public/data/MARI.json @@ -6815,10 +6815,28 @@ { "timestamp": "2026-06-05T21:20:00.000Z", "level": 467.72, + "flow": 0.7, + "inflow": 0, + "volume": 0, + "temperature": 14.4, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 467.72, + "flow": 0.7, + "inflow": 0, + "volume": 0, + "temperature": 14.4, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 467.72, "flow": 0, "inflow": 2.88, "volume": 26.49, - "temperature": 11.9, + "temperature": 11.6, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/MZHR.json b/public/data/MZHR.json index 526b1f0..76cf757 100644 --- a/public/data/MZHR.json +++ b/public/data/MZHR.json @@ -6824,10 +6824,19 @@ { "timestamp": "2026-06-05T21:30:00.000Z", "level": 352.85, - "flow": 0, + "flow": 2.53, + "inflow": 0, + "volume": 0, + "temperature": 12.1, + "precipitation": 5.7 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 352.85, + "flow": 2.53, "inflow": 1.46, "volume": 32.37, - "temperature": 12.1, + "temperature": 11.6, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLHN.json b/public/data/VLHN.json index 6286b74..cee925f 100644 --- a/public/data/VLHN.json +++ b/public/data/VLHN.json @@ -6816,9 +6816,27 @@ "timestamp": "2026-06-05T21:20:00.000Z", "level": 369.79, "flow": 1.25, + "inflow": 0, + "volume": 0, + "temperature": 18.234727964853622, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 369.79, + "flow": 1.25, + "inflow": 0, + "volume": 0, + "temperature": 18.234727964853622, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 369.79, + "flow": 1.25, "inflow": 10.82, "volume": 20.24, - "temperature": 11.3, + "temperature": 11.1, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLKO.json b/public/data/VLKO.json index 5fac878..7985a17 100644 --- a/public/data/VLKO.json +++ b/public/data/VLKO.json @@ -6816,9 +6816,27 @@ "timestamp": "2026-06-05T21:20:00.000Z", "level": 352.43, "flow": 19.01, + "inflow": 0, + "volume": 0, + "temperature": 12.4, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 352.42, + "flow": 19.01, + "inflow": 0, + "volume": 0, + "temperature": 12.4, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 352.42, + "flow": 19.01, "inflow": 14.13, "volume": 2.74, - "temperature": 11.3, + "temperature": 11.1, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLL1.json b/public/data/VLL1.json index 1e9971b..17bacbb 100644 --- a/public/data/VLL1.json +++ b/public/data/VLL1.json @@ -6816,6 +6816,24 @@ "timestamp": "2026-06-05T21:20:00.000Z", "level": 723.08, "flow": 1.51, + "inflow": 0, + "volume": 0, + "temperature": 18.62002326908434, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 723.08, + "flow": 1.51, + "inflow": 0, + "volume": 0, + "temperature": 18.62002326908434, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 723.08, + "flow": 1.51, "inflow": 2.51, "volume": 199.67, "temperature": 11.5, diff --git a/public/data/VLL2.json b/public/data/VLL2.json index 92c5f34..cb83a9e 100644 --- a/public/data/VLL2.json +++ b/public/data/VLL2.json @@ -6797,7 +6797,7 @@ { "timestamp": "2026-06-05T21:00:00.000Z", "level": 559.91, - "flow": 0, + "flow": 7.18, "inflow": 0, "volume": 0, "temperature": 17.97824695485787, @@ -6806,7 +6806,7 @@ { "timestamp": "2026-06-05T21:10:00.000Z", "level": 559.9, - "flow": 0, + "flow": 7.18, "inflow": 0, "volume": 0, "temperature": 17.97824695485787, @@ -6816,6 +6816,24 @@ "timestamp": "2026-06-05T21:20:00.000Z", "level": 559.89, "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 17.97824695485787, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 559.88, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 17.97824695485787, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 559.87, + "flow": 0, "inflow": 3.71, "volume": 0.68, "temperature": 8.7, diff --git a/public/data/VLOR.json b/public/data/VLOR.json index 5132efa..5615e22 100644 --- a/public/data/VLOR.json +++ b/public/data/VLOR.json @@ -6816,9 +6816,27 @@ "timestamp": "2026-06-05T21:20:00.000Z", "level": 345.27, "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 18.70045888971512, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 345.27, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 18.70045888971512, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 345.26, + "flow": 0, "inflow": 23.84, "volume": 522.12, - "temperature": 12.2, + "temperature": 12, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLSL.json b/public/data/VLSL.json index acf1bc7..85dd137 100644 --- a/public/data/VLSL.json +++ b/public/data/VLSL.json @@ -6816,9 +6816,27 @@ "timestamp": "2026-06-05T21:20:00.000Z", "level": 269.83, "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 16.3, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 269.83, + "flow": 0, + "inflow": 0, + "volume": 0, + "temperature": 16.3, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 269.84, + "flow": 0, "inflow": 46.5, "volume": 260.21, - "temperature": 11.8, + "temperature": 11.5, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/VLST.json b/public/data/VLST.json index 39b05ed..15368c9 100644 --- a/public/data/VLST.json +++ b/public/data/VLST.json @@ -6816,9 +6816,27 @@ "timestamp": "2026-06-05T21:20:00.000Z", "level": 218.64, "flow": 25.32, + "inflow": 0, + "volume": 0, + "temperature": 18.450684013836877, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:30:00.000Z", + "level": 218.59, + "flow": 25.39, + "inflow": 0, + "volume": 0, + "temperature": 18.450684013836877, + "precipitation": 0 + }, + { + "timestamp": "2026-06-05T21:40:00.000Z", + "level": 218.72, + "flow": 25.33, "inflow": 19.85, "volume": 9.68, - "temperature": 11.7, + "temperature": 11.5, "precipitation": 0 } ] \ No newline at end of file diff --git a/public/data/lakes_index.json b/public/data/lakes_index.json index e782084..d4c8845 100644 --- a/public/data/lakes_index.json +++ b/public/data/lakes_index.json @@ -14,8 +14,6 @@ "lat": 48.6322, "lng": 14.2215, "sparkline": [ - 1.49, - 13.76, 34.78, 37.78, 33.61, @@ -25,6 +23,8 @@ 1.51, 1.51, 1.51, + 1.51, + 1.51, 1.51 ] }, @@ -33,9 +33,9 @@ "name": "Lipno II", "river": "Vltava", "priority": false, - "level": "559.89", + "level": "559.87", "capacity": 100, - "storageDiff": 48.39, + "storageDiff": 48.37, "inflow": "3.7", "outflow": "0.0", "volume": 0.68, @@ -43,8 +43,6 @@ "lat": 48.625, "lng": 14.318, "sparkline": [ - 7.31, - 7.34, 7.48, 7.29, 7.27, @@ -52,6 +50,8 @@ 0, 0, 0, + 7.18, + 7.18, 0, 0, 0 @@ -72,8 +72,6 @@ "lat": 49.183, "lng": 14.444, "sparkline": [ - 14.18, - 14.18, 18.46, 14.28, 5, @@ -83,6 +81,8 @@ 1.25, 1.25, 1.25, + 1.25, + 1.25, 1.25 ] }, @@ -91,9 +91,9 @@ "name": "Kořensko", "river": "Vltava", "priority": false, - "level": "352.43", - "capacity": 28.7, - "storageDiff": -0.17, + "level": "352.42", + "capacity": 28, + "storageDiff": -0.18, "inflow": "14.1", "outflow": "19.0", "volume": 2.74, @@ -120,9 +120,9 @@ "name": "Orlík", "river": "Vltava", "priority": true, - "level": "345.27", + "level": "345.26", "capacity": 63.6, - "storageDiff": -4.63, + "storageDiff": -4.64, "inflow": "23.8", "outflow": "0.0", "volume": 522.12, @@ -130,8 +130,6 @@ "lat": 49.606, "lng": 14.17, "sparkline": [ - 186.83, - 454.38, 444.3, 370.39, 381.47, @@ -141,6 +139,8 @@ 432.41, 377.67, 137.48, + 0, + 0, 0 ] }, @@ -178,9 +178,9 @@ "name": "Slapy", "river": "Vltava", "priority": true, - "level": "269.83", - "capacity": 77.3, - "storageDiff": -0.77, + "level": "269.84", + "capacity": 77.5, + "storageDiff": -0.76, "inflow": "46.5", "outflow": "0.0", "volume": 260.21, @@ -188,8 +188,6 @@ "lat": 49.822, "lng": 14.436, "sparkline": [ - 119.44, - 137.14, 310.27, 308.35, 304.36, @@ -199,6 +197,8 @@ 287.91, 217.32, 79.38, + 0, + 0, 0 ] }, @@ -207,9 +207,9 @@ "name": "Štěchovice", "river": "Vltava", "priority": false, - "level": "218.64", - "capacity": 65.6, - "storageDiff": -0.76, + "level": "218.72", + "capacity": 68.8, + "storageDiff": -0.68, "inflow": "19.9", "outflow": "25.3", "volume": 9.68, @@ -217,8 +217,6 @@ "lat": 49.845, "lng": 14.412, "sparkline": [ - 25.32, - 70.8, 150.41, 150.43, 120.77, @@ -228,7 +226,9 @@ 85.34, 85.17, 52.56, - 25.32 + 25.32, + 25.39, + 25.33 ] }, { @@ -310,11 +310,11 @@ 0.7, 0.7, 0.7, - 0.7, - 0.7, 0, 0.7, 0.7, + 0.7, + 0.7, 0 ] }, @@ -327,13 +327,12 @@ "capacity": 0, "storageDiff": -1.25, "inflow": "1.5", - "outflow": "0.0", + "outflow": "2.5", "volume": 32.37, "maxVolume": 56.7, "lat": 49.789, "lng": 13.155, "sparkline": [ - 2.52, 2.52, 2.53, 2.53, @@ -344,7 +343,8 @@ 2.53, 2.53, 2.53, - 0 + 2.53, + 2.53 ] } ] \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 8fc6179..d166411 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Routes, Route, useParams, useLocation, useNavigate, Navigate } from 're import LakeDetail from './components/LakeDetail'; import LakesOverview from './components/LakesOverview'; import LakeMap from './components/LakeMap'; +import FavoritesOverview from './components/FavoritesOverview'; import Sidebar from './components/Sidebar'; import Topbar from './components/Topbar'; import SettingsModal from './components/SettingsModal'; @@ -60,6 +61,7 @@ function App() { setIsMobileMenuOpen(!isMobileMenuOpen)} /> } /> + } /> } /> } /> diff --git a/src/components/FavoritesOverview.tsx b/src/components/FavoritesOverview.tsx new file mode 100644 index 0000000..68a5977 --- /dev/null +++ b/src/components/FavoritesOverview.tsx @@ -0,0 +1,169 @@ +import { useState, useEffect } from 'react'; +import { FiStar } from 'react-icons/fi'; +import { type Language } from '../translations'; +import { useFavorites } from '../hooks/useFavorites'; +import { useNavigate } from 'react-router-dom'; +import { slugify } from '../utils/slugify'; +import { AreaChart, Area, ResponsiveContainer } from 'recharts'; +import { FiTrendingUp, FiTrendingDown } from 'react-icons/fi'; + +interface Lake { + id: string; + name: string; + river: string; + priority: boolean; + level: number; + capacity: number; + storageDiff?: number; + inflow: number; + outflow: number; + volume: number; + sparkline: number[]; +} + +interface Props { + language: Language; +} + +const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number, size?: number, strokeWidth?: number }) => { + const radius = (size - strokeWidth) / 2; + const circumference = radius * 2 * Math.PI; + const offset = circumference - (value / 100) * circumference; + return ( +
+ + + + +
+ {value > 0 ? `${value}%` : 'N/A'} +
+
+ ); +}; + +const FavoritesOverview = ({ language }: Props) => { + const [lakes, setLakes] = useState([]); + const { isFavorite, toggleFavorite } = useFavorites(); + const navigate = useNavigate(); + + useEffect(() => { + fetch('/data/lakes_index.json') + .then(res => res.json()) + .then(data => setLakes(data)) + .catch(err => console.error(err)); + }, []); + + const favoriteLakes = lakes.filter(l => isFavorite(l.id)); + + return ( +
+
+

+ + {language === 'cs' ? 'Oblíbená' : 'Favourites'} +

+

+ {language === 'cs' + ? 'Jezera připnutá v přehledu. Připnout nebo odepnout lze ikonou hvězdičky.' + : 'Lakes you pinned in the overview. Use the star icon to pin or unpin.'} +

+
+ + {favoriteLakes.length === 0 ? ( +
+ +

+ {language === 'cs' ? 'Zatím žádná oblíbená jezera.' : 'No favourites yet.'} +

+

+ {language === 'cs' + ? 'Přejdi na Jezera a nádrže a klikni na ⭐ u jezera, které tě zajímá.' + : 'Go to Lakes & Reservoirs and click the ⭐ on any lake to pin it here.'} +

+
+ ) : ( +
+ {favoriteLakes.map(lake => { + const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val })); + const isFav = isFavorite(lake.id); + return ( +
navigate(`/${slugify(lake.name)}`)} + style={{ cursor: 'pointer', padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative' }} + > + {/* Unpin button */} + + +

+ {lake.name} {lake.river ? `- ${lake.river}` : ''} +

+ +
+
+
+
Water level
+
{lake.level} m n.m.
+
+
+
+ +
+
+ = 80 ? 'var(--color-green)' : lake.capacity < 40 ? 'var(--color-red)' : 'var(--text-main)' }}> + {lake.capacity > 0 ? `${lake.capacity}%` : 'N/A'} + +
+ {lake.storageDiff !== undefined && ( +
= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 500 }}> + {lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m +
+ )} + +
+
+
+ +
+
+ + Inflow {lake.inflow} m³/s +
+
+ + Outflow {lake.outflow} m³/s +
+
+ +
+ ); + })} +
+ )} +
+ ); +}; + +export default FavoritesOverview; diff --git a/src/components/LakesOverview.tsx b/src/components/LakesOverview.tsx index 1b2532b..ff2a53d 100644 --- a/src/components/LakesOverview.tsx +++ b/src/components/LakesOverview.tsx @@ -1,9 +1,10 @@ import { useState, useEffect } from 'react'; -import { FiTrendingUp, FiTrendingDown } from 'react-icons/fi'; +import { FiTrendingUp, FiTrendingDown, FiStar } from 'react-icons/fi'; import { type Language, t } from '../translations'; import { AreaChart, Area, ResponsiveContainer } from 'recharts'; import { useNavigate } from 'react-router-dom'; import { slugify } from '../utils/slugify'; +import { useFavorites } from '../hooks/useFavorites'; interface Lake { id: string; @@ -12,6 +13,7 @@ interface Lake { priority: boolean; level: number; capacity: number; + storageDiff?: number; inflow: number; outflow: number; volume: number; @@ -26,7 +28,7 @@ const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number const radius = (size - strokeWidth) / 2; const circumference = radius * 2 * Math.PI; const offset = circumference - (value / 100) * circumference; - + return (
@@ -57,18 +59,38 @@ const CircularProgress = ({ value, size = 60, strokeWidth = 6 }: { value: number ); }; -const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => { +const LakeCard = ({ lake, language, isFav, onToggleFav }: { lake: Lake, language: Language, isFav: boolean, onToggleFav: (id: string) => void }) => { const navigate = useNavigate(); const chartData = lake.sparkline.map((val, i) => ({ name: i, value: val })); - + return ( -
navigate(`/${slugify(lake.name)}`)} style={{ cursor: 'pointer', flex: 1, padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.5rem', position: 'relative' }} > -

{lake.name} {lake.river ? `- ${lake.river}` : ''}

- + {/* Star / Favorite button */} + + +

{lake.name} {lake.river ? `- ${lake.river}` : ''}

+
@@ -80,31 +102,39 @@ const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => {
{lake.level} m n.m.
- +
-
{lake.capacity > 0 ? `${lake.capacity}%` : 'N/A'} / {lake.volume} mil. m³
-
Volume
+
+ = 80 ? 'var(--color-green)' : lake.capacity < 40 ? 'var(--color-red)' : 'var(--text-main)' }}> + {lake.capacity > 0 ? `${lake.capacity}%` : 'N/A'} + +
+ {lake.storageDiff !== undefined && ( +
= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 500 }}> + {lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m +
+ )}
- +
- - + +
- +
@@ -120,9 +150,55 @@ const LakeCard = ({ lake, language }: { lake: Lake, language: Language }) => { ); }; +const SmallLakeCard = ({ lake, isFav, onToggleFav }: { lake: Lake, isFav: boolean, onToggleFav: (id: string) => void }) => { + const navigate = useNavigate(); + + return ( +
navigate(`/${slugify(lake.name)}`)} + style={{ cursor: 'pointer', padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', position: 'relative' }} + > + {/* Star button */} + + +
{lake.name}
+
{lake.level} m n.m.
+
+ = 80 ? 'var(--color-green)' : lake.capacity < 40 ? 'var(--color-red)' : 'var(--text-muted)', fontWeight: 600 }}> + {lake.capacity > 0 ? `${lake.capacity}%` : 'N/A'} + + {lake.storageDiff !== undefined && ( + = 0 ? 'var(--color-green)' : 'var(--color-red)', marginLeft: '4px' }}> + ({lake.storageDiff > 0 ? '+' : ''}{lake.storageDiff.toFixed(2)} m) + + )} +
+
+ ); +}; + const LakesOverview = ({ language }: Props) => { const [lakes, setLakes] = useState([]); const [sortBy, setSortBy] = useState<'name' | 'level' | 'capacity' | 'inflow'>('name'); + const { isFavorite, toggleFavorite, favorites } = useFavorites(); useEffect(() => { fetch('/data/lakes_index.json') @@ -131,8 +207,9 @@ const LakesOverview = ({ language }: Props) => { .catch(err => console.error(err)); }, []); - const priorityLakes = lakes.filter(l => l.priority); - const otherLakes = lakes.filter(l => !l.priority); + const favoriteLakes = lakes.filter(l => isFavorite(l.id)); + const priorityLakes = lakes.filter(l => l.priority && !isFavorite(l.id)); + const otherLakes = lakes.filter(l => !l.priority && !isFavorite(l.id)); otherLakes.sort((a, b) => { if (sortBy === 'name') return a.name.localeCompare(b.name); @@ -143,8 +220,8 @@ const LakesOverview = ({ language }: Props) => { }); const sortButtonStyle = (type: string) => ({ - background: 'none', border: 'none', - color: sortBy === type ? 'var(--text-main)' : 'var(--text-muted)', + background: 'none', border: 'none', + color: sortBy === type ? 'var(--text-main)' : 'var(--text-muted)', cursor: 'pointer', fontSize: '0.85rem' }); @@ -164,27 +241,47 @@ const LakesOverview = ({ language }: Props) => {
+ {/* Favorites section */} + {favoriteLakes.length > 0 && ( +
+

+ Oblíbená ({favoriteLakes.length}) +

+
+ {favoriteLakes.map(lake => ( + + ))} +
+
+ )} + {priorityLakes.length > 0 && (

Priority Reservoirs

-
- {priorityLakes.map(lake => )} + {priorityLakes.map(lake => )}
)}

Other Reservoirs ({otherLakes.length})

-
- {otherLakes.map(lake => )} + {otherLakes.map(lake => ( + + ))}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 640dbdd..cee6009 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { FiDroplet, FiStar, FiMap, FiSettings, FiChevronLeft, FiChevronRight, FiDatabase } from 'react-icons/fi'; import { type Language, t } from '../translations'; +import { useFavorites } from '../hooks/useFavorites'; interface Props { language: Language; @@ -15,10 +16,11 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu const navigate = useNavigate(); const location = useLocation(); const dict = t[language].sidebar; + const { favorites } = useFavorites(); const isOverview = location.pathname === '/'; + const isFavoritesPage = location.pathname === '/favorites'; const isMap = location.pathname === '/map'; - const isDetail = !isOverview && !isMap; const handleNavigate = (path: string) => { navigate(path); @@ -49,14 +51,37 @@ const Sidebar = ({ language, onOpenSettings, isMobileMenuOpen, onCloseMobileMenu
-
handleNavigate('/lipno-1')}> - + {/* Favourites */} +
handleNavigate('/favorites')} style={{ position: 'relative' }}> + 0 ? '#f59e0b' : 'none'} color={favorites.length > 0 ? '#f59e0b' : 'currentColor'} /> {dict.favorites} + {favorites.length > 0 && ( + + {favorites.length} + + )}
+ + {/* Lakes & Reservoirs */}
handleNavigate('/')}> {dict.lakes}
+ + {/* Map */}
handleNavigate('/map')}> {dict.map} diff --git a/src/hooks/useFavorites.tsx b/src/hooks/useFavorites.tsx new file mode 100644 index 0000000..ca99ecb --- /dev/null +++ b/src/hooks/useFavorites.tsx @@ -0,0 +1,46 @@ +import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; + +const STORAGE_KEY = 'hladinator_favorites'; + +const loadFavorites = (): string[] => { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : []; + } catch { + return []; + } +}; + +interface FavoritesContextType { + favorites: string[]; + toggleFavorite: (id: string) => void; + isFavorite: (id: string) => boolean; +} + +const FavoritesContext = createContext({ + favorites: [], + toggleFavorite: () => {}, + isFavorite: () => false, +}); + +export const FavoritesProvider = ({ children }: { children: ReactNode }) => { + const [favorites, setFavorites] = useState(loadFavorites); + + const toggleFavorite = useCallback((id: string) => { + setFavorites(prev => { + const next = prev.includes(id) ? prev.filter(f => f !== id) : [...prev, id]; + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + return next; + }); + }, []); + + const isFavorite = useCallback((id: string) => favorites.includes(id), [favorites]); + + return ( + + {children} + + ); +}; + +export const useFavorites = () => useContext(FavoritesContext); diff --git a/src/main.tsx b/src/main.tsx index ade9d64..a7c6abd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' +import { FavoritesProvider } from './hooks/useFavorites' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - + + + , )