deadhead-space-freight-tycoon/apps/web/src/App.tsx

129 lines
4.7 KiB
TypeScript

import { useEffect, type ReactNode } from "react";
import { Route, Routes } from "react-router";
import { useApi } from "./api/context";
import { startClockTicker, syncClock } from "./clock/clock";
import { AwaySummary } from "./components/AwaySummary";
import { ToastFeed } from "./components/ToastFeed";
import { Toasts } from "./components/Toasts";
import { DealModal } from "./components/DealModal";
import { WelcomeGate } from "./components/Welcome";
import { SkillPointPrompt } from "./components/SkillPointPrompt";
import { formatCredits } from "./format";
import { tabAttention } from "./layout/attention";
import { Layout } from "./layout/Layout";
import { SCREENS } from "./routes";
import { Contracts } from "./screens/Contracts";
import { Fleet } from "./screens/Fleet";
import { Dashboard } from "./screens/Dashboard";
import { HQs } from "./screens/HQs";
import { Locations } from "./screens/Locations";
import { MapScreen } from "./screens/MapScreen";
import { Market } from "./screens/Market";
import { More } from "./screens/More";
import { Pilots } from "./screens/Pilots";
import { Logbook } from "./screens/Logbook";
import { Settings } from "./screens/Settings";
import { useMotionPreference } from "./state/preferences";
import { StartScreen } from "./screens/StartScreen";
import { usePlayerState, useRefetchOnArrival } from "./state/player";
import { RadioCallSheet, useRefetchOnRadio } from "./components/RadioCalls";
import { Button } from "./ui";
const SCREEN_COMPONENTS: Record<(typeof SCREENS)[number]["path"], ReactNode> = {
"/": <Dashboard />,
"/contracts": <Contracts />,
"/fleet": <Fleet />,
"/more": <More />,
};
/** Reads the game clock (the engine's device clock, CR-014), then ticks it every second. */
function useGameClock() {
const api = useApi();
useEffect(() => {
void syncClock(api);
return startClockTicker();
}, [api]);
}
/** The whole client. Router, API and query providers come from the caller. */
export function App() {
useGameClock();
useMotionPreference();
const state = usePlayerState();
useRefetchOnArrival(state.data ?? null, state.refetch);
useRefetchOnRadio(state.data ?? null, state.refetch);
const retry = () => void state.refetch();
if (state.isPending) {
return <p className="grid min-h-dvh place-items-center font-display text-ink-400">Loading…</p>;
}
if (state.data === undefined) {
// The first load failed: nothing to show yet.
return (
<div role="alert" className="grid min-h-dvh place-content-center gap-4 p-6 text-center">
<p className="font-display text-lg text-rose-300">Could not load the game.</p>
<Button onClick={retry} disabled={state.isFetching}>
{state.isFetching ? "Retrying…" : "Retry"}
</Button>
</div>
);
}
if (state.data === null) return <StartScreen />;
// A later refresh failed: keep showing the last state, with a way to retry.
const banner = state.isError ? (
<div
role="alert"
className="flex items-center justify-between gap-2 border-b border-rose-500/40 bg-rose-950/80 px-4 py-2 text-sm text-rose-200 backdrop-blur"
>
<span>Couldn't refresh the game.</span>
<Button variant="danger" size="sm" onClick={retry}>
Retry
</Button>
</div>
) : null;
return (
<>
<Routes>
<Route
element={
<Layout
credits={formatCredits(state.data.company.credits)}
banner={banner}
attention={tabAttention(state.data)}
/>
}
>
{SCREENS.map((screen) => (
<Route
key={screen.path}
index={screen.path === "/"}
path={screen.path === "/" ? undefined : screen.path}
element={SCREEN_COMPONENTS[screen.path]}
/>
))}
{/* Screens without a tab of their own (decision 0.5.167). */}
<Route path="/map" element={<MapScreen />} />
<Route path="/pilots" element={<Pilots />} />
<Route path="/hqs" element={<HQs />} />
<Route path="/market" element={<Market />} />
<Route path="/locations" element={<Locations />} />
{/* Settings opens from the header gear and More, not a tab (CR-011). */}
<Route path="/settings" element={<Settings />} />
{/* The Logbook opens from "While you were away" and the Bridge (CR-025). */}
<Route path="/logbook" element={<Logbook />} />
</Route>
</Routes>
<AwaySummary />
{/* What happens while the player watches (CR-028). */}
<ToastFeed />
<Toasts />
<RadioCallSheet />
<DealModal />
<WelcomeGate />
<SkillPointPrompt />
</>
);
}