Implement sorting in SearchView

This commit is contained in:
grimsi
2025-09-04 12:44:15 +02:00
parent 4ae78b01a8
commit cc53a6faaa
2 changed files with 134 additions and 35 deletions
@@ -251,15 +251,19 @@ export default function GameRequestView() {
</TableCell>
<TableCell>
<div className="flex flex-row gap-2">
<Tooltip content="Vote for this request">
<Button size="sm"
variant={hasUserVotedForRequest(item as GameRequestDto) ? "solid" : "bordered"}
color={hasUserVotedForRequest(item as GameRequestDto) ? "primary" : "default"}
isDisabled={!auth.state.user || item.status === GameRequestStatus.FULFILLED}
startContent={<ArrowUp/>}
onPress={async () => await toggleVote(item.id)}>
{item.voters.length}
</Button>
<Tooltip
content={auth.state.user ? (item.status === GameRequestStatus.FULFILLED ? "You cannot vote on closed requests" : "Vote for this request") : "You must be logged in to vote"}
placement="left">
<div>
<Button size="sm"
variant={hasUserVotedForRequest(item as GameRequestDto) ? "solid" : "bordered"}
color={hasUserVotedForRequest(item as GameRequestDto) ? "primary" : "default"}
isDisabled={!auth.state.user || item.status === GameRequestStatus.FULFILLED}
startContent={<ArrowUp/>}
onPress={async () => await toggleVote(item.id)}>
{item.voters.length}
</Button>
</div>
</Tooltip>
{isAdmin(auth) && <div className="flex flex-row gap-2">
<Tooltip content="Approve this request">
+121 -26
View File
@@ -1,10 +1,10 @@
import {Input, Select, SelectItem} from "@heroui/react";
import {MagnifyingGlass} from "@phosphor-icons/react";
import {Button, Input, Select, SelectItem, Tooltip} from "@heroui/react";
import {FunnelSimple, FunnelSimpleX, MagnifyingGlass, SortAscending} 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 {useEffect, useMemo, useState} from "react";
import React, {useEffect, useMemo, useState} from "react";
import {Fzf} from "fzf";
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto";
@@ -24,6 +24,9 @@ export default function SearchView() {
const [searchParams, setSearchParams] = useSearchParams();
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
const [showFilters, setShowFilters] = useState(false);
const [sortBy, setSortBy] = useState("title_asc");
// State to track selected filter values
const [searchTerm, setSearchTerm] = useState<string>("");
const [selectedLibraries, setSelectedLibraries] = useState<Set<string>>(new Set());
@@ -45,6 +48,7 @@ export default function SearchView() {
const features = searchParams.getAll("feature");
const perspectives = searchParams.getAll("perspective");
const keywords = searchParams.getAll("keyword");
const sort = searchParams.get("sort") || "title_asc";
setSearchTerm(term);
setSelectedLibraries(new Set(libs));
@@ -54,11 +58,12 @@ export default function SearchView() {
setSelectedFeatures(new Set(features));
setSelectedPerspectives(new Set(perspectives));
setSelectedKeywords(new Set(keywords));
setSortBy(sort);
setInitialLoadComplete(true);
}, []);
// Update search parameters whenever the filters change
// Update search parameters whenever the filters or sort change
useEffect(() => {
if (!initialLoadComplete) return;
@@ -112,15 +117,55 @@ export default function SearchView() {
});
}
// Add sort param
if (sortBy && sortBy !== "title_asc") {
newParams.set("sort", sortBy);
}
setSearchParams(newParams, {replace: true});
}, [searchTerm, selectedLibraries, selectedDevelopers, selectedGenres,
selectedThemes, selectedFeatures, selectedPerspectives, selectedKeywords]);
selectedThemes, selectedFeatures, selectedPerspectives, selectedKeywords, sortBy]);
const filteredGames = useMemo(() => filterGames(), [
// Sorting function (refactored to use sortKey and sortDirection)
function sortGames(games: GameDto[]): GameDto[] {
if (!sortBy) return games;
const [sortKey, sortDirection] = sortBy.split("_");
return [...games].sort((a, b) => {
let cmp: number;
switch (sortKey) {
case "title":
cmp = a.title.localeCompare(b.title);
break;
case "release":
cmp = (a.release || "").localeCompare(b.release || "");
break;
case "rating":
cmp = (a.criticRating ?? 0) - (b.criticRating ?? 0);
break;
case "added":
cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "updated":
cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
break;
default:
cmp = 0;
}
if (sortDirection === "desc") {
cmp *= -1; // Reverse the comparison if sorting in descending order
}
return cmp;
});
}
const filteredAndSortedGames = useMemo(() => sortGames(filterGames()), [
games, searchTerm,
selectedLibraries, selectedDevelopers,
selectedGenres, selectedThemes,
selectedFeatures, selectedPerspectives, selectedKeywords
selectedFeatures, selectedPerspectives, selectedKeywords, sortBy
]);
function filterGames(): GameDto[] {
@@ -185,22 +230,71 @@ export default function SearchView() {
}
return <div className="flex flex-col gap-4 items-center w-full">
<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={searchTerm}
isClearable
onChange={(event) => setSearchTerm(event.target.value)}
onClear={() => setSearchTerm("")}
/>
<div
<div className="flex w-full justify-between px-12 gap-4 flex-col lg:flex-row">
<Input
classNames={{
base: "w-full lg:w-96 flex-shrink-0",
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={searchTerm}
isClearable
onChange={(event) => setSearchTerm(event.target.value)}
onClear={() => setSearchTerm("")}
/>
<div className="flex flex-row gap-2">
<Select
startContent={<SortAscending/>}
selectedKeys={[sortBy]}
disallowEmptySelection
selectionMode="single"
onSelectionChange={keys => setSortBy(Array.from(keys)[0] as any)}
className="w-full lg:w-64"
>
<SelectItem key="title_asc">Title (A-Z)</SelectItem>
<SelectItem key="title_desc">Title (Z-A)</SelectItem>
<SelectItem key="release_desc">Release Date (Newest)</SelectItem>
<SelectItem key="release_asc">Release Date (Oldest)</SelectItem>
<SelectItem key="rating_desc">Rating (Highest)</SelectItem>
<SelectItem key="rating_asc">Rating (Lowest)</SelectItem>
<SelectItem key="added_desc">Date Added (Newest)</SelectItem>
<SelectItem key="added_asc">Date Added (Oldest)</SelectItem>
<SelectItem key="updated_desc">Last Updated (Newest)</SelectItem>
<SelectItem key="updated_asc">Last Updated (Oldest)</SelectItem>
</Select>
<Tooltip content={showFilters ? "Hide Filters" : "Show Filters"}>
<Button isIconOnly
variant={showFilters ? "solid" : "bordered"}
color={showFilters ? "primary" : "default"}
onPress={() => setShowFilters(!showFilters)}
aria-label="Toggle Filters"
>
<FunnelSimple/>
</Button>
</Tooltip>
<Tooltip content="Clear All Filters">
<Button isIconOnly
onPress={() => {
setSelectedLibraries(new Set());
setSelectedDevelopers(new Set());
setSelectedGenres(new Set());
setSelectedThemes(new Set());
setSelectedFeatures(new Set());
setSelectedPerspectives(new Set());
setSelectedKeywords(new Set());
}}
aria-label="Clear All Filters"
>
<FunnelSimpleX/>
</Button>
</Tooltip>
</div>
</div>
{showFilters && <div
className="w-full justify-center"
style={{
display: "grid",
@@ -301,9 +395,10 @@ export default function SearchView() {
))}
</Select>
</div>
<div className="mt-4 w-full px-4 select-none">
<CoverGrid games={filteredGames}/>
{filteredGames.length === 0 && (
}
<div className="mt-4 w-full select-none">
<CoverGrid games={filteredAndSortedGames}/>
{filteredAndSortedGames.length === 0 && (
<div className="text-center mt-8 text-default-500">
No games found matching your filters
</div>