This commit is contained in:
Mikan
2026-06-20 19:13:05 +03:00
parent 32575e217e
commit 8514c63ec6
193 changed files with 22105 additions and 11660 deletions

View File

@@ -1,102 +1,160 @@
import { useEffect } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { useAuthStore } from "@/store/auth";
import { useUiStore } from "@/store/ui";
import {
BrowserRouter,
Routes,
Route,
Navigate,
useLocation,
} from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useAuthStore } from "@/stores/authStore";
import { Navbar } from "@/components/ui/Navbar";
import { HomePage } from "@/pages/HomePage";
import { ToastViewport } from "@/components/ui/Toast";
import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
import { LoginPage } from "@/pages/LoginPage";
import { RegisterPage } from "@/pages/RegisterPage";
import { AdminSetupPage } from "@/pages/AdminSetupPage";
import { DashboardPage } from "@/pages/DashboardPage";
import { WorldCreatePage } from "@/pages/WorldCreatePage";
import { AdminRegisterPage } from "@/pages/AdminRegisterPage";
import { WorldsListPage } from "@/pages/WorldsListPage";
import { WorldBuilderPage } from "@/pages/WorldBuilderPage";
import { WorldEditPage } from "@/pages/WorldEditPage";
import { SessionPage } from "@/pages/SessionPage";
import { AdminPanelPage } from "@/pages/AdminPanelPage";
function PrivateRoute({ children }: { children: JSX.Element }) {
const token = useAuthStore((s) => s.token);
if (!token) return <Navigate to="/login" replace />;
return children;
}
function AdminRoute({ children }: { children: JSX.Element }) {
const { token, user } = useAuthStore();
if (!token) return <Navigate to="/login" replace />;
if (!user?.is_admin) return <Navigate to="/dashboard" replace />;
return children;
}
export default function App() {
// Load public UI settings (logo URL, etc.) once on app boot. These are
// unauthenticated and cached by the api layer, so subsequent navigations
// do not re-fetch. The admin panel calls load(true) after saving to
// pick up a new logo URL without a full page reload.
const loadUi = useUiStore((s) => s.load);
useEffect(() => {
loadUi();
}, [loadUi]);
import { PlayPage } from "@/pages/PlayPage";
import { AdminPage } from "@/pages/AdminPage";
function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex flex-col">
<div className="min-h-screen bg-bg text-fg">
<Navbar />
<main className="flex-1">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/admin/setup" element={<AdminSetupPage />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<DashboardPage />
</PrivateRoute>
}
/>
<Route
path="/worlds/new"
element={
<PrivateRoute>
<WorldCreatePage />
</PrivateRoute>
}
/>
<Route
path="/worlds/:id/edit"
element={
<PrivateRoute>
<WorldEditPage />
</PrivateRoute>
}
/>
<Route
path="/worlds/builder"
element={
<PrivateRoute>
<WorldBuilderPage />
</PrivateRoute>
}
/>
<Route
path="/sessions/:id"
element={
<PrivateRoute>
<SessionPage />
</PrivateRoute>
}
/>
<Route
path="/admin"
element={
<AdminRoute>
<AdminPanelPage />
</AdminRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</main>
{children}
<ToastViewport />
</div>
);
}
function RootRedirect() {
const status = useAuthStore((s) => s.status);
const user = useAuthStore((s) => s.user);
if (status === "authenticated" && user) return <Navigate to="/worlds" replace />;
return <Navigate to="/login" replace />;
}
function PublicOnly({ children }: { children: React.ReactNode }) {
const status = useAuthStore((s) => s.status);
const user = useAuthStore((s) => s.user);
const location = useLocation();
if (status === "authenticated" && user) {
const from = (location.state as { from?: string } | null)?.from;
return <Navigate to={from || "/worlds"} replace />;
}
return <>{children}</>;
}
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
export default function App() {
const { t } = useTranslation();
useEffect(() => {
document.title = t("common.app_name");
}, [t]);
return (
<BrowserRouter>
<ScrollToTop />
<Routes>
<Route path="/" element={<RootRedirect />} />
{/* Public auth routes — full layout but no navbar redirect */}
<Route
path="/login"
element={
<PublicOnly>
<Layout>
<LoginPage />
</Layout>
</PublicOnly>
}
/>
<Route
path="/register"
element={
<PublicOnly>
<Layout>
<RegisterPage />
</Layout>
</PublicOnly>
}
/>
<Route
path="/register/admin"
element={
<PublicOnly>
<Layout>
<AdminRegisterPage />
</Layout>
</PublicOnly>
}
/>
{/* Protected routes */}
<Route
path="/worlds"
element={
<ProtectedRoute>
<Layout>
<WorldsListPage />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/worlds/new"
element={
<ProtectedRoute>
<Layout>
<WorldBuilderPage />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/worlds/:id/edit"
element={
<ProtectedRoute>
<Layout>
<WorldEditPage />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/worlds/:id/play"
element={
<ProtectedRoute>
<Layout>
<PlayPage />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute requireAdmin>
<Layout>
<AdminPage />
</Layout>
</ProtectedRoute>
}
/>
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
);
}