From 1cb0c15c1d4e810a7fbf473d9296bd0433ffeb52 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:42:21 +0200 Subject: [PATCH 1/5] share-card modal: scope dropdowns for chain/region/venue/kind --- src/components/share-section.tsx | 241 ++++++++++++++++++++++++++++--- 1 file changed, 220 insertions(+), 21 deletions(-) diff --git a/src/components/share-section.tsx b/src/components/share-section.tsx index d7131d56..4c3fc535 100644 --- a/src/components/share-section.tsx +++ b/src/components/share-section.tsx @@ -5,6 +5,24 @@ import { Download, Image as ImageIcon, Loader2, X } from "lucide-react"; import type { Benchmark } from "@/types/benchmark"; import { Hint } from "@/components/hint"; +/** Dimensions the share-card handler on the server understands. Kept in + * sync with the pickOption() dims in + * src/app/benchmarks/[slug]/share-card/route.tsx. */ +const SCOPE_DIMS = ["chain", "region", "venue", "kind"] as const; +type ScopeDim = (typeof SCOPE_DIMS)[number]; + +/** Human-readable label for each dimension dropdown. */ +const DIM_LABEL: Record = { + chain: "Chain", + region: "Region", + venue: "Venue", + kind: "Kind", +}; + +/** Sentinel value the dropdowns use for "no filter on this dimension". + * Same as the bench detail page and the share-card server handler. */ +const ALL_VALUE = "all"; + type Template = { id: string; label: string; @@ -59,6 +77,64 @@ export function ShareSection({ slug, title, benchmark, chain }: Props) { const [activeId, setActiveId] = useState("ranking"); const [open, setOpen] = useState(false); + // Which dimensions this bench actually declares. Only those get a + // dropdown in the modal; the rest are hidden. Kept as a plain object + // so template rendering can `dimOptions.venue?.length` without a + // per-render `.filter().find()` pass. + const dimOptions = useMemo(() => { + const out: Partial> = {}; + for (const dim of SCOPE_DIMS) { + const opts = benchmark.dimensions?.[dim] ?? []; + // Drop the synthetic "all" option if the spec includes it: the + // modal always prepends its own "All" entry as the default so + // duplicating breaks the value equality check on select onChange. + const filtered = opts.filter((o) => o.value !== ALL_VALUE); + if (filtered.length > 0) out[dim] = filtered; + } + return out; + }, [benchmark]); + const hasAnyDim = Object.keys(dimOptions).length > 0; + + // Scope state: one entry per SCOPE_DIMS. Default is "all" per dim. + // Initialised once from window.location when the modal opens, so the + // dropdowns start on whatever the user was filtering on the dashboard + // (not surprising) but can be changed freely from inside the modal. + const [scope, setScope] = useState>({ + chain: chain ?? ALL_VALUE, + region: ALL_VALUE, + venue: ALL_VALUE, + kind: ALL_VALUE, + }); + // Re-sync scope from URL every time the modal opens: reader might + // have changed a chain / region tab on the dashboard between clicks, + // and expecting a fresh open to reflect that is less surprising than + // stale sticky state. + useEffect(() => { + if (!open) return; + if (typeof window === "undefined") return; + const url = new URL(window.location.href); + const next: Record = { + chain: url.searchParams.get("chain") || ALL_VALUE, + region: url.searchParams.get("region") || ALL_VALUE, + venue: url.searchParams.get("venue") || ALL_VALUE, + kind: url.searchParams.get("kind") || ALL_VALUE, + }; + // Coerce values the current bench does not offer back to "all". A + // chain the reader was previously filtering by can vanish between + // deploys (spec edit); silently falling back beats a broken picker. + for (const dim of SCOPE_DIMS) { + const opts = dimOptions[dim]; + if ( + opts && + next[dim] !== ALL_VALUE && + !opts.some((o) => o.value === next[dim]) + ) { + next[dim] = ALL_VALUE; + } + } + setScope(next); + }, [open, dimOptions]); + // Lock body scroll while modal open and close on Escape. useEffect(() => { if (!open) return; @@ -123,28 +199,19 @@ export function ShareSection({ slug, title, benchmark, chain }: Props) { setPairB(s); } - // Build the URL with the right params per template. + // Build the URL with the right params per template. Scope state is + // the source of truth: the modal dropdowns own it, so the preview + // and download always agree with what the user picked inside the + // modal (not what happened to be in window.location at open time). const cardSrc = (templateId: string) => { const tpl = TEMPLATES.find((t) => t.id === templateId); - // Read every dimension filter from the live URL so the share-card - // stays in sync when the user flips a chain / region / kind / venue - // tab client-side. - const liveUrl = - typeof window !== "undefined" - ? new URL(window.location.href) - : null; - const liveChain = liveUrl - ? liveUrl.searchParams.get("chain") - : chain ?? null; - const chainParam = liveChain ? `&chain=${encodeURIComponent(liveChain)}` : ""; - const liveRegion = liveUrl ? liveUrl.searchParams.get("region") : null; - const regionParam = liveRegion - ? `®ion=${encodeURIComponent(liveRegion)}` - : ""; - const liveKind = liveUrl ? liveUrl.searchParams.get("kind") : null; - const kindParam = liveKind ? `&kind=${encodeURIComponent(liveKind)}` : ""; - const liveVenue = liveUrl ? liveUrl.searchParams.get("venue") : null; - const venueParam = liveVenue ? `&venue=${encodeURIComponent(liveVenue)}` : ""; + const dimParams: string[] = []; + for (const dim of SCOPE_DIMS) { + const val = scope[dim]; + if (val && val !== ALL_VALUE) { + dimParams.push(`&${dim}=${encodeURIComponent(val)}`); + } + } // Mirror the active site theme so the exported PNG matches what the // user is looking at. SSR can't read the dark state - default to light // server-side, the client re-renders with `dark` once mounted. @@ -152,7 +219,7 @@ export function ShareSection({ slug, title, benchmark, chain }: Props) { typeof window !== "undefined" && document.documentElement.classList.contains("dark"); const themeParam = isDark ? "&theme=dark" : ""; - const base = `/benchmarks/${slug}/share-card?template=${templateId}${chainParam}${regionParam}${kindParam}${venueParam}${themeParam}`; + const base = `/benchmarks/${slug}/share-card?template=${templateId}${dimParams.join("")}${themeParam}`; if (!tpl) return base; if (tpl.pick === "multi") { if ( @@ -232,6 +299,25 @@ export function ShareSection({ slug, title, benchmark, chain }: Props) { Pick a layout and download a 1200×630 PNG ready for Twitter, Reddit, LinkedIn or any OG-card embed. Same data, same colors as this dashboard.

+ {hasAnyDim && ( + + setScope((prev) => ({ ...prev, [dim]: value })) + } + onReset={() => + setScope({ + chain: ALL_VALUE, + region: ALL_VALUE, + venue: ALL_VALUE, + kind: ALL_VALUE, + }) + } + slug={slug} + /> + )} + {/* Tabs */}
{TEMPLATES.map((t) => ( @@ -435,6 +521,119 @@ export function ShareSection({ slug, title, benchmark, chain }: Props) { ); } +/** Dropdown row that lets the reader override chain / region / venue / + * kind from inside the modal. Only rendered for dims the bench actually + * declares (chain-less benches never see a chain dropdown, etc.). + * + * Pings /api/stat/? after each pick so the user sees a + * "no data for this cell" hint before wasting a PNG download. The stat + * endpoint returns 404 on unknown filter combos (see route.ts comment + * "does NOT fall back to the unfiltered aggregate"), which is exactly + * the signal we need. */ +function ScopePicker({ + dimOptions, + scope, + onChange, + onReset, + slug, +}: { + dimOptions: Partial>; + scope: Record; + onChange: (dim: ScopeDim, value: string) => void; + onReset: () => void; + slug: string; +}) { + const activeDims = SCOPE_DIMS.filter((d) => dimOptions[d]); + const hasFilter = activeDims.some((d) => scope[d] !== ALL_VALUE); + const [status, setStatus] = useState<"ok" | "empty" | "checking" | "idle">( + "idle", + ); + + // Any dim change triggers a data-existence probe. AbortController + // discards stale responses if the reader clicks a second dropdown + // before the first request comes back. + useEffect(() => { + if (!hasFilter) { + setStatus("idle"); + return; + } + const ctrl = new AbortController(); + setStatus("checking"); + const qs = new URLSearchParams(); + for (const d of activeDims) { + if (scope[d] !== ALL_VALUE) qs.set(d, scope[d]); + } + const url = `/api/stat/${slug}?${qs.toString()}`; + fetch(url, { signal: ctrl.signal }) + .then((r) => { + if (r.status === 404) return { rankings: [] as unknown[] }; + return r.json(); + }) + .then((json: { rankings?: unknown[]; leader?: unknown } | undefined) => { + const hasRows = + !!json && Array.isArray(json.rankings) && json.rankings.length > 0; + setStatus(hasRows ? "ok" : "empty"); + }) + .catch((err) => { + if ((err as Error).name === "AbortError") return; + setStatus("empty"); + }); + return () => ctrl.abort(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [slug, scope.chain, scope.region, scope.venue, scope.kind]); + + return ( +
+
+ + Scope + + {activeDims.map((dim) => { + const opts = dimOptions[dim]!; + return ( + + ); + })} + {hasFilter && ( + + )} +
+ {status === "checking" && ( +

+ Checking data for this cell… +

+ )} + {status === "empty" && ( +

+ No data for this scope. Card will fall back to the aggregate. +

+ )} +
+ ); +} + /** Preview frame with a "Generating preview…" overlay while the PNG loads. */ function SharePreview({ src, alt }: { src: string; alt: string }) { const [loaded, setLoaded] = useState(false); From b30aa3b1c3601403ad11f420ae4379ce81bc64b2 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:42:42 +0200 Subject: [PATCH 2/5] fix: unified bridge table with logos, fix success rate (remove *100) --- src/app/bridge/page.tsx | 186 +++++----------------------- src/components/bridge-hub-table.tsx | 163 ++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 156 deletions(-) create mode 100644 src/components/bridge-hub-table.tsx diff --git a/src/app/bridge/page.tsx b/src/app/bridge/page.tsx index 53ca651a..a2fabdbe 100644 --- a/src/app/bridge/page.tsx +++ b/src/app/bridge/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { fetchBridgeHub } from "@/lib/bridge-hub-stats"; -import type { BridgeProviderRow } from "@/lib/bridge-hub-stats"; +import { BridgeHubTable } from "@/components/bridge-hub-table"; import { pageMetadata } from "@/lib/page-metadata"; import { safeJsonLd, buildBreadcrumbJsonLd } from "@/lib/jsonld"; import { SITE } from "@/data/site"; @@ -43,14 +43,6 @@ export default async function BridgeHubPage() { } : null; - const byFee = hub?.providers ?? []; - - const bySpeed = hub - ? [...hub.providers] - .filter((p) => p.quotep50 != null) - .sort((a, b) => (a.quotep50 ?? Infinity) - (b.quotep50 ?? Infinity)) - : []; - return (
-
+

- Cost ranking + Bridge leaderboard

- - per-corridor breakdown - +
+ + fee breakdown + + + quote speed breakdown + +

- All-in fee at $300 USDC (fees + slippage + destination gas) as - % of notional. p50 over 24h, cross-corridor average. Lower is - better. + Sorted by fee p50 (all-in cost at $300 USDC). Quote columns from + bench 002. Success from fee bench. Trailing 24h, eu-west, cross-corridor average.

- -
- -
-
-

- Quote speed ranking -

- - per-corridor breakdown - -
-

- Time to receive a usable quote, wall-clock ms. p50 over 24h, - cross-corridor average. Lower is better. + +

+ Across is not in the quote latency bench (dashes in quote columns). + Per-corridor splits on the individual bench pages.

-
@@ -279,20 +263,19 @@ export default async function BridgeHubPage() {