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
14 changes: 7 additions & 7 deletions apps/web/src/app/[locale]/epoch1000/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Epoch1000Experience from "@/components/epoch1000/Epoch1000Experience";
import Epoch1000EditionNav from "@/components/epoch1000/Epoch1000EditionNav";
import type { Metadata } from "next";
import { getAlternates, Link } from "@workspace/i18n/routing";
import { getAlternates } from "@workspace/i18n/routing";
import { getTranslations } from "@workspace/i18n/server";

type Props = { params: Promise<{ locale: string }> };
Expand All @@ -14,12 +15,11 @@ export default async function Page() {
return (
<main className="w-full max-w-6xl mx-auto px-5 sm:px-8 pt-6 sm:pt-8 pb-8 sm:pb-12 flex flex-col gap-10 sm:gap-14 min-h-screen">
<header className="flex justify-end border-b border-ep-edge/70 pb-4">
<Link
href="/epoch1000#checker"
className="text-xs font-brand-mono text-ep-dust hover:text-ep-ink transition-colors duration-200 shrink-0 rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4"
>
{t("checkerLink")}
</Link>
<Epoch1000EditionNav
active="wallet"
walletLabel={t("navWallet")}
validatorLabel={t("navValidator")}
/>
</header>

<Epoch1000Experience
Expand Down
160 changes: 160 additions & 0 deletions apps/web/src/app/[locale]/epoch1000/validator/card/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { Metadata } from "next";
import { getAlternates, Link } from "@workspace/i18n/routing";
import { getTranslations } from "@workspace/i18n/server";
import Reveal from "@/components/epoch1000/Reveal";
import { tierFor } from "@/lib/epoch1000/tiers";

interface Props {
params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}

function first(v: string | string[] | undefined): string {
return Array.isArray(v) ? (v[0] ?? "") : (v ?? "");
}

function ogQuery(
sp: Record<string, string | string[] | undefined>,
locale?: string,
): string {
const p = new URLSearchParams();
for (const key of ["s", "fe", "c", "t", "x", "w", "k", "vl"]) {
const v = first(sp[key]);
if (v) p.set(key, v);
}
if (!p.has("k")) p.set("k", "validator");
if (locale) p.set("l", locale);
return p.toString();
}

export async function generateMetadata({
params,
searchParams,
}: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({
locale,
namespace: "epoch1000.validator.card",
});
const sp = await searchParams;
const survived = first(sp.s) || "0";
const capped = first(sp.x) === "1";
const firstEpoch = first(sp.fe);
const tier = tierFor(parseInt(survived, 10) || 0);
const survivedLabel = `${survived}${capped ? "+" : ""}`;
const title = t("meta.title", { survived: survivedLabel });
const description = t(
firstEpoch
? "meta.description.withFirstEpoch"
: "meta.description.withoutFirstEpoch",
{
survived: survivedLabel,
firstEpoch,
tierLine: t(`tiers.${tier.id}.line`),
},
);
const image = `/api/epoch1000/og?${ogQuery(sp, locale)}`;
return {
title,
description,
alternates: getAlternates("/epoch1000/validator/card", locale),
openGraph: {
title,
description,
images: [{ url: image, width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [image],
},
};
}

export default async function ValidatorCardPage({
params,
searchParams,
}: Props) {
const { locale } = await params;
const t = await getTranslations({
locale,
namespace: "epoch1000.validator.card",
});
const sp = await searchParams;
const query = ogQuery(sp, locale);
const survived = Math.max(0, parseInt(first(sp.s), 10) || 0);
const capped = first(sp.x) === "1";
const firstEpoch = first(sp.fe);
const voteAccount = first(sp.w);
const tier = tierFor(survived);
const tierName = t(`tiers.${tier.id}.name`);
const survivedLabel = `${survived}${capped ? "+" : ""}`;
const voteAccountLabel = voteAccount || t("summary.defaultVoteAccount");

return (
<main className="w-full max-w-3xl mx-auto px-5 sm:px-8 py-12 sm:py-20 flex flex-col items-center text-center gap-8 sm:gap-10 min-h-screen">
<header className="flex flex-col items-center gap-4 sm:gap-5">
<p className="font-brand-mono text-xs sm:text-sm tracking-[0.2em] text-ep-dust uppercase">
{t("eyebrow")}
</p>
<h1
className="font-black tracking-tight text-4xl sm:text-6xl leading-none"
style={{ color: tier.color }}
>
{t("heading", { survived: survivedLabel })}
</h1>
<p className="text-sm sm:text-base text-ep-dim max-w-xl">
{t.rich(
firstEpoch ? "summary.withFirstEpoch" : "summary.withoutFirstEpoch",
{
voteAccountLabel,
survived: survivedLabel,
firstEpoch,
voteAccount: (chunks) =>
voteAccount ? (
<span className="font-mono text-ep-ink">{chunks}</span>
) : (
<>{chunks}</>
),
strong: (chunks) => (
<span className="font-semibold text-ep-ink">{chunks}</span>
),
},
)}
</p>
</header>

<Reveal className="w-full">
<img
src={`/api/epoch1000/og?${query}`}
alt={t("imageAlt", { tier: tierName, survived: survivedLabel })}
className="w-full rounded-xl border border-ep-edge"
style={{ boxShadow: `0 20px 100px -30px ${tier.color}66` }}
width={1200}
height={630}
/>
</Reveal>

<Reveal className="flex flex-col items-center gap-6 sm:gap-8">
<div className="flex flex-col items-center gap-3">
<div className="flex flex-wrap justify-center gap-3">
<Link
href="/epoch1000/validator#checker"
className="bg-ep-ink text-ep-void font-semibold rounded-full px-7 py-3 text-sm hover:bg-ep-dim transition-colors duration-200"
>
{t("ctaCheck")}
</Link>
<Link
href="/epoch1000/validator"
className="border border-ep-edge rounded-full px-7 py-3 text-sm text-ep-dim hover:text-ep-ink transition-colors duration-200"
>
{t("ctaExplore")}
</Link>
</div>
<p className="text-xs text-ep-dust">{t("privacyNote")}</p>
</div>
</Reveal>
</main>
);
}
71 changes: 71 additions & 0 deletions apps/web/src/app/[locale]/epoch1000/validator/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import ValidatorEpoch1000Experience from "@/components/epoch1000/ValidatorEpoch1000Experience";
import Epoch1000EditionNav from "@/components/epoch1000/Epoch1000EditionNav";
import type { Metadata } from "next";
import { getAlternates } from "@workspace/i18n/routing";
import { getTranslations } from "@workspace/i18n/server";

type Props = { params: Promise<{ locale: string }> };

const VALIDATOR_PATH = "/epoch1000/validator";
const VALIDATOR_SOCIAL_IMAGE =
"/api/epoch1000/og?s=1000&fe=0&c=1000&k=validator";

export default async function Page() {
const t = await getTranslations("epoch1000.validator.page");
const nav = await getTranslations("epoch1000.page");

return (
<main className="w-full max-w-6xl mx-auto px-5 sm:px-8 pt-6 sm:pt-8 pb-8 sm:pb-12 flex flex-col gap-10 sm:gap-14 min-h-screen">
<header className="flex justify-end border-b border-ep-edge/70 pb-4">
<Epoch1000EditionNav
active="validator"
walletLabel={nav("navWallet")}
validatorLabel={nav("navValidator")}
/>
</header>

<ValidatorEpoch1000Experience
title={t.rich("title", {
gradient: (chunks) => (
<span className="text-sol-gradient">{chunks}</span>
),
})}
subtitle={t("subtitle")}
description={t("description")}
/>
</main>
);
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations("epoch1000.validator.meta");
const title = t("title");
const description = t("description");
const alternates = getAlternates(VALIDATOR_PATH, locale);

return {
title,
description,
alternates,
openGraph: {
title: t("openGraphTitle"),
description: t("openGraphDescription"),
type: "website",
url: alternates.canonical,
images: [
{
url: VALIDATOR_SOCIAL_IMAGE,
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [VALIDATOR_SOCIAL_IMAGE],
},
};
}
Loading
Loading