("none");
+
const [address, setAddress] = useState("");
const [bond, setBond] = useState("");
const registration = useSolverRegistration();
@@ -50,6 +79,31 @@ export default function SolvePageClient() {
const canRegister =
Boolean(address) && Boolean(bond) && !addressError && !bondError && !isRegistering;
+ const sortedSolvers = [...solvers].sort((a, b) => {
+ if (!sortKey || sortDir === "none") return 0;
+ const aVal = a[sortKey];
+ const bVal = b[sortKey];
+ // Stable numeric comparison
+ if (typeof aVal === "number" && typeof bVal === "number") {
+ return sortDir === "asc" ? aVal - bVal : bVal - aVal;
+ }
+ return 0;
+ });
+
+ const handleSort = (key: SortKey) => {
+ if (sortKey !== key) {
+ setSortKey(key);
+ setSortDir("asc");
+ } else if (sortDir === "asc") {
+ setSortDir("desc");
+ } else if (sortDir === "desc") {
+ setSortDir("none");
+ setSortKey(null);
+ } else {
+ setSortDir("asc");
+ }
+ };
+
const handleRegister = () => {
if (registration.status === "success") {
registration.reset();
@@ -138,10 +192,80 @@ export default function SolvePageClient() {
aria-labelledby="tab-leaderboard"
className="card overflow-hidden"
>
-
-
- {getMessage("solve.leaderboard.title")}
-
+
+
+
+ {getMessage("solve.leaderboard.title")}
+
+
+ {([
+ ["fills", "Fills"],
+ ["volumeUsd", "Volume"],
+ ["avgFillTimeSeconds", "Avg Time"],
+ ["successRatePct", "Success %"],
+ ] as [SortKey, string][]).map(([key, label]) => {
+ const isActive = sortKey === key && sortDir !== "none";
+ const ariaSortValue: "ascending" | "descending" | "none" =
+ sortKey === key && sortDir !== "none"
+ ? sortDir === "asc" ? "ascending" : "descending"
+ : "none";
+ return (
+
+ );
+ })}
+
+
+ {/* Mobile sort: compact dropdown alternative */}
+
+ {([
+ ["fills", "Fills"],
+ ["volumeUsd", "Volume"],
+ ["avgFillTimeSeconds", "Avg Time"],
+ ["successRatePct", "Success %"],
+ ] as [SortKey, string][]).map(([key, label]) => {
+ const isActive = sortKey === key && sortDir !== "none";
+ const ariaSortValue: "ascending" | "descending" | "none" =
+ sortKey === key && sortDir !== "none"
+ ? sortDir === "asc" ? "ascending" : "descending"
+ : "none";
+ return (
+
+ );
+ })}
+
{solversLoading && solvers.length === 0 ? (
@@ -158,7 +282,7 @@ export default function SolvePageClient() {
) : (
- {solvers.map((s, i) => (
+ {sortedSolvers.map((s, i) => (
{
expect(skeletons.length).toBe(0);
});
});
+});
\ No newline at end of file
diff --git a/src/app/solve/[address]/page.tsx b/src/app/solve/[address]/page.tsx
index 0fb501b..20fe80e 100644
--- a/src/app/solve/[address]/page.tsx
+++ b/src/app/solve/[address]/page.tsx
@@ -8,6 +8,7 @@ import { useSolver } from "@/hooks/useSolver";
import { useIntentFeed } from "@/hooks/useIntentFeed";
import { timeAgo } from "@/lib/time";
import { CHAINS } from "@/lib/marketData";
+import { QrCode } from "@/components/QrCode";
function truncateAddress(address: string) {
if (address.length <= 12) return address;
@@ -78,9 +79,14 @@ export default function SolverDetailPage({ params }: { params: { address: string
-
+
Address: {params.address}
+
{/* Metrics grid */}
diff --git a/src/app/solve/page.tsx b/src/app/solve/page.tsx
index 9fc040e..dd0c88b 100644
--- a/src/app/solve/page.tsx
+++ b/src/app/solve/page.tsx
@@ -479,10 +479,5 @@ export default function SolvePage() {
- ),
- ssr: false,
-});
-
-export default function SolvePage() {
- return ;
+ );
}
diff --git a/src/components/ConnectWalletButton.tsx b/src/components/ConnectWalletButton.tsx
index a0f7150..de1a8f8 100644
--- a/src/components/ConnectWalletButton.tsx
+++ b/src/components/ConnectWalletButton.tsx
@@ -3,6 +3,7 @@
import { useWalletStore } from "@/store/wallet";
import { useToastStore } from "@/store/toast";
import { useTranslation } from "@/lib/i18n/I18nProvider";
+import { QrCode } from "./QrCode";
const FREIGHTER_INSTALL_URL = "https://www.freighter.app/";
@@ -40,6 +41,12 @@ export function ConnectWalletButton({ compact = false }: { compact?: boolean })
Disconnect
+
+
{networkMismatch && (
= {
+ title: "Components/QrCode",
+ component: QrCode,
+ parameters: {
+ layout: "centered",
+ docs: {
+ description: {
+ component:
+ "Dependency-free inline-SVG QR code with a show/hide toggle. " +
+ "Uses a from-scratch QR encoder (ISO/IEC 18004, ECC level M, versions 1–10). " +
+ "Screen-reader users receive an aria-label describing the encoded value.",
+ },
+ },
+ },
+ tags: ["autodocs"],
+ argTypes: {
+ size: { control: { type: "range", min: 80, max: 400, step: 20 } },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const StellarAddress: Story = {
+ args: {
+ value: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ label: "Stellar address GBBD…LA5",
+ size: 200,
+ },
+};
+
+export const SolverAddress: Story = {
+ name: "Solver address (longer label)",
+ args: {
+ value: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGKM0JLZWH0M0Q5O8N1BXU",
+ label: "QR code for solver address GCEZ…BXU",
+ size: 200,
+ },
+};
+
+export const Small: Story = {
+ name: "Small (80 px minimum)",
+ args: {
+ value: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ label: "Stellar address GBBD…LA5",
+ size: 80,
+ },
+};
+
+export const Large: Story = {
+ name: "Large (320 px)",
+ args: {
+ value: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ label: "Stellar address GBBD…LA5",
+ size: 320,
+ },
+};
diff --git a/src/components/QrCode.test.tsx b/src/components/QrCode.test.tsx
new file mode 100644
index 0000000..7248cdc
--- /dev/null
+++ b/src/components/QrCode.test.tsx
@@ -0,0 +1,84 @@
+import { describe, expect, it } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { encodeQrSvg } from "@/lib/qrCode";
+import { QrCode } from "./QrCode";
+
+// ── Utility tests ────────────────────────────────────────────────────────────
+
+describe("encodeQrSvg", () => {
+ it("returns a valid SVG string for a Stellar G-address (56 chars)", () => {
+ const svg = encodeQrSvg("GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5");
+ expect(svg).toMatch(/^