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
9 changes: 2 additions & 7 deletions src/app/explore/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import { useMemo } from "react";
import Link from "next/link";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import { Nav } from "@/components/Nav";
import { Footer } from "@/components/Footer";
import { CopyButton } from "@/components/CopyButton";
import { IntentStatusBadge } from "@/components/IntentStatusBadge";
import { SkeletonDetailCard } from "@/components/Skeleton";
import { useIntent } from "@/hooks/useIntent";
Expand Down Expand Up @@ -96,12 +96,7 @@ export default function IntentDetailPage({ params }: { params: { id: string } })
<div className="pt-2 border-t border-vx-line">
<div className="flex items-center gap-3 flex-wrap">
<span className="text-xs text-vx-muted num">{truncateAddress(intent.txHash)}</span>
<button
onClick={() => copy(intent.txHash)}
className="text-xs text-vx-sage hover:underline"
>
{copied ? "Copied" : "Copy"}
</button>
<CopyButton value={intent.txHash} label="Copy transaction hash" />
<a
href={`https://stellar.expert/explorer/${NETWORK}/tx/${intent.txHash}`}
target="_blank"
Expand Down
40 changes: 34 additions & 6 deletions src/app/solve/SolvePageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ import { useAcceptIntent } from "@/hooks/useAcceptIntent";
import { useSolverRegistration } from "@/hooks/useSolverRegistration";
import { timeRemaining } from "@/lib/time";
import { isValidStellarPublicKey } from "@/lib/stellarAddress";
import { useWalletStore } from "@/store/wallet";
import { getMessage } from "@/i18n/messages";
import { formatCurrency } from "@/lib/format";
import { formatCurrency, toBCP47 } from "@/lib/format";
import { useLocale } from "@/lib/i18n/I18nProvider";
import Link from "next/link";

const usdCompact = (value: number) =>
formatCurrency(value, undefined, {
notation: "compact",
maximumFractionDigits: 1,
});
function useUsdCompact() {
const locale = useLocale();
return (value: number) =>
formatCurrency(value, toBCP47(locale), {
notation: "compact",
maximumFractionDigits: 1,
});
}

const MIN_BOND_USD = 50;

Expand All @@ -29,8 +34,13 @@ const REGISTRATION_LABEL: Record<string, string> = {
};

export default function SolvePageClient() {
const usdCompact = useUsdCompact();
const [tab, setTab] = useState<"leaderboard" | "intents" | "register">("leaderboard");
const { solvers, isLoading: solversLoading, error: solversError } = useSolvers();
const connectedAddress = useWalletStore((s) => s.address);
const ownSolver = connectedAddress
? solvers.find((s) => s.address.toUpperCase() === connectedAddress.toUpperCase())
: undefined;
const { intents: openIntents, isLoading: intentsLoading, error: intentsError } = useOpenIntents();
const { accept, acceptingId, error: acceptError } = useAcceptIntent();

Expand Down Expand Up @@ -144,6 +154,15 @@ export default function SolvePageClient() {
</span>
</div>

{ownSolver && (
<Link
href={`/solve/${ownSolver.address}`}
className="block px-5 py-3 text-xs sm:text-sm text-vx-sage bg-vx-sage-bg border-b border-vx-border hover:underline"
>
You&apos;re a registered solver — view your profile →
</Link>
)}

{solversLoading && solvers.length === 0 ? (
<div className="p-5">
<SkeletonCard rows={3} rowHeight="h-16" />
Expand Down Expand Up @@ -380,6 +399,15 @@ export default function SolvePageClient() {
</p>
)}

{registration.status === "success" && registration.registeredAddress && (
<Link
href={`/solve/${registration.registeredAddress}`}
className="block text-xs text-vx-sage hover:underline"
>
View your solver profile →
</Link>
)}

<button
type="button"
onClick={handleRegister}
Expand Down
104 changes: 20 additions & 84 deletions src/app/solve/[address]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@
import Link from "next/link";
import { Nav } from "@/components/Nav";
import { Footer } from "@/components/Footer";
import { IntentStatusBadge } from "@/components/IntentStatusBadge";
import { CopyButton } from "@/components/CopyButton";
import { SolverHeaderCard } from "@/components/SolverHeaderCard";
import { SolverFillHistory } from "@/components/SolverFillHistory";
import { useSolver } from "@/hooks/useSolver";
import { useIntentFeed } from "@/hooks/useIntentFeed";
import { timeAgo } from "@/lib/time";
import { isValidStellarPublicKey } from "@/lib/stellarAddress";
import { CHAINS } from "@/lib/marketData";

function truncateAddress(address: string) {
if (address.length <= 12) return address;
return `${address.slice(0, 6)}...${address.slice(-6)}`;
}

const usdCompact = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 1,
});
import { formatCurrency, toBCP47 } from "@/lib/format";
import { useLocale } from "@/lib/i18n/I18nProvider";

export default function SolverDetailPage({ params }: { params: { address: string } }) {
const locale = useLocale();
const usdCompact = (value: number) =>
formatCurrency(value, toBCP47(locale), {
notation: "compact",
maximumFractionDigits: 1,
});
const isValidAddress = isValidStellarPublicKey(params.address);
const { solver, isLoading, error } = useSolver(isValidAddress ? params.address : null);
const { items: fillHistory, isLoading: historyLoading, error: historyError } = useIntentFeed();

return (
<div className="min-h-screen">
Expand Down Expand Up @@ -57,41 +53,22 @@ export default function SolverDetailPage({ params }: { params: { address: string
</div>
) : (
<>
{/* Header card */}
<div className="card p-4 sm:p-6 space-y-4 sm:space-y-6 mb-6">
<div className="flex items-start justify-between gap-3 sm:gap-4">
<div>
<div className="eyebrow mb-1 sm:mb-2 text-xs">Solver</div>
<h1 className="text-lg sm:text-2xl font-bold text-vx-text break-words">
{solver.name}
</h1>
</div>
<div
className={`flex-shrink-0 px-2 sm:px-3 py-1 rounded-lg text-xs font-semibold border whitespace-nowrap ${
solver.status === "active"
? "bg-vx-sage-bg text-vx-sage border-vx-sage/30"
: "bg-vx-surface text-vx-muted border-vx-border"
}`}
aria-label={`Solver status: ${solver.status}`}
>
{solver.status === "active" ? "Active" : "Inactive"}
</div>
</div>
<SolverHeaderCard solver={solver} />

{/* Extended metrics + chain coverage */}
<div className="card p-4 sm:p-6 space-y-4 sm:space-y-6 mt-6 mb-6">
<div className="flex items-center gap-2 text-xs sm:text-sm text-vx-muted font-mono break-all">
<span>Address: {params.address}</span>
<CopyButton value={params.address} label="Copy solver address" />
</div>

{/* Metrics grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
{[
{ label: "Fills", value: solver.fills },
{ label: "Failed", value: solver.failed },
{ label: "Success Rate", value: `${solver.successRatePct}%` },
{ label: "Total Volume", value: usdCompact.format(solver.volumeUsd) },
{ label: "Total Volume", value: usdCompact(solver.volumeUsd) },
{ label: "Avg Fill Time", value: `${solver.avgFillTimeSeconds}s` },
{ label: "Bond", value: usdCompact.format(solver.bondUsd) },
].map(({ label, value }) => (
<div key={label} className="bg-vx-surface/40 rounded-lg p-3">
<div className="eyebrow text-[10px] sm:text-xs mb-1">{label}</div>
Expand All @@ -110,10 +87,10 @@ export default function SolverDetailPage({ params }: { params: { address: string
const chainName = chainMeta?.name ?? chainId;
const chainColor = chainMeta?.color ?? "#6B7280";
return (
<span
key={chainId}
<span
key={chainId}
className="text-xs px-2 py-1 rounded text-white border font-medium"
style={{
style={{
backgroundColor: chainColor,
borderColor: chainColor,
}}
Expand All @@ -135,48 +112,7 @@ export default function SolverDetailPage({ params }: { params: { address: string
<h2 className="text-sm font-semibold text-vx-text">Recent Fills by Solver</h2>
</div>

{historyLoading && fillHistory.length === 0 ? (
<div className="p-4 sm:p-5">
<SkeletonCard rows={3} rowHeight="h-16" />
</div>
) : historyError ? (
<div role="alert" className="p-6 sm:p-8 text-center text-sm text-vx-muted">
Couldn&apos;t load fill history right now.
</div>
) : fillHistory.filter(item => item.solver === solver.address).length === 0 ? (
<div className="p-6 sm:p-8 text-center text-sm text-vx-muted">
No fills from this solver in the history.
</div>
) : (
<div className="divide-y divide-vx-line">
{fillHistory
.filter(item => item.solver === solver.address)
.slice(0, 10)
.map(fill => (
<div
key={fill.id}
className="px-4 sm:px-5 py-4 hover:bg-vx-surface/30 transition-colors"
>
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between gap-2 min-w-0">
<div className="min-w-0 flex-1">
<div className="num text-xs text-vx-muted mb-1 truncate">
ID: {fill.id}
</div>
<div className="text-xs sm:text-sm font-medium text-vx-text capitalize">
{fill.srcAmount} {fill.srcToken} → {fill.dstToken}
</div>
</div>
<IntentStatusBadge status={fill.status} />
</div>
<div className="text-xs text-vx-muted">
{fill.srcChain} · {timeAgo(fill.createdAt)}
</div>
</div>
</div>
))}
</div>
)}
<SolverFillHistory solverAddress={solver.address} />
</div>
</>
)}
Expand Down
63 changes: 1 addition & 62 deletions src/components/SolverFillHistory.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import type { FeedItem } from "@/lib/types";
import { SolverFillHistory } from "./SolverFillHistory";

const { useIntentFeedMock } = vi.hoisted(() => ({
useIntentFeedMock: vi.fn(),
Expand Down Expand Up @@ -29,68 +30,6 @@ vi.mock("@/lib/time", () => ({
},
}));

function SolverFillHistory({ solverAddress }: { solverAddress: string }) {
const { items: fillHistory, isLoading, error } = useIntentFeedMock();

const solverFills = fillHistory
.filter((item: FeedItem) => item.solver === solverAddress)
.slice(0, 10);

if (isLoading) {
return (
<div className="p-4 space-y-3">
{[0, 1, 2].map((i) => (
<div key={i} className="h-16 bg-vx-surface/40 rounded-lg animate-pulse" />
))}
</div>
);
}

if (error) {
return (
<div role="alert" className="p-6 text-center text-sm text-vx-muted">
Couldn&apos;t load fill history right now.
</div>
);
}

if (solverFills.length === 0) {
return (
<div className="p-6 text-center text-sm text-vx-muted">
No fills from this solver in the history.
</div>
);
}

return (
<div className="divide-y divide-vx-line">
{solverFills.map((fill: FeedItem) => (
<div
key={fill.id}
className="px-4 py-4 hover:bg-vx-surface/30 transition-colors"
>
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="num text-xs text-vx-muted mb-1 truncate">
ID: {fill.id}
</div>
<div className="text-xs font-medium text-vx-text capitalize">
{fill.srcAmount} {fill.srcToken} → {fill.dstToken}
</div>
</div>
<div data-testid="status-badge">{fill.status}</div>
</div>
<div className="text-xs text-vx-muted">
{fill.srcChain} · a moment ago
</div>
</div>
</div>
))}
</div>
);
}

describe("SolverFillHistory", () => {
it("shows loading skeleton while fills are being fetched", () => {
useIntentFeedMock.mockReturnValue({
Expand Down
66 changes: 66 additions & 0 deletions src/components/SolverFillHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useIntentFeed } from "@/hooks/useIntentFeed";
import { IntentStatusBadge } from "@/components/IntentStatusBadge";
import { timeAgo } from "@/lib/time";
import type { FeedItem } from "@/lib/types";

export function SolverFillHistory({ solverAddress }: { solverAddress: string }) {
const { items: fillHistory, isLoading, error } = useIntentFeed();

const solverFills = fillHistory
.filter((item: FeedItem) => item.solver === solverAddress)
.slice(0, 10);

if (isLoading) {
return (
<div className="p-4 space-y-3">
{[0, 1, 2].map((i) => (
<div key={i} className="h-16 bg-vx-surface/40 rounded-lg animate-pulse" />
))}
</div>
);
}

if (error) {
return (
<div role="alert" className="p-6 text-center text-sm text-vx-muted">
Couldn&apos;t load fill history right now.
</div>
);
}

if (solverFills.length === 0) {
return (
<div className="p-6 text-center text-sm text-vx-muted">
No fills from this solver in the history.
</div>
);
}

return (
<div className="divide-y divide-vx-line">
{solverFills.map((fill: FeedItem) => (
<div
key={fill.id}
className="px-4 py-4 hover:bg-vx-surface/30 transition-colors"
>
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="num text-xs text-vx-muted mb-1 truncate">
ID: {fill.id}
</div>
<div className="text-xs font-medium text-vx-text capitalize">
{fill.srcAmount} {fill.srcToken} → {fill.dstToken}
</div>
</div>
<IntentStatusBadge status={fill.status} />
</div>
<div className="text-xs text-vx-muted">
{fill.srcChain} · {timeAgo(fill.createdAt)}
</div>
</div>
</div>
))}
</div>
);
}
Loading
Loading