From a41a99dad4624c00c5b7cccf7451b12ae9f159a6 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 28 Feb 2026 21:37:42 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Stage=203=20=E2=80=94=20application=20s?= =?UTF-8?q?hell=20and=20navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/Tasks.md | 14 +- frontend/src/App.tsx | 39 +++- frontend/src/layouts/MainLayout.tsx | 279 ++++++++++++++++++++++++++ frontend/src/pages/BlocklistsPage.tsx | 23 +++ frontend/src/pages/ConfigPage.tsx | 23 +++ frontend/src/pages/HistoryPage.tsx | 23 +++ frontend/src/pages/JailDetailPage.tsx | 25 +++ frontend/src/pages/JailsPage.tsx | 23 +++ frontend/src/pages/MapPage.tsx | 23 +++ 9 files changed, 455 insertions(+), 17 deletions(-) create mode 100644 frontend/src/layouts/MainLayout.tsx create mode 100644 frontend/src/pages/BlocklistsPage.tsx create mode 100644 frontend/src/pages/ConfigPage.tsx create mode 100644 frontend/src/pages/HistoryPage.tsx create mode 100644 frontend/src/pages/JailDetailPage.tsx create mode 100644 frontend/src/pages/JailsPage.tsx create mode 100644 frontend/src/pages/MapPage.tsx diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 47c65d3..1a84c33 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -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. -### 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. --- diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0d6cfb8..4cb621a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,11 +7,16 @@ * 3. `AuthProvider` — manages session state and exposes `useAuth()`. * * Routes: - * - `/setup` — first-run setup wizard (always accessible, redirected to by backend middleware) - * - `/login` — master password login - * - `/` — dashboard (protected) - * All other paths fall through to the dashboard guard; the full route tree - * is wired up in Stage 3. + * - `/setup` — first-run setup wizard (always accessible) + * - `/login` — master password login + * - `/` — dashboard (protected, inside MainLayout) + * - `/map` — world map (protected) + * - `/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"; @@ -19,9 +24,16 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { lightTheme } from "./theme/customTheme"; import { AuthProvider } from "./providers/AuthProvider"; import { RequireAuth } from "./components/RequireAuth"; +import { MainLayout } from "./layouts/MainLayout"; import { SetupPage } from "./pages/SetupPage"; import { LoginPage } from "./pages/LoginPage"; 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. @@ -36,17 +48,24 @@ function App(): JSX.Element { } /> } /> - {/* Protected routes */} + {/* Protected routes — all rendered inside MainLayout */} - + } - /> + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + - {/* Fallback — redirect unknown paths to dashboard (guard will redirect to login if needed) */} + {/* Fallback — redirect unknown paths to dashboard */} } /> diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx new file mode 100644 index 0000000..f5bf06e --- /dev/null +++ b/frontend/src/layouts/MainLayout.tsx @@ -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: , end: true }, + { label: "World Map", to: "/map", icon: }, + { label: "Jails", to: "/jails", icon: }, + { label: "Configuration", to: "/config", icon: }, + { label: "History", to: "/history", icon: }, + { label: "Blocklists", to: "/blocklists", icon: }, +]; + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +/** + * Main application shell with sidebar navigation and content area. + * + * Renders child routes via ``. 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 ( +
+ {/* ---------------------------------------------------------------- */} + {/* Sidebar */} + {/* ---------------------------------------------------------------- */} + + + {/* ---------------------------------------------------------------- */} + {/* Main content */} + {/* ---------------------------------------------------------------- */} +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/pages/BlocklistsPage.tsx b/frontend/src/pages/BlocklistsPage.tsx new file mode 100644 index 0000000..17bc425 --- /dev/null +++ b/frontend/src/pages/BlocklistsPage.tsx @@ -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 ( +
+ + Blocklists + + + Blocklist management will be implemented in Stage 10. + +
+ ); +} diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx new file mode 100644 index 0000000..08b0f10 --- /dev/null +++ b/frontend/src/pages/ConfigPage.tsx @@ -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 ( +
+ + Configuration + + + fail2ban configuration editor will be implemented in Stage 8. + +
+ ); +} diff --git a/frontend/src/pages/HistoryPage.tsx b/frontend/src/pages/HistoryPage.tsx new file mode 100644 index 0000000..c83c85c --- /dev/null +++ b/frontend/src/pages/HistoryPage.tsx @@ -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 ( +
+ + History + + + Historical ban query view will be implemented in Stage 9. + +
+ ); +} diff --git a/frontend/src/pages/JailDetailPage.tsx b/frontend/src/pages/JailDetailPage.tsx new file mode 100644 index 0000000..2f545e5 --- /dev/null +++ b/frontend/src/pages/JailDetailPage.tsx @@ -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 ( +
+ + Jail: {name} + + + Jail detail view will be implemented in Stage 6. + +
+ ); +} diff --git a/frontend/src/pages/JailsPage.tsx b/frontend/src/pages/JailsPage.tsx new file mode 100644 index 0000000..543485b --- /dev/null +++ b/frontend/src/pages/JailsPage.tsx @@ -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 ( +
+ + Jails + + + Jail management will be implemented in Stage 6. + +
+ ); +} diff --git a/frontend/src/pages/MapPage.tsx b/frontend/src/pages/MapPage.tsx new file mode 100644 index 0000000..088b0c5 --- /dev/null +++ b/frontend/src/pages/MapPage.tsx @@ -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 ( +
+ + World Map + + + Geographical ban overview will be implemented in Stage 5. + +
+ ); +}