diff --git a/backend/src/main.rs b/backend/src/main.rs index 1e641af..69fae17 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -85,23 +85,51 @@ fn get_users( #[get("/game/")] 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], diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d9a9b8..ae27d2b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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> diff --git a/frontend/src/GameList.tsx b/frontend/src/GameList.tsx new file mode 100644 index 0000000..a5bd135 --- /dev/null +++ b/frontend/src/GameList.tsx @@ -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> + ); +} diff --git a/protobuf/items.proto b/protobuf/items.proto index c4c68b7..49ffb1e 100644 --- a/protobuf/items.proto +++ b/protobuf/items.proto @@ -71,5 +71,6 @@ message AddOpinionRequest { service MainService { rpc GetGame(GameRequest) returns (Game); + rpc AddGame(Game) returns (Game); rpc AddOpinion(AddOpinionRequest) returns (Person); } \ No newline at end of file