Compare commits
1 Commits
3bfeb110dc
...
3d7bf67408
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d7bf67408 |
@ -44,19 +44,6 @@ async fn get_game(
|
|||||||
games.iter().find(|g| g.title == title).cloned()
|
games.iter().find(|g| g.title == title).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/games/batch", data = "<req>")]
|
|
||||||
async fn get_games_batch(
|
|
||||||
_token: auth::Token,
|
|
||||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
|
||||||
req: proto_utils::Proto<items::GetGameInfoRequest>,
|
|
||||||
) -> items::GameList {
|
|
||||||
let games = game_list.lock().await;
|
|
||||||
let req = req.into_inner();
|
|
||||||
let mut games = games.clone();
|
|
||||||
games.retain(|g| req.games.contains(&g.title));
|
|
||||||
items::GameList { games }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/games")]
|
#[get("/games")]
|
||||||
async fn get_games(
|
async fn get_games(
|
||||||
_token: auth::Token,
|
_token: auth::Token,
|
||||||
@ -332,8 +319,7 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
add_opinion,
|
add_opinion,
|
||||||
remove_opinion,
|
remove_opinion,
|
||||||
add_game,
|
add_game,
|
||||||
get_game_thumbnail,
|
get_game_thumbnail
|
||||||
get_games_batch
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
|
|||||||
@ -116,14 +116,6 @@ export interface RemoveOpinionRequest {
|
|||||||
gameTitle: string;
|
gameTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetGameInfoRequest {
|
|
||||||
games: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameInfoResponse {
|
|
||||||
games: Game[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBasePerson(): Person {
|
function createBasePerson(): Person {
|
||||||
return { name: "", opinion: [] };
|
return { name: "", opinion: [] };
|
||||||
}
|
}
|
||||||
@ -1221,122 +1213,6 @@ export const RemoveOpinionRequest: MessageFns<RemoveOpinionRequest> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createBaseGetGameInfoRequest(): GetGameInfoRequest {
|
|
||||||
return { games: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GetGameInfoRequest: MessageFns<GetGameInfoRequest> = {
|
|
||||||
encode(message: GetGameInfoRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
for (const v of message.games) {
|
|
||||||
writer.uint32(10).string(v!);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): GetGameInfoRequest {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
const end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseGetGameInfoRequest();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.games.push(reader.string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): GetGameInfoRequest {
|
|
||||||
return { games: globalThis.Array.isArray(object?.games) ? object.games.map((e: any) => globalThis.String(e)) : [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: GetGameInfoRequest): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.games?.length) {
|
|
||||||
obj.games = message.games;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<GetGameInfoRequest>, I>>(base?: I): GetGameInfoRequest {
|
|
||||||
return GetGameInfoRequest.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<GetGameInfoRequest>, I>>(object: I): GetGameInfoRequest {
|
|
||||||
const message = createBaseGetGameInfoRequest();
|
|
||||||
message.games = object.games?.map((e) => e) || [];
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseGameInfoResponse(): GameInfoResponse {
|
|
||||||
return { games: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GameInfoResponse: MessageFns<GameInfoResponse> = {
|
|
||||||
encode(message: GameInfoResponse, 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): GameInfoResponse {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
const end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseGameInfoResponse();
|
|
||||||
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): GameInfoResponse {
|
|
||||||
return { games: globalThis.Array.isArray(object?.games) ? object.games.map((e: any) => Game.fromJSON(e)) : [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: GameInfoResponse): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.games?.length) {
|
|
||||||
obj.games = message.games.map((e) => Game.toJSON(e));
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<GameInfoResponse>, I>>(base?: I): GameInfoResponse {
|
|
||||||
return GameInfoResponse.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<GameInfoResponse>, I>>(object: I): GameInfoResponse {
|
|
||||||
const message = createBaseGameInfoResponse();
|
|
||||||
message.games = object.games?.map((e) => Game.fromPartial(e)) || [];
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Authentication service */
|
/** Authentication service */
|
||||||
export interface AuthService {
|
export interface AuthService {
|
||||||
Login(request: LoginRequest): Promise<LoginResponse>;
|
Login(request: LoginRequest): Promise<LoginResponse>;
|
||||||
@ -1379,7 +1255,6 @@ export interface MainService {
|
|||||||
GetGames(request: GetGamesRequest): Promise<GameList>;
|
GetGames(request: GetGamesRequest): Promise<GameList>;
|
||||||
AddGame(request: Game): Promise<Game>;
|
AddGame(request: Game): Promise<Game>;
|
||||||
AddOpinion(request: AddOpinionRequest): Promise<Person>;
|
AddOpinion(request: AddOpinionRequest): Promise<Person>;
|
||||||
GetGameInfo(request: GetGameInfoRequest): Promise<GameInfoResponse>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MainServiceServiceName = "items.MainService";
|
export const MainServiceServiceName = "items.MainService";
|
||||||
@ -1393,7 +1268,6 @@ export class MainServiceClientImpl implements MainService {
|
|||||||
this.GetGames = this.GetGames.bind(this);
|
this.GetGames = this.GetGames.bind(this);
|
||||||
this.AddGame = this.AddGame.bind(this);
|
this.AddGame = this.AddGame.bind(this);
|
||||||
this.AddOpinion = this.AddOpinion.bind(this);
|
this.AddOpinion = this.AddOpinion.bind(this);
|
||||||
this.GetGameInfo = this.GetGameInfo.bind(this);
|
|
||||||
}
|
}
|
||||||
GetGame(request: GameRequest): Promise<Game> {
|
GetGame(request: GameRequest): Promise<Game> {
|
||||||
const data = GameRequest.encode(request).finish();
|
const data = GameRequest.encode(request).finish();
|
||||||
@ -1418,12 +1292,6 @@ export class MainServiceClientImpl implements MainService {
|
|||||||
const promise = this.rpc.request(this.service, "AddOpinion", data);
|
const promise = this.rpc.request(this.service, "AddOpinion", data);
|
||||||
return promise.then((data) => Person.decode(new BinaryReader(data)));
|
return promise.then((data) => Person.decode(new BinaryReader(data)));
|
||||||
}
|
}
|
||||||
|
|
||||||
GetGameInfo(request: GetGameInfoRequest): Promise<GameInfoResponse> {
|
|
||||||
const data = GetGameInfoRequest.encode(request).finish();
|
|
||||||
const promise = this.rpc.request(this.service, "GetGameInfo", data);
|
|
||||||
return promise.then((data) => GameInfoResponse.decode(new BinaryReader(data)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Rpc {
|
interface Rpc {
|
||||||
|
|||||||
@ -34,108 +34,47 @@
|
|||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link.active {
|
.nav-link:hover, .nav-link.active {
|
||||||
color: var(--text-color);
|
color: var(--accent-color);
|
||||||
border-bottom: 2px solid var(--accent-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast Styles */
|
.form-group {
|
||||||
.toast-container {
|
|
||||||
position: fixed;
|
|
||||||
top: 2rem;
|
|
||||||
right: 2rem;
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.form-group label {
|
||||||
display: flex;
|
font-size: 0.9rem;
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: var(--secondary-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
||||||
min-width: 300px;
|
|
||||||
animation: slideInRight 0.3s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-success { border-left: 4px solid #4caf50; }
|
|
||||||
.toast-error { border-left: 4px solid #f44336; }
|
|
||||||
.toast-info { border-left: 4px solid var(--accent-color); }
|
|
||||||
|
|
||||||
.toast-icon { font-size: 1.2rem; }
|
|
||||||
.toast-message { flex: 1; font-weight: 500; }
|
|
||||||
.toast-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInRight {
|
.btn-secondary {
|
||||||
from { transform: translateX(100%); opacity: 0; }
|
background-color: var(--secondary-alt-bg);
|
||||||
to { transform: translateX(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Bar */
|
|
||||||
.loading-bar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: linear-gradient(90deg, var(--accent-color), #4da3ff);
|
|
||||||
z-index: 2000;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme Switcher */
|
|
||||||
.theme-switcher {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
background: var(--secondary-alt-bg);
|
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-btn:not(.game-btn) {
|
.btn-secondary:hover {
|
||||||
width: 32px;
|
background-color: var(--border-color);
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-btn:hover { transform: scale(1.1); }
|
.list-item {
|
||||||
.theme-btn.active { border-color: var(--text-color); }
|
|
||||||
|
|
||||||
.theme-default { background: #23283d; }
|
|
||||||
.theme-blackhole { background: #000000; }
|
|
||||||
.theme-star { background: #0a0a2a; }
|
|
||||||
.theme-ball { background: #1a1a1a; }
|
|
||||||
.theme-reflect { background: #333333; }
|
|
||||||
.theme-clouds { background: #23283d; }
|
|
||||||
|
|
||||||
.game-entry {
|
|
||||||
gap: 0.5rem;
|
|
||||||
background-color: var(--secondary-alt-bg);
|
background-color: var(--secondary-alt-bg);
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
transition: transform 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-entry:hover {
|
.list-item:hover {
|
||||||
background-color: var(--primary-bg);
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,17 +7,9 @@ import { GameList } from "./GameList";
|
|||||||
import { GameFilter } from "./GameFilter";
|
import { GameFilter } from "./GameFilter";
|
||||||
import { GameDetails } from "./GameDetails";
|
import { GameDetails } from "./GameDetails";
|
||||||
import { ShaderBackground } from "./ShaderBackground";
|
import { ShaderBackground } from "./ShaderBackground";
|
||||||
import { BrowserRouter, Routes, Route, NavLink } 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";
|
||||||
import { Toast } from "./Toast";
|
|
||||||
import type { ToastType } from "./Toast";
|
|
||||||
|
|
||||||
interface ToastMessage {
|
|
||||||
id: number;
|
|
||||||
message: string;
|
|
||||||
type: ToastType;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [people, setPeople] = useState<Person[]>([]);
|
const [people, setPeople] = useState<Person[]>([]);
|
||||||
@ -25,8 +17,6 @@ function App() {
|
|||||||
localStorage.getItem("token") || ""
|
localStorage.getItem("token") || ""
|
||||||
);
|
);
|
||||||
const [theme, setTheme] = useState<string>("default");
|
const [theme, setTheme] = useState<string>("default");
|
||||||
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (theme !== "default") {
|
if (theme !== "default") {
|
||||||
@ -36,18 +26,8 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
const addToast = (message: string, type: ToastType = "info") => {
|
|
||||||
const id = Date.now();
|
|
||||||
setToasts((prev) => [...prev, { id, message, type }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeToast = (id: number) => {
|
|
||||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPeople = () => {
|
const fetchPeople = () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
apiFetch("/api")
|
apiFetch("/api")
|
||||||
.then((res) => res.arrayBuffer())
|
.then((res) => res.arrayBuffer())
|
||||||
@ -55,11 +35,7 @@ function App() {
|
|||||||
const list = PersonListProto.decode(new Uint8Array(buffer));
|
const list = PersonListProto.decode(new Uint8Array(buffer));
|
||||||
setPeople(list.person);
|
setPeople(list.person);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => console.error("Failed to fetch people:", err));
|
||||||
console.error("Failed to fetch people:", err);
|
|
||||||
addToast("Failed to fetch people list", "error");
|
|
||||||
})
|
|
||||||
.finally(() => setIsLoading(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -70,80 +46,49 @@ function App() {
|
|||||||
const handleLogin = (newToken: string) => {
|
const handleLogin = (newToken: string) => {
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
localStorage.setItem("token", newToken);
|
localStorage.setItem("token", newToken);
|
||||||
addToast("Welcome back!", "success");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
setToken("");
|
setToken("");
|
||||||
setPeople([]);
|
setPeople([]);
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
addToast("Logged out successfully", "info");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return <Login onLogin={handleLogin} />;
|
return <Login onLogin={handleLogin} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const themes = [
|
|
||||||
{ id: "default", label: "Default", icon: "🏠" },
|
|
||||||
{ id: "blackhole", label: "Blackhole", icon: "🕳️" },
|
|
||||||
{ id: "star", label: "Star", icon: "⭐" },
|
|
||||||
{ id: "ball", label: "Ball", icon: "⚽" },
|
|
||||||
{ id: "reflect", label: "Reflect", icon: "🪞" },
|
|
||||||
{ id: "clouds", label: "Clouds", icon: "☁️" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
{isLoading && <div className="loading-bar" style={{ width: "50%" }} />}
|
|
||||||
<div className="toast-container">
|
|
||||||
{toasts.map((toast) => (
|
|
||||||
<Toast
|
|
||||||
key={toast.id}
|
|
||||||
message={toast.message}
|
|
||||||
type={toast.type}
|
|
||||||
onClose={() => removeToast(toast.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="navbar">
|
<div className="navbar">
|
||||||
<div className="nav-links">
|
<div className="nav-links">
|
||||||
<NavLink to="/" className="nav-link">
|
<Link to="/" className="nav-link">
|
||||||
People
|
People List
|
||||||
</NavLink>
|
</Link>
|
||||||
<NavLink to="/games" className="nav-link">
|
<Link to="/games" className="nav-link">
|
||||||
Games
|
Games
|
||||||
</NavLink>
|
</Link>
|
||||||
<NavLink to="/filter" className="nav-link">
|
<Link to="/filter" className="nav-link">
|
||||||
Filter
|
Filter
|
||||||
</NavLink>
|
</Link>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "1.5rem" }}>
|
|
||||||
<div className="theme-switcher">
|
|
||||||
{themes.map((t) => (
|
|
||||||
<button
|
|
||||||
key={t.id}
|
|
||||||
className={`theme-btn theme-${t.id} ${
|
|
||||||
theme === t.id ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setTheme(t.id)}
|
|
||||||
title={t.label}
|
|
||||||
>
|
|
||||||
{t.icon}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleLogout} className="btn-secondary">
|
<button onClick={handleLogout} className="btn-secondary">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
|
||||||
|
<option value="default">Default Theme</option>
|
||||||
|
<option value="blackhole">Blackhole Theme</option>
|
||||||
|
<option value="star">Star Theme</option>
|
||||||
|
<option value="ball">Universe Ball Theme</option>
|
||||||
|
<option value="reflect">Ball Cage Theme</option>
|
||||||
|
<option value="clouds">Clouds Theme</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ShaderBackground theme= {theme} />
|
||||||
<ShaderBackground theme={theme} />
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<PersonList people={people} />} />
|
<Route path="/" element={<PersonList people={people} />} />
|
||||||
<Route path="/games" element={<GameList onShowToast={addToast} />} />
|
<Route path="/games" element={<GameList />} />
|
||||||
<Route path="/filter" element={<GameFilter />} />
|
<Route path="/filter" element={<GameFilter />} />
|
||||||
<Route path="/person/:name" element={<PersonDetails />} />
|
<Route path="/person/:name" element={<PersonDetails />} />
|
||||||
<Route path="/game/:title" element={<GameDetails />} />
|
<Route path="/game/:title" element={<GameDetails />} />
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
.gamefilter-entry {
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background-color: var(--secondary-alt-bg);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
width: 30%;
|
|
||||||
text-align: center;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gamefilter-entry:hover {
|
|
||||||
background-color: var(--primary-bg);
|
|
||||||
}
|
|
||||||
@ -3,13 +3,10 @@ import {
|
|||||||
Person,
|
Person,
|
||||||
PersonList as PersonListProto,
|
PersonList as PersonListProto,
|
||||||
Game as GameProto,
|
Game as GameProto,
|
||||||
GetGameInfoRequest,
|
|
||||||
GameInfoResponse,
|
|
||||||
} from "../items";
|
} from "../items";
|
||||||
import { apiFetch } from "./api";
|
import { apiFetch } from "./api";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { GameImage } from "./GameImage";
|
import { GameImage } from "./GameImage";
|
||||||
import "./GameFilter.css"
|
|
||||||
|
|
||||||
export function GameFilter() {
|
export function GameFilter() {
|
||||||
const [people, setPeople] = useState<Person[]>([]);
|
const [people, setPeople] = useState<Person[]>([]);
|
||||||
@ -74,29 +71,22 @@ export function GameFilter() {
|
|||||||
.filter(([, players]) => players.size === 0)
|
.filter(([, players]) => players.size === 0)
|
||||||
.map(([game]) => game);
|
.map(([game]) => game);
|
||||||
|
|
||||||
let games = game_titles.filter((title) => metaData[title]).map((title) => metaData[title]);
|
const games = game_titles.map(async (title) => {
|
||||||
const gamesToFetch = GetGameInfoRequest.encode(
|
if (metaData[title]) {
|
||||||
GetGameInfoRequest.create({
|
console.log("returned cached metadata");
|
||||||
games: game_titles.filter((title) => !metaData[title]),
|
return metaData[title];
|
||||||
})
|
}
|
||||||
).finish();
|
return await apiFetch(`/api/game/${encodeURIComponent(title)}`)
|
||||||
|
|
||||||
apiFetch("/api/games/batch", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
},
|
|
||||||
body: gamesToFetch,
|
|
||||||
})
|
|
||||||
.then((res) => res.arrayBuffer())
|
.then((res) => res.arrayBuffer())
|
||||||
.then((buffer) => {
|
.then((buffer) => {
|
||||||
const list = GameInfoResponse.decode(new Uint8Array(buffer));
|
const game = GameProto.decode(new Uint8Array(buffer)) as GameProto;
|
||||||
games = games.concat(list.games);
|
metaData[title] = game;
|
||||||
|
return game;
|
||||||
games.forEach((game) => {
|
})
|
||||||
metaData[game.title] = game;
|
.catch((err) => console.error("Failed to fetch game:", err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Promise.all(games).then((games) => {
|
||||||
const filteredGames = games.filter((g) => {
|
const filteredGames = games.filter((g) => {
|
||||||
const game = g as GameProto;
|
const game = g as GameProto;
|
||||||
return (
|
return (
|
||||||
@ -127,27 +117,28 @@ export function GameFilter() {
|
|||||||
|
|
||||||
<div style={{ marginBottom: "3rem" }}>
|
<div style={{ marginBottom: "3rem" }}>
|
||||||
<h3>Select People</h3>
|
<h3>Select People</h3>
|
||||||
<div
|
<div className="grid-container">
|
||||||
className="grid-container"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "1rem",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{people.map((person) => (
|
{people.map((person) => (
|
||||||
<div
|
<div
|
||||||
key={person.name}
|
key={person.name}
|
||||||
className="list-item gamefilter-entry"
|
className="list-item"
|
||||||
style={{
|
style={{
|
||||||
borderColor: selectedPeople.has(person.name)
|
borderColor: selectedPeople.has(person.name)
|
||||||
? "var(--accent-color)"
|
? "var(--accent-color)"
|
||||||
: "var(--border-color)",
|
: "var(--border-color)",
|
||||||
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => togglePerson(person.name)}
|
onClick={() => togglePerson(person.name)}
|
||||||
>
|
>
|
||||||
<div style={{ gap: "0.5rem" }}>
|
<div
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedPeople.has(person.name)}
|
||||||
|
onChange={() => togglePerson(person.name)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
/>
|
||||||
<strong>{person.name}</strong>
|
<strong>{person.name}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -173,7 +164,7 @@ export function GameFilter() {
|
|||||||
<Link
|
<Link
|
||||||
to={`/game/${encodeURIComponent(game)}`}
|
to={`/game/${encodeURIComponent(game)}`}
|
||||||
key={game}
|
key={game}
|
||||||
className="list-item game-entry"
|
className="list-item"
|
||||||
style={{
|
style={{
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
color: "inherit",
|
color: "inherit",
|
||||||
@ -191,8 +182,7 @@ export function GameFilter() {
|
|||||||
marginTop: "0.5rem",
|
marginTop: "0.5rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>✓</span> {gameToPositive.get(game)!.size} selected
|
✓ {gameToPositive.get(game)!.size} selected would play
|
||||||
would play
|
|
||||||
</div>
|
</div>
|
||||||
{selectedPeople.size - gameToPositive.get(game)!.size >
|
{selectedPeople.size - gameToPositive.get(game)!.size >
|
||||||
0 && (
|
0 && (
|
||||||
@ -203,13 +193,8 @@ export function GameFilter() {
|
|||||||
marginTop: "0.3rem",
|
marginTop: "0.3rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>?</span>{" "}
|
? {selectedPeople.size - gameToPositive.get(game)!.size}{" "}
|
||||||
{selectedPeople.size - gameToPositive.get(game)!.size}{" "}
|
{(selectedPeople.size - gameToPositive.get(game)!.size) > 1 ? "are" : "is"} neutral
|
||||||
{selectedPeople.size - gameToPositive.get(game)!.size >
|
|
||||||
1
|
|
||||||
? "are"
|
|
||||||
: "is"}{" "}
|
|
||||||
neutral
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -218,22 +203,9 @@ export function GameFilter() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<p style={{ color: "var(--text-muted)", fontStyle: "italic" }}>
|
||||||
style={{
|
No games found where all selected people would play
|
||||||
padding: "3rem",
|
|
||||||
textAlign: "center",
|
|
||||||
background: "var(--secondary-alt-bg)",
|
|
||||||
borderRadius: "16px",
|
|
||||||
border: "1px dashed var(--border-color)",
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🔍</div>
|
|
||||||
<p>No games found where all selected people would play.</p>
|
|
||||||
<p style={{ fontSize: "0.9rem" }}>
|
|
||||||
Try selecting fewer people or adding more opinions!
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,13 +11,8 @@ import {
|
|||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { apiFetch, get_auth_status } from "./api";
|
import { apiFetch, get_auth_status } from "./api";
|
||||||
import { GameImage } from "./GameImage";
|
import { GameImage } from "./GameImage";
|
||||||
import type { ToastType } from "./Toast";
|
|
||||||
|
|
||||||
interface Props {
|
export function GameList() {
|
||||||
onShowToast?: (message: string, type?: ToastType) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GameList({ onShowToast }: Props) {
|
|
||||||
const [games, setGames] = useState<Game[]>([]);
|
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);
|
||||||
@ -25,6 +20,7 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
const [maxPlayers, setMaxPlayers] = useState(1);
|
const [maxPlayers, setMaxPlayers] = useState(1);
|
||||||
const [price, setPrice] = useState(0);
|
const [price, setPrice] = useState(0);
|
||||||
const [remoteId, setRemoteId] = useState(0);
|
const [remoteId, setRemoteId] = useState(0);
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [opinions, setOpinions] = useState<Opinion[]>([]);
|
const [opinions, setOpinions] = useState<Opinion[]>([]);
|
||||||
|
|
||||||
@ -100,19 +96,22 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
onShowToast?.("Game added successfully!", "success");
|
setMessage("success");
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setMinPlayers(1);
|
setMinPlayers(1);
|
||||||
setMaxPlayers(1);
|
setMaxPlayers(1);
|
||||||
setPrice(0);
|
setPrice(0);
|
||||||
setRemoteId(0);
|
setRemoteId(0);
|
||||||
fetchGames();
|
fetchGames();
|
||||||
|
setTimeout(() => setMessage(""), 3000);
|
||||||
} else {
|
} else {
|
||||||
onShowToast?.("Failed to add game. Please try again.", "error");
|
setMessage("error");
|
||||||
|
setTimeout(() => setMessage(""), 3000);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
onShowToast?.("An error occurred while adding the game.", "error");
|
setMessage("error");
|
||||||
|
setTimeout(() => setMessage(""), 3000);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@ -129,8 +128,7 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formHeaderStyles: React.CSSProperties = {
|
const formHeaderStyles: React.CSSProperties = {
|
||||||
background:
|
background: "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)",
|
||||||
"linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)",
|
|
||||||
padding: "1.5rem 2rem",
|
padding: "1.5rem 2rem",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -197,8 +195,7 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
const submitButtonStyles: React.CSSProperties = {
|
const submitButtonStyles: React.CSSProperties = {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: "1rem",
|
padding: "1rem",
|
||||||
background:
|
background: "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)",
|
||||||
"linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)",
|
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
color: "white",
|
color: "white",
|
||||||
@ -214,6 +211,26 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
transform: isSubmitting ? "none" : undefined,
|
transform: isSubmitting ? "none" : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const messageStyles: React.CSSProperties = {
|
||||||
|
padding: "1rem",
|
||||||
|
borderRadius: "12px",
|
||||||
|
marginBottom: "1.5rem",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.75rem",
|
||||||
|
animation: "slideIn 0.3s ease",
|
||||||
|
backgroundColor:
|
||||||
|
message === "success"
|
||||||
|
? "rgba(76, 175, 80, 0.15)"
|
||||||
|
: "rgba(244, 67, 54, 0.15)",
|
||||||
|
border: `1px solid ${
|
||||||
|
message === "success"
|
||||||
|
? "rgba(76, 175, 80, 0.3)"
|
||||||
|
: "rgba(244, 67, 54, 0.3)"
|
||||||
|
}`,
|
||||||
|
color: message === "success" ? "#4caf50" : "#f44336",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<style>
|
<style>
|
||||||
@ -286,6 +303,19 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={formBodyStyles}>
|
<div style={formBodyStyles}>
|
||||||
|
{message && (
|
||||||
|
<div style={messageStyles}>
|
||||||
|
<span style={{ fontSize: "1.2rem" }}>
|
||||||
|
{message === "success" ? "✓" : "✕"}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontWeight: 500 }}>
|
||||||
|
{message === "success"
|
||||||
|
? "Game added successfully!"
|
||||||
|
: "Failed to add game. Please try again."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* Basic Info Section */}
|
{/* Basic Info Section */}
|
||||||
<div style={sectionStyles}>
|
<div style={sectionStyles}>
|
||||||
@ -459,7 +489,7 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
},
|
},
|
||||||
body: RemoveOpinionRequest.encode(
|
body: RemoveOpinionRequest.encode(
|
||||||
RemoveOpinionRequest.create({
|
AddOpinionRequest.create({
|
||||||
gameTitle: title,
|
gameTitle: title,
|
||||||
})
|
})
|
||||||
).finish(),
|
).finish(),
|
||||||
@ -471,11 +501,9 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setOpinions(response.opinion);
|
setOpinions(response.opinion);
|
||||||
onShowToast?.(`Updated opinion for ${title}`, "info");
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
onShowToast?.("Failed to update opinion", "error");
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -499,11 +527,9 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setOpinions(response.opinion);
|
setOpinions(response.opinion);
|
||||||
onShowToast?.(`Updated opinion for ${title}`, "info");
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
onShowToast?.("Failed to update opinion", "error");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -530,7 +556,7 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
? opinion.wouldPlay
|
? opinion.wouldPlay
|
||||||
? "#4caf50" // would play (green)
|
? "#4caf50" // would play (green)
|
||||||
: "#f44336" // would not play (red)
|
: "#f44336" // would not play (red)
|
||||||
: "#ffff00", // no opinion (yellow)
|
: "#191f2e", // no opinion (bg-2)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong
|
<strong
|
||||||
@ -539,7 +565,7 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
? opinion.wouldPlay
|
? opinion.wouldPlay
|
||||||
? "0 0 10px #4caf50" // would play (green)
|
? "0 0 10px #4caf50" // would play (green)
|
||||||
: "0 0 10px #f44336" // would not play (red)
|
: "0 0 10px #f44336" // would not play (red)
|
||||||
: "0 0 10px #ffff00", // no opinion (yellow)
|
: "none", // no opinion (bg-2)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{game.title}
|
{game.title}
|
||||||
@ -556,47 +582,30 @@ export function GameList({ onShowToast }: Props) {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpinion(game.title, 1)}
|
onClick={() => handleOpinion(game.title, 1)}
|
||||||
className="theme-btn game-btn"
|
|
||||||
style={{
|
style={{
|
||||||
width: "33%",
|
width: "50%",
|
||||||
borderColor: opinion?.wouldPlay
|
borderColor: "#4caf50",
|
||||||
? "#4caf50"
|
|
||||||
: "transparent",
|
|
||||||
background: "rgba(76, 175, 80, 0.1)",
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
}}
|
}}
|
||||||
title="Would Play"
|
|
||||||
>
|
>
|
||||||
👍
|
Would Play
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpinion(game.title, 2)}
|
onClick={() => handleOpinion(game.title, 2)}
|
||||||
className="theme-btn game-btn"
|
|
||||||
style={{
|
style={{
|
||||||
width: "33%",
|
width: "50%",
|
||||||
borderColor: !opinion ? "#ffff00" : "transparent",
|
borderColor: "#ffff00",
|
||||||
background: "rgba(255, 255, 0, 0.1)",
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
}}
|
}}
|
||||||
title="Neutral"
|
|
||||||
>
|
>
|
||||||
😐
|
Neutral
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpinion(game.title, 0)}
|
onClick={() => handleOpinion(game.title, 0)}
|
||||||
className="theme-btn game-btn"
|
|
||||||
style={{
|
style={{
|
||||||
width: "33%",
|
width: "50%",
|
||||||
borderColor:
|
borderColor: "#f44336",
|
||||||
opinion && !opinion.wouldPlay
|
|
||||||
? "#f44336"
|
|
||||||
: "transparent",
|
|
||||||
background: "rgba(244, 67, 54, 0.1)",
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
}}
|
}}
|
||||||
title="Would Not Play"
|
|
||||||
>
|
>
|
||||||
👎
|
Would Not Play
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
.list-item {
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background-color: var(--secondary-alt-bg);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-item:hover {
|
|
||||||
background-color: var(--primary-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ import { Person } from "../items";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { get_auth_status } from "./api";
|
import { get_auth_status } from "./api";
|
||||||
import "./PersonList.css"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
people: Person[];
|
people: Person[];
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
|
|
||||||
export type ToastType = "success" | "error" | "info";
|
|
||||||
|
|
||||||
interface ToastProps {
|
|
||||||
message: string;
|
|
||||||
type: ToastType;
|
|
||||||
onClose: () => void;
|
|
||||||
duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Toast: React.FC<ToastProps> = ({
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
onClose,
|
|
||||||
duration = 3000,
|
|
||||||
}) => {
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
onClose();
|
|
||||||
}, duration);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [onClose, duration]);
|
|
||||||
|
|
||||||
const getIcon = () => {
|
|
||||||
switch (type) {
|
|
||||||
case "success":
|
|
||||||
return "✓";
|
|
||||||
case "error":
|
|
||||||
return "✕";
|
|
||||||
default:
|
|
||||||
return "ℹ";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`toast toast-${type}`}>
|
|
||||||
<span className="toast-icon">{getIcon()}</span>
|
|
||||||
<span className="toast-message">{message}</span>
|
|
||||||
<button className="toast-close" onClick={onClose}>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -82,17 +82,11 @@ input, select {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus, select:focus {
|
input:focus, select:focus {
|
||||||
outline: none;
|
outline: 2px solid var(--accent-color);
|
||||||
border-color: var(--accent-color);
|
border-color: transparent;
|
||||||
box-shadow: 0 0 0 2px rgba(9, 109, 192, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,6 @@ enum Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message PersonList { repeated Person person = 1; }
|
message PersonList { repeated Person person = 1; }
|
||||||
|
|
||||||
message GameList { repeated Game games = 1; }
|
message GameList { repeated Game games = 1; }
|
||||||
|
|
||||||
// Authentication messages
|
// Authentication messages
|
||||||
@ -66,7 +65,6 @@ service AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message GameRequest { string title = 1; }
|
message GameRequest { string title = 1; }
|
||||||
|
|
||||||
message GetGamesRequest {}
|
message GetGamesRequest {}
|
||||||
|
|
||||||
message AddOpinionRequest {
|
message AddOpinionRequest {
|
||||||
@ -76,18 +74,9 @@ message AddOpinionRequest {
|
|||||||
|
|
||||||
message RemoveOpinionRequest { string game_title = 1; }
|
message RemoveOpinionRequest { string game_title = 1; }
|
||||||
|
|
||||||
message GetGameInfoRequest {
|
|
||||||
repeated string games = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GameInfoResponse {
|
|
||||||
repeated Game games = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
service MainService {
|
service MainService {
|
||||||
rpc GetGame(GameRequest) returns (Game);
|
rpc GetGame(GameRequest) returns (Game);
|
||||||
rpc GetGames(GetGamesRequest) returns (GameList);
|
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);
|
||||||
rpc GetGameInfo(GetGameInfoRequest) returns (GameInfoResponse);
|
|
||||||
}
|
}
|
||||||
1980
state.json
1980
state.json
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user