diff options
| -rw-r--r-- | 174bg/manager/package-lock.json | 48 | ||||
| -rw-r--r-- | 174bg/manager/package.json | 3 | ||||
| -rw-r--r-- | 174bg/manager/src/App.jsx | 98 | ||||
| -rw-r--r-- | 174bg/manager/src/components/Sidebar.jsx | 56 | ||||
| -rw-r--r-- | 174bg/manager/src/main.jsx | 5 | ||||
| -rw-r--r-- | 174bg/manager/src/pages/Overview.jsx | 11 | ||||
| -rw-r--r-- | 174bg/manager/src/theme.css | 194 |
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; |
