import {useEffect, useState} from 'react'; import {useNavigate} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {toast} from 'sonner'; import {ArrowLeft, Loader2, Plus, X} from 'lucide-react'; import {Button} from '@/components/ui/button'; import {Card} from '@/components/ui/card'; import {Input} from '@/components/ui/input'; import {Label} from '@/components/ui/label'; import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'; import GameCover from './GameCover.jsx'; import FitText from './FitText.jsx'; import GameSearch from './GameSearch.jsx'; import usePointsCheck from '../hooks/usePointsCheck.js'; import useStartImport from '../hooks/useStartImport.js'; import {GAME_STATUSES, MAX_PLATFORM_LENGTH} from '../utils/limits.js'; import {addGame, hoursToMinutes} from '../utils/manualGames.js'; import {preselectPlatform} from '../utils/platforms.js'; import {useCurrentUserContext} from '../context/CurrentUserContext.jsx'; /** * 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. * * @param onAdded called with the new appid before the navigation, for a page that shows the same address * @param initialQuery the search text to start with, e.g. the name of a game seen elsewhere */ export default function AddGamePanel({onClose, onAdded, initialQuery = ''}) { const {t} = useTranslation(); const navigate = useNavigate(); const [query, setQuery] = useState(initialQuery); const [picked, setPicked] = useState(null); // igdbId -> hit, over several searches. const [selected, setSelected] = useState(() => new Map()); const [busy, setBusy] = useState(false); const favorite = useCurrentUserContext().user?.settings?.favoritePlatform ?? null; 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 (

{t('MANUAL_GAME.ADD_TITLE')}

{picked ? ( setPicked(null)} navigate={navigate} onAdded={onAdded}/> ) : ( <>

{t('MANUAL_GAME.ADD_HINT')}

{selected.size > 0 && (
{t('MANUAL_GAME.SELECTED', {count: selected.size})}
)} )}
); } function AddForm({hit, favorite, onBack, navigate, onAdded}) { const {t} = useTranslation(); 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); const check = usePointsCheck(); const submit = async (e) => { e.preventDefault(); setBusy(true); try { const response = await addGame({ igdbId: hit.igdbId, platform: platform.trim(), status, playtimeMinutes: hoursToMinutes(hours), }); if (response.status === 401) return; if (response.status === 409) { toast.error(t('MANUAL_GAME.ALREADY_IN_LIBRARY')); return; } if (!response.ok) { toast.error(t('MANUAL_GAME.ADD_FAILED')); return; } const {appid} = await response.json(); toast.success(t('MANUAL_GAME.ADDED', {name: hit.name})); check(appid, {status, tell: true}); onAdded?.(appid); navigate(`/library/${appid}`); } catch (err) { console.error('add game error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } finally { setBusy(false); } }; return (
{hit.platforms.length > 0 ? ( ) : ( setPlatform(e.target.value)}/> )}
setHours(e.target.value)}/>
); }