feat(library): search IGDB and add games by hand
Build, Test & Push Frontend / quality-check (push) Successful in 13s
Build, Test & Push Frontend / docker (push) Successful in 23s

This commit is contained in:
lobo committed 2026-09-25 14:37:21 +02:00
1 parent d9a39dda22
commit 444b03acee
17 files changed
+699 -35

No files matched your search

+18 -5
View File
@@ -1,7 +1,7 @@
import {useEffect, useMemo, useState} from 'react';
import {Link, useNavigate, useParams} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {ArrowLeft, Clock, ExternalLink, Image as ImageIcon, ImageOff, PanelTop, RotateCcw, ShieldOff, Trophy} from 'lucide-react';
import {ArrowLeft, Clock, ExternalLink, Gamepad2, Image as ImageIcon, ImageOff, PanelTop, RotateCcw, ShieldOff, Trophy} from 'lucide-react';
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
@@ -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 ManualGameCard from '../components/ManualGameCard.jsx';
import CompletedHint from '../components/CompletedHint.jsx';
import PlayingHint from '../components/PlayingHint.jsx';
import TimeToBeat from '../components/TimeToBeat.jsx';
@@ -21,6 +22,7 @@ import {usePreferences} from '../context/PreferencesContext.jsx';
import PageContainer from '../components/PageContainer.jsx';
import {apiFetch} from '../utils/api.js';
import {hasSession} from '../utils/auth.js';
import {isSteamKey} from '../utils/manualGames.js';
const formatUnlockedAt = (instant, locale) => {
if (!instant) return '';
@@ -99,6 +101,8 @@ export default function GameDetail() {
});
}, [data]);
const manual = data?.source === 'MANUAL';
const onSteam = !!data && isSteamKey(data.appid);
const isBackground = !!data && appearance.backgroundAppid === data.appid;
const isBanner = !!data && appearance.showBanner && appearance.bannerAppid === data.appid;
@@ -131,7 +135,7 @@ export default function GameDetail() {
{!isLoading && !error && data && (
<>
<Card className="relative overflow-hidden p-0 gap-0 md:flex-row">
<GameCover appid={data.appid} className="w-full rounded-none md:w-96 md:shrink-0"/>
<GameCover appid={data.appid} imageUrl={data.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">
@@ -143,12 +147,16 @@ export default function GameDetail() {
{t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(data.playtimeMinutes)})}
</Badge>
<GameStatusBadge status={data.status}/>
{data.platform && (
<Badge variant="outline"><Gamepad2 className="size-3"/>{data.platform}</Badge>
)}
</div>
<div className="mt-3">
<TimeToBeat time={data.timeToBeat}/>
</div>
</div>
<div className="flex flex-wrap gap-2">
{/* Store page, background and banner need Steam art, so a game off Steam has none of them. */}
{onSteam && <div className="flex flex-wrap gap-2">
<Button asChild variant="outline" size="sm">
<a href={`https://store.steampowered.com/app/${data.appid}`} target="_blank" rel="noopener noreferrer">
{t('GAME_DETAIL.OPEN_IN_STEAM')}<ExternalLink/>
@@ -173,7 +181,7 @@ export default function GameDetail() {
<PanelTop/>{t('GAME_DETAIL.SET_BANNER')}
</Button>
)}
</div>
</div>}
</div>
</Card>
@@ -183,10 +191,15 @@ export default function GameDetail() {
<BacklogCard key={data.appid} game={data}
onSaved={(entry) => setData((d) => ({...d, ...entry}))}/>
{manual && (
<ManualGameCard key={`manual-${data.appid}`} game={data}
onSaved={(changes) => setData((d) => ({...d, ...changes}))}/>
)}
{!data.hasAchievements && (
<Card className="items-center py-10 text-center text-sm text-muted-foreground">
<Trophy className="size-8"/>
{t('GAME_DETAIL.NO_ACHIEVEMENTS')}
{t(manual ? 'MANUAL_GAME.NO_ACHIEVEMENTS' : 'GAME_DETAIL.NO_ACHIEVEMENTS')}
</Card>
)}