feat: implement game filter to find games all selected people would play and add supporting test data

This commit is contained in:
code002lover 2025-12-03 23:41:54 +01:00
parent deae3a106b
commit af721e7716
3 changed files with 188 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import { Login } from "./Login";
import { PersonList } from "./PersonList"; import { PersonList } from "./PersonList";
import { PersonDetails } from "./PersonDetails"; import { PersonDetails } from "./PersonDetails";
import { GameList } from "./GameList"; import { GameList } from "./GameList";
import { GameFilter } from "./GameFilter";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import "./App.css"; import "./App.css";
import { apiFetch } from "./api"; import { apiFetch } from "./api";
@ -52,6 +53,9 @@ function App() {
<Link to="/games" className="nav-link"> <Link to="/games" className="nav-link">
Games Games
</Link> </Link>
<Link to="/filter" className="nav-link">
Filter
</Link>
</div> </div>
<button onClick={handleLogout} className="btn-secondary"> <button onClick={handleLogout} className="btn-secondary">
Logout Logout
@ -60,6 +64,7 @@ function App() {
<Routes> <Routes>
<Route path="/" element={<PersonList people={people} />} /> <Route path="/" element={<PersonList people={people} />} />
<Route path="/games" element={<GameList />} /> <Route path="/games" element={<GameList />} />
<Route path="/filter" element={<GameFilter />} />
<Route path="/person/:name" element={<PersonDetails />} /> <Route path="/person/:name" element={<PersonDetails />} />
</Routes> </Routes>
</div> </div>

142
frontend/src/GameFilter.tsx Normal file
View File

@ -0,0 +1,142 @@
import { useState, useEffect } from "react";
import { Person, PersonList as PersonListProto } from "../items";
import { apiFetch } from "./api";
export function GameFilter() {
const [people, setPeople] = useState<Person[]>([]);
const [selectedPeople, setSelectedPeople] = useState<Set<string>>(new Set());
const [filteredGames, setFilteredGames] = useState<string[]>([]);
useEffect(() => {
apiFetch("/api")
.then((res) => res.arrayBuffer())
.then((buffer) => {
const list = PersonListProto.decode(new Uint8Array(buffer));
setPeople(list.person);
})
.catch((err) => console.error("Failed to fetch people:", err));
}, []);
useEffect(() => {
if (selectedPeople.size === 0) {
setFilteredGames([]);
return;
}
// Get all games where ALL selected people have "Would Play"
const selectedPersons = people.filter((p) => selectedPeople.has(p.name));
if (selectedPersons.length === 0) {
setFilteredGames([]);
return;
}
// Create a map of game -> set of people who would play it
const gameToPlayers = new Map<string, Set<string>>();
selectedPersons.forEach((person) => {
person.opinion.forEach((op) => {
if (op.wouldPlay) {
if (!gameToPlayers.has(op.title)) {
gameToPlayers.set(op.title, new Set());
}
gameToPlayers.get(op.title)!.add(person.name);
}
});
});
// Filter games where ALL selected people would play
const games = Array.from(gameToPlayers.entries())
.filter(([, players]) => players.size === selectedPeople.size)
.map(([game]) => game);
setFilteredGames(games);
}, [selectedPeople, people]);
const togglePerson = (name: string) => {
const newSelected = new Set(selectedPeople);
if (newSelected.has(name)) {
newSelected.delete(name);
} else {
newSelected.add(name);
}
setSelectedPeople(newSelected);
};
return (
<div>
<h2>Game Filter</h2>
<p style={{ color: "var(--text-muted)", marginBottom: "2rem" }}>
Select multiple people to find games that everyone would play
</p>
<div style={{ marginBottom: "3rem" }}>
<h3>Select People</h3>
<div className="grid-container">
{people.map((person) => (
<div
key={person.name}
className="list-item"
style={{
borderColor: selectedPeople.has(person.name)
? "var(--accent-color)"
: "var(--border-color)",
cursor: "pointer",
}}
onClick={() => togglePerson(person.name)}
>
<div
style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
>
<input
type="checkbox"
checked={selectedPeople.has(person.name)}
onChange={() => togglePerson(person.name)}
style={{ cursor: "pointer" }}
/>
<strong>{person.name}</strong>
</div>
<div
style={{
fontSize: "0.9em",
color: "var(--text-muted)",
marginTop: "0.5rem",
}}
>
{person.opinion.length} opinion(s)
</div>
</div>
))}
</div>
</div>
{selectedPeople.size > 0 && (
<div>
<h3>Games Everyone Would Play ({filteredGames.length})</h3>
{filteredGames.length > 0 ? (
<ul className="grid-container">
{filteredGames.map((game) => (
<li key={game} className="list-item">
<strong>{game}</strong>
<div
style={{
fontSize: "0.9em",
color: "#4caf50",
marginTop: "0.5rem",
}}
>
All {selectedPeople.size} selected would play
</div>
</li>
))}
</ul>
) : (
<p style={{ color: "var(--text-muted)", fontStyle: "italic" }}>
No games found where all selected people would play
</p>
)}
</div>
)}
</div>
);
}

View File

@ -8,12 +8,53 @@
"max_players": 90, "max_players": 90,
"price": 0, "price": 0,
"remote_id": 0 "remote_id": 0
},
{
"title": "Test2",
"source": 1,
"multiplayer": true,
"min_players": 1,
"max_players": 1,
"price": 0,
"remote_id": 0
} }
], ],
"users": [ "users": [
{ {
"person": { "person": {
"name": "John", "name": "John",
"opinion": [
{
"title": "Naramo Nuclear Plant V2",
"would_play": true
},
{
"title": "Test2",
"would_play": true
}
]
},
"password_hash": "$2b$12$DRvTP/ibTWULkuJJr285bumRd7SG3n5bYkDpb09Qpklqf6FeTiHkC"
},
{
"person": {
"name": "John2",
"opinion": [
{
"title": "Naramo Nuclear Plant V2",
"would_play": false
},
{
"title": "Test2",
"would_play": true
}
]
},
"password_hash": "$2b$12$DRvTP/ibTWULkuJJr285bumRd7SG3n5bYkDpb09Qpklqf6FeTiHkC"
},
{
"person": {
"name": "John3",
"opinion": [ "opinion": [
{ {
"title": "Naramo Nuclear Plant V2", "title": "Naramo Nuclear Plant V2",