This commit is contained in:
code002lover 2026-01-12 14:27:01 +01:00
parent 8601d7ced1
commit 76281892a2
5 changed files with 42 additions and 16 deletions

View File

@ -3,6 +3,7 @@ use rocket::fs::FileServer;
use rocket::futures::lock::Mutex; use rocket::futures::lock::Mutex;
use backend::auth; use backend::auth;
use backend::auth::AdminState;
use backend::items::{self, Game}; use backend::items::{self, Game};
use backend::proto_utils; use backend::proto_utils;
use backend::store::{self, User, save_state}; use backend::store::{self, User, save_state};
@ -111,6 +112,7 @@ async fn update_game(
game: proto_utils::Proto<items::Game>, game: proto_utils::Proto<items::Game>,
) -> Option<items::Game> { ) -> Option<items::Game> {
let mut games = game_list.lock().await; let mut games = game_list.lock().await;
let mut users = user_list.lock().await;
let mut game = game.into_inner(); let mut game = game.into_inner();
game.title = game.title.trim().to_string(); game.title = game.title.trim().to_string();
@ -125,13 +127,14 @@ async fn update_game(
(g.remote_id == game.remote_id && g.source == game.source) || (g.title == game.title) (g.remote_id == game.remote_id && g.source == game.source) || (g.title == game.title)
}) { }) {
if existing.title != game.title { if existing.title != game.title {
let old_title = existing.title.clone();
// Update title for every opinion // Update title for every opinion
for person in user_list.lock().await.iter_mut() { for person in users.iter_mut() {
let opinion = person let opinion = person
.person .person
.opinion .opinion
.iter_mut() .iter_mut()
.find(|o| o.title == existing.title); .find(|o| o.title == old_title);
if let Some(opinion) = opinion { if let Some(opinion) = opinion {
opinion.title = game.title.clone(); opinion.title = game.title.clone();
} }
@ -149,7 +152,6 @@ async fn update_game(
games.sort_unstable_by(|g1, g2| g1.title.cmp(&g2.title)); games.sort_unstable_by(|g1, g2| g1.title.cmp(&g2.title));
let users = user_list.lock().await;
save_state(&games, &users); save_state(&games, &users);
r_existing r_existing
@ -189,13 +191,16 @@ async fn refresh_state(
_token: auth::AdminToken, _token: auth::AdminToken,
game_list: &rocket::State<Mutex<Vec<Game>>>, game_list: &rocket::State<Mutex<Vec<Game>>>,
user_list: &rocket::State<Mutex<Vec<User>>>, user_list: &rocket::State<Mutex<Vec<User>>>,
admin_state: &rocket::State<AdminState>,
) -> items::RefreshResponse { ) -> items::RefreshResponse {
if let Some((new_games, new_users)) = store::load_state() { if let Some((new_games, new_users)) = store::load_state() {
let mut games = game_list.lock().await; let mut games = game_list.lock().await;
let mut users = user_list.lock().await; let mut users = user_list.lock().await;
let mut admins = admin_state.admins.lock().await;
*games = new_games; *games = new_games;
*users = new_users; *users = new_users;
*admins = store::load_admins();
items::RefreshResponse { items::RefreshResponse {
success: true, success: true,
@ -212,12 +217,12 @@ async fn refresh_state(
#[post("/opinion", data = "<req>")] #[post("/opinion", data = "<req>")]
async fn add_opinion( async fn add_opinion(
token: auth::Token, token: auth::Token,
user_list: &rocket::State<Mutex<Vec<User>>>,
game_list: &rocket::State<Mutex<Vec<Game>>>, game_list: &rocket::State<Mutex<Vec<Game>>>,
user_list: &rocket::State<Mutex<Vec<User>>>,
req: proto_utils::Proto<items::AddOpinionRequest>, req: proto_utils::Proto<items::AddOpinionRequest>,
) -> Option<items::Person> { ) -> Option<items::Person> {
let mut users = user_list.lock().await;
let games = game_list.lock().await; let games = game_list.lock().await;
let mut users = user_list.lock().await;
let mut result = None; let mut result = None;
// Validate game exists // Validate game exists
@ -261,12 +266,12 @@ async fn add_opinion(
#[patch("/opinion", data = "<req>")] #[patch("/opinion", data = "<req>")]
async fn remove_opinion( async fn remove_opinion(
token: auth::Token, token: auth::Token,
user_list: &rocket::State<Mutex<Vec<User>>>,
game_list: &rocket::State<Mutex<Vec<Game>>>, game_list: &rocket::State<Mutex<Vec<Game>>>,
user_list: &rocket::State<Mutex<Vec<User>>>,
req: proto_utils::Proto<items::RemoveOpinionRequest>, req: proto_utils::Proto<items::RemoveOpinionRequest>,
) -> Option<items::Person> { ) -> Option<items::Person> {
let mut users = user_list.lock().await;
let games = game_list.lock().await; let games = game_list.lock().await;
let mut users = user_list.lock().await;
let mut result = None; let mut result = None;
if let Some(user) = users if let Some(user) = users
@ -340,9 +345,13 @@ async fn get_game_thumbnail(
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await .await
.ok()? .ok()?
.get("universeId")? .get("universeId")
.as_u64() .and_then(|v| v.as_u64())
.unwrap() };
let universe_id = match universe_id {
Some(id) => id,
None => return None.into(),
}; };
let api_url = format!( let api_url = format!(

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { Person, PersonList as PersonListProto } from "../items"; import { Person, PersonList as PersonListProto } from "../items";
import { Login } from "./Login"; import { Login } from "./Login";
import { PersonList } from "./PersonList"; import { PersonList } from "./PersonList";
@ -62,10 +62,21 @@ function App() {
} }
}, [theme]); }, [theme]);
const addToast = (message: string, type: ToastType = "info") => { useEffect(() => {
const handleUnauthorized = () => {
setToken("");
setPeople([]);
addToast("Session expired. Please log in again.", "info");
};
window.addEventListener("unauthorized", handleUnauthorized);
return () => window.removeEventListener("unauthorized", handleUnauthorized);
}, [addToast]);
const addToast = useCallback((message: string, type: ToastType = "info") => {
const id = Date.now(); const id = Date.now();
setToasts((prev) => [...prev, { id, message, type }]); setToasts((prev) => [...prev, { id, message, type }]);
}; }, []);
const removeToast = (id: number) => { const removeToast = (id: number) => {
setToasts((prev) => prev.filter((t) => t.id !== id)); setToasts((prev) => prev.filter((t) => t.id !== id));

View File

@ -67,7 +67,7 @@ export function GameDetails({ onShowToast }: Props) {
}; };
if (loading) return <LoadingState message="Loading game details..." />; if (loading) return <LoadingState message="Loading game details..." />;
if (error) return <ErrorState message={error} onRetry={() => window.location.reload()} />; if (error) return <ErrorState message={error} onRetry={() => navigate(0)} />;
if (!game) return <EmptyState icon="🎮" title="Game not found" description="This game doesn't exist or has been deleted" />; if (!game) return <EmptyState icon="🎮" title="Game not found" description="This game doesn't exist or has been deleted" />;
const getExternalLink = () => { const getExternalLink = () => {

View File

@ -22,7 +22,7 @@ export const apiFetch = async (
if (response.status == 401) { if (response.status == 401) {
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem("isAdmin"); localStorage.removeItem("isAdmin");
window.location.href = "/"; window.dispatchEvent(new CustomEvent("unauthorized"));
} }
throw new Error(`Request failed with status ${response.status}`); throw new Error(`Request failed with status ${response.status}`);
} }

View File

@ -17,6 +17,12 @@ export function useGameFilter(
const [fetchedTitles, setFetchedTitles] = useState<string[]>([]); const [fetchedTitles, setFetchedTitles] = useState<string[]>([]);
const metaDataRef = useRef<{ [key: string]: GameProto }>({}); const metaDataRef = useRef<{ [key: string]: GameProto }>({});
useEffect(() => {
return () => {
metaDataRef.current = {};
};
}, []);
const { gameToNegative, gameToPositiveOpinion } = useMemo(() => { const { gameToNegative, gameToPositiveOpinion } = useMemo(() => {
const gameToNegative = new Map<string, Set<string>>(); const gameToNegative = new Map<string, Set<string>>();
const gameToPositiveOpinion = new Map<string, Set<string>>(); const gameToPositiveOpinion = new Map<string, Set<string>>();
@ -108,7 +114,7 @@ export function useGameFilter(
const gamesMap = useMemo(() => { const gamesMap = useMemo(() => {
return new Map(Object.entries(metaDataRef.current)); return new Map(Object.entries(metaDataRef.current));
}, [fetchedTitles]); }, []);
return { filteredGames, gameToPositive: gameToPositiveOpinion, games: gamesMap }; return { filteredGames, gameToPositive: gameToPositiveOpinion, games: gamesMap };
} }