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

116 lines
3.7 KiB
TypeScript

import { lazy, Suspense, 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 { DealModal } from "./components/DealModal";
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 { HQ } from "./screens/HQ";
import { Locations } from "./screens/Locations";
import { Market } from "./screens/Market";
import { Pilots } from "./screens/Pilots";
import { StartScreen } from "./screens/StartScreen";
import { usePlayerState, useRefetchOnArrival } from "./state/player";
import { Button } from "./ui";
// The map pulls in PixiJS; load it only when the Map tab is opened.
const MapScreen = lazy(() => import("./screens/MapScreen").then((m) => ({ default: m.MapScreen })));
const SCREEN_COMPONENTS: Record<(typeof SCREENS)[number]["path"], ReactNode> = {
"/": <HQ />,
"/map": (
<Suspense fallback={<p className="p-4 text-ink-400">Loading map…</p>}>
<MapScreen />
</Suspense>
),
"/contracts": <Contracts />,
"/fleet": <Fleet />,
"/pilots": <Pilots />,
"/market": <Market />,
"/locations": <Locations />,
};
/** Syncs the clock with the server once, then ticks it every second. */
function useServerClock() {
const api = useApi();
useEffect(() => {
syncClock(api).catch(() => {
// Offline or server down: keep the device clock until the next launch.
});
return startClockTicker();
}, [api]);
}
/** The whole client. Router, API and query providers come from the caller. */
export function App() {
useServerClock();
const state = usePlayerState();
useRefetchOnArrival(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 tracking-[0.3em] text-ink-400 uppercase">
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 reach the server.</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>Offline — showing last update.</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]}
/>
))}
</Route>
</Routes>
<AwaySummary />
<DealModal />
</>
);
}