feat: implement game creation with a new frontend form, backend API endpoint, and protobuf definition.

This commit is contained in:
code002lover 2025-12-03 18:50:52 +01:00
parent 434eef5e1c
commit e192829fdd
4 changed files with 181 additions and 8 deletions

View File

@ -85,23 +85,51 @@ fn get_users(
#[get("/game/<title>")]
fn get_game(
_token: auth::Token,
game_list: &rocket::State<Vec<Game>>,
game_list: &rocket::State<Mutex<Vec<Game>>>,
title: &str,
) -> Option<items::Game> {
game_list.iter().find(|g| g.title == title).cloned()
let games = game_list.lock().unwrap();
games.iter().find(|g| g.title == title).cloned()
}
#[post("/game", data = "<game>")]
fn add_game(
_token: auth::Token,
game_list: &rocket::State<Mutex<Vec<Game>>>,
user_list: &rocket::State<Mutex<Vec<User>>>,
game: proto_utils::Proto<items::Game>,
) -> Option<items::Game> {
let mut games = game_list.lock().unwrap();
let game = game.into_inner();
if games.iter().any(|g| g.title == game.title) {
return None;
}
games.push(game.clone());
let users = user_list.lock().unwrap();
save_state(&games, &users);
Some(game)
}
#[post("/opinion", data = "<req>")]
fn add_opinion(
token: auth::Token,
user_list: &rocket::State<Mutex<Vec<User>>>,
game_list: &rocket::State<Vec<Game>>,
game_list: &rocket::State<Mutex<Vec<Game>>>,
req: proto_utils::Proto<items::AddOpinionRequest>,
) -> Option<items::Person> {
let mut users = user_list.lock().unwrap();
let games = game_list.lock().unwrap();
let mut result = None;
// Validate game exists
if !games.iter().any(|g| g.title == req.game_title) {
return None;
}
if let Some(user) = users.iter_mut().find(|u| u.person.name == token.username) {
let req = req.into_inner();
let opinion = items::Opinion {
@ -123,7 +151,7 @@ fn add_opinion(
}
if result.is_some() {
save_state(game_list, &users);
save_state(&games, &users);
}
result
}
@ -172,8 +200,11 @@ async fn main() -> Result<(), std::io::Error> {
rocket::build()
.manage(Mutex::new(user_list))
.manage(auth::AuthState::new())
.manage(game_list)
.mount("/api", routes![get_users, get_user, get_game, add_opinion])
.manage(Mutex::new(game_list))
.mount(
"/api",
routes![get_users, get_user, get_game, add_opinion, add_game],
)
.mount(
"/auth",
routes![auth::login, auth::logout, auth::get_auth_status],

View File

@ -3,6 +3,7 @@ import { Person, PersonList as PersonListProto } from "../items";
import { Login } from "./Login";
import { PersonList } from "./PersonList";
import { PersonDetails } from "./PersonDetails";
import { GameList } from "./GameList";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import "./App.css";
import { apiFetch } from "./api";
@ -52,14 +53,28 @@ function App() {
}}
>
<h2>
<Link to="/" style={{ textDecoration: "none", color: "inherit" }}>
<Link
to="/"
style={{
textDecoration: "none",
color: "inherit",
marginRight: "1rem",
}}
>
People List
</Link>
<Link
to="/games"
style={{ textDecoration: "none", color: "inherit" }}
>
Games
</Link>
</h2>
<button onClick={handleLogout}>Logout</button>
</div>
<Routes>
<Route path="/" element={<PersonList people={people} />} />
<Route path="/games" element={<GameList />} />
<Route path="/person/:name" element={<PersonDetails />} />
</Routes>
</div>

126
frontend/src/GameList.tsx Normal file
View File

@ -0,0 +1,126 @@
import { useState } from "react";
import { Game, Source } from "../items";
import { apiFetch } from "./api";
export function GameList() {
const [title, setTitle] = useState("");
const [source, setSource] = useState<Source>(Source.STEAM);
const [multiplayer, setMultiplayer] = useState(false);
const [minPlayers, setMinPlayers] = useState(1);
const [maxPlayers, setMaxPlayers] = useState(1);
const [price, setPrice] = useState(0);
const [remoteId, setRemoteId] = useState(0);
const [message, setMessage] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const game = {
title,
source,
multiplayer,
minPlayers,
maxPlayers,
price,
remoteId,
};
try {
const encoded = Game.encode(game).finish();
const res = await apiFetch("/api/game", {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
},
body: encoded,
});
if (res.ok) {
setMessage("Game added successfully!");
setTitle("");
// Reset other fields if needed
} else {
setMessage("Failed to add game.");
}
} catch (err) {
console.error(err);
setMessage("Error adding game.");
}
};
return (
<div>
<h2>Add New Game</h2>
{message && <p>{message}</p>}
<form
onSubmit={handleSubmit}
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
maxWidth: "400px",
}}
>
<label>
Title:
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</label>
<label>
Source:
<select
value={source}
onChange={(e) => setSource(Number(e.target.value))}
>
<option value={Source.STEAM}>Steam</option>
<option value={Source.ROBLOX}>Roblox</option>
</select>
</label>
<label>
Multiplayer:
<input
type="checkbox"
checked={multiplayer}
onChange={(e) => setMultiplayer(e.target.checked)}
/>
</label>
<label>
Min Players:
<input
type="number"
value={minPlayers}
onChange={(e) => setMinPlayers(Number(e.target.value))}
/>
</label>
<label>
Max Players:
<input
type="number"
value={maxPlayers}
onChange={(e) => setMaxPlayers(Number(e.target.value))}
/>
</label>
<label>
Price:
<input
type="number"
value={price}
onChange={(e) => setPrice(Number(e.target.value))}
/>
</label>
<label>
Remote ID:
<input
type="number"
value={remoteId}
onChange={(e) => setRemoteId(Number(e.target.value))}
/>
</label>
<button type="submit">Add Game</button>
</form>
</div>
);
}

View File

@ -71,5 +71,6 @@ message AddOpinionRequest {
service MainService {
rpc GetGame(GameRequest) returns (Game);
rpc AddGame(Game) returns (Game);
rpc AddOpinion(AddOpinionRequest) returns (Person);
}