Public Access
feat(dashboard): Steam avatar and a banner from the most played game
This commit is contained in:
1 parent
2c360c9421
commit
23e502bf5b
11 files changed
+222
-37
No files matched your search
+34
-15
@@ -19,6 +19,10 @@ import useForceSync from '../hooks/useForceSync.js';
|
||||
import {formatPlaytime} from '../utils/games.js';
|
||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||
import PageContainer from '../components/PageContainer.jsx';
|
||||
import HeroArt from '../components/HeroArt.jsx';
|
||||
import SteamAvatar from '../components/SteamAvatar.jsx';
|
||||
import useSteamProfile from '../hooks/useSteamProfile.js';
|
||||
import {cn} from '@/lib/utils';
|
||||
|
||||
const TOP_GAMES = 5;
|
||||
// The games endpoint accepts only the page sizes 20, 40 and 80.
|
||||
@@ -185,6 +189,8 @@ const Dashboard = () => {
|
||||
}, [navigate, expireSession, t, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary, fetchActivity]);
|
||||
|
||||
const hasSteamId = !!userData?.steamId;
|
||||
const steamProfile = useSteamProfile(hasSteamId);
|
||||
const bannerAppid = activity?.recentGames?.[0]?.appid ?? null;
|
||||
const sync = useLibrarySync({
|
||||
enabled: hasSteamId,
|
||||
expectInitialSync: hasSteamId && steamData.count === 0,
|
||||
@@ -211,22 +217,35 @@ const Dashboard = () => {
|
||||
return (
|
||||
<PageContainer className="p-4 md:p-8 space-y-6">
|
||||
<Card className="relative overflow-hidden p-6 md:p-8 flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-primary/15 via-transparent to-transparent"/>
|
||||
<div className="relative min-w-0">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('DASHBOARD.WELCOME_BACK')}</p>
|
||||
<h1 className="mt-1 text-3xl md:text-4xl font-bold tracking-tight truncate">
|
||||
{t('DASHBOARD.HELLO')} {userData?.displayName ?? userData?.email?.split('@')[0]} 👋
|
||||
</h1>
|
||||
{hasSteamId ? (
|
||||
<Badge variant="outline" className="mt-3 gap-1.5 font-mono text-muted-foreground">
|
||||
<Hash className="size-3"/>
|
||||
{userData.steamId}
|
||||
</Badge>
|
||||
) : (
|
||||
<Button asChild variant="link" className="mt-2 h-auto p-0">
|
||||
<Link to="/settings"><Link2 className="size-4"/>{t('DASHBOARD.NO_STEAM_ID')}</Link>
|
||||
</Button>
|
||||
{/* The banner is the game played most in the last two weeks; the gradient keeps the text readable. */}
|
||||
<HeroArt key={bannerAppid} appid={bannerAppid}
|
||||
className="pointer-events-none absolute inset-0 size-full object-cover opacity-60"/>
|
||||
<div className={cn('pointer-events-none absolute inset-0', bannerAppid
|
||||
? 'bg-gradient-to-r from-card via-card/85 to-card/60'
|
||||
: 'bg-gradient-to-br from-primary/15 via-transparent to-transparent')}/>
|
||||
<div className="relative flex min-w-0 items-center gap-4">
|
||||
{/* The slot is there before the avatar loads, so the title does not jump. */}
|
||||
{hasSteamId && (
|
||||
<span className="flex size-16 shrink-0 items-center justify-center rounded-full bg-muted ring-2 ring-primary/60">
|
||||
<SteamAvatar url={steamProfile?.avatarUrl} className="size-16" iconClassName="size-8 text-muted-foreground"/>
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t('DASHBOARD.WELCOME_BACK')}</p>
|
||||
<h1 className="mt-1 text-3xl md:text-4xl font-bold tracking-tight truncate">
|
||||
{t('DASHBOARD.HELLO')} {userData?.displayName ?? userData?.email?.split('@')[0]} 👋
|
||||
</h1>
|
||||
{hasSteamId ? (
|
||||
<Badge variant="outline" className="mt-3 gap-1.5 font-mono text-muted-foreground">
|
||||
<Hash className="size-3"/>
|
||||
{userData.steamId}
|
||||
</Badge>
|
||||
) : (
|
||||
<Button asChild variant="link" className="mt-2 h-auto p-0">
|
||||
<Link to="/settings"><Link2 className="size-4"/>{t('DASHBOARD.NO_STEAM_ID')}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasSteamId && (
|
||||
|
||||
@@ -47,4 +47,24 @@ describe('Dashboard', () => {
|
||||
expect(current.location.state).toEqual({sessionExpired: true, from: '/dashboard'});
|
||||
expect(screen.queryByText('LIBRARY.EMPTY')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the Steam avatar and the most played game of the last two weeks as banner', async () => {
|
||||
const avatar = 'https://avatars.steamstatic.com/abc_full.jpg';
|
||||
globalThis.fetch = vi.fn(async (url) => {
|
||||
if (url.includes('/api/users/me')) return ok({email: 'a@b.de', displayName: 'Lobo', steamId: '76561198000000000'});
|
||||
if (url.includes('/api/library/steam-profile')) return ok({personaName: 'Lobo', avatarUrl: avatar});
|
||||
if (url.includes('/api/library/activity')) return ok({totalMinutes: 90, days: [{day: '2026-09-23', minutes: 0}, {day: '2026-09-24', minutes: 90}], recentGames: [
|
||||
{appid: 1086940, name: "Baldur's Gate 3", playtimeMinutes: 900, playtime2Weeks: 90, status: 'PLAYING'}]});
|
||||
if (url.includes('/api/library/games-count')) return ok({count: 1, hasSteamId: true});
|
||||
return ok({content: [], total: 0, byStatus: {}});
|
||||
});
|
||||
const {container} = render(
|
||||
<MemoryRouter initialEntries={['/dashboard']}><PreferencesProvider>
|
||||
<Dashboard/>
|
||||
</PreferencesProvider></MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(container.querySelector(`img[src="${avatar}"]`)).not.toBeNull());
|
||||
expect(container.querySelector('img[src$="/1086940/library_hero.jpg"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user