feat(import): add the import page, multi-select add and the favorite platform

This commit is contained in:
lobo committed 2026-09-29 10:20:54 +02:00
1 parent aa9e83e0cb
commit 14dd59f9e0
21 files changed
+1436 -48

No files matched your search

+49 -7
View File
@@ -1,4 +1,4 @@
import {useState} from 'react';
import {useEffect, useState} from 'react';
import {useNavigate} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
@@ -12,11 +12,14 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c
import GameCover from './GameCover.jsx';
import GameSearch from './GameSearch.jsx';
import useSessionExpired from '../hooks/useSessionExpired.js';
import useStartImport from '../hooks/useStartImport.js';
import {GAME_STATUSES} from '../utils/backlog.js';
import {addGame, hoursToMinutes} from '../utils/manualGames.js';
import {loadFavoritePlatform, preselectPlatform} from '../utils/platforms.js';
/**
* Search IGDB and add a game the user owns outside Steam, e.g. on PlayStation or Switch.
* Search IGDB and add a game the user owns outside Steam, e.g. on PlayStation or Switch; or check several and review
* them on the import page.
*/
export default function AddGamePanel({onClose}) {
const {t} = useTranslation();
@@ -24,6 +27,29 @@ export default function AddGamePanel({onClose}) {
const expireSession = useSessionExpired();
const [query, setQuery] = useState('');
const [picked, setPicked] = useState(null);
// igdbId -> hit, over several searches.
const [selected, setSelected] = useState(() => new Map());
const [busy, setBusy] = useState(false);
const [favorite, setFavorite] = useState(null);
useEffect(() => {
loadFavoritePlatform().then(setFavorite);
}, []);
const startImport = useStartImport();
const toggle = (hit) => {
const next = new Map(selected);
if (!next.delete(hit.igdbId)) next.set(hit.igdbId, hit);
setSelected(next);
};
// Several games go through a SEARCH import: its review picks the platforms, and it can be undone.
const reviewSelected = async () => {
setBusy(true);
const id = await startImport('SEARCH', [...selected.values()]
.map((hit) => ({igdbId: hit.igdbId, title: hit.name.slice(0, 100)})));
setBusy(false);
if (id) navigate(`/import?id=${id}`);
};
return (
<Card className="p-5 gap-4">
@@ -38,20 +64,34 @@ export default function AddGamePanel({onClose}) {
</div>
{picked ? (
<AddForm hit={picked} onBack={() => setPicked(null)} navigate={navigate} expireSession={expireSession}/>
<AddForm hit={picked} favorite={favorite} onBack={() => setPicked(null)} navigate={navigate}
expireSession={expireSession}/>
) : (
<>
<p className="text-sm text-muted-foreground">{t('MANUAL_GAME.ADD_HINT')}</p>
<GameSearch query={query} onQueryChange={setQuery} onPick={setPicked} blockInLibrary/>
<GameSearch query={query} onQueryChange={setQuery} onPick={setPicked} blockInLibrary
selected={selected} onToggle={toggle}/>
{selected.size > 0 && (
<div className="flex flex-wrap items-center justify-between gap-3">
<span className="text-sm text-muted-foreground">{t('MANUAL_GAME.SELECTED', {count: selected.size})}</span>
<Button onClick={reviewSelected} disabled={busy}>
{busy ? <Loader2 className="animate-spin"/> : <Plus/>}{t('MANUAL_GAME.ADD_SELECTED')}
</Button>
</div>
)}
</>
)}
</Card>
);
}
function AddForm({hit, onBack, navigate, expireSession}) {
function AddForm({hit, favorite, onBack, navigate, expireSession}) {
const {t} = useTranslation();
const [platform, setPlatform] = useState(hit.platforms[0] ?? '');
const [platform, setPlatform] = useState(() => preselectPlatform(null, hit.platforms, favorite));
// The favorite can arrive after the pick; it fills only a platform the user has not picked yet.
useEffect(() => {
setPlatform((current) => current || preselectPlatform(null, hit.platforms, favorite));
}, [favorite, hit.platforms]);
const [status, setStatus] = useState('BACKLOG');
const [hours, setHours] = useState('');
const [busy, setBusy] = useState(false);
@@ -97,7 +137,9 @@ function AddForm({hit, onBack, navigate, expireSession}) {
<Label htmlFor="add-platform">{t('MANUAL_GAME.PLATFORM')}</Label>
{hit.platforms.length > 0 ? (
<Select value={platform} onValueChange={setPlatform}>
<SelectTrigger id="add-platform" className="w-full"><SelectValue/></SelectTrigger>
<SelectTrigger id="add-platform" className="w-full">
<SelectValue placeholder={t('IMPORT.PICK_PLATFORM')}/>
</SelectTrigger>
<SelectContent>
{hit.platforms.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
</SelectContent>