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
110 changes: 56 additions & 54 deletions src/app/solve/SolvePageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { useAcceptIntent } from "@/hooks/useAcceptIntent";
import { useSolverRegistration } from "@/hooks/useSolverRegistration";
import { timeRemaining } from "@/lib/time";
import { isValidStellarPublicKey } from "@/lib/stellarAddress";
import { getMessage } from "@/i18n/messages";
import { useTranslation } from "@/lib/i18n/I18nProvider";
import type { MessageKey } from "@/lib/i18n";
import { formatCurrency } from "@/lib/format";
import Link from "next/link";

Expand All @@ -21,14 +22,15 @@ const usdCompact = (value: number) =>

const MIN_BOND_USD = 50;

const REGISTRATION_LABEL: Record<string, string> = {
connecting: getMessage("solve.register.states.connecting"),
building: getMessage("solve.register.states.building"),
"awaiting-signature": getMessage("solve.register.states.awaitingSignature"),
submitting: getMessage("solve.register.states.submitting"),
const REGISTRATION_LABEL_KEY: Record<string, MessageKey> = {
connecting: "solve.register.states.connecting",
building: "solve.register.states.building",
"awaiting-signature": "solve.register.states.awaitingSignature",
submitting: "solve.register.states.submitting",
};

export default function SolvePageClient() {
const { t } = useTranslation();
const [tab, setTab] = useState<"leaderboard" | "intents" | "register">("leaderboard");
const { solvers, isLoading: solversLoading, error: solversError } = useSolvers();
const { intents: openIntents, isLoading: intentsLoading, error: intentsError } = useOpenIntents();
Expand All @@ -37,15 +39,15 @@ export default function SolvePageClient() {
const [address, setAddress] = useState("");
const [bond, setBond] = useState("");
const registration = useSolverRegistration();
const isRegistering = registration.status in REGISTRATION_LABEL;
const isRegistering = registration.status in REGISTRATION_LABEL_KEY;

const addressError =
address && !isValidStellarPublicKey(address)
? getMessage("solve.register.validation.invalidAddress")
? t("solve.register.validation.invalidAddress")
: null;
const bondError =
bond && (isNaN(parseFloat(bond)) || parseFloat(bond) < MIN_BOND_USD)
? getMessage("solve.register.validation.minimumBond", { minBond: MIN_BOND_USD })
? t("solve.register.validation.minimumBond", { minBond: MIN_BOND_USD })
: null;
const canRegister =
Boolean(address) && Boolean(bond) && !addressError && !bondError && !isRegistering;
Expand All @@ -63,7 +65,7 @@ export default function SolvePageClient() {

return (
<div className="min-h-screen">
<Nav variant="breadcrumb" label={getMessage("solve.nav.label")} />
<Nav variant="breadcrumb" label={t("solve.nav.label")} />

<main id="main-content" className="max-w-5xl mx-auto px-3 sm:px-5 py-8 sm:py-12">
<div className="mb-8 sm:mb-10">
Expand All @@ -81,19 +83,19 @@ export default function SolvePageClient() {
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 sm:gap-4 mb-8 sm:mb-10">
{[
{
n: getMessage("solve.steps.registerBond.number"),
title: getMessage("solve.steps.registerBond.title"),
body: getMessage("solve.steps.registerBond.body"),
n: t("solve.steps.registerBond.number"),
title: t("solve.steps.registerBond.title"),
body: t("solve.steps.registerBond.body"),
},
{
n: getMessage("solve.steps.watchIntentFeed.number"),
title: getMessage("solve.steps.watchIntentFeed.title"),
body: getMessage("solve.steps.watchIntentFeed.body"),
n: t("solve.steps.watchIntentFeed.number"),
title: t("solve.steps.watchIntentFeed.title"),
body: t("solve.steps.watchIntentFeed.body"),
},
{
n: getMessage("solve.steps.fillAndEarn.number"),
title: getMessage("solve.steps.fillAndEarn.title"),
body: getMessage("solve.steps.fillAndEarn.body"),
n: t("solve.steps.fillAndEarn.number"),
title: t("solve.steps.fillAndEarn.title"),
body: t("solve.steps.fillAndEarn.body"),
},
].map((item) => (
<div key={item.n} className="card p-4 sm:p-5">
Expand All @@ -110,22 +112,22 @@ export default function SolvePageClient() {
aria-label="Solver portal sections"
className="flex gap-1 mb-6 bg-vx-surface/50 p-1 rounded-lg w-fit overflow-x-auto"
>
{(["leaderboard", "intents", "register"] as const).map((t) => (
{(["leaderboard", "intents", "register"] as const).map((tabId) => (
<button
key={t}
key={tabId}
type="button"
role="tab"
id={`tab-${t}`}
aria-selected={tab === t}
aria-controls={`panel-${t}`}
onClick={() => setTab(t)}
id={`tab-${tabId}`}
aria-selected={tab === tabId}
aria-controls={`panel-${tabId}`}
onClick={() => setTab(tabId)}
className={`px-3 sm:px-4 py-2 rounded-md text-xs sm:text-sm font-medium capitalize transition-all whitespace-nowrap
${tab === t
${tab === tabId
? "bg-vx-card text-vx-text border border-vx-border"
: "text-vx-muted hover:text-vx-text"
}`}
>
{getMessage(`solve.tabs.${t}`)}
{t(`solve.tabs.${tabId}`)}
</button>
))}
</div>
Expand All @@ -140,7 +142,7 @@ export default function SolvePageClient() {
>
<div className="px-5 py-3.5 border-b border-vx-border bg-vx-surface/30">
<span className="text-sm font-semibold text-vx-text">
{getMessage("solve.leaderboard.title")}
{t("solve.leaderboard.title")}
</span>
</div>

Expand All @@ -150,11 +152,11 @@ export default function SolvePageClient() {
</div>
) : solversError ? (
<div className="p-8 text-center text-sm text-vx-muted">
{getMessage("solve.leaderboard.error")}
{t("solve.leaderboard.error")}
</div>
) : solvers.length === 0 ? (
<div className="p-8 text-center text-sm text-vx-muted">
{getMessage("solve.leaderboard.empty")}
{t("solve.leaderboard.empty")}
</div>
) : (
<div className="divide-y divide-vx-line">
Expand Down Expand Up @@ -188,23 +190,23 @@ export default function SolvePageClient() {
<div>
<div className="num text-xs sm:text-sm font-semibold text-vx-text">{s.fills}</div>
<div className="eyebrow text-[10px] sm:text-xs">
{getMessage("solve.leaderboard.fills")}
{t("solve.leaderboard.fills")}
</div>
</div>
<div>
<div className="num text-xs sm:text-sm font-semibold text-vx-text">
{usdCompact(s.volumeUsd)}
</div>
<div className="eyebrow text-[10px] sm:text-xs">
{getMessage("solve.leaderboard.volume")}
{t("solve.leaderboard.volume")}
</div>
</div>
<div>
<div className="num text-xs sm:text-sm font-semibold text-vx-text">
{s.avgFillTimeSeconds}s
</div>
<div className="eyebrow text-[10px] sm:text-xs">
{getMessage("solve.leaderboard.avgTime")}
{t("solve.leaderboard.avgTime")}
</div>
</div>
<div>
Expand All @@ -216,7 +218,7 @@ export default function SolvePageClient() {
{s.successRatePct}%
</div>
<div className="eyebrow text-[10px] sm:text-xs">
{getMessage("solve.leaderboard.success")}
{t("solve.leaderboard.success")}
</div>
</div>
</div>
Expand All @@ -238,11 +240,11 @@ export default function SolvePageClient() {
>
<div className="px-5 py-3.5 border-b border-vx-border bg-vx-surface/30 flex items-center justify-between">
<span className="text-sm font-semibold text-vx-text">
{getMessage("solve.intents.title")}
{t("solve.intents.title")}
</span>
<span className="chip bg-vx-sage-bg text-vx-sage text-[10px]">
<span className="w-1.5 h-1.5 rounded-full bg-vx-sage animate-pulse" />
{getMessage("solve.intents.available", { count: openIntents.length })}
{t("solve.intents.available", { count: openIntents.length })}
</span>
</div>

Expand All @@ -258,11 +260,11 @@ export default function SolvePageClient() {
</div>
) : intentsError ? (
<div className="p-8 text-center text-sm text-vx-muted">
{getMessage("solve.intents.error")}
{t("solve.intents.error")}
</div>
) : openIntents.length === 0 ? (
<div className="p-8 text-center text-sm text-vx-muted">
{getMessage("solve.intents.empty")}
{t("solve.intents.empty")}
</div>
) : (
<div className="divide-y divide-vx-line">
Expand All @@ -273,13 +275,13 @@ export default function SolvePageClient() {
>
<div className="min-w-0 flex-1">
<div className="num text-xs text-vx-muted mb-1 capitalize">
{getMessage("solve.intents.id", { id: intent.id })}
{t("solve.intents.id", { id: intent.id })}
</div>
<div className="text-sm font-medium text-vx-text capitalize">
{intent.srcAmount} {intent.srcToken} on {intent.srcChain}
</div>
<div className="text-xs text-vx-muted">
{getMessage("solve.intents.details", {
{t("solve.intents.details", {
minOut: intent.minOut,
dstToken: intent.dstToken,
timeRemaining: timeRemaining(intent.deadline),
Expand All @@ -296,8 +298,8 @@ export default function SolvePageClient() {
w-full sm:w-auto disabled:opacity-60 disabled:cursor-wait"
>
{acceptingId === intent.id
? getMessage("solve.intents.accepting")
: getMessage("solve.intents.accept")}
? t("solve.intents.accepting")
: t("solve.intents.accept")}
</button>
</div>
))}
Expand All @@ -317,21 +319,21 @@ export default function SolvePageClient() {
<div className="card p-4 sm:p-6 space-y-4 sm:space-y-5">
<div>
<h3 className="text-base font-semibold text-vx-text mb-1">
{getMessage("solve.register.title")}
{t("solve.register.title")}
</h3>
<p className="text-xs text-vx-muted">{getMessage("solve.register.description")}</p>
<p className="text-xs text-vx-muted">{t("solve.register.description")}</p>
</div>

<div>
<label htmlFor="solver-address" className="eyebrow block mb-2 text-xs">
{getMessage("solve.register.addressLabel")}
{t("solve.register.addressLabel")}
</label>
<input
id="solver-address"
type="text"
value={address}
onChange={(e) => setAddress(e.target.value.trim())}
placeholder={getMessage("solve.register.addressPlaceholder")}
placeholder={t("solve.register.addressPlaceholder")}
aria-invalid={Boolean(addressError)}
aria-describedby={addressError ? "solver-address-error" : undefined}
className="w-full bg-vx-surface border border-vx-border rounded-lg px-3 py-2.5
Expand All @@ -347,14 +349,14 @@ export default function SolvePageClient() {

<div>
<label htmlFor="solver-bond" className="eyebrow block mb-2 text-xs">
{getMessage("solve.register.bondLabel")}
{t("solve.register.bondLabel")}
</label>
<input
id="solver-bond"
type="number"
value={bond}
onChange={(e) => setBond(e.target.value)}
placeholder={getMessage("solve.register.bondPlaceholder")}
placeholder={t("solve.register.bondPlaceholder")}
aria-invalid={Boolean(bondError)}
aria-describedby={bondError ? "solver-bond-error" : undefined}
className="w-full bg-vx-surface border border-vx-border rounded-lg px-3 py-2.5
Expand All @@ -369,9 +371,9 @@ export default function SolvePageClient() {
</div>

<div className="bg-vx-surface/50 rounded-lg p-3 text-xs text-vx-muted space-y-1">
<div>{getMessage("solve.register.info.minimumBond")}</div>
<div>{getMessage("solve.register.info.slash")}</div>
<div>{getMessage("solve.register.info.withdraw")}</div>
<div>{t("solve.register.info.minimumBond")}</div>
<div>{t("solve.register.info.slash")}</div>
<div>{t("solve.register.info.withdraw")}</div>
</div>

{registration.status === "error" && (
Expand All @@ -388,10 +390,10 @@ export default function SolvePageClient() {
className="btn-swap"
>
{isRegistering
? REGISTRATION_LABEL[registration.status]
? t(REGISTRATION_LABEL_KEY[registration.status]!)
: registration.status === "success"
? getMessage("solve.register.button.registered")
: getMessage("solve.register.button.connect")}
? t("solve.register.button.registered")
: t("solve.register.button.connect")}
</button>
</div>
</div>
Expand Down
Loading
Loading