From aa5124ea1ed64677e323420afec996b552933072 Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 24 Sep 2026 13:58:44 +0200 Subject: [PATCH] feat(library): Steam wishlist tab with prices and discounts The library page gets a Bibliothek | Wunschliste tab bar. The wishlist shows the current store price, the discount and can show only games on sale. --- public/i18n/de.json | 9 ++ public/i18n/en.json | 9 ++ src/App.jsx | 2 + src/components/AppHeader.jsx | 13 +-- src/components/LibraryTabs.jsx | 32 +++++++ src/pages/GameLibrary.jsx | 2 + src/pages/WishlistPage.jsx | 147 ++++++++++++++++++++++++++++++++ src/pages/WishlistPage.test.jsx | 72 ++++++++++++++++ 8 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 src/components/LibraryTabs.jsx create mode 100644 src/pages/WishlistPage.jsx create mode 100644 src/pages/WishlistPage.test.jsx diff --git a/public/i18n/de.json b/public/i18n/de.json index f5860c9..5a5f964 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -207,5 +207,14 @@ "MEDIUM": "Mittel", "LOW": "Niedrig" } + }, + "WISHLIST": { + "TITLE": "Wunschliste", + "ONLY_ON_SALE": "Nur im Angebot", + "EMPTY": "Deine Steam-Wunschliste ist leer oder privat. Stelle sie in Steam auf öffentlich; der nächste Sync holt sie.", + "NONE_ON_SALE": "Gerade ist nichts von deiner Wunschliste im Angebot.", + "NO_PRICE": "Noch kein Preis", + "ADDED": "Hinzugefügt am {{date}}", + "ERROR_LOAD": "Wunschliste konnte nicht geladen werden." } } diff --git a/public/i18n/en.json b/public/i18n/en.json index 3e5349d..3646153 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -207,5 +207,14 @@ "MEDIUM": "Medium", "LOW": "Low" } + }, + "WISHLIST": { + "TITLE": "Wishlist", + "ONLY_ON_SALE": "On sale only", + "EMPTY": "Your Steam wishlist is empty or private. Make it public in Steam; the next sync fetches it.", + "NONE_ON_SALE": "Nothing on your wishlist is on sale right now.", + "NO_PRICE": "No price yet", + "ADDED": "Added {{date}}", + "ERROR_LOAD": "Could not load the wishlist." } } diff --git a/src/App.jsx b/src/App.jsx index 86675b5..ffad131 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,6 +10,7 @@ import RegistrationPage from './pages/RegistrationPage.jsx'; import SettingsPage from './pages/SettingsPage.jsx'; import GameLibrary from './pages/GameLibrary.jsx'; import GameDetail from './pages/GameDetail.jsx'; +import WishlistPage from './pages/WishlistPage.jsx'; import AppHeader from './components/AppHeader.jsx'; import {Toaster} from '@/components/ui/sonner'; import {TooltipProvider} from '@/components/ui/tooltip'; @@ -47,6 +48,7 @@ function App() { }/> }/> }/> + }/> }/> }/> diff --git a/src/components/AppHeader.jsx b/src/components/AppHeader.jsx index db78e25..f56e51a 100644 --- a/src/components/AppHeader.jsx +++ b/src/components/AppHeader.jsx @@ -1,4 +1,4 @@ -import {Link, NavLink} from 'react-router-dom'; +import {Link, NavLink, useLocation} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {Gamepad2, Languages, LayoutDashboard, Library, LogOut, Moon, Settings, Sun, UserRound} from 'lucide-react'; @@ -15,10 +15,11 @@ import {cn} from '@/lib/utils'; const NAV_ITEMS = [ {to: '/dashboard', labelKey: 'NAV.DASHBOARD', Icon: LayoutDashboard}, - {to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library}, + // The wishlist is a tab of the library. + {to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library, alsoActiveOn: ['/wishlist']}, ]; -const navLinkClass = ({isActive}) => cn( +const navLinkClass = (isActive) => cn( 'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors', isActive ? 'bg-accent text-accent-foreground' @@ -28,6 +29,7 @@ const navLinkClass = ({isActive}) => cn( export default function AppHeader({isAuthenticated, onLogout}) { const {t, i18n} = useTranslation(); const {isDarkMode, setDarkMode, setLanguage} = usePreferences(); + const {pathname} = useLocation(); const toggleLanguage = () => setLanguage(i18n.language === 'de' ? 'en' : 'de'); const toggleTheme = () => setDarkMode(!isDarkMode); @@ -45,8 +47,9 @@ export default function AppHeader({isAuthenticated, onLogout}) { {isAuthenticated && (