From d560b6db6f1efab0c6ea917ccfa8a79fe15a893e Mon Sep 17 00:00:00 2001 From: code002lover Date: Mon, 12 Jan 2026 08:57:42 +0100 Subject: [PATCH] add extra filters --- frontend/src/GameFilter.css | 78 +++++++++++++++++++ frontend/src/GameFilter.tsx | 62 ++++++++++++++- frontend/src/components/FilteredGamesList.tsx | 20 +++++ frontend/src/hooks/useGameFilter.ts | 61 +++++++++++++-- 4 files changed, 213 insertions(+), 8 deletions(-) diff --git a/frontend/src/GameFilter.css b/frontend/src/GameFilter.css index 6281398..f04f868 100644 --- a/frontend/src/GameFilter.css +++ b/frontend/src/GameFilter.css @@ -11,4 +11,82 @@ .gamefilter-entry:hover { background-color: var(--primary-bg); +} + +.filter-controls { + background-color: var(--secondary-alt-bg); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 2rem; + border: 1px solid var(--border-color); +} + +.filter-controls h3 { + margin: 0 0 1rem 0; + font-size: 1.1rem; + color: var(--text-color); +} + +.filter-groups { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.filter-group label { + font-size: 0.9rem; + color: var(--text-color); + font-weight: 500; +} + +.checkbox-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.checkbox-wrapper input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--accent-color); +} + +.checkbox-wrapper span { + font-size: 0.9rem; + color: var(--text-color); +} + +.price-input { + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--primary-bg); + color: var(--text-color); + width: 120px; + font-size: 0.9rem; +} + +.price-input:focus { + outline: none; + border-color: var(--accent-color); +} + +.tooltip-icon { + display: inline-block; + margin-left: 0.3rem; + cursor: help; + font-size: 0.9rem; + opacity: 0.6; +} + +.tooltip-icon:hover { + opacity: 1; } \ No newline at end of file diff --git a/frontend/src/GameFilter.tsx b/frontend/src/GameFilter.tsx index 06fe709..95a0dc2 100644 --- a/frontend/src/GameFilter.tsx +++ b/frontend/src/GameFilter.tsx @@ -11,10 +11,16 @@ export function GameFilter() { const [people, setPeople] = useState([]); const [loading, setLoading] = useState(true); const [selectedPeople, setSelectedPeople] = useState>(new Set()); + const [freeGamesOnly, setFreeGamesOnly] = useState(false); + const [maxPrice, setMaxPrice] = useState(null); + const [ownershipMode, setOwnershipMode] = useState(false); - const { filteredGames, gameToPositive } = useGameFilter( + const { filteredGames, gameToPositive, games } = useGameFilter( people, - selectedPeople + selectedPeople, + freeGamesOnly, + maxPrice, + ownershipMode ); useEffect(() => { @@ -57,10 +63,62 @@ export function GameFilter() { onTogglePerson={togglePerson} /> +
+

Additional Filters

+
+
+ +
+ +
+ + { + const value = e.target.value; + setMaxPrice(value === "" ? null : parseInt(value, 10)); + }} + /> +
+ +
+ +
+
+
+ ); diff --git a/frontend/src/components/FilteredGamesList.tsx b/frontend/src/components/FilteredGamesList.tsx index f8079db..09411ce 100644 --- a/frontend/src/components/FilteredGamesList.tsx +++ b/frontend/src/components/FilteredGamesList.tsx @@ -1,17 +1,20 @@ import { Link } from "react-router-dom"; import { GameImage } from "../GameImage"; import { EmptyState } from "./EmptyState"; +import { Game as GameProto } from "../../items"; interface FilteredGamesListProps { filteredGames: string[]; gameToPositive: Map>; selectedPeopleCount: number; + games: Map; } export function FilteredGamesList({ filteredGames, gameToPositive, selectedPeopleCount, + games, }: FilteredGamesListProps) { if (selectedPeopleCount === 0) { return ( @@ -31,6 +34,8 @@ export function FilteredGamesList({ {filteredGames.map((game) => { const positiveCount = gameToPositive.get(game)?.size || 0; const neutralCount = selectedPeopleCount - positiveCount; + const gameData = games.get(game); + const price = gameData?.price ?? 0; return ( 1 ? "are" : "is"} neutral )} +
+ ${price === 0 ? "0 (Free)" : price} +
diff --git a/frontend/src/hooks/useGameFilter.ts b/frontend/src/hooks/useGameFilter.ts index 5f1b617..2e46224 100644 --- a/frontend/src/hooks/useGameFilter.ts +++ b/frontend/src/hooks/useGameFilter.ts @@ -7,7 +7,13 @@ import { } from "../../items"; import { apiFetch } from "../api"; -export function useGameFilter(people: Person[], selectedPeople: Set) { +export function useGameFilter( + people: Person[], + selectedPeople: Set, + freeGamesOnly: boolean, + maxPrice: number | null, + ownershipMode: boolean +) { const [fetchedTitles, setFetchedTitles] = useState([]); const metaDataRef = useRef<{ [key: string]: GameProto }>({}); @@ -79,20 +85,63 @@ export function useGameFilter(people: Person[], selectedPeople: Set) { .filter((title) => metaDataRef.current[title]) .map((title) => metaDataRef.current[title]); - return filterByPlayerCount(games, selectedPeople.size); + return filterGames( + games, + selectedPeople.size, + freeGamesOnly, + maxPrice, + ownershipMode, + selectedPeople, + people + ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [titlesEveryoneWouldPlay, selectedPeople.size, fetchedTitles]); + }, [ + titlesEveryoneWouldPlay, + selectedPeople.size, + fetchedTitles, + freeGamesOnly, + maxPrice, + ownershipMode, + people, + selectedPeople, + ]); - return { filteredGames, gameToPositive: gameToPositiveOpinion }; + const gamesMap = useMemo(() => { + return new Map(Object.entries(metaDataRef.current)); + }, [fetchedTitles]); + + return { filteredGames, gameToPositive: gameToPositiveOpinion, games: gamesMap }; } -function filterByPlayerCount( +function filterGames( games: GameProto[], - playerCount: number + playerCount: number, + freeGamesOnly: boolean, + maxPrice: number | null, + ownershipMode: boolean, + selectedPeople: Set, + people: Person[] ): string[] { + const selectedPersons = people.filter((p) => selectedPeople.has(p.name)); + return games .filter( (game) => game.maxPlayers >= playerCount && game.minPlayers <= playerCount ) + .filter((game) => { + if (freeGamesOnly) return game.price === 0; + if (maxPrice !== null) return game.price <= maxPrice; + return true; + }) + .filter((game) => { + if (!ownershipMode) return true; + if (game.price === 0) return true; + + return selectedPersons.every((person) => + person.opinion.some( + (op) => op.title === game.title && op.wouldPlay + ) + ); + }) .map((game) => game.title); }