2026-06-19 19:14:27 +03:00
|
|
|
import { useEffect } from "react";
|
2026-06-20 19:13:05 +03:00
|
|
|
import {
|
|
|
|
|
BrowserRouter,
|
|
|
|
|
Routes,
|
|
|
|
|
Route,
|
|
|
|
|
Navigate,
|
|
|
|
|
useLocation,
|
|
|
|
|
} from "react-router-dom";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { useAuthStore } from "@/stores/authStore";
|
2026-06-21 02:41:48 +03:00
|
|
|
import { useUiSettingsStore } from "@/stores/uiSettingsStore";
|
2026-06-19 11:28:04 +03:00
|
|
|
import { Navbar } from "@/components/ui/Navbar";
|
2026-06-20 19:13:05 +03:00
|
|
|
import { ToastViewport } from "@/components/ui/Toast";
|
|
|
|
|
import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
|
2026-06-19 11:28:04 +03:00
|
|
|
import { LoginPage } from "@/pages/LoginPage";
|
|
|
|
|
import { RegisterPage } from "@/pages/RegisterPage";
|
2026-06-20 19:13:05 +03:00
|
|
|
import { AdminRegisterPage } from "@/pages/AdminRegisterPage";
|
|
|
|
|
import { WorldsListPage } from "@/pages/WorldsListPage";
|
2026-06-19 11:28:04 +03:00
|
|
|
import { WorldBuilderPage } from "@/pages/WorldBuilderPage";
|
|
|
|
|
import { WorldEditPage } from "@/pages/WorldEditPage";
|
2026-06-20 19:13:05 +03:00
|
|
|
import { PlayPage } from "@/pages/PlayPage";
|
|
|
|
|
import { AdminPage } from "@/pages/AdminPage";
|
2026-06-19 11:28:04 +03:00
|
|
|
|
2026-06-20 19:13:05 +03:00
|
|
|
function Layout({ children }: { children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-bg text-fg">
|
|
|
|
|
<Navbar />
|
|
|
|
|
{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}</>;
|
2026-06-19 11:28:04 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-20 19:13:05 +03:00
|
|
|
function ScrollToTop() {
|
|
|
|
|
const { pathname } = useLocation();
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
window.scrollTo(0, 0);
|
|
|
|
|
}, [pathname]);
|
|
|
|
|
return null;
|
2026-06-19 11:28:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
2026-06-20 19:13:05 +03:00
|
|
|
const { t } = useTranslation();
|
2026-06-21 02:41:48 +03:00
|
|
|
const uiSettings = useUiSettingsStore((s) => s.settings);
|
|
|
|
|
// On mount, fetch public UI settings (page title, favicon, logo).
|
|
|
|
|
const fetchUiSettings = useUiSettingsStore((s) => s.fetch);
|
2026-06-19 19:14:27 +03:00
|
|
|
useEffect(() => {
|
2026-06-21 02:41:48 +03:00
|
|
|
void fetchUiSettings();
|
|
|
|
|
}, [fetchUiSettings]);
|
|
|
|
|
|
|
|
|
|
// Keep document.title in sync with public settings (or default to app name).
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (uiSettings?.page_title) document.title = uiSettings.page_title;
|
|
|
|
|
else document.title = t("common.app_name");
|
|
|
|
|
}, [uiSettings?.page_title, t]);
|
2026-06-19 19:14:27 +03:00
|
|
|
|
2026-06-19 11:28:04 +03:00
|
|
|
return (
|
2026-06-20 19:13:05 +03:00
|
|
|
<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>
|
2026-06-19 11:28:04 +03:00
|
|
|
<WorldBuilderPage />
|
2026-06-20 19:13:05 +03:00
|
|
|
</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>
|
2026-06-19 11:28:04 +03:00
|
|
|
);
|
|
|
|
|
}
|