diff --git a/backend/src/main.rs b/backend/src/main.rs index 0f283e5..d34d3a7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -185,7 +185,6 @@ async fn main() -> Result<(), std::io::Error> { game_list.push(Game { title: "Naramo Nuclear Plant V2".to_string(), source: items::Source::Roblox.into(), - multiplayer: true, min_players: 1, max_players: 90, price: 0, diff --git a/frontend/items.ts b/frontend/items.ts index 3789ccb..bb86e81 100644 --- a/frontend/items.ts +++ b/frontend/items.ts @@ -55,7 +55,6 @@ export interface Opinion { export interface Game { title: string; source: Source; - multiplayer: boolean; minPlayers: number; maxPlayers: number; price: number; @@ -266,7 +265,7 @@ export const Opinion: MessageFns = { }; function createBaseGame(): Game { - return { title: "", source: 0, multiplayer: false, minPlayers: 0, maxPlayers: 0, price: 0, remoteId: 0 }; + return { title: "", source: 0, minPlayers: 0, maxPlayers: 0, price: 0, remoteId: 0 }; } export const Game: MessageFns = { @@ -277,9 +276,6 @@ export const Game: MessageFns = { if (message.source !== 0) { writer.uint32(16).int32(message.source); } - if (message.multiplayer !== false) { - writer.uint32(24).bool(message.multiplayer); - } if (message.minPlayers !== 0) { writer.uint32(32).uint32(message.minPlayers); } @@ -318,14 +314,6 @@ export const Game: MessageFns = { message.source = reader.int32() as any; continue; } - case 3: { - if (tag !== 24) { - break; - } - - message.multiplayer = reader.bool(); - continue; - } case 4: { if (tag !== 32) { break; @@ -371,7 +359,6 @@ export const Game: MessageFns = { return { title: isSet(object.title) ? globalThis.String(object.title) : "", source: isSet(object.source) ? sourceFromJSON(object.source) : 0, - multiplayer: isSet(object.multiplayer) ? globalThis.Boolean(object.multiplayer) : false, minPlayers: isSet(object.minPlayers) ? globalThis.Number(object.minPlayers) : 0, maxPlayers: isSet(object.maxPlayers) ? globalThis.Number(object.maxPlayers) : 0, price: isSet(object.price) ? globalThis.Number(object.price) : 0, @@ -387,9 +374,6 @@ export const Game: MessageFns = { if (message.source !== 0) { obj.source = sourceToJSON(message.source); } - if (message.multiplayer !== false) { - obj.multiplayer = message.multiplayer; - } if (message.minPlayers !== 0) { obj.minPlayers = Math.round(message.minPlayers); } @@ -412,7 +396,6 @@ export const Game: MessageFns = { const message = createBaseGame(); message.title = object.title ?? ""; message.source = object.source ?? 0; - message.multiplayer = object.multiplayer ?? false; message.minPlayers = object.minPlayers ?? 0; message.maxPlayers = object.maxPlayers ?? 0; message.price = object.price ?? 0; diff --git a/frontend/package.json b/frontend/package.json index fa6d17d..a988099 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "pnpm run gen:proto && tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", "gen:proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=. -I ../protobuf items.proto" @@ -36,4 +36,4 @@ "vite": "npm:rolldown-vite@7.2.5" } } -} \ No newline at end of file +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 70b2656..a1369ef 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import { PersonList } from "./PersonList"; import { PersonDetails } from "./PersonDetails"; import { GameList } from "./GameList"; import { GameFilter } from "./GameFilter"; +import { GameDetails } from "./GameDetails"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import "./App.css"; import { apiFetch } from "./api"; @@ -15,7 +16,7 @@ function App() { localStorage.getItem("token") || "" ); - useEffect(() => { + const fetchPeople = () => { if (!token) return; apiFetch("/api") @@ -25,6 +26,11 @@ function App() { setPeople(list.person); }) .catch((err) => console.error("Failed to fetch people:", err)); + }; + + useEffect(() => { + fetchPeople(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); const handleLogin = (newToken: string) => { @@ -62,10 +68,14 @@ function App() { - } /> + } + /> } /> } /> } /> + } /> diff --git a/frontend/src/GameDetails.tsx b/frontend/src/GameDetails.tsx new file mode 100644 index 0000000..68bba3e --- /dev/null +++ b/frontend/src/GameDetails.tsx @@ -0,0 +1,69 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { Game, Source } from "../items"; +import { apiFetch } from "./api"; + +export function GameDetails() { + const { title } = useParams<{ title: string }>(); + const [game, setGame] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!title) return; + + apiFetch(`/api/game/${encodeURIComponent(title)}`) + .then(async (res) => { + if (!res.ok) throw new Error("Game not found"); + return Game.decode(new Uint8Array(await res.arrayBuffer())); + }) + .then((data) => { + setGame(data); + setLoading(false); + }) + .catch((err) => { + console.error(err); + setLoading(false); + }); + }, [title]); + + if (loading) return
Loading...
; + if (!game) return
Game not found
; + + const getExternalLink = () => { + if (game.source === Source.STEAM) { + return `https://store.steampowered.com/app/${game.remoteId}`; + } else if (game.source === Source.ROBLOX) { + return `https://www.roblox.com/games/${game.remoteId}`; + } + return "#"; + }; + + return ( +
+

{game.title}

+
+
+ Source:{" "} + {game.source === Source.STEAM ? "Steam" : "Roblox"} +
+
+ Players: {game.minPlayers} - {game.maxPlayers} +
+
+ Price: ${game.price} +
+
+ +
+ ); +} diff --git a/frontend/src/GameFilter.tsx b/frontend/src/GameFilter.tsx index e12665a..eaf8fd1 100644 --- a/frontend/src/GameFilter.tsx +++ b/frontend/src/GameFilter.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { Person, PersonList as PersonListProto } from "../items"; import { apiFetch } from "./api"; +import { Link } from "react-router-dom"; export function GameFilter() { const [people, setPeople] = useState([]); @@ -116,7 +117,16 @@ export function GameFilter() { {filteredGames.length > 0 ? (
    {filteredGames.map((game) => ( -
  • + {game}
    ✓ All {selectedPeople.size} selected would play
    -
  • + ))}
) : ( diff --git a/frontend/src/GameList.tsx b/frontend/src/GameList.tsx index 2914017..c1e1573 100644 --- a/frontend/src/GameList.tsx +++ b/frontend/src/GameList.tsx @@ -1,12 +1,12 @@ import { useState, useEffect } from "react"; import { Game, Source, GameList as GameListProto } from "../items"; +import { Link } from "react-router-dom"; import { apiFetch } from "./api"; export function GameList() { const [games, setGames] = useState([]); const [title, setTitle] = useState(""); const [source, setSource] = useState(Source.STEAM); - const [multiplayer, setMultiplayer] = useState(false); const [minPlayers, setMinPlayers] = useState(1); const [maxPlayers, setMaxPlayers] = useState(1); const [price, setPrice] = useState(0); @@ -36,7 +36,6 @@ export function GameList() { const game = { title, source, - multiplayer, minPlayers, maxPlayers, price, @@ -102,23 +101,6 @@ export function GameList() { -
- setMultiplayer(e.target.checked)} - id="multiplayer" - /> - -
Existing Games
    {games.map((game) => ( -
  • + {game.title}
    {game.source === Source.STEAM ? "Steam" : "Roblox"}
    -
  • + ))}
diff --git a/frontend/src/PersonList.tsx b/frontend/src/PersonList.tsx index 5b37c56..451018d 100644 --- a/frontend/src/PersonList.tsx +++ b/frontend/src/PersonList.tsx @@ -3,28 +3,51 @@ import { Link } from "react-router-dom"; interface Props { people: Person[]; + onRefresh: () => void; } -export const PersonList = ({ people }: Props) => { +export const PersonList = ({ people, onRefresh }: Props) => { return ( -
- {people.map((person, index) => ( -
-

- {person.name} -

-
    - {person.opinion.map((op, i) => ( -
  • - {op.title} - {op.wouldPlay ? "Would Play" : "Would Not Play"} -
  • - ))} -
-
- ))} +
+
+

People List

+ +
+
+ {people.map((person, index) => ( + +

{person.name}

+
    + {person.opinion.map((op, i) => ( +
  • + {op.title} - {op.wouldPlay ? "Would Play" : "Would Not Play"} +
  • + ))} +
+ + ))} +
); }; diff --git a/protobuf/items.proto b/protobuf/items.proto index 4730baf..5a7a2b2 100644 --- a/protobuf/items.proto +++ b/protobuf/items.proto @@ -13,9 +13,9 @@ message Opinion { } message Game { + reserved 3; string title = 1; Source source = 2; - bool multiplayer = 3; uint32 min_players = 4; uint32 max_players = 5; uint32 price = 6; diff --git a/state.json b/state.json index 49e289a..ea96021 100644 --- a/state.json +++ b/state.json @@ -3,7 +3,6 @@ { "title": "Naramo Nuclear Plant V2", "source": 1, - "multiplayer": true, "min_players": 1, "max_players": 90, "price": 0, @@ -12,7 +11,6 @@ { "title": "Test2", "source": 1, - "multiplayer": true, "min_players": 1, "max_players": 1, "price": 0, @@ -69,4 +67,4 @@ "password_hash": "$2b$12$DRvTP/ibTWULkuJJr285bumRd7SG3n5bYkDpb09Qpklqf6FeTiHkC" } ] -} \ No newline at end of file +}