refactor(frontend): lint, one session guard, one request helper, one user context

This commit is contained in:
lobo committed 2026-09-29 15:40:55 +02:00
1 parent 111ab1e200
commit d0ea9e900d
78 files changed
+2138 -624

No files matched your search

+33 -135
View File
@@ -1,5 +1,5 @@
import React, {useCallback, useEffect, useState} from 'react';
import {Link, useNavigate} from 'react-router-dom';
import {useCallback, useEffect, useState} from 'react';
import {Link} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
import {ArrowRight, Hash, LayoutGrid, Link2, RefreshCw, Trophy} from 'lucide-react';
@@ -19,7 +19,6 @@ import SyncStatusBadge from '../components/SyncStatusBadge.jsx';
import useLibrarySync from '../hooks/useLibrarySync.js';
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 HeroBanner from '../components/HeroBanner.jsx';
import SteamAvatar from '../components/SteamAvatar.jsx';
@@ -30,8 +29,8 @@ import TileGrid from '../components/TileGrid.jsx';
import statGroupTile from '../components/stats/statGroupTile.jsx';
import {DASHBOARD_TILES, DASHBOARD_TITLES, DEFAULT_DASHBOARD, normalizeLayout, saveTiles, statGroup} from '../utils/tiles.js';
import {usePreferences} from '../context/PreferencesContext.jsx';
import {apiFetch} from '../utils/api.js';
import {hasSession} from '../utils/auth.js';
import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
import {request} from '../utils/api.js';
// Games per tile size; the page loads the most for the large tile.
const TOP_GAMES = {S: 3, M: 5, L: 10};
@@ -40,13 +39,15 @@ const PLAYING_GAMES = {S: 2, M: 4, L: 8};
const TOP_GAMES_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc';
const PLAYING_URL = '/api/library/games?page=0&size=20&sort=playtimeMinutes%2Cdesc&status=PLAYING';
const JSON_HEADERS = {'Content-Type': 'application/json'};
// A card whose request fails or has no JSON body stays empty.
async function loadInto(path, set, pick = (data) => data) {
const {ok, data} = await request(`${import.meta.env.VITE_CORE_API_URL}${path}`);
if (ok && data != null) set(pick(data));
}
const Dashboard = () => {
const {t} = useTranslation();
const navigate = useNavigate();
const expireSession = useSessionExpired();
const [userData, setUserData] = useState(null);
const {user: userData, updateUser, reload: reloadUser} = useCurrentUserContext();
const [steamData, setSteamData] = useState({count: 0, hasSteamId: false});
const [topGames, setTopGames] = useState([]);
const [playing, setPlaying] = useState([]);
@@ -67,109 +68,26 @@ const Dashboard = () => {
])].sort();
// No groups would mean "all groups" to the stats endpoint.
const hasStatGroups = statGroups.length > 0;
const stats = useUserStats(statGroups, expireSession, hasSession() && userData != null && hasStatGroups);
const stats = useUserStats(statGroups, userData != null && hasStatGroups);
const reloadStats = stats.reload;
const fetchSteamCount = useCallback(async () => {
try {
const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games-count`, {headers: JSON_HEADERS});
if (r.status === 401) {
expireSession();
return;
}
if (r.ok) {
setSteamData(await r.json());
}
} catch (err) {
console.error('Error fetching steam data:', err);
}
}, [expireSession]);
const fetchTopGames = useCallback(async () => {
try {
const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}${TOP_GAMES_URL}`, {headers: JSON_HEADERS});
if (r.status === 401) {
expireSession();
return;
}
if (r.ok) {
const page = await r.json();
setTopGames((page.content ?? []).slice(0, TOP_GAMES.L));
}
} catch (err) {
console.error('Error fetching top games:', err);
}
}, [expireSession]);
const fetchPlaying = useCallback(async () => {
try {
const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}${PLAYING_URL}`, {headers: JSON_HEADERS});
if (r.status === 401) {
expireSession();
return;
}
if (r.ok) {
const page = await r.json();
setPlaying((page.content ?? []).slice(0, PLAYING_GAMES.L));
}
} catch (err) {
console.error('Error fetching playing games:', err);
}
}, [expireSession]);
const fetchSteamCount = useCallback(() => loadInto('/api/library/games-count', setSteamData), []);
const fetchTopGames = useCallback(() => loadInto(TOP_GAMES_URL, setTopGames, (page) => (page.content ?? []).slice(0, TOP_GAMES.L)), []);
const fetchPlaying = useCallback(() => loadInto(PLAYING_URL, setPlaying, (page) => (page.content ?? []).slice(0, PLAYING_GAMES.L)), []);
const fetchActivity = useCallback(() => loadInto('/api/library/activity', setActivity), []);
const fetchAchievements = useCallback(() => loadInto('/api/library/achievements/summary', setAchievements), []);
/**
* @param excludeAppid the current suggestion, so the new pick differs from it
* @returns false when the request failed
* @returns false when the request failed; true on a 401, which ends the session without a "failed" message
*/
const fetchSummary = useCallback(async (excludeAppid) => {
const query = excludeAppid ? `?exclude=${excludeAppid}` : '';
try {
const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/summary${query}`, {headers: JSON_HEADERS});
if (r.status === 401) {
expireSession();
return true;
}
if (r.ok) {
setSummary(await r.json());
setSummaryFailed(false);
return true;
}
} catch (err) {
console.error('Error fetching library summary:', err);
}
setSummaryFailed(true);
return false;
}, [expireSession]);
const fetchActivity = useCallback(async () => {
try {
const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/activity`, {headers: JSON_HEADERS});
if (r.status === 401) {
expireSession();
return;
}
if (r.ok) {
setActivity(await r.json());
}
} catch (err) {
console.error('Error fetching activity:', err);
}
}, [expireSession]);
const fetchAchievements = useCallback(async () => {
try {
const r = await apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/achievements/summary`, {headers: JSON_HEADERS});
if (r.status === 401) {
expireSession();
return;
}
if (r.ok) {
setAchievements(await r.json());
}
} catch (err) {
console.error('Error fetching achievements:', err);
}
}, [expireSession]);
const {ok, status, data} = await request(`${import.meta.env.VITE_CORE_API_URL}/api/library/summary${query}`);
if (ok) setSummary(data);
setSummaryFailed(!ok && status !== 401);
return ok || status === 401;
}, []);
const shuffleSuggestion = async () => {
setShuffling(true);
@@ -191,40 +109,21 @@ const Dashboard = () => {
useEffect(() => {
const fetchUserData = async () => {
if (!hasSession()) {
navigate('/login');
return;
}
try {
const profileResponse = await apiFetch(`${import.meta.env.VITE_API_URL}/api/users/me?v=1`, {
method: 'GET',
headers: JSON_HEADERS,
});
if (profileResponse.ok) {
const data = await profileResponse.json();
setUserData(data);
// Games added by hand need no Steam ID; history and achievements come only from Steam.
await Promise.all(data.steamId
? [fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchSummary(), fetchActivity(), fetchAchievements()]
: [fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchSummary()]);
} else if (profileResponse.status === 401) {
expireSession();
} else {
setError('INFO.ERROR.LOAD_PROFILE');
}
} catch (err) {
console.error('Dashboard fetch error:', err);
const {ok, status, data} = await reloadUser();
if (ok) {
// Games added by hand need no Steam ID; history and achievements come only from Steam.
await Promise.all([fetchSteamCount(), fetchTopGames(), fetchPlaying(), fetchSummary(),
...(data.steamId ? [fetchActivity(), fetchAchievements()] : [])]);
} else if (status === 0) {
setError('INFO.ERROR.NETWORK_FAILURE');
} finally {
setIsLoading(false);
} else if (status !== 401) {
setError('INFO.ERROR.LOAD_PROFILE');
}
setIsLoading(false);
};
fetchUserData();
}, [navigate, expireSession, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary, fetchActivity, fetchAchievements]);
}, [reloadUser, fetchSteamCount, fetchTopGames, fetchPlaying, fetchSummary, fetchActivity, fetchAchievements]);
const hasSteamId = !!userData?.steamId;
const showLibrary = hasSteamId || steamData.count > 0;
@@ -237,7 +136,6 @@ const Dashboard = () => {
enabled: hasSteamId,
expectInitialSync: hasSteamId && steamData.count === 0,
onComplete: reloadLibrary,
onUnauthorized: expireSession,
});
const forceSync = useForceSync(reloadLibrary);
@@ -259,7 +157,7 @@ const Dashboard = () => {
// The default is saved as "never changed", so a later change of the default reaches this user too.
const tiles = JSON.stringify(next) === JSON.stringify(DEFAULT_DASHBOARD) ? null : next;
if (!await runSave(() => saveTiles('dashboard-tiles', tiles))) return;
setUserData((d) => ({...d, settings: {...d.settings, dashboardTiles: tiles}}));
updateUser((d) => ({...d, settings: {...d.settings, dashboardTiles: tiles}}));
setEditing(false);
};