140 lines
6.7 KiB
TypeScript
140 lines
6.7 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { Navigate, NavLink, Route, Routes, useLocation } from "react-router-dom";
|
|
import { useAuth } from "./auth";
|
|
import { ChangeCredentials } from "./pages/ChangeCredentials";
|
|
import { GamePage } from "./pages/GamePage";
|
|
import { Library } from "./pages/Library";
|
|
import { Login } from "./pages/Login";
|
|
import { NewGame } from "./pages/NewGame";
|
|
import { ModEditor } from "./pages/ModEditor";
|
|
import { Play } from "./pages/Play";
|
|
import { Roles } from "./pages/Roles";
|
|
import { Saves } from "./pages/Saves";
|
|
import { TranslationEditor } from "./pages/TranslationEditor";
|
|
import { Users } from "./pages/Users";
|
|
import { useTheme } from "./theme";
|
|
import { Icon } from "./ui/Icon";
|
|
import { useSpatialNav } from "./ui/nav";
|
|
import { THEME_ICON, THEME_LABEL } from "./ui/ThemeButton";
|
|
import { UploadProvider } from "./uploads";
|
|
|
|
function RequirePerm({ perm, children }: { perm: string; children: JSX.Element }) {
|
|
const { can } = useAuth();
|
|
return can(perm) ? children : (
|
|
<div className="page"><p className="muted">Нет доступа к этой странице.</p></div>
|
|
);
|
|
}
|
|
|
|
function UserMenu() {
|
|
const { me, logout, can } = useAuth();
|
|
const { choice, cycle } = useTheme();
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const { pathname } = useLocation();
|
|
|
|
useEffect(() => setOpen(false), [pathname]);
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onDown = (e: MouseEvent) => !ref.current?.contains(e.target as Node) && setOpen(false);
|
|
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
|
|
document.addEventListener("mousedown", onDown);
|
|
document.addEventListener("keydown", onKey);
|
|
return () => {
|
|
document.removeEventListener("mousedown", onDown);
|
|
document.removeEventListener("keydown", onKey);
|
|
};
|
|
}, [open]);
|
|
|
|
if (!me) return null;
|
|
return (
|
|
<div className="menu" ref={ref}>
|
|
<button className="btn btn-ghost menu-trigger" onClick={() => setOpen((o) => !o)}
|
|
aria-haspopup="menu" aria-expanded={open} aria-label="Меню профиля">
|
|
<span className="avatar">{me.username[0]?.toUpperCase()}</span>
|
|
<span className="menu-label">{me.username}</span>
|
|
<Icon name="chevron" size={14} className="menu-chevron" style={{ transform: "rotate(90deg)" }} />
|
|
</button>
|
|
{open && (
|
|
<div className="menu-panel" role="menu">
|
|
<div className="menu-head">
|
|
<b>{me.username}</b>
|
|
<div className="muted small">{me.role.name}</div>
|
|
</div>
|
|
<NavLink to="/account" className="menu-item" role="menuitem"><Icon name="key" size={18} />Аккаунт и пароль</NavLink>
|
|
<NavLink to="/saves" className="menu-item" role="menuitem"><Icon name="download" size={18} />Сохранения</NavLink>
|
|
{(can("users.manage") || can("roles.manage")) && <div className="menu-section">Администрирование</div>}
|
|
{can("users.manage") && (
|
|
<NavLink to="/admin/users" className="menu-item" role="menuitem"><Icon name="users" size={18} />Пользователи</NavLink>
|
|
)}
|
|
{can("roles.manage") && (
|
|
<NavLink to="/admin/roles" className="menu-item" role="menuitem"><Icon name="shield" size={18} />Роли и права</NavLink>
|
|
)}
|
|
<div className="menu-sep" />
|
|
<button className="menu-item" role="menuitem" onClick={cycle}>
|
|
<Icon name={THEME_ICON[choice]} size={18} />{THEME_LABEL[choice]}
|
|
</button>
|
|
<button className="menu-item danger" role="menuitem" onClick={() => void logout()}>
|
|
<Icon name="logout" size={18} />Выйти
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function App() {
|
|
const { me, loading, can } = useAuth();
|
|
useSpatialNav();
|
|
if (loading) return <div className="center"><span className="spinner" /></div>;
|
|
if (!me) return <Login />;
|
|
if (me.must_change_password || me.must_change_username) return <ChangeCredentials forced />;
|
|
|
|
return (
|
|
<UploadProvider key={me.id}>
|
|
<Routes>
|
|
<Route path="/play/:versionId" element={<RequirePerm perm="games.play"><Play /></RequirePerm>} />
|
|
<Route
|
|
path="*"
|
|
element={
|
|
<div className="layout">
|
|
<header className="topbar">
|
|
<NavLink to="/" className="brand">
|
|
<span className="brand-mark"><Icon name="play" size={16} /></span>PGames
|
|
</NavLink>
|
|
<nav aria-label="Основная навигация">
|
|
<NavLink to="/" end className="nav-link"><Icon name="library" size={18} />Библиотека</NavLink>
|
|
{can("games.upload") && (
|
|
<NavLink to="/games/new" className="nav-link"><Icon name="upload" size={18} />Загрузить игру</NavLink>
|
|
)}
|
|
</nav>
|
|
<UserMenu />
|
|
</header>
|
|
<main>
|
|
<Routes>
|
|
<Route path="/" element={<RequirePerm perm="games.play"><Library /></RequirePerm>} />
|
|
<Route path="/games/new" element={<RequirePerm perm="games.upload"><NewGame /></RequirePerm>} />
|
|
<Route path="/games/:gameId" element={<RequirePerm perm="games.play"><GamePage /></RequirePerm>} />
|
|
<Route path="/versions/:versionId/translate" element={<RequirePerm perm="games.play"><TranslationEditor /></RequirePerm>} />
|
|
<Route path="/versions/:versionId/edit" element={<RequirePerm perm="games.play"><ModEditor /></RequirePerm>} />
|
|
<Route path="/admin/users" element={<RequirePerm perm="users.manage"><Users /></RequirePerm>} />
|
|
<Route path="/admin/roles" element={<RequirePerm perm="roles.manage"><Roles /></RequirePerm>} />
|
|
<Route path="/account" element={<ChangeCredentials />} />
|
|
<Route path="/saves" element={<Saves />} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</main>
|
|
<nav className="tabbar" aria-label="Нижняя навигация">
|
|
<NavLink to="/" end><Icon name="library" />Библиотека</NavLink>
|
|
{can("games.upload") && <NavLink to="/games/new"><Icon name="upload" />Загрузить</NavLink>}
|
|
{can("users.manage") && <NavLink to="/admin/users"><Icon name="users" />Люди</NavLink>}
|
|
{can("roles.manage") && <NavLink to="/admin/roles"><Icon name="shield" />Роли</NavLink>}
|
|
<NavLink to="/account"><Icon name="user" />Профиль</NavLink>
|
|
</nav>
|
|
</div>
|
|
}
|
|
/>
|
|
</Routes>
|
|
</UploadProvider>
|
|
);
|
|
}
|