177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Person, PersonList as PersonListProto } from "../items";
|
|
import { Login } from "./Login";
|
|
import { PersonList } from "./PersonList";
|
|
import { PersonDetails } from "./PersonDetails";
|
|
import { GameList } from "./GameList";
|
|
import { GameFilter } from "./GameFilter";
|
|
import { GameDetails } from "./GameDetails";
|
|
import { ShaderBackground } from "./ShaderBackground";
|
|
import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom";
|
|
import "./App.css";
|
|
import { apiFetch } from "./api";
|
|
import { Toast } from "./Toast";
|
|
import type { ToastType } from "./Toast";
|
|
|
|
interface ToastMessage {
|
|
id: number;
|
|
message: string;
|
|
type: ToastType;
|
|
}
|
|
|
|
function App() {
|
|
const [people, setPeople] = useState<Person[]>([]);
|
|
const [token, setToken] = useState<string>(
|
|
localStorage.getItem("token") || ""
|
|
);
|
|
const [theme, _setTheme] = useState<string>(
|
|
localStorage.getItem("theme") || "default"
|
|
);
|
|
const setTheme = (theme: string) => {
|
|
_setTheme(theme);
|
|
localStorage.setItem("theme", theme);
|
|
};
|
|
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (theme !== "default" && theme !== "sakura") {
|
|
document.body.classList.remove("sakura-theme");
|
|
document.body.classList.add("shader-theme");
|
|
if (theme === "clouds" || theme === "blackhole" || theme === "ball") {
|
|
document.body.classList.add("black-theme");
|
|
return;
|
|
}
|
|
document.body.classList.remove("black-theme");
|
|
} else {
|
|
document.body.classList.remove("shader-theme");
|
|
document.body.classList.remove("black-theme");
|
|
|
|
if (theme === "sakura") {
|
|
document.body.classList.add("sakura-theme");
|
|
return;
|
|
}
|
|
document.body.classList.remove("sakura-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 = () => {
|
|
if (!token) return;
|
|
setIsLoading(true);
|
|
|
|
apiFetch("/api")
|
|
.then((res) => res.arrayBuffer())
|
|
.then((buffer) => {
|
|
const list = PersonListProto.decode(new Uint8Array(buffer));
|
|
setPeople(list.person);
|
|
})
|
|
.catch((err) => {
|
|
console.error("Failed to fetch people:", err);
|
|
addToast("Failed to fetch people list", "error");
|
|
})
|
|
.finally(() => setIsLoading(false));
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchPeople();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [token]);
|
|
|
|
const handleLogin = (newToken: string) => {
|
|
setToken(newToken);
|
|
localStorage.setItem("token", newToken);
|
|
addToast("Welcome back!", "success");
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
setToken("");
|
|
setPeople([]);
|
|
localStorage.removeItem("token");
|
|
addToast("Logged out successfully", "info");
|
|
};
|
|
|
|
if (!token) {
|
|
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: "☁️" },
|
|
{ id: "sakura", label: "Sakura", icon: "🌸" },
|
|
];
|
|
|
|
return (
|
|
<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="navbar">
|
|
<div className="nav-links">
|
|
<NavLink to="/" className="nav-link">
|
|
People
|
|
</NavLink>
|
|
<NavLink to="/games" className="nav-link">
|
|
Games
|
|
</NavLink>
|
|
<NavLink to="/filter" className="nav-link">
|
|
Filter
|
|
</NavLink>
|
|
</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>
|
|
<button onClick={handleLogout} className="btn-secondary">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<ShaderBackground theme={theme} />
|
|
<Routes>
|
|
<Route path="/" element={<PersonList people={people} />} />
|
|
<Route path="/games" element={<GameList onShowToast={addToast} />} />
|
|
<Route path="/filter" element={<GameFilter />} />
|
|
<Route path="/person/:name" element={<PersonDetails />} />
|
|
<Route path="/game/:title" element={<GameDetails />} />
|
|
</Routes>
|
|
</div>
|
|
</BrowserRouter>
|
|
);
|
|
}
|
|
|
|
export default App;
|