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
433 changes: 428 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"next": "15.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"recharts": "^3.8.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"zod": "^3.25.76"
Expand Down
4 changes: 1 addition & 3 deletions src/app/(dashboard)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { DashboardShell } from "@/components/layout/DashboardShell";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return <DashboardShell>{children}</DashboardShell>;
return children;
}
43 changes: 43 additions & 0 deletions src/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,50 @@
import { DollarSign, FileText, TrendingUp, Users } from "lucide-react";

import RecentActivity from "@/components/RecentActivity";
import RevenueChart from "@/components/RevenueChart";
import { ApiKeyManagement } from "@/components/dashboard/api-key-management";
import { StatCard } from "@/components/StatCard";
import type { ActivityEvent } from "@/lib/activity";
import { DashboardActions } from "./dashboard-actions";

export const metadata = {
title: "Dashboard | Shade",
description: "Shade merchant dashboard.",
};

const RECENT_ACTIVITY_EVENTS: ActivityEvent[] = [
{
id: "evt_1",
type: "invoice_paid",
description: "Invoice INV-1042 was paid",
amount: 1840,
currency: "USD",
timestamp: "2026-06-23T10:15:00Z",
},
{
id: "evt_2",
type: "invoice_created",
description: "New invoice INV-1043 was created",
amount: 620,
currency: "USD",
timestamp: "2026-06-23T08:40:00Z",
},
{
id: "evt_3",
type: "customer_added",
description: "Acme Labs was added as a customer",
timestamp: "2026-06-22T16:20:00Z",
},
{
id: "evt_4",
type: "payout_sent",
description: "Weekly payout sent to connected wallet",
amount: 3120,
currency: "USD",
timestamp: "2026-06-21T12:00:00Z",
},
];

export default function DashboardPage() {
return (
<div className="space-y-6">
Expand Down Expand Up @@ -46,6 +82,13 @@ export default function DashboardPage() {
/>
</div>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<div className="lg:col-span-2">
<RevenueChart />
</div>
<RecentActivity events={RECENT_ACTIVITY_EVENTS} className="h-full" />
</div>

<DashboardActions />

<ApiKeyManagement />
Expand Down
4 changes: 3 additions & 1 deletion src/app/(dashboard)/invoice-tools/invoice-tools-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export function InvoiceToolsClient() {
return (
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<section className="space-y-3">
<h2 className="text-lg font-semibold">Create invoice (with validation)</h2>
<h2 className="text-lg font-semibold">
Create invoice (with validation)
</h2>
<InvoiceCreateForm
onDraft={(draft) => {
console.log("Draft saved:", draft);
Expand Down
13 changes: 7 additions & 6 deletions src/app/(dashboard)/invoices/invoices-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ export function InvoicesClient() {
})
.sort((a, b) => {
if (sortBy === "newest") {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
return (
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
}
if (sortBy === "oldest") {
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
return (
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
}
if (sortBy === "amount-high") {
return Number(b.amount) - Number(a.amount);
Expand Down Expand Up @@ -80,9 +84,7 @@ export function InvoicesClient() {
return (
<div className="flex flex-col gap-8">
<InvoiceForm
onSubmit={(invoice) =>
setInvoices((current) => [invoice, ...current])
}
onSubmit={(invoice) => setInvoices((current) => [invoice, ...current])}
/>
<section className="space-y-4">
<h2 className="text-lg font-semibold text-foreground">Your invoices</h2>
Expand All @@ -109,4 +111,3 @@ export function InvoicesClient() {
</div>
);
}

1 change: 0 additions & 1 deletion src/app/(dashboard)/settings/api-keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,3 @@ export default function ApiKeysSettingsPage() {
</div>
);
}

4 changes: 1 addition & 3 deletions src/app/pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ export default function PayPage() {
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
<div className="mx-auto w-full max-w-3xl space-y-6">
<header className="space-y-1">
<p className="text-sm font-semibold uppercase text-primary">
Shade
</p>
<p className="text-sm font-semibold uppercase text-primary">Shade</p>
<h1 className="text-3xl font-bold">Pay invoice</h1>
<p className="text-sm text-muted-foreground">
Choose how you want to settle this invoice.
Expand Down
4 changes: 3 additions & 1 deletion src/app/payment/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ export default function PublicPaymentPage() {
Loading invoice details...
</p>
</div>
) : !invoice || invoice.status === "invalid" || invoice.status === "expired" ? (
) : !invoice ||
invoice.status === "invalid" ||
invoice.status === "expired" ? (
<InvalidInvoiceState
message={
invoice?.status === "expired"
Expand Down
2 changes: 1 addition & 1 deletion src/app/sign-in/sign-in-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function createChallenge(address: string) {
// const keypair = Keypair.fromPublicKey(signerAddress);
// const messageBytes = Buffer.from(challenge, "utf8");
// const signatureBytes = Buffer.from(signedMessage, "base64");
//
//
// return keypair.verify(messageBytes, signatureBytes);
// }

Expand Down
4 changes: 2 additions & 2 deletions src/components/MobileNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export function MobileNav() {
<nav className="flex-1 overflow-y-auto px-3 py-4">
<ul className="flex flex-col gap-0.5">
{navItems.map(({ href, label, icon: Icon }) => {
const active = pathname === href || pathname.startsWith(href + "/");
const active =
pathname === href || pathname.startsWith(href + "/");
return (
<li key={href}>
<Link
Expand Down Expand Up @@ -133,4 +134,3 @@ export function MobileNav() {
</div>
);
}

33 changes: 27 additions & 6 deletions src/components/RevenueChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,29 @@ export interface RevenueDataPoint {
}

interface TooltipPayload {
value?: number;
value?: number | string | readonly (number | string)[];
}

interface CustomTooltipProps {
active?: boolean;
payload?: TooltipPayload[];
payload?: readonly TooltipPayload[];
label?: string;
}

function CustomTooltip({ active, payload, label }: CustomTooltipProps) {
if (!active || !payload?.length) return null;

const value = payload[0].value;
const formattedValue = Array.isArray(value)
? value.join(", ")
: typeof value === "number"
? value.toLocaleString()
: value;

return (
<div className="rounded-md border bg-card px-3 py-2 text-sm shadow-md">
<p className="font-medium text-card-foreground">{label}</p>
<p className="text-primary">{payload[0].value?.toLocaleString()} XLM</p>
<p className="text-primary">{formattedValue} XLM</p>
</div>
);
}
Expand All @@ -51,9 +59,14 @@ interface RevenueChartProps {
export default function RevenueChart({ data = MOCK_DATA }: RevenueChartProps) {
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<h2 className="mb-4 text-lg font-bold text-card-foreground">Revenue Over Time</h2>
<h2 className="mb-4 text-lg font-bold text-card-foreground">
Revenue Over Time
</h2>
<ResponsiveContainer width="100%" height={260}>
<AreaChart data={data} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
<AreaChart
data={data}
margin={{ top: 4, right: 8, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--primary)" stopOpacity={0.25} />
Expand All @@ -73,7 +86,15 @@ export default function RevenueChart({ data = MOCK_DATA }: RevenueChartProps) {
tickLine={false}
width={48}
/>
<Tooltip content={(props) => <CustomTooltip active={props.active} payload={props.payload} label={String(props.label ?? "")} />} />
<Tooltip
content={(props) => (
<CustomTooltip
active={props.active}
payload={props.payload}
label={String(props.label ?? "")}
/>
)}
/>
<Area
type="monotone"
dataKey="amount"
Expand Down
3 changes: 1 addition & 2 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ export function Sidebar() {
<nav className="flex-1 overflow-y-auto px-3 py-4">
<ul className="flex flex-col gap-0.5">
{navItems.map(({ href, label, icon: Icon }) => {
const active =
pathname === href || pathname.startsWith(href + "/");
const active = pathname === href || pathname.startsWith(href + "/");
return (
<li key={href}>
<Link
Expand Down
4 changes: 3 additions & 1 deletion src/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {

useEffect(() => {
const stored = localStorage.getItem("shade:theme");
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const systemDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const dark = stored === "dark" || (stored !== "light" && systemDark);
setIsDark(dark);
document.documentElement.classList.toggle("dark", dark);
Expand Down
8 changes: 1 addition & 7 deletions src/components/avatar/avatar-upload.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
"use client";

import {
ChangeEvent,
KeyboardEvent,
useEffect,
useRef,
useState,
} from "react";
import { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from "react";
import { ImagePlus } from "lucide-react";

import { cn } from "@/lib/utils";
Expand Down
13 changes: 7 additions & 6 deletions src/components/billing/billing-plan-form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ describe("BillingPlanForm", () => {

await user.click(screen.getByRole("button", { name: /create plan/i }));

expect(await screen.findByText(/plan name is required/i)).toBeInTheDocument();
expect(
await screen.findByText(/plan name is required/i),
).toBeInTheDocument();
expect(screen.getByText(/description is required/i)).toBeInTheDocument();
expect(screen.getByText(/amount must be/i)).toBeInTheDocument();
expect(handleSubmit).not.toHaveBeenCalled();
Expand All @@ -61,10 +63,7 @@ describe("BillingPlanForm", () => {
await user.type(screen.getByLabelText(/plan name/i), "Starter");
await user.type(screen.getByLabelText(/description/i), "Entry tier");
await user.type(screen.getByLabelText(/^amount$/i), "0");
await user.type(
screen.getByLabelText(/customer email/i),
"not-an-email",
);
await user.type(screen.getByLabelText(/customer email/i), "not-an-email");

await user.click(screen.getByRole("button", { name: /create plan/i }));

Expand All @@ -86,6 +85,8 @@ describe("BillingPlanForm", () => {
).toBeInTheDocument();

await user.type(screen.getByLabelText(/plan name/i), "Pro");
expect(screen.queryByText(/plan name is required/i)).not.toBeInTheDocument();
expect(
screen.queryByText(/plan name is required/i),
).not.toBeInTheDocument();
});
});
12 changes: 4 additions & 8 deletions src/components/billing/billing-plan-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ export function BillingPlanForm({ onSubmit }: BillingPlanFormProps) {
<div className="space-y-1">
<h2 className="text-xl font-semibold">Create billing plan</h2>
<p className="text-sm text-muted-foreground">
Define a recurring charge. Validate every field before activating
the plan.
Define a recurring charge. Validate every field before activating the
plan.
</p>
</div>

Expand Down Expand Up @@ -145,9 +145,7 @@ export function BillingPlanForm({ onSubmit }: BillingPlanFormProps) {
id="plan-interval"
required
aria-invalid={Boolean(errors.interval)}
aria-describedby={
errors.interval ? "plan-interval-error" : undefined
}
aria-describedby={errors.interval ? "plan-interval-error" : undefined}
value={draft.interval}
onChange={(event) => updateField("interval", event.target.value)}
disabled={isSubmitting}
Expand Down Expand Up @@ -175,9 +173,7 @@ export function BillingPlanForm({ onSubmit }: BillingPlanFormProps) {
errors.customerEmail ? "plan-customer-email-error" : undefined
}
value={draft.customerEmail}
onChange={(event) =>
updateField("customerEmail", event.target.value)
}
onChange={(event) => updateField("customerEmail", event.target.value)}
disabled={isSubmitting}
className={inputClass(Boolean(errors.customerEmail))}
placeholder="customer@example.com"
Expand Down
10 changes: 8 additions & 2 deletions src/components/invoice/invoice-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export function InvoiceControls({
<div className="flex flex-wrap items-center gap-3">
{/* Status Dropdown */}
<div className="flex items-center gap-2">
<label htmlFor="status-filter" className="text-xs font-semibold text-muted-foreground uppercase tracking-wide hidden md:inline">
<label
htmlFor="status-filter"
className="text-xs font-semibold text-muted-foreground uppercase tracking-wide hidden md:inline"
>
Status:
</label>
<select
Expand All @@ -67,7 +70,10 @@ export function InvoiceControls({

{/* Sort Dropdown */}
<div className="flex items-center gap-2">
<label htmlFor="sort-by" className="text-xs font-semibold text-muted-foreground uppercase tracking-wide hidden md:inline">
<label
htmlFor="sort-by"
className="text-xs font-semibold text-muted-foreground uppercase tracking-wide hidden md:inline"
>
Sort by:
</label>
<select
Expand Down
Loading
Loading