Skip to content
Merged
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
Binary file removed public/logos/aster.jpg
Binary file not shown.
36 changes: 18 additions & 18 deletions public/logos/aster.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/logos/defi-saver.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 0 additions & 33 deletions public/logos/defi-saver.svg

This file was deleted.

64 changes: 43 additions & 21 deletions src/app/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
PERP_PRODUCT_PILL_SLUGS,
} from "@/lib/perp-venue-context";
import { PerpVenueSection } from "@/components/perp-venue-section";
import { VenueKpiToggle } from "@/components/venue-kpi-toggle";
import { PmDataFeedSection } from "@/components/pm-data-feed-section";
import { RpcProviderChainsSection } from "@/components/rpc-provider-chains-section";

Expand Down Expand Up @@ -540,16 +541,48 @@ export default async function ProviderPage({

{hlStats && <HlBuilderDashboard stats={hlStats} name={p.name} />}

{pmContext?.kind === "venue" && (
<PmVenueSection
slug={pmContext.slug}
name={pmContext.name}
chainLabel={pmContext.chainLabel}
externalUrl={pmContext.externalUrl}
venueType={pmContext.venueType}
benchRows={pmContext.benchRows}
/>
)}
{(() => {
// Cohort sections. When a product belongs to BOTH the PM venue
// cohort and the perp cohort, wrap the two sections in a pill
// toggle so both stay reachable on the same page. With a single
// cohort the section renders directly, no toggle bar.
const pmVenueSection =
pmContext?.kind === "venue" ? (
<PmVenueSection
slug={pmContext.slug}
name={pmContext.name}
chainLabel={pmContext.chainLabel}
externalUrl={pmContext.externalUrl}
venueType={pmContext.venueType}
benchRows={pmContext.benchRows}
/>
) : null;
const perpVenueSection = perpContext ? (
<PerpVenueSection
slug={perpContext.slug}
cohortSlug={perpContext.cohortSlug}
name={perpContext.name}
chainLabel={perpContext.chainLabel}
externalUrl={perpContext.externalUrl}
benchRows={perpContext.benchRows}
/>
) : null;
if (pmVenueSection && perpVenueSection) {
return (
<VenueKpiToggle
sections={[
{
id: "pm",
label: "Prediction markets",
content: pmVenueSection,
},
{ id: "perp", label: "Perpetuals", content: perpVenueSection },
]}
/>
);
}
return pmVenueSection ?? perpVenueSection;
})()}
{pmContext?.kind === "feed" && (
<PmDataFeedSection
slug={pmContext.slug}
Expand All @@ -560,17 +593,6 @@ export default async function ProviderPage({
/>
)}

{perpContext && (
<PerpVenueSection
slug={perpContext.slug}
cohortSlug={perpContext.cohortSlug}
name={perpContext.name}
chainLabel={perpContext.chainLabel}
externalUrl={perpContext.externalUrl}
benchRows={perpContext.benchRows}
/>
)}

{reg && (
<section className="mt-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:gap-8">
<p className="text-base text-ink-soft leading-relaxed max-w-2xl">
Expand Down
66 changes: 66 additions & 0 deletions src/components/venue-kpi-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { useState } from "react";

/**
* Pill toggle that swaps between pre-rendered venue sections on
* /products/<slug> without navigation. The sections are server
* components rendered by the page and passed in as ReactNode content,
* so switching tabs costs zero network round trips.
*
* Callers only mount this when at least two sections exist; with a
* single cohort the page renders the section directly (a one-button
* toggle bar would be noise). Inactive sections stay in the DOM under
* `hidden` so tab switches are instant and anchors keep working.
*
* Pill styling mirrors PmHubTabs / PerpHubTabs.
*/

export type VenueToggleSection = {
id: string;
label: string;
content: React.ReactNode;
};

export function VenueKpiToggle({
sections,
}: {
sections: VenueToggleSection[];
}) {
const [active, setActive] = useState(sections[0]?.id ?? "");

if (sections.length === 0) return null;
if (sections.length === 1) return <>{sections[0].content}</>;

return (
<div className="mt-10">
<div
className="inline-flex rounded-lg border border-ink/15 p-1 bg-paper-soft/40"
role="tablist"
aria-label="Venue cohorts"
>
{sections.map((s) => (
<button
key={s.id}
type="button"
role="tab"
aria-selected={active === s.id}
onClick={() => setActive(s.id)}
className={`px-4 py-1.5 rounded-md text-[13px] font-medium transition-colors ${
active === s.id
? "bg-paper text-ink shadow-sm"
: "text-ink-soft hover:text-ink"
}`}
>
{s.label}
</button>
))}
</div>
{sections.map((s) => (
<div key={s.id} hidden={active !== s.id}>
{s.content}
</div>
))}
</div>
);
}
2 changes: 1 addition & 1 deletion src/lib/logo-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ const RAW: Record<string, string> = {
// Identified via on-chain HL referral codes + brand cross-reference
// (DFS, UNITYWALLET, INVO, MARSGO, BITGETWALLET). See builders.json
// notes on the HL node for the provenance trail per address.
"defi-saver": "/logos/defi-saver.svg",
"defi-saver": "/logos/defi-saver.jpg",
unitywallet: "/logos/unitywallet.png",
invo: "/logos/invo.png",
marsgo: "/logos/marsgo.jpg",
Expand Down
31 changes: 28 additions & 3 deletions src/lib/perp-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function promUrl(): string | null {
* Bumped any time the PerpCohortSummary shape changes so a stale-shape
* blob from a previous deploy can never deserialize into a misaligned
* payload. The cohort-snapshot module appends its own `:v1` suffix. */
const PERP_COHORT_KEY = "perp-cohort";
export const PERP_COHORT_KEY = "perp-cohort";

/**
* Fetch the cohort in one Promise.all fan out. Returns null when Prom
Expand Down Expand Up @@ -352,7 +352,7 @@ export async function fetchPerpCohort(): Promise<PerpCohortSummary | null> {
* three asset columns plus the fee column round-trip Prom at most once
* every two minutes regardless of page traffic.
*/
async function fetchPerpByAssetMatrixRaw(): Promise<PerpAssetRow[]> {
export async function fetchPerpByAssetMatrixFresh(): Promise<PerpAssetRow[]> {
const url = promUrl();
if (!url) return [];
let prom: Prometheus;
Expand Down Expand Up @@ -427,9 +427,34 @@ async function fetchPerpByAssetMatrixRaw(): Promise<PerpAssetRow[]> {
);
}

const PERP_BY_ASSET_KEY = "perp-by-asset";

// Snapshot-first, mirroring fetchPerpCohortRaw: Vercel has no
// PROMETHEUS_URL (the VPS Prom is not public), so the live path only
// works for the worker; readers get the worker-written blob.
async function fetchPerpByAssetMatrixRaw(): Promise<PerpAssetRow[]> {
const snapshot = await readCohortSnapshot<PerpAssetRow[]>(PERP_BY_ASSET_KEY);
if (snapshot && Array.isArray(snapshot.data) && snapshot.data.length > 0) {
return snapshot.data;
}
const fresh = await fetchPerpByAssetMatrixFresh();
if (fresh.length > 0) {
try {
await writeCohortSnapshot(PERP_BY_ASSET_KEY, fresh);
} catch (err) {
console.warn(
`perp-by-asset writeback failed: ${
err instanceof Error ? err.message : String(err)
}`,
);
}
}
return fresh;
}

const fetchPerpByAssetMatrixCached = unstable_cache(
fetchPerpByAssetMatrixRaw,
["perp-by-asset-matrix-v1"],
["perp-by-asset-matrix-v2"],
{ revalidate: 120, tags: ["perp-by-asset"] },
);

Expand Down
Loading
Loading