126 lines
4.4 KiB
TypeScript
126 lines
4.4 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 { 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 />,
|
|
"/map": <MapScreen />,
|
|
"/contracts": <Contracts />,
|
|
"/fleet": <Fleet />,
|
|
"/pilots": <Pilots />,
|
|
"/hqs": <HQs />,
|
|
"/market": <Market />,
|
|
"/locations": <Locations />,
|
|
};
|
|
|
|
/** 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]}
|
|
/>
|
|
))}
|
|
{/* Settings opens from the header gear, 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 />
|
|
</>
|
|
);
|
|
}
|