feat(dashboard): currently playing and pile of shame tiles
Build, Test & Push Frontend / quality-check (push) Successful in 10s
Build, Test & Push Frontend / docker (push) Successful in 22s

Pile of Shame shows the backlog count and share, a status bar and a random
backlog pick. Both tiles show the first-sync text while the initial sync runs;
a failed summary load shows a retry button.
This commit is contained in:
lobo committed 2026-09-24 11:25:12 +02:00
1 parent f94e0aff15
commit c24c22d5ed
8 files changed
+262 -11

No files matched your search

+72 -3
View File
@@ -1,13 +1,16 @@
import React, {useCallback, useEffect, useState} from 'react';
import {Link, useNavigate} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
import {ArrowRight, Hash, Link2, RefreshCw, Trophy} from 'lucide-react';
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
import {Card} from '@/components/ui/card';
import {Skeleton} from '@/components/ui/skeleton';
import CurrentlyPlayingCard from '../components/CurrentlyPlayingCard.jsx';
import GameCover from '../components/GameCover.jsx';
import PileOfShameCard from '../components/PileOfShameCard.jsx';
import SteamGameCount from '../components/SteamGameCount';
import SyncStatusBadge from '../components/SyncStatusBadge.jsx';
import useLibrarySync from '../hooks/useLibrarySync.js';
@@ -17,6 +20,8 @@ import {formatPlaytime} from '../utils/games.js';
const TOP_GAMES = 5;
// The games endpoint accepts only the page sizes 20, 40 and 80.
const TOP_GAMES_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc';
const PLAYING_GAMES = 4;
const PLAYING_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc&status=PLAYING';
const authHeaders = () => ({
'Authorization': `Bearer ${localStorage.getItem('tbp_jwt')}`,
@@ -29,6 +34,10 @@ const Dashboard = () => {
const [userData, setUserData] = useState(null);
const [steamData, setSteamData] = useState({count: 0, hasSteamId: false});
const [topGames, setTopGames] = useState([]);
const [playing, setPlaying] = useState([]);
const [summary, setSummary] = useState(null);
const [summaryFailed, setSummaryFailed] = useState(false);
const [shuffling, setShuffling] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
@@ -55,10 +64,52 @@ const Dashboard = () => {
}
}, []);
const fetchPlaying = useCallback(async () => {
try {
const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}${PLAYING_URL}`, {headers: authHeaders()});
if (r.ok) {
const page = await r.json();
setPlaying((page.content ?? []).slice(0, PLAYING_GAMES));
}
} catch (err) {
console.error('Error fetching playing games:', err);
}
}, []);
/**
* @param excludeAppid the current suggestion, so the new pick differs from it
* @returns false when the request failed
*/
const fetchSummary = useCallback(async (excludeAppid) => {
const query = excludeAppid ? `?exclude=${excludeAppid}` : '';
try {
const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/summary${query}`, {headers: authHeaders()});
if (r.ok) {
setSummary(await r.json());
setSummaryFailed(false);
return true;
}
} catch (err) {
console.error('Error fetching library summary:', err);
}
setSummaryFailed(true);
return false;
}, []);
const shuffleSuggestion = async () => {
setShuffling(true);
if (!await fetchSummary(summary?.suggestion?.appid)) {
toast.error(t('DASHBOARD.SUMMARY_FAILED'));
}
setShuffling(false);
};
const reloadLibrary = useCallback(() => {
fetchSteamCount();
fetchTopGames();
}, [fetchSteamCount, fetchTopGames]);
fetchPlaying();
fetchSummary();
}, [fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary]);
useEffect(() => {
const fetchUserData = async () => {
@@ -78,7 +129,7 @@ const Dashboard = () => {
setUserData(data);
if (data.steamId) {
await Promise.all([fetchSteamCount(), fetchTopGames()]);
await Promise.all([fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchSummary()]);
}
} else if (profileResponse.status === 401 || profileResponse.status === 403) {
localStorage.removeItem('tbp_jwt');
@@ -95,7 +146,7 @@ const Dashboard = () => {
};
fetchUserData();
}, [navigate, t, fetchSteamCount, fetchTopGames]);
}, [navigate, t, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary]);
const hasSteamId = !!userData?.steamId;
const sync = useLibrarySync({
@@ -209,6 +260,24 @@ const Dashboard = () => {
</Card>
</div>
)}
{hasSteamId && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<CurrentlyPlayingCard games={playing} syncing={sync.isRunning}/>
{summary && (
<PileOfShameCard summary={summary} syncing={sync.isRunning}
onShuffle={shuffleSuggestion} shuffling={shuffling}/>
)}
{!summary && summaryFailed && (
<Card className="items-center justify-center gap-3 p-6 text-center text-sm text-muted-foreground">
{t('DASHBOARD.SUMMARY_FAILED')}
<Button variant="outline" size="sm" onClick={() => fetchSummary()}>
<RefreshCw/>{t('DASHBOARD.RETRY')}
</Button>
</Card>
)}
</div>
)}
</div>
);
};