feat: implement game listing API and integrate into frontend for display and selection.

This commit is contained in:
code002lover 2025-12-03 19:01:38 +01:00
parent e192829fdd
commit 0c3c9e61b6
5 changed files with 204 additions and 8 deletions

View File

@ -92,6 +92,14 @@ fn get_game(
games.iter().find(|g| g.title == title).cloned() games.iter().find(|g| g.title == title).cloned()
} }
#[get("/games")]
fn get_games(_token: auth::Token, game_list: &rocket::State<Mutex<Vec<Game>>>) -> items::GameList {
let games = game_list.lock().unwrap();
items::GameList {
games: games.clone(),
}
}
#[post("/game", data = "<game>")] #[post("/game", data = "<game>")]
fn add_game( fn add_game(
_token: auth::Token, _token: auth::Token,
@ -203,7 +211,14 @@ async fn main() -> Result<(), std::io::Error> {
.manage(Mutex::new(game_list)) .manage(Mutex::new(game_list))
.mount( .mount(
"/api", "/api",
routes![get_users, get_user, get_game, add_opinion, add_game], routes![
get_users,
get_user,
get_game,
get_games,
add_opinion,
add_game
],
) )
.mount( .mount(
"/auth", "/auth",

View File

@ -66,6 +66,10 @@ export interface PersonList {
person: Person[]; person: Person[];
} }
export interface GameList {
games: Game[];
}
/** Authentication messages */ /** Authentication messages */
export interface LoginRequest { export interface LoginRequest {
username: string; username: string;
@ -101,6 +105,9 @@ export interface GameRequest {
title: string; title: string;
} }
export interface GetGamesRequest {
}
export interface AddOpinionRequest { export interface AddOpinionRequest {
gameTitle: string; gameTitle: string;
wouldPlay: boolean; wouldPlay: boolean;
@ -474,6 +481,64 @@ export const PersonList: MessageFns<PersonList> = {
}, },
}; };
function createBaseGameList(): GameList {
return { games: [] };
}
export const GameList: MessageFns<GameList> = {
encode(message: GameList, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.games) {
Game.encode(v!, writer.uint32(10).fork()).join();
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): GameList {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseGameList();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.games.push(Game.decode(reader, reader.uint32()));
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): GameList {
return { games: globalThis.Array.isArray(object?.games) ? object.games.map((e: any) => Game.fromJSON(e)) : [] };
},
toJSON(message: GameList): unknown {
const obj: any = {};
if (message.games?.length) {
obj.games = message.games.map((e) => Game.toJSON(e));
}
return obj;
},
create<I extends Exact<DeepPartial<GameList>, I>>(base?: I): GameList {
return GameList.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<GameList>, I>>(object: I): GameList {
const message = createBaseGameList();
message.games = object.games?.map((e) => Game.fromPartial(e)) || [];
return message;
},
};
function createBaseLoginRequest(): LoginRequest { function createBaseLoginRequest(): LoginRequest {
return { username: "", password: "" }; return { username: "", password: "" };
} }
@ -984,6 +1049,49 @@ export const GameRequest: MessageFns<GameRequest> = {
}, },
}; };
function createBaseGetGamesRequest(): GetGamesRequest {
return {};
}
export const GetGamesRequest: MessageFns<GetGamesRequest> = {
encode(_: GetGamesRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): GetGamesRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseGetGamesRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(_: any): GetGamesRequest {
return {};
},
toJSON(_: GetGamesRequest): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<GetGamesRequest>, I>>(base?: I): GetGamesRequest {
return GetGamesRequest.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<GetGamesRequest>, I>>(_: I): GetGamesRequest {
const message = createBaseGetGamesRequest();
return message;
},
};
function createBaseAddOpinionRequest(): AddOpinionRequest { function createBaseAddOpinionRequest(): AddOpinionRequest {
return { gameTitle: "", wouldPlay: false }; return { gameTitle: "", wouldPlay: false };
} }
@ -1099,6 +1207,8 @@ export class AuthServiceClientImpl implements AuthService {
export interface MainService { export interface MainService {
GetGame(request: GameRequest): Promise<Game>; GetGame(request: GameRequest): Promise<Game>;
GetGames(request: GetGamesRequest): Promise<GameList>;
AddGame(request: Game): Promise<Game>;
AddOpinion(request: AddOpinionRequest): Promise<Person>; AddOpinion(request: AddOpinionRequest): Promise<Person>;
} }
@ -1110,6 +1220,8 @@ export class MainServiceClientImpl implements MainService {
this.service = opts?.service || MainServiceServiceName; this.service = opts?.service || MainServiceServiceName;
this.rpc = rpc; this.rpc = rpc;
this.GetGame = this.GetGame.bind(this); this.GetGame = this.GetGame.bind(this);
this.GetGames = this.GetGames.bind(this);
this.AddGame = this.AddGame.bind(this);
this.AddOpinion = this.AddOpinion.bind(this); this.AddOpinion = this.AddOpinion.bind(this);
} }
GetGame(request: GameRequest): Promise<Game> { GetGame(request: GameRequest): Promise<Game> {
@ -1118,6 +1230,18 @@ export class MainServiceClientImpl implements MainService {
return promise.then((data) => Game.decode(new BinaryReader(data))); return promise.then((data) => Game.decode(new BinaryReader(data)));
} }
GetGames(request: GetGamesRequest): Promise<GameList> {
const data = GetGamesRequest.encode(request).finish();
const promise = this.rpc.request(this.service, "GetGames", data);
return promise.then((data) => GameList.decode(new BinaryReader(data)));
}
AddGame(request: Game): Promise<Game> {
const data = Game.encode(request).finish();
const promise = this.rpc.request(this.service, "AddGame", data);
return promise.then((data) => Game.decode(new BinaryReader(data)));
}
AddOpinion(request: AddOpinionRequest): Promise<Person> { AddOpinion(request: AddOpinionRequest): Promise<Person> {
const data = AddOpinionRequest.encode(request).finish(); const data = AddOpinionRequest.encode(request).finish();
const promise = this.rpc.request(this.service, "AddOpinion", data); const promise = this.rpc.request(this.service, "AddOpinion", data);

View File

@ -1,8 +1,9 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { Game, Source } from "../items"; import { Game, Source, GameList as GameListProto } from "../items";
import { apiFetch } from "./api"; import { apiFetch } from "./api";
export function GameList() { export function GameList() {
const [games, setGames] = useState<Game[]>([]);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [source, setSource] = useState<Source>(Source.STEAM); const [source, setSource] = useState<Source>(Source.STEAM);
const [multiplayer, setMultiplayer] = useState(false); const [multiplayer, setMultiplayer] = useState(false);
@ -12,6 +13,24 @@ export function GameList() {
const [remoteId, setRemoteId] = useState(0); const [remoteId, setRemoteId] = useState(0);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const fetchGames = () => {
apiFetch("/api/games")
.then((res) => res.arrayBuffer())
.then((buffer) => {
try {
const list = GameListProto.decode(new Uint8Array(buffer));
setGames(list.games);
} catch (e) {
console.error("Failed to decode games:", e);
}
})
.catch(console.error);
};
useEffect(() => {
fetchGames();
}, []);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const game = { const game = {
@ -37,6 +56,7 @@ export function GameList() {
if (res.ok) { if (res.ok) {
setMessage("Game added successfully!"); setMessage("Game added successfully!");
setTitle(""); setTitle("");
fetchGames();
// Reset other fields if needed // Reset other fields if needed
} else { } else {
setMessage("Failed to add game."); setMessage("Failed to add game.");
@ -121,6 +141,17 @@ export function GameList() {
</label> </label>
<button type="submit">Add Game</button> <button type="submit">Add Game</button>
</form> </form>
<div style={{ marginTop: "2rem" }}>
<h3>Existing Games</h3>
<ul>
{games.map((game) => (
<li key={game.title}>
{game.title} ({game.source === Source.STEAM ? "Steam" : "Roblox"})
</li>
))}
</ul>
</div>
</div> </div>
); );
} }

View File

@ -1,14 +1,32 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Person, AddOpinionRequest } from "../items"; import { Person, AddOpinionRequest, Game, GameList } from "../items";
import { apiFetch } from "./api"; import { apiFetch } from "./api";
export const PersonDetails = () => { export const PersonDetails = () => {
const { name } = useParams<{ name: string }>(); const { name } = useParams<{ name: string }>();
const [person, setPerson] = useState<Person | null>(null); const [person, setPerson] = useState<Person | null>(null);
const [games, setGames] = useState<Game[]>([]);
const [gameTitle, setGameTitle] = useState(""); const [gameTitle, setGameTitle] = useState("");
const [wouldPlay, setWouldPlay] = useState(false); const [wouldPlay, setWouldPlay] = useState(false);
useEffect(() => {
apiFetch("/api/games")
.then((res) => res.arrayBuffer())
.then((buffer) => {
try {
const list = GameList.decode(new Uint8Array(buffer));
setGames(list.games);
if (list.games.length > 0) {
setGameTitle(list.games[0].title);
}
} catch (e) {
console.error("Failed to decode games:", e);
}
})
.catch(console.error);
}, []);
useEffect(() => { useEffect(() => {
if (name) { if (name) {
apiFetch(`/api/${name}`) apiFetch(`/api/${name}`)
@ -76,12 +94,16 @@ export const PersonDetails = () => {
> >
<h3>Add Opinion</h3> <h3>Add Opinion</h3>
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}> <div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
<input <select
type="text"
placeholder="Game Title"
value={gameTitle} value={gameTitle}
onChange={(e) => setGameTitle(e.target.value)} onChange={(e) => setGameTitle(e.target.value)}
/> >
{games.map((g) => (
<option key={g.title} value={g.title}>
{g.title}
</option>
))}
</select>
<label> <label>
<input <input
type="checkbox" type="checkbox"

View File

@ -28,6 +28,8 @@ enum Source {
} }
message PersonList { repeated Person person = 1; } message PersonList { repeated Person person = 1; }
message GameList { repeated Game games = 1; }
// Authentication messages // Authentication messages
message LoginRequest { message LoginRequest {
string username = 1; string username = 1;
@ -63,6 +65,7 @@ service AuthService {
} }
message GameRequest { string title = 1; } message GameRequest { string title = 1; }
message GetGamesRequest {}
message AddOpinionRequest { message AddOpinionRequest {
string game_title = 1; string game_title = 1;
@ -71,6 +74,7 @@ message AddOpinionRequest {
service MainService { service MainService {
rpc GetGame(GameRequest) returns (Game); rpc GetGame(GameRequest) returns (Game);
rpc GetGames(GetGamesRequest) returns (GameList);
rpc AddGame(Game) returns (Game); rpc AddGame(Game) returns (Game);
rpc AddOpinion(AddOpinionRequest) returns (Person); rpc AddOpinion(AddOpinionRequest) returns (Person);
} }