feat: implement game creation with a new frontend form, backend API endpoint, and protobuf definition.
This commit is contained in:
parent
434eef5e1c
commit
e192829fdd
@ -85,23 +85,51 @@ fn get_users(
|
|||||||
#[get("/game/<title>")]
|
#[get("/game/<title>")]
|
||||||
fn get_game(
|
fn get_game(
|
||||||
_token: auth::Token,
|
_token: auth::Token,
|
||||||
game_list: &rocket::State<Vec<Game>>,
|
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||||
title: &str,
|
title: &str,
|
||||||
) -> Option<items::Game> {
|
) -> 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>")]
|
#[post("/opinion", data = "<req>")]
|
||||||
fn add_opinion(
|
fn add_opinion(
|
||||||
token: auth::Token,
|
token: auth::Token,
|
||||||
|
|
||||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
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>,
|
req: proto_utils::Proto<items::AddOpinionRequest>,
|
||||||
) -> Option<items::Person> {
|
) -> Option<items::Person> {
|
||||||
let mut users = user_list.lock().unwrap();
|
let mut users = user_list.lock().unwrap();
|
||||||
|
let games = game_list.lock().unwrap();
|
||||||
let mut result = None;
|
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) {
|
if let Some(user) = users.iter_mut().find(|u| u.person.name == token.username) {
|
||||||
let req = req.into_inner();
|
let req = req.into_inner();
|
||||||
let opinion = items::Opinion {
|
let opinion = items::Opinion {
|
||||||
@ -123,7 +151,7 @@ fn add_opinion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
save_state(game_list, &users);
|
save_state(&games, &users);
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@ -172,8 +200,11 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
rocket::build()
|
rocket::build()
|
||||||
.manage(Mutex::new(user_list))
|
.manage(Mutex::new(user_list))
|
||||||
.manage(auth::AuthState::new())
|
.manage(auth::AuthState::new())
|
||||||
.manage(game_list)
|
.manage(Mutex::new(game_list))
|
||||||
.mount("/api", routes![get_users, get_user, get_game, add_opinion])
|
.mount(
|
||||||
|
"/api",
|
||||||
|
routes![get_users, get_user, get_game, add_opinion, add_game],
|
||||||
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/auth",
|
"/auth",
|
||||||
routes![auth::login, auth::logout, auth::get_auth_status],
|
routes![auth::login, auth::logout, auth::get_auth_status],
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Person, PersonList as PersonListProto } from "../items";
|
|||||||
import { Login } from "./Login";
|
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 { 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,14 +53,28 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2>
|
<h2>
|
||||||
<Link to="/" style={{ textDecoration: "none", color: "inherit" }}>
|
<Link
|
||||||
|
to="/"
|
||||||
|
style={{
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "inherit",
|
||||||
|
marginRight: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
People List
|
People List
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/games"
|
||||||
|
style={{ textDecoration: "none", color: "inherit" }}
|
||||||
|
>
|
||||||
|
Games
|
||||||
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<button onClick={handleLogout}>Logout</button>
|
<button onClick={handleLogout}>Logout</button>
|
||||||
</div>
|
</div>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<PersonList people={people} />} />
|
<Route path="/" element={<PersonList people={people} />} />
|
||||||
|
<Route path="/games" element={<GameList />} />
|
||||||
<Route path="/person/:name" element={<PersonDetails />} />
|
<Route path="/person/:name" element={<PersonDetails />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
126
frontend/src/GameList.tsx
Normal file
126
frontend/src/GameList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -71,5 +71,6 @@ message AddOpinionRequest {
|
|||||||
|
|
||||||
service MainService {
|
service MainService {
|
||||||
rpc GetGame(GameRequest) returns (Game);
|
rpc GetGame(GameRequest) returns (Game);
|
||||||
|
rpc AddGame(Game) returns (Game);
|
||||||
rpc AddOpinion(AddOpinionRequest) returns (Person);
|
rpc AddOpinion(AddOpinionRequest) returns (Person);
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user