Public Access
feat(import): add the import page, multi-select add and the favorite platform
This commit is contained in:
1 parent
aa9e83e0cb
commit
14dd59f9e0
21 files changed
+1436
-48
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user