feat(web): frontend foundation — Tailwind terminal theme, Query, Router, Zustand + live booth screen
Add tailwindcss (Bloomberg-terminal theme in index.css), @tanstack/react-query + react-router, zustand, and Radix primitives. Router with role-guarded routes; QueryClient wrapping the existing apiFetch; a small Zustand live store fed by a /api/ws client that invalidates Query caches. Booth screen: live occupancy gauge + streaming entry/exit/payment feed. Vite proxies the WS upgrade. Note: BoothScreen references the pay/exit modal + active-sessions panel added in following commits; final HEAD builds.
This commit is contained in:
+22
-39
@@ -1,15 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchMe, logout, type SessionUser } from "./api.js";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
import { fetchMe, type SessionUser } from "./api.js";
|
||||
import { Login } from "./Login.js";
|
||||
import { PermitManager } from "./PermitManager.js";
|
||||
import { SetupWizard } from "./SetupWizard.js";
|
||||
import { ShiftControl } from "./ShiftControl.js";
|
||||
import { SiteSettings } from "./SiteSettings.js";
|
||||
import { TariffComposer } from "./TariffComposer.js";
|
||||
import { queryClient } from "./lib/query.js";
|
||||
import { router } from "./router.js";
|
||||
|
||||
// Operator UI shell. Plain React (no admin framework) — the operator UI is
|
||||
// simple enough that a framework's abstractions cost more than they save.
|
||||
// Auth is cookie-based; the SPA bootstraps the session from /api/auth/me.
|
||||
// App root: bootstraps the session (cookie-based, from /api/auth/me), then hands
|
||||
// off to TanStack Router inside the QueryClient provider. The router renders the
|
||||
// terminal chrome + screens; auth gating stays here (Login until signed in), and
|
||||
// the signed-in user flows into the router context for role-based route guards.
|
||||
// See wiki/entities/react-vite-spa.md and local-jwt-auth.md.
|
||||
|
||||
export function App() {
|
||||
@@ -22,37 +22,20 @@ export function App() {
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) return <p style={{ fontFamily: "system-ui", padding: "2rem" }}>Loading…</p>;
|
||||
if (!user) return <Login onLoggedIn={setUser} />;
|
||||
if (loading) {
|
||||
return <div className="flex h-screen items-center justify-center text-term-muted">loading…</div>;
|
||||
}
|
||||
if (!user) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Login onLoggedIn={setUser} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{ fontFamily: "system-ui", padding: "2rem", maxWidth: 720 }}>
|
||||
<header style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
|
||||
<h1 style={{ margin: 0 }}>Parking System</h1>
|
||||
<span style={{ color: "#555" }}>
|
||||
{user.username} ({user.role}){" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await logout();
|
||||
setUser(null);
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</span>
|
||||
</header>
|
||||
<SiteSettings canEdit={user.role === "admin"} />
|
||||
{user.role !== "readonly" && <ShiftControl />}
|
||||
{user.role === "admin" ? (
|
||||
<>
|
||||
<SetupWizard />
|
||||
<TariffComposer />
|
||||
<PermitManager />
|
||||
</>
|
||||
) : (
|
||||
<p style={{ marginTop: "1rem" }}>Signed in. (Operator console coming soon.)</p>
|
||||
)}
|
||||
</main>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} context={{ user, setUser }} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user