mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +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 {gameState} from "Frontend/state/GameState";
|
||||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||||
import {useNavigate} from "react-router";
|
import {useNavigate} from "react-router";
|
||||||
import {Key, KeyboardEvent, useState} from "react";
|
|
||||||
import {GameCover} from "Frontend/components/general/covers/GameCover";
|
import {GameCover} from "Frontend/components/general/covers/GameCover";
|
||||||
|
|
||||||
export default function SearchBar() {
|
export default function SearchBar() {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const state = useSnapshot(gameState);
|
const state = useSnapshot(gameState);
|
||||||
const games = state.games as GameDto[];
|
const games = state.sortedByMostRecentlyUpdated 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Autocomplete
|
return <Autocomplete
|
||||||
aria-label="Search for games"
|
aria-label="Search for games"
|
||||||
classNames={{
|
classNames={{
|
||||||
listboxWrapper: "max-h-[320px]",
|
|
||||||
selectorButton: "text-default-500",
|
selectorButton: "text-default-500",
|
||||||
|
endContentWrapper: "display-none"
|
||||||
}}
|
}}
|
||||||
defaultItems={games}
|
defaultItems={games}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
@@ -57,8 +42,6 @@ export default function SearchBar() {
|
|||||||
}}
|
}}
|
||||||
placeholder="Type to search..."
|
placeholder="Type to search..."
|
||||||
startContent={<MagnifyingGlass/>}
|
startContent={<MagnifyingGlass/>}
|
||||||
onSelectionChange={updateSelectedId}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
isVirtualized={true}
|
isVirtualized={true}
|
||||||
maxListboxHeight={300}
|
maxListboxHeight={300}
|
||||||
itemHeight={91} // 75px (cover) + 16px (margin top/bottom) = 91px
|
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 {SystemManagement} from "Frontend/components/administration/SystemManagement";
|
||||||
import GameView from "Frontend/views/GameView";
|
import GameView from "Frontend/views/GameView";
|
||||||
import LibraryManagementView from "Frontend/views/LibraryManagementView";
|
import LibraryManagementView from "Frontend/views/LibraryManagementView";
|
||||||
|
import SearchView from "Frontend/views/SearchView";
|
||||||
|
|
||||||
export const routes = protectRoutes([
|
export const routes = protectRoutes([
|
||||||
{
|
{
|
||||||
@@ -38,6 +39,10 @@ export const routes = protectRoutes([
|
|||||||
path: 'game/:gameId',
|
path: 'game/:gameId',
|
||||||
element: <GameView/>
|
element: <GameView/>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/search',
|
||||||
|
element: <SearchView/>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
element: <ProfileView/>,
|
element: <ProfileView/>,
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ type GameState = {
|
|||||||
sortedByMostRecentlyAdded: GameDto[];
|
sortedByMostRecentlyAdded: GameDto[];
|
||||||
sortedByMostRecentlyUpdated: GameDto[];
|
sortedByMostRecentlyUpdated: GameDto[];
|
||||||
randomlyOrderedGamesByLibraryId: Record<number, 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>({
|
export const gameState = proxy<GameState>({
|
||||||
@@ -48,6 +55,27 @@ export const gameState = proxy<GameState>({
|
|||||||
.sort(() => rand.next() - 0.5);
|
.sort(() => rand.next() - 0.5);
|
||||||
}
|
}
|
||||||
return result;
|
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 {useRouteMetadata} from 'Frontend/util/routing.js';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import ProfileMenu from "Frontend/components/ProfileMenu";
|
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 GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
|
||||||
import * as PackageJson from "../../../../package.json";
|
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 {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 Confetti, {ConfettiProps} from "react-confetti-boom";
|
||||||
import {useTheme} from "next-themes";
|
import {useTheme} from "next-themes";
|
||||||
import {UserPreferenceService} from "Frontend/util/user-preference-service";
|
import {UserPreferenceService} from "Frontend/util/user-preference-service";
|
||||||
@@ -14,9 +14,11 @@ import SearchBar from "Frontend/components/general/SearchBar";
|
|||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const routeMetadata = useRouteMetadata();
|
const routeMetadata = useRouteMetadata();
|
||||||
const {setTheme} = useTheme();
|
const {setTheme} = useTheme();
|
||||||
|
const isSearchPage = location.pathname.startsWith("/search");
|
||||||
const [isExploding, setIsExploding] = useState(false);
|
const [isExploding, setIsExploding] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,9 +65,14 @@ export default function MainLayout() {
|
|||||||
<GameyfinLogo className="h-10 fill-foreground"/>
|
<GameyfinLogo className="h-10 fill-foreground"/>
|
||||||
</div>
|
</div>
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
<NavbarContent justify="center" className="flex-1 max-w-96">
|
{!isSearchPage && <NavbarContent justify="center" className="flex-1 max-w-96">
|
||||||
<SearchBar/>
|
<SearchBar/>
|
||||||
</NavbarContent>
|
<Tooltip content="Advanced search" placement="bottom">
|
||||||
|
<Button isIconOnly variant="light" onPress={() => navigate("/search")}>
|
||||||
|
<ListMagnifyingGlass/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</NavbarContent>}
|
||||||
<NavbarContent justify="end">
|
<NavbarContent justify="end">
|
||||||
{auth.state.user?.emailConfirmed === false ?
|
{auth.state.user?.emailConfirmed === false ?
|
||||||
<NavbarItem>
|
<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