mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Implement sorting for search page
This commit is contained in:
@@ -251,7 +251,10 @@ export default function GameRequestView() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<Tooltip content="Vote for this request">
|
<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"
|
<Button size="sm"
|
||||||
variant={hasUserVotedForRequest(item as GameRequestDto) ? "solid" : "bordered"}
|
variant={hasUserVotedForRequest(item as GameRequestDto) ? "solid" : "bordered"}
|
||||||
color={hasUserVotedForRequest(item as GameRequestDto) ? "primary" : "default"}
|
color={hasUserVotedForRequest(item as GameRequestDto) ? "primary" : "default"}
|
||||||
@@ -260,6 +263,7 @@ export default function GameRequestView() {
|
|||||||
onPress={async () => await toggleVote(item.id)}>
|
onPress={async () => await toggleVote(item.id)}>
|
||||||
{item.voters.length}
|
{item.voters.length}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isAdmin(auth) && <div className="flex flex-row gap-2">
|
{isAdmin(auth) && <div className="flex flex-row gap-2">
|
||||||
<Tooltip content="Approve this request">
|
<Tooltip content="Approve this request">
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {Input, Select, SelectItem} from "@heroui/react";
|
import {Button, Input, Select, SelectItem, Tooltip} from "@heroui/react";
|
||||||
import {MagnifyingGlass} from "@phosphor-icons/react";
|
import {FunnelSimple, FunnelSimpleX, MagnifyingGlass, SortAscending} from "@phosphor-icons/react";
|
||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import {gameState} from "Frontend/state/GameState";
|
import {gameState} from "Frontend/state/GameState";
|
||||||
import {libraryState} from "Frontend/state/LibraryState";
|
import {libraryState} from "Frontend/state/LibraryState";
|
||||||
import {useSearchParams} from "react-router";
|
import {useSearchParams} from "react-router";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import React, {useEffect, useMemo, useState} from "react";
|
||||||
import {Fzf} from "fzf";
|
import {Fzf} from "fzf";
|
||||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||||
import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto";
|
import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto";
|
||||||
@@ -24,6 +24,9 @@ export default function SearchView() {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||||
|
|
||||||
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
const [sortBy, setSortBy] = useState("title_asc");
|
||||||
|
|
||||||
// State to track selected filter values
|
// State to track selected filter values
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [selectedLibraries, setSelectedLibraries] = useState<Set<string>>(new Set());
|
const [selectedLibraries, setSelectedLibraries] = useState<Set<string>>(new Set());
|
||||||
@@ -45,6 +48,7 @@ export default function SearchView() {
|
|||||||
const features = searchParams.getAll("feature");
|
const features = searchParams.getAll("feature");
|
||||||
const perspectives = searchParams.getAll("perspective");
|
const perspectives = searchParams.getAll("perspective");
|
||||||
const keywords = searchParams.getAll("keyword");
|
const keywords = searchParams.getAll("keyword");
|
||||||
|
const sort = searchParams.get("sort") || "title_asc";
|
||||||
|
|
||||||
setSearchTerm(term);
|
setSearchTerm(term);
|
||||||
setSelectedLibraries(new Set(libs));
|
setSelectedLibraries(new Set(libs));
|
||||||
@@ -54,11 +58,12 @@ export default function SearchView() {
|
|||||||
setSelectedFeatures(new Set(features));
|
setSelectedFeatures(new Set(features));
|
||||||
setSelectedPerspectives(new Set(perspectives));
|
setSelectedPerspectives(new Set(perspectives));
|
||||||
setSelectedKeywords(new Set(keywords));
|
setSelectedKeywords(new Set(keywords));
|
||||||
|
setSortBy(sort);
|
||||||
|
|
||||||
setInitialLoadComplete(true);
|
setInitialLoadComplete(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Update search parameters whenever the filters change
|
// Update search parameters whenever the filters or sort change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialLoadComplete) return;
|
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});
|
setSearchParams(newParams, {replace: true});
|
||||||
}, [searchTerm, selectedLibraries, selectedDevelopers, selectedGenres,
|
}, [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,
|
games, searchTerm,
|
||||||
selectedLibraries, selectedDevelopers,
|
selectedLibraries, selectedDevelopers,
|
||||||
selectedGenres, selectedThemes,
|
selectedGenres, selectedThemes,
|
||||||
selectedFeatures, selectedPerspectives, selectedKeywords
|
selectedFeatures, selectedPerspectives, selectedKeywords, sortBy
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function filterGames(): GameDto[] {
|
function filterGames(): GameDto[] {
|
||||||
@@ -185,9 +230,10 @@ export default function SearchView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className="flex flex-col gap-4 items-center w-full">
|
return <div className="flex flex-col gap-4 items-center w-full">
|
||||||
|
<div className="flex w-full justify-between px-12 gap-4 flex-col lg:flex-row">
|
||||||
<Input
|
<Input
|
||||||
classNames={{
|
classNames={{
|
||||||
base: "w-1/3",
|
base: "w-full lg:w-96 flex-shrink-0",
|
||||||
mainWrapper: "h-full",
|
mainWrapper: "h-full",
|
||||||
inputWrapper:
|
inputWrapper:
|
||||||
"h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
|
"h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
|
||||||
@@ -200,7 +246,55 @@ export default function SearchView() {
|
|||||||
onChange={(event) => setSearchTerm(event.target.value)}
|
onChange={(event) => setSearchTerm(event.target.value)}
|
||||||
onClear={() => setSearchTerm("")}
|
onClear={() => setSearchTerm("")}
|
||||||
/>
|
/>
|
||||||
<div
|
<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"
|
className="w-full justify-center"
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
@@ -301,9 +395,10 @@ export default function SearchView() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 w-full px-4 select-none">
|
}
|
||||||
<CoverGrid games={filteredGames}/>
|
<div className="mt-4 w-full select-none">
|
||||||
{filteredGames.length === 0 && (
|
<CoverGrid games={filteredAndSortedGames}/>
|
||||||
|
{filteredAndSortedGames.length === 0 && (
|
||||||
<div className="text-center mt-8 text-default-500">
|
<div className="text-center mt-8 text-default-500">
|
||||||
No games found matching your filters
|
No games found matching your filters
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user