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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,6 @@ vite.config.ts.timestamp-*
# Pull request description docs (local only, not for version control)
pr-*.md
PULL_REQUEST.md

# Storybook build output
storybook-static/
22 changes: 22 additions & 0 deletions frontend/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { StorybookConfig } from "@storybook/nextjs";

const config: StorybookConfig = {
stories: [
"../components/ui/**/*.stories.@(js|jsx|mjs|ts|tsx)",
"../components/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
docs: {
autodocs: "tag",
},
};

export default config;
22 changes: 22 additions & 0 deletions frontend/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Preview } from "@storybook/react";
import "../app/globals.css";

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: "light",
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#0f172a" },
],
},
},
};

export default preview;
39 changes: 28 additions & 11 deletions frontend/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
*/

import { useCallback, useEffect, useState } from "react";

import { useAuth } from "../hooks/useAuth";
import { Button } from "../../components/ui/Button";
import { Badge } from "../../components/ui/Badge";
import { Card } from "../../components/ui/Card";
import { Spinner } from "../../components/ui/Spinner";
import { Modal } from "../../components/ui/Modal";
import { StellarExplorerLink } from "../../components/StellarExplorerLink";

interface Metrics {
totalUsers: number;
Expand Down Expand Up @@ -169,7 +174,11 @@ export default function AdminDashboardPage(): JSX.Element {
}

if (isLoading) {
return <main className="p-10 text-sm text-zinc-400">Loading…</main>;
return (
<main className="flex justify-center p-16">
<Spinner size="lg" label="Loading admin dashboard…" />
</main>
);
}

if (!user || user.role !== "admin") {
Expand Down Expand Up @@ -213,13 +222,13 @@ export default function AdminDashboardPage(): JSX.Element {
}),
},
].map((stat) => (
<div
<Card
key={stat.label}
className="rounded-xl border border-white/10 bg-white/5 p-4"
className="border-white/10 bg-white/5 p-4"
>
<p className="text-xs uppercase tracking-wide text-zinc-500">{stat.label}</p>
<p className="text-xs uppercase tracking-wide text-zinc-400">{stat.label}</p>
<p className="mt-1 text-2xl font-semibold text-white">{stat.value}</p>
</div>
</Card>
))}
</section>
)}
Expand Down Expand Up @@ -256,17 +265,17 @@ export default function AdminDashboardPage(): JSX.Element {
{new Date(trade.created_at).toLocaleDateString()}
</td>
<td className="py-2 text-right">
<button
type="button"
<Button
size="sm"
variant="secondary"
onClick={() => {
setResolving(trade);
setResolution("release_to_seller");
setResolveError(null);
}}
className="rounded-lg border border-white/15 px-3 py-1 text-xs text-zinc-200 hover:bg-white/10"
>
Resolve
</button>
</Button>
</td>
</tr>
))}
Expand Down Expand Up @@ -312,7 +321,15 @@ export default function AdminDashboardPage(): JSX.Element {
<dd className="text-zinc-200">{lookup.user.fiat_balance ?? "0.00"}</dd>
<dt className="text-zinc-500">Stellar key</dt>
<dd className="truncate font-mono text-xs text-zinc-400">
{lookup.user.stellar_public_key ?? "—"}
{lookup.user.stellar_public_key ? (
<StellarExplorerLink
type="account"
value={lookup.user.stellar_public_key}
truncate={false}
/>
) : (
"—"
)}
</dd>
</dl>

Expand Down
35 changes: 35 additions & 0 deletions frontend/app/api/trades/[id]/dispute/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";

export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const { id } = params;
const apiUrl = process.env.NEXT_PUBLIC_API_URL || process.env.API_URL || "http://localhost:3001";

try {
const body = await req.json().catch(() => ({}));
const authHeader = req.headers.get("authorization");

const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (authHeader) {
headers["authorization"] = authHeader;
}

const backendRes = await fetch(`${apiUrl}/api/v1/trades/${encodeURIComponent(id)}/dispute`, {
method: "POST",
headers,
body: JSON.stringify(body),
});

const data = await backendRes.json().catch(() => ({}));
return NextResponse.json(data, { status: backendRes.status });
} catch (error) {
return NextResponse.json(
{ error: "Internal server error connecting to trade dispute service" },
{ status: 500 }
);
}
}
2 changes: 2 additions & 0 deletions frontend/app/components/StellarExplorerLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "../../components/StellarExplorerLink";
export { default } from "../../components/StellarExplorerLink";
5 changes: 3 additions & 2 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TradeOffer } from "../../server/src/types/trade";
import ThemeToggle from "../components/ThemeToggle";
import { Card } from "../components/ui/Card";

interface TradesResponse {
data: TradeOffer[];
Expand Down Expand Up @@ -47,7 +48,7 @@ function AssetBadge({ assetType }: { assetType: string }) {
function TradeCard({ trade }: { trade: TradeOffer }) {
const sellerAlias = `@seller_${trade.seller_id.slice(-8)}`;
return (
<article className="flex flex-col gap-4 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800">
<Card className="flex flex-col gap-4 transition-shadow hover:shadow-md">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">Seller</p>
Expand Down Expand Up @@ -85,7 +86,7 @@ function TradeCard({ trade }: { trade: TradeOffer }) {
>
View &amp; Buy
</a>
</article>
</Card>
);
}

Expand Down
78 changes: 36 additions & 42 deletions frontend/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import { useState, useEffect, useCallback } from "react";
import { getToken, getUser, isAuthenticated } from "../lib/auth";
import type { TradeOffer, TradeStatus } from "../../../server/src/types/trade";
import { Badge } from "../../components/ui/Badge";
import { Button } from "../../components/ui/Button";
import { Card } from "../../components/ui/Card";
import { Spinner } from "../../components/ui/Spinner";
import { StellarExplorerLink } from "../../components/StellarExplorerLink";

// ---------------------------------------------------------------------------
// Types
Expand Down Expand Up @@ -81,42 +86,9 @@ function formatDateTime(iso: string): string {
// Sub-components
// ---------------------------------------------------------------------------

function Spinner({ label = "Loading…" }: { label?: string }) {
return (
<svg
className="h-5 w-5 animate-spin text-violet-600 dark:text-violet-400"
viewBox="0 0 24 24"
fill="none"
aria-label={label}
role="img"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4l3-3-3-3v4a8 8 0 00-8 8h4z" />
</svg>
);
}

/** Status badge — matches design system used in TradeDetailClient */
function StatusBadge({ status }: { status: TradeStatus }) {
const styles: Record<TradeStatus, string> = {
Active: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
Locked: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
Completed: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
Cancelled: "bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400",
};

// Display "Disputed" in the UI for Locked trades to match filter label
const label = status === "Locked" ? "Disputed" : status;

return (
<span
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold ${
styles[status] ?? "bg-gray-100 text-gray-500"
}`}
>
{label}
</span>
);
const variant = status === "Active" ? "Open" : status;
return <Badge variant={variant as any} />;
}

/** A single stat card in the profile summary */
Expand All @@ -130,11 +102,11 @@ function StatCard({
icon: string;
}) {
return (
<div className="flex flex-col gap-1 rounded-2xl border border-gray-100 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<Card className="flex flex-col gap-1 p-5">
<span aria-hidden="true" className="text-2xl">{icon}</span>
<p className="mt-1 text-2xl font-extrabold text-gray-900 dark:text-gray-100">{value}</p>
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">{label}</p>
</div>
</Card>
);
}

Expand Down Expand Up @@ -258,6 +230,15 @@ function TradeRow({
{counterparty}
</td>

{/* Explorer */}
<td className="px-4 py-3.5 text-xs">
{trade.escrow_tx_hash ? (
<StellarExplorerLink type="transaction" value={trade.escrow_tx_hash} />
) : (
<span className="text-gray-400 font-mono">—</span>
)}
</td>

{/* Status */}
<td className="py-3.5 pl-4 pr-5 text-right">
<StatusBadge status={trade.status} />
Expand Down Expand Up @@ -308,6 +289,13 @@ function TradeMobileCard({
<span className="text-gray-500 dark:text-gray-400">Counterparty</span>
<span className="font-mono text-gray-600 dark:text-gray-400 text-xs">{counterparty}</span>
</div>

{trade.escrow_tx_hash && (
<div className="flex items-center justify-between text-sm pt-1 border-t border-gray-100 dark:border-gray-700">
<span className="text-gray-500 dark:text-gray-400">Explorer</span>
<StellarExplorerLink type="transaction" value={trade.escrow_tx_hash} />
</div>
)}
</div>
);
}
Expand Down Expand Up @@ -497,14 +485,17 @@ export default function ProfilePage() {

{/* Stellar public key */}
{profile.stellarPublicKey && (
<div className="mt-4 flex flex-col gap-1 rounded-xl border border-gray-100 bg-white px-5 py-4 dark:border-gray-700 dark:bg-gray-800">
<Card className="mt-4 flex flex-col gap-1 p-5">
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">
Stellar Public Key
</p>
<p className="break-all font-mono text-xs text-gray-700 dark:text-gray-300">
{profile.stellarPublicKey}
</p>
</div>
<StellarExplorerLink
type="account"
value={profile.stellarPublicKey}
className="text-xs font-mono break-all"
truncate={false}
/>
</Card>
)}
</section>
) : null}
Expand Down Expand Up @@ -603,6 +594,9 @@ export default function ProfilePage() {
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
Counterparty
</th>
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
Explorer
</th>
<th className="py-3 pl-4 pr-5 text-right text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
Status
</th>
Expand Down
Loading