feat: modularize game filtering logic and UI into a custom hook and dedicated components.feat: modularize game filtering logic and UI into a custom hook and dedicated components.

This commit is contained in:
2025-12-19 14:06:31 +01:00
parent d916014872
commit 5b397e2265
5 changed files with 267 additions and 205 deletions
+98
View File
@@ -0,0 +1,98 @@
import { useState, useEffect, useRef, useMemo } from "react";
import {
Person,
Game as GameProto,
GetGameInfoRequest,
GameInfoResponse,
} from "../../items";
import { apiFetch } from "../api";
export function useGameFilter(people: Person[], selectedPeople: Set<string>) {
const [fetchedTitles, setFetchedTitles] = useState<string[]>([]);
const metaDataRef = useRef<{ [key: string]: GameProto }>({});
const { gameToNegative, gameToPositiveOpinion } = useMemo(() => {
const gameToNegative = new Map<string, Set<string>>();
const gameToPositiveOpinion = new Map<string, Set<string>>();
if (selectedPeople.size === 0)
return { gameToNegative, gameToPositiveOpinion };
const selectedPersons = people.filter((p) => selectedPeople.has(p.name));
selectedPersons.forEach((person) => {
person.opinion.forEach((op) => {
if (!gameToNegative.has(op.title))
gameToNegative.set(op.title, new Set());
if (!gameToPositiveOpinion.has(op.title))
gameToPositiveOpinion.set(op.title, new Set());
if (!op.wouldPlay) {
gameToNegative.get(op.title)!.add(person.name);
} else {
gameToPositiveOpinion.get(op.title)!.add(person.name);
}
});
});
return { gameToNegative, gameToPositiveOpinion };
}, [people, selectedPeople]);
const titlesEveryoneWouldPlay = useMemo(() => {
return Array.from(gameToNegative.entries())
.filter(([, players]) => players.size === 0)
.map(([game]) => game);
}, [gameToNegative]);
useEffect(() => {
const titlesToFetch = titlesEveryoneWouldPlay.filter(
(title) => !metaDataRef.current[title]
);
if (titlesToFetch.length === 0) return;
const gamesToFetch = GetGameInfoRequest.encode(
GetGameInfoRequest.create({
games: titlesToFetch,
})
).finish();
apiFetch("/api/games/batch", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: gamesToFetch,
})
.then((res) => res.arrayBuffer())
.then((buffer) => {
const list = GameInfoResponse.decode(new Uint8Array(buffer));
list.games.forEach((game) => {
metaDataRef.current[game.title] = game;
});
// Trigger a re-render to update filteredGames
setFetchedTitles([...titlesToFetch]);
})
.catch((err) => console.error("Failed to fetch game metadata:", err));
}, [titlesEveryoneWouldPlay]);
const filteredGames = useMemo(() => {
if (selectedPeople.size === 0) return [];
const games = titlesEveryoneWouldPlay
.filter((title) => metaDataRef.current[title])
.map((title) => metaDataRef.current[title]);
return filterByPlayerCount(games, selectedPeople.size);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [titlesEveryoneWouldPlay, selectedPeople.size, fetchedTitles]);
return { filteredGames, gameToPositive: gameToPositiveOpinion };
}
function filterByPlayerCount(
games: GameProto[],
playerCount: number
): string[] {
return games
.filter(
(game) => game.maxPlayers >= playerCount && game.minPlayers <= playerCount
)
.map((game) => game.title);
}