dnd-hub/apps/web/src/App.js
2026-03-16 22:15:15 -04:00

80 lines
9.6 KiB
JavaScript

import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useEffect, useState } from "react";
import { Link, Navigate, Route, Routes, useLocation } from "react-router-dom";
import { api } from "./api/client";
import { CharacterSheetDrawer } from "./components/CharacterSheetDrawer";
import { SrdDrawer } from "./components/SrdDrawer";
import { Toast } from "./components/Toast";
import { CharacterSheetProvider } from "./contexts/CharacterSheetContext";
import { DebugProvider, useDebugMode } from "./contexts/DebugContext";
import { ToastProvider, useToast } from "./contexts/ToastContext";
import { AdminPage } from "./pages/AdminPage";
import { CallbackPage } from "./pages/CallbackPage";
import { CampaignDetailPage } from "./pages/CampaignDetailPage";
import { CampaignsPage } from "./pages/CampaignsPage";
import { CharacterPage } from "./pages/CharacterPage";
import { DashboardPage } from "./pages/DashboardPage";
import { LoginPage } from "./pages/LoginPage";
// ─── SVG icon components ───────────────────────────────────────────────────────
function IconHome() {
return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" }), _jsx("polyline", { points: "9,22 9,12 15,12 15,22" })] }));
}
function IconScroll() {
return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" }), _jsx("polyline", { points: "14,2 14,8 20,8" }), _jsx("line", { x1: "16", y1: "13", x2: "8", y2: "13" }), _jsx("line", { x1: "16", y1: "17", x2: "8", y2: "17" }), _jsx("polyline", { points: "10,9 9,9 8,9" })] }));
}
function IconSettings() {
return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" })] }));
}
function IconLogout() {
return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" }), _jsx("polyline", { points: "16,17 21,12 16,7" }), _jsx("line", { x1: "21", y1: "12", x2: "9", y2: "12" })] }));
}
// ─── Nav item ─────────────────────────────────────────────────────────────────
function NavItem({ to, icon, label, active }) {
return (_jsxs(Link, { to: to, className: `nav-item${active ? " active" : ""}`, children: [icon, label] }));
}
// ─── Sidebar ──────────────────────────────────────────────────────────────────
function Sidebar({ me, onLogout }) {
const location = useLocation();
const { debugMode, toggle } = useDebugMode();
// Real role from server — never affected by debug mode
const realRole = me?.role;
// Effective role shown in the UI
const effectiveRole = debugMode ? "player" : realRole;
// Show the debug toggle when the real role is elevated OR debug is already on.
// This prevents lock-out: even with debug on, the button stays visible.
const showDebugToggle = realRole === "admin" || realRole === "dm" || debugMode;
return (_jsxs("aside", { className: "sidebar", children: [_jsxs("div", { className: "sidebar-logo", children: [_jsx("div", { className: "sidebar-logo-mark", children: "\u2694\uFE0F" }), _jsxs("div", { className: "sidebar-logo-text", children: [_jsx("div", { className: "sidebar-logo-title", children: "Campaign Hub" }), _jsx("div", { className: "sidebar-logo-sub", children: "D&D Session Manager" })] })] }), _jsxs("nav", { className: "sidebar-nav", children: [_jsx(NavItem, { to: "/dashboard", icon: _jsx(IconHome, {}), label: "Dashboard", active: location.pathname === "/dashboard" }), _jsx(NavItem, { to: "/campaigns", icon: _jsx(IconScroll, {}), label: "Campaigns", active: location.pathname.startsWith("/campaigns") }), (realRole === "admin" || realRole === "dm") && !debugMode && (_jsx(NavItem, { to: "/admin", icon: _jsx(IconSettings, {}), label: "Admin", active: location.pathname === "/admin" }))] }), showDebugToggle && (_jsxs("button", { className: `debug-toggle${debugMode ? " active" : ""}`, onClick: toggle, title: debugMode ? "Debug mode ON — click to restore your role" : "Toggle player debug mode", children: [_jsx("span", { className: "debug-toggle-dot" }), _jsx("span", { className: "debug-toggle-label", children: debugMode ? "Player view (debug)" : "Debug: Player View" })] })), _jsxs("div", { className: "sidebar-footer", children: [_jsx("div", { className: "user-avatar-placeholder", children: me?.user?.username?.[0]?.toUpperCase() ?? "?" }), _jsxs("div", { className: "user-info", children: [_jsx("div", { className: "user-name", children: me?.user.username ?? "Loading…" }), _jsxs("div", { className: "user-role", children: [effectiveRole ?? "player", debugMode && _jsxs("span", { className: "debug-role-tag", children: [" (real: ", realRole, ")"] })] })] }), _jsx("button", { className: "logout-btn", onClick: onLogout, title: "Sign out", children: _jsx(IconLogout, {}) })] })] }));
}
// ─── Protected route ──────────────────────────────────────────────────────────
function ProtectedRoute({ children }) {
if (!localStorage.getItem("token"))
return _jsx(Navigate, { to: "/login", replace: true });
return _jsx(_Fragment, { children: children });
}
// ─── Auth-only routes (no sidebar) ───────────────────────────────────────────
const AUTH_ROUTES = ["/login", "/callback"];
// ─── App root ─────────────────────────────────────────────────────────────────
function AppInner() {
const location = useLocation();
const [me, setMe] = useState(null);
const { toasts, removeToast, addToast } = useToast();
const { debugMode } = useDebugMode();
const isAuthRoute = AUTH_ROUTES.includes(location.pathname);
const hasToken = Boolean(localStorage.getItem("token"));
useEffect(() => {
if (hasToken && !isAuthRoute) {
api("/me").then(setMe).catch(() => { });
}
}, [hasToken, isAuthRoute]);
function logout() {
localStorage.removeItem("token");
window.location.href = "/login";
}
if (isAuthRoute || !hasToken) {
return (_jsxs(Routes, { children: [_jsx(Route, { path: "/login", element: _jsx(LoginPage, {}) }), _jsx(Route, { path: "/callback", element: _jsx(CallbackPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/login", replace: true }) })] }));
}
return (_jsxs("div", { className: "layout", children: [_jsx(Sidebar, { me: me, onLogout: logout }), _jsxs("div", { className: "layout-body", children: [_jsx(CharacterSheetDrawer, {}), _jsxs("main", { className: "main", children: [_jsx(Toast, { toasts: toasts, removeToast: removeToast }), debugMode && (_jsxs("div", { style: { position: "fixed", bottom: "20px", right: "20px", zIndex: 999, display: "flex", gap: "8px" }, children: [_jsx("button", { className: "btn btn-primary btn-sm", onClick: () => addToast("Test success toast", "success"), children: "Toast \u2705" }), _jsx("button", { className: "btn btn-primary btn-sm", onClick: () => addToast("Test error toast", "error"), children: "Toast \u274C" })] })), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: "/dashboard", replace: true }) }), _jsx(Route, { path: "/dashboard", element: _jsx(ProtectedRoute, { children: _jsx(DashboardPage, {}) }) }), _jsx(Route, { path: "/campaigns", element: _jsx(ProtectedRoute, { children: _jsx(CampaignsPage, {}) }) }), _jsx(Route, { path: "/campaigns/:id", element: _jsx(ProtectedRoute, { children: _jsx(CampaignDetailPage, {}) }) }), _jsx(Route, { path: "/campaigns/:id/characters/:charId", element: _jsx(ProtectedRoute, { children: _jsx(CharacterPage, {}) }) }), _jsx(Route, { path: "/admin", element: _jsx(ProtectedRoute, { children: _jsx(AdminPage, {}) }) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/dashboard", replace: true }) })] })] }), _jsx(SrdDrawer, {})] })] }));
}
export function App() {
return (_jsx(ToastProvider, { children: _jsx(DebugProvider, { children: _jsx(CharacterSheetProvider, { children: _jsx(AppInner, {}) }) }) }));
}