From 23e502bf5bc9eb1008cbe54ac2982505e2728a4f Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 24 Sep 2026 15:27:33 +0200 Subject: [PATCH] feat(dashboard): Steam avatar and a banner from the most played game --- src/App.jsx | 2 +- src/components/AppHeader.jsx | 9 ++-- src/components/HeroArt.jsx | 17 +++++++ ...geBackground.test.jsx => HeroArt.test.jsx} | 28 ++++++++--- src/components/PageBackground.jsx | 14 ++---- src/components/SteamAvatar.jsx | 21 ++++++++ src/components/SteamAvatar.test.jsx | 25 ++++++++++ src/hooks/useSteamProfile.js | 35 +++++++++++++ src/hooks/useSteamProfile.test.jsx | 39 +++++++++++++++ src/pages/Dashboard.jsx | 49 +++++++++++++------ src/pages/Dashboard.test.jsx | 20 ++++++++ 11 files changed, 222 insertions(+), 37 deletions(-) create mode 100644 src/components/HeroArt.jsx rename src/components/{PageBackground.test.jsx => HeroArt.test.jsx} (60%) create mode 100644 src/components/SteamAvatar.jsx create mode 100644 src/components/SteamAvatar.test.jsx create mode 100644 src/hooks/useSteamProfile.js create mode 100644 src/hooks/useSteamProfile.test.jsx diff --git a/src/App.jsx b/src/App.jsx index 84b35e4..6a1d5c3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -49,7 +49,7 @@ function App() { {/* isolate: the background layer sits behind the content but in front of this div's own background. */}
- {isAuthenticated && } + {isAuthenticated && }
diff --git a/src/components/AppHeader.jsx b/src/components/AppHeader.jsx index 35d686c..8bb3a56 100644 --- a/src/components/AppHeader.jsx +++ b/src/components/AppHeader.jsx @@ -1,6 +1,6 @@ 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'; +import {Gamepad2, Languages, LayoutDashboard, Library, LogOut, Moon, Settings, Sun} from 'lucide-react'; import {Button} from '@/components/ui/button'; import { @@ -13,6 +13,8 @@ import { import {usePreferences} from '@/context/PreferencesContext.jsx'; import {cn} from '@/lib/utils'; import PageContainer from './PageContainer.jsx'; +import SteamAvatar from './SteamAvatar.jsx'; +import useSteamProfile from '../hooks/useSteamProfile.js'; const NAV_ITEMS = [ {to: '/dashboard', labelKey: 'NAV.DASHBOARD', Icon: LayoutDashboard}, @@ -31,6 +33,7 @@ export default function AppHeader({isAuthenticated, onLogout}) { const {t, i18n} = useTranslation(); const {isDarkMode, setDarkMode, setLanguage} = usePreferences(); const {pathname} = useLocation(); + const steamProfile = useSteamProfile(isAuthenticated); const toggleLanguage = () => setLanguage(i18n.language === 'de' ? 'en' : 'de'); const toggleTheme = () => setDarkMode(!isDarkMode); @@ -62,8 +65,8 @@ export default function AppHeader({isAuthenticated, onLogout}) { {isAuthenticated ? ( - diff --git a/src/components/HeroArt.jsx b/src/components/HeroArt.jsx new file mode 100644 index 0000000..6ea3844 --- /dev/null +++ b/src/components/HeroArt.jsx @@ -0,0 +1,17 @@ +import {useState} from 'react'; +import {coverUrl, heroUrl} from '../utils/games.js'; + +/** + * The wide library art of a game, else its store header, else nothing. Give it `key={appid}`, so a new game + * starts with its library art again. + */ +export default function HeroArt({appid, className}) { + const [failures, setFailures] = useState(0); + + if (!appid || failures > 1) return null; + + return ( + setFailures((count) => count + 1)} className={className}/> + ); +} diff --git a/src/components/PageBackground.test.jsx b/src/components/HeroArt.test.jsx similarity index 60% rename from src/components/PageBackground.test.jsx rename to src/components/HeroArt.test.jsx index d643e5b..8e43d01 100644 --- a/src/components/PageBackground.test.jsx +++ b/src/components/HeroArt.test.jsx @@ -1,10 +1,25 @@ // @vitest-environment jsdom import {afterEach, describe, expect, it} from 'vitest'; import {cleanup, fireEvent, render} from '@testing-library/react'; +import HeroArt from './HeroArt.jsx'; import PageBackground from './PageBackground.jsx'; afterEach(cleanup); +describe('HeroArt', () => { + it('shows the library art, then the store header, then nothing', () => { + const {container} = render(); + const img = () => container.querySelector('img'); + expect(img().getAttribute('src')).toContain('/620/library_hero.jpg'); + + fireEvent.error(img()); + expect(img().getAttribute('src')).toContain('/620/header.jpg'); + + fireEvent.error(img()); + expect(img()).toBeNull(); + }); +}); + describe('PageBackground', () => { it('shows nothing without a game', () => { const {container} = render(); @@ -12,15 +27,12 @@ describe('PageBackground', () => { expect(container.innerHTML).toBe(''); }); - it('shows the library art, then the store header, then nothing', () => { - const {container} = render(); - const img = () => container.querySelector('img'); - expect(img().getAttribute('src')).toContain('/620/library_hero.jpg'); + it('starts with the library art again for another game', () => { + const {container, rerender} = render(); + fireEvent.error(container.querySelector('img')); - fireEvent.error(img()); - expect(img().getAttribute('src')).toContain('/620/header.jpg'); + rerender(); - fireEvent.error(img()); - expect(container.innerHTML).toBe(''); + expect(container.querySelector('img').getAttribute('src')).toContain('/220/library_hero.jpg'); }); }); diff --git a/src/components/PageBackground.jsx b/src/components/PageBackground.jsx index a72fef5..36479df 100644 --- a/src/components/PageBackground.jsx +++ b/src/components/PageBackground.jsx @@ -1,20 +1,14 @@ -import {useState} from 'react'; -import {coverUrl, heroUrl} from '../utils/games.js'; +import HeroArt from './HeroArt.jsx'; /** - * Game art behind the whole page, blurred and dimmed so text on it stays readable. Falls back to the store - * header for apps without library art. Give it `key={appid}`, so a new game starts with its library art again. + * Game art behind the whole page, blurred and dimmed so text on it stays readable. */ export default function PageBackground({appid}) { - const [failures, setFailures] = useState(0); - - if (!appid || failures > 1) return null; + if (!appid) return null; return (