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:
2026-06-18 11:00:42 +02:00
parent c2f06a5d2a
commit 49df2015c8
13 changed files with 1870 additions and 49 deletions
+33
View File
@@ -0,0 +1,33 @@
import type { ReactNode } from "react";
// Terminal panel: a bordered, titled box — the basic building block of the dense
// booth layout. Title bar in amber, square corners, subtle layered surfaces.
export function Panel({
title,
right,
children,
className = "",
}: {
title?: string;
/** Optional right-aligned content in the title bar (e.g. a status dot). */
right?: ReactNode;
children: ReactNode;
className?: string;
}) {
return (
<section
className={`flex flex-col border border-term-border bg-term-panel rounded-term overflow-hidden ${className}`}
>
{title && (
<header className="flex items-center justify-between px-3 py-1.5 bg-term-panel-2 border-b border-term-border">
<h2 className="m-0 text-[11px] font-semibold uppercase tracking-wider text-term-amber">
{title}
</h2>
{right}
</header>
)}
<div className="flex-1 min-h-0 p-3">{children}</div>
</section>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { useLiveStore, type WsStatus } from "../lib/live-store.js";
// Small live-connection indicator for the booth chrome: a coloured dot + label
// reflecting the WebSocket status. Green = live, amber = connecting, red = down.
const COLOR: Record<WsStatus, string> = {
open: "bg-term-green",
connecting: "bg-term-amber",
closed: "bg-term-red",
};
const LABEL: Record<WsStatus, string> = {
open: "LIVE",
connecting: "CONNECTING",
closed: "OFFLINE",
};
export function StatusDot() {
const status = useLiveStore((s) => s.status);
return (
<span className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-term-muted">
<span
className={`inline-block h-2 w-2 rounded-full ${COLOR[status]} ${status === "open" ? "" : "animate-pulse"}`}
/>
{LABEL[status]}
</span>
);
}