(WIP) SearchView

This commit is contained in:
grimsi
2025-05-24 14:52:22 +02:00
parent 442f83eabe
commit 2acbc0d654
5 changed files with 86 additions and 24 deletions
@@ -4,34 +4,19 @@ import {useSnapshot} from "valtio/react";
import {gameState} from "Frontend/state/GameState";
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
import {useNavigate} from "react-router";
import {Key, KeyboardEvent, useState} from "react";
import {GameCover} from "Frontend/components/general/covers/GameCover";
export default function SearchBar() {
const navigate = useNavigate();
const state = useSnapshot(gameState);
const games = state.games as GameDto[];
const [selectedId, setSelectedId] = useState<number>();
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === "Enter") {
event.preventDefault();
navigate("/game/" + selectedId);
}
}
function updateSelectedId(key: Key | null) {
if (key === null) return;
setSelectedId(key as number);
}
const games = state.sortedByMostRecentlyUpdated as GameDto[];
return <Autocomplete
aria-label="Search for games"
classNames={{
listboxWrapper: "max-h-[320px]",
selectorButton: "text-default-500",
endContentWrapper: "display-none"
}}
defaultItems={games}
inputProps={{
@@ -57,8 +42,6 @@ export default function SearchBar() {
}}
placeholder="Type to search..."
startContent={<MagnifyingGlass/>}
onSelectionChange={updateSelectedId}
onKeyDown={handleKeyDown}
isVirtualized={true}
maxListboxHeight={300}
itemHeight={91} // 75px (cover) + 16px (margin top/bottom) = 91px
+5
View File
@@ -21,6 +21,7 @@ import PluginManagement from "Frontend/components/administration/PluginManagemen
import {SystemManagement} from "Frontend/components/administration/SystemManagement";
import GameView from "Frontend/views/GameView";
import LibraryManagementView from "Frontend/views/LibraryManagementView";
import SearchView from "Frontend/views/SearchView";
export const routes = protectRoutes([
{
@@ -38,6 +39,10 @@ export const routes = protectRoutes([
path: 'game/:gameId',
element: <GameView/>
},
{
path: '/search',
element: <SearchView/>
},
{
path: 'settings',
element: <ProfileView/>,
@@ -14,6 +14,13 @@ type GameState = {
sortedByMostRecentlyAdded: GameDto[];
sortedByMostRecentlyUpdated: GameDto[];
randomlyOrderedGamesByLibraryId: Record<number, GameDto[]>;
knownPublishers: Set<string>;
knownDevelopers: Set<string>;
knownGenres: Set<string>;
knownThemes: Set<string>;
knownKeywords: Set<string>;
knownFeatures: Set<string>;
knownPerspectives: Set<string>;
};
export const gameState = proxy<GameState>({
@@ -48,6 +55,27 @@ export const gameState = proxy<GameState>({
.sort(() => rand.next() - 0.5);
}
return result;
},
get knownPublishers() {
return new Set<string>(this.games.flatMap((game: GameDto) => game.publishers ? game.publishers : []));
},
get knownDevelopers() {
return new Set<string>(this.games.flatMap((game: GameDto) => game.developers ? game.developers : []));
},
get knownGenres() {
return new Set<string>(this.games.flatMap((game: GameDto) => game.genres ? game.genres : []));
},
get knownThemes() {
return new Set<string>(this.games.flatMap((game: GameDto) => game.themes ? game.themes : []));
},
get knownKeywords() {
return new Set<string>(this.games.flatMap((game: GameDto) => game.keywords ? game.keywords : []));
},
get knownFeatures() {
return new Set<string>(this.games.flatMap((game: GameDto) => game.features ? game.features : []));
},
get knownPerspectives() {
return new Set<string>(this.games.flatMap((game: GameDto) => game.perspectives ? game.perspectives : []));
}
});
@@ -1,12 +1,12 @@
import {useRouteMetadata} from 'Frontend/util/routing.js';
import {useEffect, useState} from 'react';
import ProfileMenu from "Frontend/components/ProfileMenu";
import {Divider, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem} from "@heroui/react";
import {Button, Divider, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, Tooltip} from "@heroui/react";
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
import * as PackageJson from "../../../../package.json";
import {Outlet, useNavigate} from "react-router";
import {Outlet, useLocation, useNavigate} from "react-router";
import {useAuth} from "Frontend/util/auth";
import {Heart} from "@phosphor-icons/react";
import {Heart, ListMagnifyingGlass} from "@phosphor-icons/react";
import Confetti, {ConfettiProps} from "react-confetti-boom";
import {useTheme} from "next-themes";
import {UserPreferenceService} from "Frontend/util/user-preference-service";
@@ -14,9 +14,11 @@ import SearchBar from "Frontend/components/general/SearchBar";
export default function MainLayout() {
const navigate = useNavigate();
const location = useLocation();
const auth = useAuth();
const routeMetadata = useRouteMetadata();
const {setTheme} = useTheme();
const isSearchPage = location.pathname.startsWith("/search");
const [isExploding, setIsExploding] = useState(false);
useEffect(() => {
@@ -63,9 +65,14 @@ export default function MainLayout() {
<GameyfinLogo className="h-10 fill-foreground"/>
</div>
</NavbarBrand>
<NavbarContent justify="center" className="flex-1 max-w-96">
{!isSearchPage && <NavbarContent justify="center" className="flex-1 max-w-96">
<SearchBar/>
</NavbarContent>
<Tooltip content="Advanced search" placement="bottom">
<Button isIconOnly variant="light" onPress={() => navigate("/search")}>
<ListMagnifyingGlass/>
</Button>
</Tooltip>
</NavbarContent>}
<NavbarContent justify="end">
{auth.state.user?.emailConfirmed === false ?
<NavbarItem>
@@ -0,0 +1,39 @@
import {Input} from "@heroui/react";
import {MagnifyingGlass} from "@phosphor-icons/react";
import {useSnapshot} from "valtio/react";
import {gameState} from "Frontend/state/GameState";
import {libraryState} from "Frontend/state/LibraryState";
import {useSearchParams} from "react-router";
import {ChangeEvent} from "react";
export default function SearchView() {
const gamesState = useSnapshot(gameState);
const librariesState = useSnapshot(libraryState);
const [searchParams, setSearchParams] = useSearchParams();
const term = searchParams.get("term") ?? "";
const updateSearchParam = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (value) {
setSearchParams({term: value});
} else {
setSearchParams({});
}
};
return <div className="flex flex-col items-center">
<Input
classNames={{
base: "w-1/3",
mainWrapper: "h-full",
inputWrapper:
"h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
}}
placeholder="Type to search..."
startContent={<MagnifyingGlass/>}
type="search"
value={term}
onChange={updateSearchParam}
/>
</div>
}