diff --git a/app/src/main/frontend/views/GameRequestView.tsx b/app/src/main/frontend/views/GameRequestView.tsx index 427229e..c734979 100644 --- a/app/src/main/frontend/views/GameRequestView.tsx +++ b/app/src/main/frontend/views/GameRequestView.tsx @@ -251,15 +251,19 @@ export default function GameRequestView() {
- - + +
+ +
{isAdmin(auth) &&
diff --git a/app/src/main/frontend/views/SearchView.tsx b/app/src/main/frontend/views/SearchView.tsx index a7c7074..1aa83c7 100644 --- a/app/src/main/frontend/views/SearchView.tsx +++ b/app/src/main/frontend/views/SearchView.tsx @@ -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(""); const [selectedLibraries, setSelectedLibraries] = useState>(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
- } - type="search" - value={searchTerm} - isClearable - onChange={(event) => setSearchTerm(event.target.value)} - onClear={() => setSearchTerm("")} - /> -
+ } + type="search" + value={searchTerm} + isClearable + onChange={(event) => setSearchTerm(event.target.value)} + onClear={() => setSearchTerm("")} + /> +
+ + + + + + + +
+
+ {showFilters &&
-
- - {filteredGames.length === 0 && ( + } +
+ + {filteredAndSortedGames.length === 0 && (
No games found matching your filters