Public Access
feat(dashboard): playtime chart, recently played and a playing hint
The dashboard shows the playtime of the last 30 days and Steam's recently played games. The detail page offers to set a backlog game to Playing when Steam reports recent playtime; unsaved edits in the backlog card stay.
This commit is contained in:
1 parent
d4c0f8d8f3
commit
89c5ba5253
11 files changed
+334
-15
No files matched your search
+23
-3
@@ -8,6 +8,7 @@ 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 ActivityCard from '../components/ActivityCard.jsx';
|
||||
import CurrentlyPlayingCard from '../components/CurrentlyPlayingCard.jsx';
|
||||
import GameCover from '../components/GameCover.jsx';
|
||||
import PileOfShameCard from '../components/PileOfShameCard.jsx';
|
||||
@@ -38,6 +39,7 @@ const Dashboard = () => {
|
||||
const [topGames, setTopGames] = useState([]);
|
||||
const [playing, setPlaying] = useState([]);
|
||||
const [summary, setSummary] = useState(null);
|
||||
const [activity, setActivity] = useState(null);
|
||||
const [summaryFailed, setSummaryFailed] = useState(false);
|
||||
const [shuffling, setShuffling] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -114,6 +116,21 @@ const Dashboard = () => {
|
||||
return false;
|
||||
}, [expireSession]);
|
||||
|
||||
const fetchActivity = useCallback(async () => {
|
||||
try {
|
||||
const r = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/activity`, {headers: authHeaders()});
|
||||
if (r.status === 401) {
|
||||
expireSession();
|
||||
return;
|
||||
}
|
||||
if (r.ok) {
|
||||
setActivity(await r.json());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching activity:', err);
|
||||
}
|
||||
}, [expireSession]);
|
||||
|
||||
const shuffleSuggestion = async () => {
|
||||
setShuffling(true);
|
||||
if (!await fetchSummary(summary?.suggestion?.appid)) {
|
||||
@@ -127,7 +144,8 @@ const Dashboard = () => {
|
||||
fetchTopGames();
|
||||
fetchPlaying();
|
||||
fetchSummary();
|
||||
}, [fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary]);
|
||||
fetchActivity();
|
||||
}, [fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary, fetchActivity]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
@@ -147,7 +165,7 @@ const Dashboard = () => {
|
||||
setUserData(data);
|
||||
|
||||
if (data.steamId) {
|
||||
await Promise.all([fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchSummary()]);
|
||||
await Promise.all([fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchSummary(), fetchActivity()]);
|
||||
}
|
||||
} else if (profileResponse.status === 401) {
|
||||
expireSession();
|
||||
@@ -163,7 +181,7 @@ const Dashboard = () => {
|
||||
};
|
||||
|
||||
fetchUserData();
|
||||
}, [navigate, expireSession, t, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary]);
|
||||
}, [navigate, expireSession, t, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary, fetchActivity]);
|
||||
|
||||
const hasSteamId = !!userData?.steamId;
|
||||
const sync = useLibrarySync({
|
||||
@@ -296,6 +314,8 @@ const Dashboard = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasSteamId && activity && <ActivityCard activity={activity}/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {cn} from '@/lib/utils';
|
||||
import BacklogCard from '../components/BacklogCard.jsx';
|
||||
import GameCover from '../components/GameCover.jsx';
|
||||
import GameStatusBadge from '../components/GameStatusBadge.jsx';
|
||||
import PlayingHint from '../components/PlayingHint.jsx';
|
||||
import {formatPlaytime} from '../utils/games.js';
|
||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||
|
||||
@@ -142,7 +143,10 @@ export default function GameDetail() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<BacklogCard key={data.appid} game={data} onSaved={(entry) => setData((d) => ({...d, ...entry}))}/>
|
||||
<PlayingHint game={data} onChanged={(entry) => setData((d) => ({...d, ...entry}))}/>
|
||||
|
||||
<BacklogCard key={data.appid} game={data}
|
||||
onSaved={(entry) => setData((d) => ({...d, ...entry}))}/>
|
||||
|
||||
{!data.hasAchievements && (
|
||||
<Card className="items-center py-10 text-center text-sm text-muted-foreground">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter, Route, Routes} from 'react-router-dom';
|
||||
import GameDetail from './GameDetail.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
|
||||
const game = {
|
||||
appid: 220, name: 'Half-Life 2', playtimeMinutes: 100, playtime2Weeks: 90, status: 'BACKLOG',
|
||||
rating: null, notes: null, priority: null, hasAchievements: false, statsVisible: false, achievements: [],
|
||||
};
|
||||
|
||||
const renderDetail = () => render(
|
||||
<MemoryRouter initialEntries={['/library/220']}>
|
||||
<Routes><Route path="/library/:appid" element={<GameDetail/>}/></Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_jwt', 'token');
|
||||
globalThis.fetch = vi.fn(async (url, init) => init?.method === 'PUT'
|
||||
? {ok: true, status: 200, json: async () => JSON.parse(init.body)}
|
||||
: {ok: true, status: 200, json: async () => game});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe('GameDetail', () => {
|
||||
it('keeps unsaved notes in the backlog card when the hint sets the game to playing', async () => {
|
||||
renderDetail();
|
||||
const notes = await screen.findByLabelText('BACKLOG.NOTES_LABEL');
|
||||
fireEvent.change(notes, {target: {value: 'my draft'}});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'}));
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('button', {name: 'GAME_DETAIL.SET_PLAYING'})).toBeNull());
|
||||
expect(screen.getByLabelText('BACKLOG.NOTES_LABEL').value).toBe('my draft');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user