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