refactor(game-detail): load in useGame, hero and achievements cards

This commit is contained in:
lobo committed 2026-09-30 08:46:51 +02:00
1 parent 628920a006
commit 05f4ca42e5
4 files changed
+294 -254

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
import {useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Clock, ExternalLink, Eye, EyeOff, Gamepad2, Image as ImageIcon, ImageOff, Merge, PanelTop, PencilLine, RotateCcw, Timer} from 'lucide-react';
import {toast} from 'sonner';
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
import {Card} from '@/components/ui/card';
import GameCover from './GameCover.jsx';
import GameStatusBadge from './GameStatusBadge.jsx';
import ProfileArtMenu from './ProfileArtMenu.jsx';
import TimeToBeat from './TimeToBeat.jsx';
import useSaveAppearance from '../hooks/useSaveAppearance.js';
import {usePreferences} from '../context/PreferencesContext.jsx';
import {formatPlaytime} from '../utils/games.js';
import {setGameHidden} from '../utils/hiddenGames.js';
import {isSteamKey} from '../utils/manualGames.js';
import {platformLabel} from '../utils/platforms.js';
const SPEEDRUN_PREFIX = 'https://www.speedrun.com/';
export default function GameHeroCard({game, manual, auditor, profileStyle, onCorrect, onMerge, onHiddenChanged, onProfileStyleSaved}) {
const {t} = useTranslation();
const {appearance} = usePreferences();
const saveAppearance = useSaveAppearance();
const [hiding, setHiding] = useState(false);
const onSteam = isSteamKey(game.appid);
// Core checks the link too; a link from the API must never run script.
const speedrunUrl = typeof game.speedrunUrl === 'string' && game.speedrunUrl.startsWith(SPEEDRUN_PREFIX)
? game.speedrunUrl : null;
const isBackground = appearance.backgroundAppid === game.appid;
const isBanner = appearance.showBanner && appearance.bannerAppid === game.appid;
const toggleHidden = async () => {
const hidden = !game.hidden;
setHiding(true);
try {
const response = await setGameHidden(game.appid, hidden);
if (response.status === 401) return;
if (!response.ok) {
toast.error(t('GAME_DETAIL.HIDE_ERROR'));
return;
}
onHiddenChanged(hidden);
toast.success(t(hidden ? 'GAME_DETAIL.HIDDEN_TOAST' : 'GAME_DETAIL.SHOWN_TOAST'));
} catch (err) {
console.error('Hide game error:', err);
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
} finally {
setHiding(false);
}
};
return (
<Card className="relative overflow-hidden p-0 gap-0 md:flex-row">
<GameCover appid={game.appid} imageUrl={game.imageUrl} className="w-full rounded-none md:w-96 md:shrink-0"/>
<div className="flex flex-1 flex-col justify-between gap-4 p-6">
<div>
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
{game.name ?? `App ${game.appid}`}
</h1>
<div className="mt-3 flex flex-wrap gap-2">
<Badge variant="secondary" className="tabular-nums">
<Clock className="size-3"/>
{t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(game.playtimeMinutes)})}
</Badge>
<GameStatusBadge status={game.status}/>
{platformLabel(game) && (
<Badge variant="outline"><Gamepad2 className="size-3"/>{platformLabel(game)}</Badge>
)}
</div>
<div className="mt-3">
<TimeToBeat time={game.timeToBeat}/>
</div>
{speedrunUrl && (
<Button asChild variant="outline" size="sm" className="mt-3">
<a href={speedrunUrl} target="_blank" rel="noopener noreferrer">
<Timer/>{t('GAME_DETAIL.OPEN_IN_SPEEDRUN')}<ExternalLink/>
</a>
</Button>
)}
{auditor && (
<Button variant="outline" size="sm" className="mt-3 ml-2" onClick={onCorrect}>
<PencilLine/>{t('CORRECTION.OPEN')}
</Button>
)}
{auditor && (
<Button variant="outline" size="sm" className="mt-3 ml-2" onClick={onMerge}>
<Merge/>{t('MERGE.OPEN')}
</Button>
)}
</div>
{/* Store page, background and banner need Steam art, so a game off Steam has none of them.
Hide is here too: only a Steam game can be hidden. */}
{onSteam && <div className="flex flex-wrap gap-2">
<Button asChild variant="outline" size="sm">
<a href={`https://store.steampowered.com/app/${game.appid}`} target="_blank" rel="noopener noreferrer">
{t('GAME_DETAIL.OPEN_IN_STEAM')}<ExternalLink/>
</a>
</Button>
{isBackground ? (
<Button variant="outline" size="sm" onClick={() => saveAppearance({backgroundAppid: null})}>
<ImageOff/>{t('GAME_DETAIL.REMOVE_BACKGROUND')}
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => saveAppearance({backgroundAppid: game.appid})}>
<ImageIcon/>{t('GAME_DETAIL.SET_BACKGROUND')}
</Button>
)}
{isBanner ? (
<Button variant="outline" size="sm" onClick={() => saveAppearance({bannerAppid: null})}>
<RotateCcw/>{t('SETTINGS.BANNER_AUTO')}
</Button>
) : (
<Button variant="outline" size="sm"
onClick={() => saveAppearance({bannerAppid: game.appid, showBanner: true})}>
<PanelTop/>{t('GAME_DETAIL.SET_BANNER')}
</Button>
)}
{profileStyle && (
<ProfileArtMenu appid={game.appid} style={profileStyle} onSaved={onProfileStyleSaved}/>
)}
{!manual && (
<Button variant="outline" size="sm" onClick={toggleHidden} disabled={hiding}>
{game.hidden ? <Eye/> : <EyeOff/>}
{t(game.hidden ? 'GAME_DETAIL.UNHIDE' : 'GAME_DETAIL.HIDE')}
</Button>
)}
</div>}
</div>
</Card>
);
}