aboutsummaryrefslogtreecommitdiff
path: root/174bg/manager
diff options
context:
space:
mode:
authorAlex Pooley (@zuedev) <zuedev@gmail.com>2026-07-25 17:45:15 +0100
committerAlex Pooley (@zuedev) <zuedev@gmail.com>2026-07-25 17:45:15 +0100
commit20c46e54690f7c7befb7059960447935231d86be (patch)
tree9b46cd39f29f9887873836cf1bfb02afecc036fc /174bg/manager
parenta85a54651070765d6254287369835d747be1c276 (diff)
downloadunnamed-group-20c46e54690f7c7befb7059960447935231d86be.tar
unnamed-group-20c46e54690f7c7befb7059960447935231d86be.tar.gz
unnamed-group-20c46e54690f7c7befb7059960447935231d86be.tar.bz2
unnamed-group-20c46e54690f7c7befb7059960447935231d86be.tar.xz
unnamed-group-20c46e54690f7c7befb7059960447935231d86be.zip
2-col layout
Diffstat (limited to '174bg/manager')
-rw-r--r--174bg/manager/package-lock.json48
-rw-r--r--174bg/manager/package.json3
-rw-r--r--174bg/manager/src/App.jsx98
-rw-r--r--174bg/manager/src/components/Sidebar.jsx56
-rw-r--r--174bg/manager/src/main.jsx5
-rw-r--r--174bg/manager/src/pages/Overview.jsx11
-rw-r--r--174bg/manager/src/theme.css194
7 files changed, 372 insertions, 43 deletions
diff --git a/174bg/manager/package-lock.json b/174bg/manager/package-lock.json
index 505b3ad..1f14e69 100644
--- a/174bg/manager/package-lock.json
+++ b/174bg/manager/package-lock.json
@@ -7,7 +7,8 @@
"dependencies": {
"pocketbase": "^0.27.0",
"react": "^19.1.0",
- "react-dom": "^19.1.0"
+ "react-dom": "^19.1.0",
+ "react-router-dom": "^7.1.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
@@ -2071,7 +2072,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2431,6 +2431,44 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.18.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
+ "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.18.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
+ "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.18.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/rollup": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
@@ -2555,6 +2593,12 @@
"node": ">=10"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
diff --git a/174bg/manager/package.json b/174bg/manager/package.json
index 0e4769e..d863c0a 100644
--- a/174bg/manager/package.json
+++ b/174bg/manager/package.json
@@ -9,7 +9,8 @@
"dependencies": {
"pocketbase": "^0.27.0",
"react": "^19.1.0",
- "react-dom": "^19.1.0"
+ "react-dom": "^19.1.0",
+ "react-router-dom": "^7.1.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
diff --git a/174bg/manager/src/App.jsx b/174bg/manager/src/App.jsx
index 5f455bb..095506a 100644
--- a/174bg/manager/src/App.jsx
+++ b/174bg/manager/src/App.jsx
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from "react";
+import { Navigate, Route, Routes } from "react-router-dom";
import { pb } from "./lib/pocketbase";
-import Welcome from "./components/Welcome.jsx";
-import RequiredInfo from "./components/RequiredInfo.jsx";
+import Sidebar from "./components/Sidebar.jsx";
+import Overview from "./pages/Overview.jsx";
import RolePreferences from "./components/RolePreferences.jsx";
import OnCallSchedule from "./components/OnCallSchedule.jsx";
import Ledger from "./components/Ledger.jsx";
@@ -143,40 +144,75 @@ export default function App() {
return (
<>
- <h1>174th Battle Group: Manager</h1>
-
{!loggedIn && (
- <section id="anonymous">
- <p>You are not logged in. Please log in to access this site:</p>
- <button id="login" onClick={handleLogin} disabled={loginDisabled}>
- Login with Discord
- </button>
- </section>
- )}
-
- {oauthStatus.text && (
- <p id="oauth-status" style={oauthStatusStyle}>
- {oauthStatus.text}
- </p>
+ <div className="center-view">
+ <h1>174th Battle Group: Manager</h1>
+ <section id="anonymous">
+ <p>You are not logged in. Please log in to access this site:</p>
+ <button id="login" onClick={handleLogin} disabled={loginDisabled}>
+ Login with Discord
+ </button>
+ </section>
+ {oauthStatus.text && (
+ <p id="oauth-status" style={oauthStatusStyle}>
+ {oauthStatus.text}
+ </p>
+ )}
+ </div>
)}
{loggedIn && (
- <section
- id="authed"
- style={{ display: "flex", flexDirection: "column", gap: "1em" }}
- >
- <Welcome record={record} />
- <RequiredInfo record={record} />
- <RolePreferences record={record} onUpdate={handleRecordUpdate} />
- <OnCallSchedule record={record} onUpdate={handleRecordUpdate} />
- <Ledger />
- <div>
- <button id="logout" onClick={handleLogout}>
- Logout
- </button>
- </div>
- </section>
+ <DashboardLayout
+ record={record}
+ onLogout={handleLogout}
+ onUpdate={handleRecordUpdate}
+ />
)}
</>
);
}
+
+function DashboardLayout({ record, onLogout, onUpdate }) {
+ const [navOpen, setNavOpen] = useState(false);
+
+ return (
+ <div className="app-shell">
+ <Sidebar
+ record={record}
+ open={navOpen}
+ onClose={() => setNavOpen(false)}
+ onLogout={onLogout}
+ />
+ <div className="app-content">
+ <header className="topbar">
+ <button
+ type="button"
+ className="nav-toggle"
+ onClick={() => setNavOpen((v) => !v)}
+ aria-label="Toggle navigation"
+ >
+ ☰
+ </button>
+ <span className="topbar-title">174th Battle Group: Manager</span>
+ </header>
+ <main className="page">
+ <Routes>
+ <Route path="/" element={<Overview record={record} />} />
+ <Route
+ path="/preferences"
+ element={
+ <RolePreferences record={record} onUpdate={onUpdate} />
+ }
+ />
+ <Route
+ path="/oncall"
+ element={<OnCallSchedule record={record} onUpdate={onUpdate} />}
+ />
+ <Route path="/ledger" element={<Ledger />} />
+ <Route path="*" element={<Navigate to="/" replace />} />
+ </Routes>
+ </main>
+ </div>
+ </div>
+ );
+}
diff --git a/174bg/manager/src/components/Sidebar.jsx b/174bg/manager/src/components/Sidebar.jsx
new file mode 100644
index 0000000..64f0cf5
--- /dev/null
+++ b/174bg/manager/src/components/Sidebar.jsx
@@ -0,0 +1,56 @@
+import { NavLink } from "react-router-dom";
+
+const NAV_ITEMS = [
+ { to: "/", label: "Overview", end: true },
+ { to: "/preferences", label: "Role Preferences" },
+ { to: "/oncall", label: "On-Call Schedule" },
+ { to: "/ledger", label: "Ledger" },
+];
+
+export default function Sidebar({ record, open, onClose, onLogout }) {
+ const name = record?.RSI_Handle || record?.name || "Pilot";
+
+ return (
+ <>
+ {open && (
+ <div
+ className="sidebar-backdrop"
+ onClick={onClose}
+ aria-hidden="true"
+ />
+ )}
+ <aside className={`sidebar${open ? " sidebar-open" : ""}`}>
+ <div className="sidebar-header">
+ <span className="sidebar-title">174BG Manager</span>
+ <button
+ type="button"
+ className="sidebar-close"
+ onClick={onClose}
+ aria-label="Close navigation"
+ >
+ ✕
+ </button>
+ </div>
+ <p className="sidebar-user">{name}</p>
+ <nav className="sidebar-nav">
+ {NAV_ITEMS.map((item) => (
+ <NavLink
+ key={item.to}
+ to={item.to}
+ end={item.end}
+ className={({ isActive }) =>
+ "sidebar-link" + (isActive ? " sidebar-link-active" : "")
+ }
+ onClick={onClose}
+ >
+ {item.label}
+ </NavLink>
+ ))}
+ </nav>
+ <button type="button" className="sidebar-logout" onClick={onLogout}>
+ Logout
+ </button>
+ </aside>
+ </>
+ );
+}
diff --git a/174bg/manager/src/main.jsx b/174bg/manager/src/main.jsx
index ed1507f..cbd234d 100644
--- a/174bg/manager/src/main.jsx
+++ b/174bg/manager/src/main.jsx
@@ -1,10 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
import App from "./App.jsx";
import "./theme.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
- <App />
+ <BrowserRouter>
+ <App />
+ </BrowserRouter>
</StrictMode>,
);
diff --git a/174bg/manager/src/pages/Overview.jsx b/174bg/manager/src/pages/Overview.jsx
new file mode 100644
index 0000000..980c04b
--- /dev/null
+++ b/174bg/manager/src/pages/Overview.jsx
@@ -0,0 +1,11 @@
+import Welcome from "../components/Welcome.jsx";
+import RequiredInfo from "../components/RequiredInfo.jsx";
+
+export default function Overview({ record }) {
+ return (
+ <div className="page-stack">
+ <Welcome record={record} />
+ <RequiredInfo record={record} />
+ </div>
+ );
+}
diff --git a/174bg/manager/src/theme.css b/174bg/manager/src/theme.css
index 907a77e..7694f0d 100644
--- a/174bg/manager/src/theme.css
+++ b/174bg/manager/src/theme.css
@@ -28,7 +28,6 @@ html {
body {
margin: 0;
min-height: 100vh;
- padding: 2.5rem 1.25rem 4rem;
color: var(--text);
background-color: var(--bg);
background-image:
@@ -44,11 +43,23 @@ body {
44px 44px,
44px 44px;
background-attachment: fixed;
+}
+
+/* Centered layout for the logged-out login screen ------------------- */
+.center-view {
+ width: 100%;
+ min-height: 100vh;
+ padding: 2.5rem 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
}
+.center-view > * {
+ width: 100%;
+ max-width: 760px;
+}
+
h1 {
font-family: "Orbitron", sans-serif;
font-weight: 900;
@@ -83,12 +94,169 @@ a:hover {
border-bottom-color: var(--accent-2);
}
-body > h1,
-#anonymous,
-#oauth-status,
-#authed {
+/* App shell / sidebar navigation ------------------------------------ */
+.app-shell {
width: 100%;
- max-width: 760px;
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: 220px 1fr;
+}
+
+.sidebar {
+ position: sticky;
+ top: 0;
+ align-self: start;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ background: var(--panel);
+ border-right: 1px solid var(--panel-border);
+ padding: 1.25rem 1rem;
+ overflow-y: auto;
+}
+
+.sidebar-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.sidebar-title {
+ font-family: "Orbitron", sans-serif;
+ font-size: 0.95rem;
+ letter-spacing: 0.04em;
+ color: var(--accent);
+}
+
+.sidebar-close {
+ display: none;
+ width: auto;
+ padding: 0.3rem 0.55rem;
+}
+
+.sidebar-user {
+ margin: 0;
+ font-size: 0.85rem;
+ color: var(--text-dim);
+}
+
+.sidebar-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.sidebar-link {
+ display: block;
+ padding: 0.55rem 0.7rem;
+ border-radius: 8px;
+ color: var(--text);
+ font-weight: 600;
+ letter-spacing: 0.01em;
+ border-bottom: none;
+}
+
+.sidebar-link:hover {
+ border-bottom: none;
+ background: rgba(56, 189, 248, 0.1);
+}
+
+.sidebar-link-active,
+.sidebar-link-active:hover {
+ background: linear-gradient(120deg, var(--accent), var(--accent-2));
+ color: #04121f;
+}
+
+.sidebar-logout {
+ margin-top: auto;
+}
+
+.sidebar-backdrop {
+ display: none;
+}
+
+.app-content {
+ min-width: 0;
+ padding: 1.5rem 1.75rem 2.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+.topbar {
+ display: none;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem 1.25rem 0;
+}
+
+.nav-toggle {
+ width: auto;
+ padding: 0.5rem 0.75rem;
+}
+
+.topbar-title {
+ font-family: "Orbitron", sans-serif;
+ font-size: 1rem;
+ letter-spacing: 0.03em;
+ color: var(--accent);
+}
+
+.page,
+.page-stack {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+ width: 100%;
+ max-width: 1000px;
+}
+
+@media (max-width: 900px) {
+ .app-shell {
+ display: block;
+ min-height: 0;
+ }
+
+ .topbar {
+ display: flex;
+ }
+
+ .app-content {
+ padding: 1rem 1rem 2rem;
+ }
+
+ .sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ height: auto;
+ width: 78vw;
+ max-width: 300px;
+ border-right: 1px solid var(--panel-border);
+ box-shadow: 18px 0 48px rgba(0, 0, 0, 0.45);
+ transform: translateX(-100%);
+ transition: transform 0.25s ease;
+ z-index: 40;
+ }
+
+ .sidebar-open {
+ transform: translateX(0);
+ }
+
+ .sidebar-close {
+ display: inline-flex;
+ }
+
+ .sidebar-backdrop {
+ display: block;
+ position: fixed;
+ inset: 0;
+ background: rgba(2, 6, 16, 0.6);
+ z-index: 30;
+ }
}
/* Panels ----------------------------------------------------------- */
@@ -101,7 +269,7 @@ body > h1,
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 14px;
- padding: 1.25rem 1.4rem;
+ padding: 1rem 1.15rem;
backdrop-filter: blur(10px);
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.3),
@@ -308,10 +476,15 @@ input[type="checkbox"] {
/* Responsive — tablet */
/* ================================================================= */
@media (max-width: 640px) {
- body {
+ .center-view {
padding: 1.5rem 0.85rem 3rem;
}
+ .app-content {
+ padding: 0.85rem 0.85rem 2rem;
+ gap: 1rem;
+ }
+
#anonymous,
#required-info,
#preferences,
@@ -346,6 +519,11 @@ input[type="checkbox"] {
width: 100%;
}
+ .nav-toggle,
+ .sidebar-close {
+ width: auto;
+ }
+
#preferences thead th:first-child,
#preferences tbody td:first-child {
white-space: normal;