Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/lib/hex.test.ts src/lib/chain.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/components/dashboard/DashboardStates.test.tsx src/hooks/useDashboardData.test.ts src/lib/countdown.test.ts && node --experimental-test-module-mocks --import tsx --test src/hooks/useRoundSession.reveal.test.tsx src/hooks/useRoundSession.refresh.test.tsx src/hooks/useLiveRound.test.tsx",
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/lib/hex.test.ts src/lib/chain.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/hooks/useDashboardData.test.ts src/lib/countdown.test.ts src/passkey-session.test.ts && node --experimental-test-module-mocks --import tsx --test src/hooks/useRoundSession.reveal.test.tsx src/hooks/useRoundSession.refresh.test.tsx src/hooks/useLiveRound.test.tsx",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
Expand Down
91 changes: 91 additions & 0 deletions apps/web/src/components/dashboard/DashboardContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2026 Sub Rosa contributors
import { motion, type Transition } from "framer-motion";
import type { DashboardState } from "../../hooks/useDashboardData";
import { BidderProgressCard } from "./BidderProgressCard";
import { DashboardEmptyState } from "./DashboardEmptyState";
import { DashboardErrorState } from "./DashboardErrorState";
import { KeeperStatusCard } from "./KeeperStatusCard";
import { RoundStatusCard } from "./RoundStatusCard";
import { SettlementCard } from "./SettlementCard";

function StaleBanner({ fetchedAt }: { fetchedAt: string }) {
const formatted = new Intl.DateTimeFormat(undefined, {
dateStyle: "short",
timeStyle: "short",
}).format(Date.parse(fetchedAt));
return (
<div className="dashboard-stale-banner">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span>
Data may be stale. Last fetched:{" "}
{formatted}
</span>
</div>
);
}

function LoadingState() {
return (
<div className="dashboard-loading-state">
<div className="dashboard-spinner" />
<p>Loading dashboard...</p>
</div>
);
}

// Round data is only reachable in the `ready` branch: narrowing on `status`
// means the loading, empty and error branches have no payload to pass along.
export function DashboardContent({
state,
onRetry,
transition,
}: {
state: DashboardState;
onRetry: () => void;
transition?: Transition;
}) {
switch (state.status) {
case "loading":
return <LoadingState />;
case "error":
return <DashboardErrorState code={state.code} onRetry={onRetry} />;
case "empty":
return <DashboardEmptyState />;
case "ready": {
const { data, stale } = state;
return (
<>
{stale && <StaleBanner fetchedAt={data.meta.fetchedAt} />}
<motion.div
className="dashboard-grid"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ ...transition, delay: 0.1 }}
>
<div className="dashboard-column">
<RoundStatusCard data={data} />
<SettlementCard data={data} />
</div>
<div className="dashboard-column">
<KeeperStatusCard data={data} />
<BidderProgressCard data={data} />
</div>
</motion.div>
</>
);
}
}
}
7 changes: 4 additions & 3 deletions apps/web/src/components/dashboard/DashboardEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Copyright (c) 2026 Sub Rosa contributors
// Rendered only when the load succeeds and reports no round. It takes no props,
// so there is no round or bid data it could display.
export function DashboardEmptyState() {
return (
<div className="dashboard-empty-state">
Expand All @@ -22,12 +24,11 @@ export function DashboardEmptyState() {
</div>
<h2>No Round Data Available</h2>
<p>
The dashboard could not find any round data to display. This might mean:
The dashboard loaded successfully but there is no round to display. This might mean:
</p>
<ul>
<li>No rounds have been created yet</li>
<li>The configured endpoint returned no data</li>
<li>The data source is temporarily unavailable</li>
<li>The configured endpoint returned no round</li>
</ul>
<p className="dashboard-empty-hint">
If using a custom endpoint, verify <code>VITE_DASHBOARD_ENDPOINT</code> is set correctly.
Expand Down
15 changes: 12 additions & 3 deletions apps/web/src/components/dashboard/DashboardErrorState.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
// Copyright (c) 2026 Sub Rosa contributors
import { SDK_ERROR_CODES, type SdkErrorCode } from "@sub-rosa/sdk";

// The only input is an SDK error code. This component deliberately accepts no
// message, round, or bid object, so a failed load has nothing sealed to render.
export function DashboardErrorState({
error,
code,
onRetry,
}: {
error: string;
code: SdkErrorCode;
onRetry: () => void;
}) {
// Re-check against the closed set so an untyped caller cannot pass free text.
const safeCode: SdkErrorCode = SDK_ERROR_CODES.includes(code) ? code : "UNKNOWN";

return (
<div className="dashboard-error-state" role="alert">
<div className="dashboard-error-icon">
Expand All @@ -27,7 +34,9 @@ export function DashboardErrorState({
</svg>
</div>
<h2>Failed to Load Dashboard</h2>
<p className="dashboard-error-message">{error}</p>
<p className="dashboard-error-message">
The dashboard could not load round data. Error code: <code>{safeCode}</code>
</p>
<button type="button" className="primary-action" onClick={onRetry}>
Retry
</button>
Expand Down
Loading