feature/ignore-self-toggle #1
@@ -88,21 +88,21 @@ This stage implements the very first user experience: the setup wizard that runs
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Stage 3 — Application Shell & Navigation
|
## Stage 3 — Application Shell & Navigation ✅ DONE
|
||||||
|
|
||||||
With authentication working, this stage builds the persistent layout that every page shares: the navigation sidebar, the header, and the routing skeleton.
|
With authentication working, this stage builds the persistent layout that every page shares: the navigation sidebar, the header, and the routing skeleton.
|
||||||
|
|
||||||
### 3.1 Build the main layout component
|
### 3.1 Build the main layout component ✅
|
||||||
|
|
||||||
Create `frontend/src/layouts/MainLayout.tsx`. This is the outer shell visible on every authenticated page. It contains a fixed-width sidebar navigation (240 px, collapsing to 48 px on small screens) and a main content area. Use the Fluent UI `Nav` component for the sidebar with groups for Dashboard, World Map, Jails, Configuration, History, Blocklists, and a Logout action at the bottom. The layout must be responsive following the breakpoints in [Web-Design.md § 4](Web-Design.md). The main content area is capped at 1440 px and centred on wide screens.
|
**Done.** `frontend/src/layouts/MainLayout.tsx` — fixed-width sidebar (240 px, collapses to 48 px via toggle button), Fluent UI v9 `makeStyles`/`tokens`. Nav items: Dashboard, World Map, Jails, Configuration, History, Blocklists. Active link highlighted using `NavLink` `isActive` callback. Logout button at the bottom. Main content area: `flex: 1`, `maxWidth: 1440px`, centred.
|
||||||
|
|
||||||
### 3.2 Set up client-side routing
|
### 3.2 Set up client-side routing ✅
|
||||||
|
|
||||||
Configure React Router in `frontend/src/App.tsx` (or a dedicated `AppRoutes.tsx`). Define routes for every page: `/` (dashboard), `/map`, `/jails`, `/jails/:name`, `/config`, `/history`, `/blocklists`, `/setup`, `/login`. Wrap all routes except setup and login inside the auth guard from Stage 2. Use the `MainLayout` for authenticated routes. Create placeholder page components for each route so navigation works end to end.
|
**Done.** `frontend/src/App.tsx` updated — layout route wraps all protected paths in `RequireAuth > MainLayout`. Routes: `/` (DashboardPage), `/map` (MapPage), `/jails` (JailsPage), `/jails/:name` (JailDetailPage), `/config` (ConfigPage), `/history` (HistoryPage), `/blocklists` (BlocklistsPage). Placeholder page components created for all routes not yet fully implemented. `*` falls back to `/`. tsc --noEmit: 0 errors.
|
||||||
|
|
||||||
### 3.3 Implement the logout flow
|
### 3.3 Implement the logout flow ✅
|
||||||
|
|
||||||
Wire the Logout button in the sidebar to call `POST /api/auth/logout`, clear the client-side session state, and redirect to the login page. The logout option must be accessible from every page as specified in [Features.md § 2](Features.md).
|
**Done.** `MainLayout.tsx` logout button calls `useAuth().logout()` (which POSTs `POST /api/auth/logout` and clears sessionStorage) then `navigate('/login', { replace: true })`. Accessible from every authenticated page via the persistent sidebar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,16 @@
|
|||||||
* 3. `AuthProvider` — manages session state and exposes `useAuth()`.
|
* 3. `AuthProvider` — manages session state and exposes `useAuth()`.
|
||||||
*
|
*
|
||||||
* Routes:
|
* Routes:
|
||||||
* - `/setup` — first-run setup wizard (always accessible, redirected to by backend middleware)
|
* - `/setup` — first-run setup wizard (always accessible)
|
||||||
* - `/login` — master password login
|
* - `/login` — master password login
|
||||||
* - `/` — dashboard (protected)
|
* - `/` — dashboard (protected, inside MainLayout)
|
||||||
* All other paths fall through to the dashboard guard; the full route tree
|
* - `/map` — world map (protected)
|
||||||
* is wired up in Stage 3.
|
* - `/jails` — jail list (protected)
|
||||||
|
* - `/jails/:name` — jail detail (protected)
|
||||||
|
* - `/config` — configuration editor (protected)
|
||||||
|
* - `/history` — event history (protected)
|
||||||
|
* - `/blocklists` — blocklist management (protected)
|
||||||
|
* All unmatched paths redirect to `/`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FluentProvider } from "@fluentui/react-components";
|
import { FluentProvider } from "@fluentui/react-components";
|
||||||
@@ -19,9 +24,16 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
|||||||
import { lightTheme } from "./theme/customTheme";
|
import { lightTheme } from "./theme/customTheme";
|
||||||
import { AuthProvider } from "./providers/AuthProvider";
|
import { AuthProvider } from "./providers/AuthProvider";
|
||||||
import { RequireAuth } from "./components/RequireAuth";
|
import { RequireAuth } from "./components/RequireAuth";
|
||||||
|
import { MainLayout } from "./layouts/MainLayout";
|
||||||
import { SetupPage } from "./pages/SetupPage";
|
import { SetupPage } from "./pages/SetupPage";
|
||||||
import { LoginPage } from "./pages/LoginPage";
|
import { LoginPage } from "./pages/LoginPage";
|
||||||
import { DashboardPage } from "./pages/DashboardPage";
|
import { DashboardPage } from "./pages/DashboardPage";
|
||||||
|
import { MapPage } from "./pages/MapPage";
|
||||||
|
import { JailsPage } from "./pages/JailsPage";
|
||||||
|
import { JailDetailPage } from "./pages/JailDetailPage";
|
||||||
|
import { ConfigPage } from "./pages/ConfigPage";
|
||||||
|
import { HistoryPage } from "./pages/HistoryPage";
|
||||||
|
import { BlocklistsPage } from "./pages/BlocklistsPage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Root application component — mounts providers and top-level routes.
|
* Root application component — mounts providers and top-level routes.
|
||||||
@@ -36,17 +48,24 @@ function App(): JSX.Element {
|
|||||||
<Route path="/setup" element={<SetupPage />} />
|
<Route path="/setup" element={<SetupPage />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
|
||||||
{/* Protected routes */}
|
{/* Protected routes — all rendered inside MainLayout */}
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
|
||||||
element={
|
element={
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<DashboardPage />
|
<MainLayout />
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<Route index element={<DashboardPage />} />
|
||||||
|
<Route path="/map" element={<MapPage />} />
|
||||||
|
<Route path="/jails" element={<JailsPage />} />
|
||||||
|
<Route path="/jails/:name" element={<JailDetailPage />} />
|
||||||
|
<Route path="/config" element={<ConfigPage />} />
|
||||||
|
<Route path="/history" element={<HistoryPage />} />
|
||||||
|
<Route path="/blocklists" element={<BlocklistsPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* Fallback — redirect unknown paths to dashboard (guard will redirect to login if needed) */}
|
{/* Fallback — redirect unknown paths to dashboard */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
279
frontend/src/layouts/MainLayout.tsx
Normal file
279
frontend/src/layouts/MainLayout.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* Main application layout.
|
||||||
|
*
|
||||||
|
* Provides the persistent sidebar navigation and the main content area
|
||||||
|
* for all authenticated pages. The sidebar collapses from 240 px to
|
||||||
|
* icon-only (48 px) on small screens.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
makeStyles,
|
||||||
|
mergeClasses,
|
||||||
|
Text,
|
||||||
|
tokens,
|
||||||
|
Tooltip,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import {
|
||||||
|
GridRegular,
|
||||||
|
MapRegular,
|
||||||
|
ShieldRegular,
|
||||||
|
SettingsRegular,
|
||||||
|
HistoryRegular,
|
||||||
|
ListRegular,
|
||||||
|
SignOutRegular,
|
||||||
|
NavigationRegular,
|
||||||
|
} from "@fluentui/react-icons";
|
||||||
|
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||||
|
import { useAuth } from "../providers/AuthProvider";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Styles
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SIDEBAR_FULL = "240px";
|
||||||
|
const SIDEBAR_COLLAPSED = "48px";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
display: "flex",
|
||||||
|
height: "100vh",
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: tokens.colorNeutralBackground3,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
sidebar: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: SIDEBAR_FULL,
|
||||||
|
minWidth: SIDEBAR_COLLAPSED,
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
|
borderRightWidth: "1px",
|
||||||
|
borderRightStyle: "solid",
|
||||||
|
borderRightColor: tokens.colorNeutralStroke2,
|
||||||
|
transition: "width 200ms ease",
|
||||||
|
overflow: "hidden",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
sidebarCollapsed: {
|
||||||
|
width: SIDEBAR_COLLAPSED,
|
||||||
|
},
|
||||||
|
|
||||||
|
sidebarHeader: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "52px",
|
||||||
|
paddingLeft: tokens.spacingHorizontalM,
|
||||||
|
paddingRight: tokens.spacingHorizontalS,
|
||||||
|
gap: tokens.spacingHorizontalS,
|
||||||
|
borderBottomWidth: "1px",
|
||||||
|
borderBottomStyle: "solid",
|
||||||
|
borderBottomColor: tokens.colorNeutralStroke2,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "16px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
color: tokens.colorBrandForeground1,
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Nav items list
|
||||||
|
navList: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "2px",
|
||||||
|
padding: tokens.spacingVerticalS,
|
||||||
|
overflowY: "auto",
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
navLink: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: tokens.spacingHorizontalS,
|
||||||
|
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalS}`,
|
||||||
|
borderRadius: tokens.borderRadiusMedium,
|
||||||
|
textDecoration: "none",
|
||||||
|
color: tokens.colorNeutralForeground2,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1Hover,
|
||||||
|
color: tokens.colorNeutralForeground1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
navLinkActive: {
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1Selected,
|
||||||
|
color: tokens.colorBrandForeground1,
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1Selected,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
navLabel: {
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "20px",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sidebar footer (logout)
|
||||||
|
sidebarFooter: {
|
||||||
|
borderTopWidth: "1px",
|
||||||
|
borderTopStyle: "solid",
|
||||||
|
borderTopColor: tokens.colorNeutralStroke2,
|
||||||
|
padding: tokens.spacingVerticalS,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
main: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexGrow: 1,
|
||||||
|
maxWidth: "1440px",
|
||||||
|
width: "100%",
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: tokens.spacingVerticalL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Nav item data
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
label: string;
|
||||||
|
to: string;
|
||||||
|
icon: React.ReactElement;
|
||||||
|
end?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAV_ITEMS: NavItem[] = [
|
||||||
|
{ label: "Dashboard", to: "/", icon: <GridRegular />, end: true },
|
||||||
|
{ label: "World Map", to: "/map", icon: <MapRegular /> },
|
||||||
|
{ label: "Jails", to: "/jails", icon: <ShieldRegular /> },
|
||||||
|
{ label: "Configuration", to: "/config", icon: <SettingsRegular /> },
|
||||||
|
{ label: "History", to: "/history", icon: <HistoryRegular /> },
|
||||||
|
{ label: "Blocklists", to: "/blocklists", icon: <ListRegular /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Component
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main application shell with sidebar navigation and content area.
|
||||||
|
*
|
||||||
|
* Renders child routes via `<Outlet />`. Use inside React Router
|
||||||
|
* as a layout route wrapping all authenticated pages.
|
||||||
|
*/
|
||||||
|
export function MainLayout(): JSX.Element {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const toggleCollapse = useCallback(() => {
|
||||||
|
setCollapsed((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogout = useCallback(async () => {
|
||||||
|
await logout();
|
||||||
|
navigate("/login", { replace: true });
|
||||||
|
}, [logout, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
{/* ---------------------------------------------------------------- */}
|
||||||
|
{/* Sidebar */}
|
||||||
|
{/* ---------------------------------------------------------------- */}
|
||||||
|
<nav
|
||||||
|
className={mergeClasses(
|
||||||
|
styles.sidebar,
|
||||||
|
collapsed && styles.sidebarCollapsed,
|
||||||
|
)}
|
||||||
|
aria-label="Main navigation"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.sidebarHeader}>
|
||||||
|
{!collapsed && <span className={styles.logo}>BanGUI</span>}
|
||||||
|
<Tooltip content={collapsed ? "Expand sidebar" : "Collapse sidebar"} relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
icon={<NavigationRegular />}
|
||||||
|
onClick={toggleCollapse}
|
||||||
|
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav links */}
|
||||||
|
<ul className={styles.navList} role="list" aria-label="Pages">
|
||||||
|
{NAV_ITEMS.map((item) => (
|
||||||
|
<li key={item.to} role="listitem">
|
||||||
|
<Tooltip
|
||||||
|
content={collapsed ? item.label : ""}
|
||||||
|
relationship="label"
|
||||||
|
positioning="after"
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
to={item.to}
|
||||||
|
end={item.end}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
mergeClasses(
|
||||||
|
styles.navLink,
|
||||||
|
isActive && styles.navLinkActive,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
aria-label={collapsed ? item.label : undefined}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{!collapsed && (
|
||||||
|
<Text className={styles.navLabel}>{item.label}</Text>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
</Tooltip>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Footer — Logout */}
|
||||||
|
<div className={styles.sidebarFooter}>
|
||||||
|
<Tooltip
|
||||||
|
content={collapsed ? "Sign out" : ""}
|
||||||
|
relationship="label"
|
||||||
|
positioning="after"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
icon={<SignOutRegular />}
|
||||||
|
onClick={() => void handleLogout()}
|
||||||
|
aria-label="Sign out"
|
||||||
|
style={{ width: "100%", justifyContent: collapsed ? "center" : "flex-start" }}
|
||||||
|
>
|
||||||
|
{!collapsed && "Sign out"}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* ---------------------------------------------------------------- */}
|
||||||
|
{/* Main content */}
|
||||||
|
{/* ---------------------------------------------------------------- */}
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/pages/BlocklistsPage.tsx
Normal file
23
frontend/src/pages/BlocklistsPage.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Blocklists placeholder page — full implementation in Stage 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: { padding: tokens.spacingVerticalXXL },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function BlocklistsPage(): JSX.Element {
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Text as="h1" size={700} weight="semibold">
|
||||||
|
Blocklists
|
||||||
|
</Text>
|
||||||
|
<Text as="p" size={300}>
|
||||||
|
Blocklist management will be implemented in Stage 10.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/pages/ConfigPage.tsx
Normal file
23
frontend/src/pages/ConfigPage.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Configuration placeholder page — full implementation in Stage 8.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: { padding: tokens.spacingVerticalXXL },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ConfigPage(): JSX.Element {
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Text as="h1" size={700} weight="semibold">
|
||||||
|
Configuration
|
||||||
|
</Text>
|
||||||
|
<Text as="p" size={300}>
|
||||||
|
fail2ban configuration editor will be implemented in Stage 8.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/pages/HistoryPage.tsx
Normal file
23
frontend/src/pages/HistoryPage.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Ban history placeholder page — full implementation in Stage 9.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: { padding: tokens.spacingVerticalXXL },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function HistoryPage(): JSX.Element {
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Text as="h1" size={700} weight="semibold">
|
||||||
|
History
|
||||||
|
</Text>
|
||||||
|
<Text as="p" size={300}>
|
||||||
|
Historical ban query view will be implemented in Stage 9.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
frontend/src/pages/JailDetailPage.tsx
Normal file
25
frontend/src/pages/JailDetailPage.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Jail detail placeholder page — full implementation in Stage 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: { padding: tokens.spacingVerticalXXL },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JailDetailPage(): JSX.Element {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { name } = useParams<{ name: string }>();
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Text as="h1" size={700} weight="semibold">
|
||||||
|
Jail: {name}
|
||||||
|
</Text>
|
||||||
|
<Text as="p" size={300}>
|
||||||
|
Jail detail view will be implemented in Stage 6.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/pages/JailsPage.tsx
Normal file
23
frontend/src/pages/JailsPage.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Jails overview placeholder page — full implementation in Stage 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: { padding: tokens.spacingVerticalXXL },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JailsPage(): JSX.Element {
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Text as="h1" size={700} weight="semibold">
|
||||||
|
Jails
|
||||||
|
</Text>
|
||||||
|
<Text as="p" size={300}>
|
||||||
|
Jail management will be implemented in Stage 6.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/pages/MapPage.tsx
Normal file
23
frontend/src/pages/MapPage.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* World Map placeholder page — full implementation in Stage 5.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: { padding: tokens.spacingVerticalXXL },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function MapPage(): JSX.Element {
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Text as="h1" size={700} weight="semibold">
|
||||||
|
World Map
|
||||||
|
</Text>
|
||||||
|
<Text as="p" size={300}>
|
||||||
|
Geographical ban overview will be implemented in Stage 5.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user