aboutsummaryrefslogtreecommitdiff
path: root/174bg/manager/src
diff options
context:
space:
mode:
Diffstat (limited to '174bg/manager/src')
-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
5 files changed, 324 insertions, 40 deletions
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;