mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
(WIP) SearchView
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
Reference in New Issue
Block a user