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
66 changes: 64 additions & 2 deletions backend/src/routes/payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -947,8 +947,70 @@ function createPaymentsRouter({
*/
router.get("/metrics/7day", async (req, res, next) => {
try {
const result = await paymentService.getRollingMetrics(req.merchant.id);
res.json(result);
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const { data: payments, error } = await supabase
.from("payments")
.select("amount, created_at, status")
.eq("merchant_id", req.merchant.id)
.gte("created_at", sevenDaysAgo.toISOString())
.order("created_at", { ascending: true });

if (error) {
error.status = 500;
throw error;
}

const metricsMap = new Map();
let totalVolume = 0;
let confirmedCount = 0;

payments.forEach((payment) => {
const date = new Date(payment.created_at).toISOString().split("T")[0];
const volume = Number(payment.amount) || 0;

if (!metricsMap.has(date)) {
metricsMap.set(date, { date, volume: 0, count: 0, confirmed_count: 0 });
}

const dayMetric = metricsMap.get(date);
dayMetric.volume += volume;
dayMetric.count += 1;

if (payment.status === "confirmed") {
dayMetric.confirmed_count += 1;
confirmedCount += 1;
}

totalVolume += volume;
});

const data = [];
for (let i = 6; i >= 0; i -= 1) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split("T")[0];

if (metricsMap.has(dateStr)) {
data.push(metricsMap.get(dateStr));
} else {
data.push({ date: dateStr, volume: 0, count: 0, confirmed_count: 0 });
}
}

const totalPayments = payments.length;
const successRate = totalPayments > 0
? Number(((confirmedCount / totalPayments) * 100).toFixed(1))
: 0;

res.json({
data,
total_volume: Number(totalVolume.toFixed(2)),
total_payments: totalPayments,
confirmed_count: confirmedCount,
success_rate: successRate,
});
} catch (err) {
next(err);
}
Expand Down
201 changes: 87 additions & 114 deletions frontend/src/app/(authenticated)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import {
} from "@/lib/merchant-store";
import { useTranslations } from "next-intl";
import FirstApiKeyModal from "@/components/FirstApiKeyModal";
import FirstPaymentCelebration from "@/components/FirstPaymentCelebration";
import PaymentMetrics from "@/components/PaymentMetrics";
import RecentPayments from "@/components/RecentPayments";
import WithdrawModal from "@/components/WithdrawModal";

export default function DashboardPage() {
const t = useTranslations("dashboardPage");
const [isFirstKeyModalOpen, setIsFirstKeyModalOpen] = useState(false);
const [isWithdrawOpen, setIsWithdrawOpen] = useState(false);
const hydrated = useMerchantHydrated();
const apiKey = useMerchantApiKey();
const merchant = useMerchantMetadata();
Expand All @@ -41,130 +44,100 @@ export default function DashboardPage() {
if (!hydrated || loading) return <DashboardSkeleton />;

return (
<div className="flex flex-col gap-10 animate-in fade-in duration-500">
{/* Header */}
<header className="flex flex-col gap-2">
<p className="text-[10px] font-bold uppercase tracking-[0.4em] text-[#6B6B6B]">Overview</p>
<h1 className="text-4xl font-bold text-[#0A0A0A] tracking-tight">
{merchant?.business_name ?? t("title")}
</h1>
<p className="text-sm font-medium text-[#6B6B6B]">{t("description")}</p>
</header>
<div className="flex flex-col gap-12">
{/* ── Welcome & Quick Actions ────────────────────────────────────────── */}
<header className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-2">
<h1 className="text-4xl font-bold tracking-tight text-white">
Merchant Hub
</h1>
<p className="text-slate-400">
Overview of your Stellar payment ecosystem and performance.
</p>
</div>

<div className="grid gap-10 lg:grid-cols-3">
{/* Left: Metrics + Activity */}
<div className="flex flex-col gap-10 lg:col-span-2">
<section className="flex flex-col gap-4">
<h2 className="text-xs font-bold uppercase tracking-[0.2em] text-[#6B6B6B]">Business Overview</h2>
<AnalyticsCards />
</section>
<div className="flex flex-wrap items-center gap-3">
<Link
href="/dashboard/create"
className="group relative flex items-center gap-2.5 overflow-hidden rounded-xl bg-mint px-5 py-3 text-sm font-bold text-black transition-all hover:scale-[1.02] hover:bg-glow"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 4v16m8-8H4" />
</svg>
Create Link
<div className="absolute inset-0 -z-10 bg-white/20 opacity-0 transition-opacity group-hover:opacity-100" />
</Link>

<section className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xs font-bold uppercase tracking-[0.2em] text-[#6B6B6B]">Recent Activity</h2>
<Link href="/payment-history" className="text-[10px] font-bold uppercase tracking-widest text-[#0A0A0A] underline underline-offset-4 hover:text-[#6B6B6B] transition-colors">
{t("viewAllPayments")} →
</Link>
</div>
<ActivityFeed />
</section>
</div>
<Link
href="/docs"
className="flex items-center gap-2.5 rounded-xl border border-white/10 bg-white/5 px-5 py-3 text-sm font-medium text-slate-300 transition-all hover:bg-white/10 hover:text-white"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
View Docs
</Link>

{/* Right: Quick Actions */}
<aside className="flex flex-col gap-6">
<section className="rounded-2xl border border-[#E8E8E8] bg-white p-6">
<h3 className="mb-5 text-xs font-bold uppercase tracking-[0.2em] text-[#6B6B6B]">{t("quickActions")}</h3>
<div className="flex flex-col gap-2">
<Link
href="/create"
className="flex items-center gap-3 rounded-xl border border-[var(--pluto-200)] bg-[var(--pluto-50)] px-4 py-3 text-sm font-bold text-[var(--pluto-700)] transition-all hover:bg-[var(--pluto-500)] hover:text-white hover:border-[var(--pluto-500)]"
>
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{t("createPaymentLink")}
</Link>
<Link
href="/settings"
className="flex items-center gap-3 rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] px-4 py-3 text-sm font-bold text-[#0A0A0A] transition-all hover:bg-[#0A0A0A] hover:text-white hover:border-[#0A0A0A]"
>
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</Link>
<a
href="/docs"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] px-4 py-3 text-sm font-bold text-[#0A0A0A] transition-all hover:bg-[#0A0A0A] hover:text-white hover:border-[#0A0A0A]"
>
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
API Docs
</a>
</div>
</section>
<Link
href="/settings"
className="flex items-center gap-2.5 rounded-xl border border-white/10 bg-white/5 px-5 py-3 text-sm font-medium text-slate-300 transition-all hover:bg-white/10 hover:text-white"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</Link>

<section className="rounded-2xl border border-[#E8E8E8] bg-white p-6">
<h3 className="mb-4 text-xs font-bold uppercase tracking-[0.2em] text-[#6B6B6B]">{t("development")}</h3>
<div className="flex flex-col gap-4 text-sm text-[#6B6B6B]">
<div className="flex items-start gap-3">
<div className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-[#0A0A0A]" />
<p>{t("apiKeysTip")}</p>
</div>
<div className="flex items-start gap-3">
<div className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-[#0A0A0A]" />
<p>{t("webhookLogsTip")}</p>
</div>
</div>
</section>
<button
onClick={() => setIsWithdrawOpen(true)}
className="flex items-center gap-2.5 rounded-xl border border-mint/20 bg-mint/5 px-5 py-3 text-sm font-medium text-mint transition-all hover:bg-mint/10"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
Withdraw
</button>
</div>
</header>

<section className="rounded-2xl border border-[#E8E8E8] bg-white p-6">
<h3 className="mb-4 text-xs font-bold uppercase tracking-[0.2em] text-[#6B6B6B]">API Endpoint</h3>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] px-4 py-3">
<code className="flex-1 truncate font-mono text-xs text-[#0A0A0A]">https://pluto-api.up.railway.app/api</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText("https://pluto-api.up.railway.app/api")}
className="rounded-lg border border-[#E8E8E8] bg-white p-1.5 text-[#6B6B6B] hover:text-[#0A0A0A] transition-colors"
title="Copy API URL"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
</div>
<p className="text-[10px] text-[#6B6B6B]">Use this base URL for all subscription and x402 API requests.</p>
{/* ── Main Dashboard Content ────────────────────────────────────────── */}
<div className="grid gap-10">
{/* Performance Metrics Section */}
<section className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-white">Performance</h2>
<div className="flex items-center gap-2 text-xs text-slate-500">
<span className="h-2 w-2 rounded-full bg-mint animate-pulse" />
Live monitoring active
</div>
</section>
</div>
<PaymentMetrics />
</section>

<section className="rounded-2xl border border-[#E8E8E8] bg-white p-6">
<h3 className="mb-4 text-xs font-bold uppercase tracking-[0.2em] text-[#6B6B6B]">x402 Integration</h3>
<p className="mb-4 text-xs text-[#6B6B6B]">
Build pay-per-request flows with the production x402 setup guide.
</p>
<div className="flex flex-col gap-2">
<Link
href="/docs/x402-agentic-payments"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between rounded-xl border border-[var(--pluto-200)] bg-[var(--pluto-50)] px-4 py-3 text-sm font-bold text-[var(--pluto-700)] transition-colors hover:bg-[var(--pluto-100)]"
>
<span>Open x402 Integration Guide</span>
<span className="text-[10px] uppercase tracking-widest text-[var(--pluto-600)]">Docs</span>
</Link>
</div>
</section>
</aside>
{/* Activity Table Section */}
<section className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-white">Recent Activity</h2>
<Link
href="/payments"
className="group flex items-center gap-1.5 text-sm text-mint hover:text-glow transition-all"
>
View all payments
<svg className="h-4 w-4 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7-7 7" />
</svg>
</Link>
</div>
<div className="rounded-2xl border border-white/5 bg-white/[0.02] p-1">
<RecentPayments />
</div>
</section>
</div>

<FirstApiKeyModal isOpen={isFirstKeyModalOpen} onClose={() => setIsFirstKeyModalOpen(false)} />
<FirstPaymentCelebration />
<WithdrawModal isOpen={isWithdrawOpen} onClose={() => setIsWithdrawOpen(false)} />
</div>
);
}
2 changes: 1 addition & 1 deletion frontend/src/components/PaymentDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ export default function PaymentDetailModal({
aria-modal="true"
aria-label="Payment details"
tabIndex={-1}
className={`fixed right-0 top-0 z-50 flex h-full w-full max-w-lg flex-col bg-black/90 shadow-2xl outline-none backdrop-blur-xl transition-transform duration-300 ease-in-out ${visible ? "translate-x-0" : "translate-x-full"
className={`fixed right-0 top-0 z-50 flex h-full w-full max-w-lg flex-col bg-[#050608] shadow-2xl outline-none backdrop-blur-xl transition-transform duration-300 ease-in-out ${visible ? "translate-x-0" : "translate-x-full"
}`}
>
{/* Header */}
Expand Down
Loading
Loading