Public Access
Target fields on the admin hints page. The dashboard shows the four automatic hints after admin hints when their condition is true, using data Core already serves. HintText renders **bold** and safe links (app path or https) as React elements, never HTML, in hints and news. Auto-close without tick hides for this session, X for 7 days, tick + X for good.
153 lines
7.0 KiB
React
153 lines
7.0 KiB
React
import {useState} from 'react';
|
|
import {useTranslation} from 'react-i18next';
|
|
import {toast} from 'sonner';
|
|
import {RefreshCw} from 'lucide-react';
|
|
|
|
import {Button} from '@/components/ui/button';
|
|
import {Card} from '@/components/ui/card';
|
|
import {Skeleton} from '@/components/ui/skeleton';
|
|
import AchievementsCard from '../components/AchievementsCard.jsx';
|
|
import ActivityCard from '../components/ActivityCard.jsx';
|
|
import CurrentlyPlayingCard from '../components/CurrentlyPlayingCard.jsx';
|
|
import DashboardHint from '../components/DashboardHint.jsx';
|
|
import DashboardWelcomeCard from '../components/DashboardWelcomeCard.jsx';
|
|
import MostPlayedCard from '../components/MostPlayedCard.jsx';
|
|
import PileOfShameCard from '../components/PileOfShameCard.jsx';
|
|
import SteamGameCount from '../components/SteamGameCount';
|
|
import WhatsNewDialog from '../components/WhatsNewDialog.jsx';
|
|
import useDashboardData from '../hooks/useDashboardData.js';
|
|
import useLibrarySync from '../hooks/useLibrarySync.js';
|
|
import useForceSync from '../hooks/useForceSync.js';
|
|
import PageContainer from '../components/PageContainer.jsx';
|
|
import useSteamProfile from '../hooks/useSteamProfile.js';
|
|
import useUserStats from '../hooks/useUserStats.js';
|
|
import useSaveResponse from '../hooks/useSaveResponse.js';
|
|
import TileGrid from '../components/TileGrid.jsx';
|
|
import statGroupTile from '../components/stats/statGroupTile.jsx';
|
|
import {
|
|
DASHBOARD_TILES, DASHBOARD_TITLES, DEFAULT_DASHBOARD, normalizeLayout, PLAYING_GAMES, saveTiles, statGroup, TOP_GAMES,
|
|
} from '../utils/tiles.js';
|
|
import {usePreferences} from '../context/PreferencesContext.jsx';
|
|
import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
|
|
|
|
const Dashboard = () => {
|
|
const {t} = useTranslation();
|
|
const {user: userData, updateUser} = useCurrentUserContext();
|
|
const {
|
|
steamData, topGames, playing, summary, summaryFailed, activity, achievements, isLoading, error,
|
|
fetchSummary, reloadLibrary,
|
|
} = useDashboardData();
|
|
const [shuffling, setShuffling] = useState(false);
|
|
const [editing, setEditing] = useState(false);
|
|
const runSave = useSaveResponse();
|
|
const layout = normalizeLayout(userData?.settings?.dashboardTiles, DASHBOARD_TILES, DEFAULT_DASHBOARD);
|
|
// The stat groups of the shown stat tiles, and TREND for the pile card and the "pile grew" hint. Waits for the
|
|
// saved layout in /me.
|
|
const statGroups = [...new Set([...layout.map((tile) => statGroup(tile.id)).filter(Boolean), 'TREND'])].sort();
|
|
const stats = useUserStats(statGroups, userData != null);
|
|
|
|
const shuffleSuggestion = async () => {
|
|
setShuffling(true);
|
|
if (!await fetchSummary(summary?.suggestion?.appid)) {
|
|
toast.error(t('DASHBOARD.SUMMARY_FAILED'));
|
|
}
|
|
setShuffling(false);
|
|
};
|
|
|
|
const hasSteamId = !!userData?.steamId;
|
|
const showLibrary = hasSteamId || steamData.count > 0;
|
|
const {appearance, equipped} = usePreferences();
|
|
const steamProfile = useSteamProfile(hasSteamId);
|
|
const bannerAppid = appearance.showBanner
|
|
? appearance.bannerAppid ?? activity?.recentGames?.[0]?.appid ?? null
|
|
: null;
|
|
const sync = useLibrarySync({
|
|
enabled: hasSteamId,
|
|
expectInitialSync: hasSteamId && steamData.count === 0,
|
|
onComplete: () => {
|
|
reloadLibrary();
|
|
stats.reload();
|
|
},
|
|
});
|
|
const forceSync = useForceSync(sync.watch);
|
|
|
|
if (isLoading) {
|
|
return <DashboardSkeleton/>;
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="py-24 text-center text-destructive">
|
|
{t(error)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const saveLayout = async (next) => {
|
|
// 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;
|
|
updateUser((d) => ({...d, settings: {...d.settings, dashboardTiles: tiles}}));
|
|
setEditing(false);
|
|
};
|
|
|
|
const renderTile = (id, size) => {
|
|
switch (id) {
|
|
case 'GAME_COUNT': return showLibrary && <SteamGameCount targetCount={steamData.count}/>;
|
|
case 'MOST_PLAYED':
|
|
return showLibrary && <MostPlayedCard games={topGames.slice(0, TOP_GAMES[size])} syncing={sync.isRunning}
|
|
equipped={equipped}/>;
|
|
case 'PLAYING':
|
|
return showLibrary && (
|
|
<CurrentlyPlayingCard games={playing.slice(0, PLAYING_GAMES[size])} syncing={sync.isRunning} wide={size === 'L'}/>
|
|
);
|
|
case 'PILE_OF_SHAME':
|
|
if (!showLibrary) return null;
|
|
if (summary) {
|
|
return <PileOfShameCard summary={summary} trend={stats.stats?.trend} syncing={sync.isRunning}
|
|
onShuffle={shuffleSuggestion} shuffling={shuffling}/>;
|
|
}
|
|
return 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>
|
|
);
|
|
case 'ACTIVITY': return hasSteamId && activity && <ActivityCard activity={activity}/>;
|
|
case 'ACHIEVEMENTS': return hasSteamId && achievements && <AchievementsCard summary={achievements}/>;
|
|
default: return statGroupTile(id, stats.stats);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<PageContainer className="p-4 md:p-8 space-y-6">
|
|
<DashboardWelcomeCard bannerAppid={bannerAppid} avatarUrl={steamProfile?.avatarUrl}
|
|
equipped={equipped}
|
|
sync={sync} forceSync={forceSync} editing={editing} onCustomize={() => setEditing(true)}/>
|
|
|
|
<TileGrid tiles={DASHBOARD_TILES} titles={DASHBOARD_TITLES} layout={layout} renderTile={renderTile} editing={editing}
|
|
defaultLayout={DEFAULT_DASHBOARD} onSave={saveLayout} onCancel={() => setEditing(false)}/>
|
|
<WhatsNewDialog/>
|
|
{/* Last child: space-y gives every other child a bottom margin, which would move the fixed card. */}
|
|
<DashboardHint user={userData} summary={summaryFailed ? null : summary ?? undefined}
|
|
trend={stats.failed ? null : stats.stats ? stats.stats.trend ?? null : undefined}/>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
function DashboardSkeleton() {
|
|
return (
|
|
<PageContainer className="p-4 md:p-8 space-y-6">
|
|
<Skeleton className="h-40 rounded-xl"/>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<Skeleton className="h-64 rounded-xl"/>
|
|
<Skeleton className="h-64 rounded-xl md:col-span-2"/>
|
|
</div>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
export default Dashboard;
|