feat(search): add game search page (#50)
Build, Test & Push Frontend / quality-check (push) Successful in 44s
Build, Test & Push Frontend / docker (push) Successful in 27s

Add /search to find games by name on IGDB.
A game in the library opens its game page. Other hits open a
detail view with an Add button. The query stays in the URL.
To fit the new nav icon on 360 px phones, the header padding is
smaller and the points chip hides while the Questions icon shows.
This commit is contained in:
lobo committed 2026-10-20 10:42:06 +02:00
1 parent 5be0d42bca
commit 60dcfe8992
13 files changed
+276 -16

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import {useEffect, useState} from 'react';
import {useNavigate, useSearchParams} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {Search} from 'lucide-react';
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import GameSearch from '../components/GameSearch.jsx';
import PageContainer from '../components/PageContainer.jsx';
import SearchHitCard from '../components/SearchHitCard.jsx';
/**
* Find games by name on IGDB. A game the user owns opens its page; any other hit opens a detail view with "Add".
* The query lives in the URL, so it stays when the user comes back from a game page.
*/
export default function GameSearchPage() {
const {t} = useTranslation();
const navigate = useNavigate();
const [params, setParams] = useSearchParams();
const [query, setQuery] = useState(params.get('q') ?? '');
const [picked, setPicked] = useState(null);
// The input keeps its own state: a router update per keystroke runs in a transition and drops keys of a burst.
useEffect(() => {
const timer = setTimeout(() => setParams(query ? {q: query} : {}, {replace: true}), 400);
return () => clearTimeout(timer);
}, [query, setParams]);
const onQueryChange = (q) => {
setPicked(null);
setQuery(q);
};
const onPick = (hit) => (hit.inLibrary ? navigate(`/library/${hit.appid}`) : setPicked(hit));
return (
<PageContainer className="space-y-4 px-4 py-6 md:px-8">
{picked ? (
<SearchHitCard hit={picked} onBack={() => setPicked(null)}/>
) : (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2"><Search className="size-4 text-primary"/>{t('GAME_SEARCH.TITLE')}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">{t('GAME_SEARCH.HINT')}</p>
<GameSearch query={query} onQueryChange={onQueryChange} onPick={onPick} markInLibrary/>
</CardContent>
</Card>
)}
</PageContainer>
);
}