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
92 changes: 56 additions & 36 deletions app/(dashboard)/finances/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Calendar as CalendarComponent } from "@/components/ui/calendar"
import { MechanicHelp } from "@/components/patterns/MechanicHelp"
import { platformFeesHelp } from "@/components/patterns/mechanic-help-content"
import { format } from "date-fns"
import { FinancesEmptyState } from "@/components/empty-states/finances"

// Mock data for financial overview
const financialData = {
Expand Down Expand Up @@ -188,6 +189,14 @@ export default function FinancesPage() {
<TabsTrigger value="distribution">Fee Distribution</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
{financialData.totalFees === 0 ? (
/* Empty state: no deposits/revenue — prompt user to connect wallet */
<Card>
<CardContent className="pt-6">
<FinancesEmptyState variant="deposits" />
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
Expand All @@ -212,6 +221,7 @@ export default function FinancesPage() {
</CardContent>
</Card>
</div>
)}
</TabsContent>
<TabsContent value="transactions" className="space-y-4">
<Card>
Expand All @@ -220,26 +230,31 @@ export default function FinancesPage() {
<CardDescription>Platform fee transactions for the selected period</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Event</TableHead>
<TableHead>Type</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{financialData.transactions.map((transaction) => (
<TableRow key={transaction.id}>
<TableCell>{new Date(transaction.date).toLocaleDateString()}</TableCell>
<TableCell className="font-medium">{transaction.eventTitle}</TableCell>
<TableCell>{transaction.type === "platform_fee" ? "Platform Fee" : transaction.type}</TableCell>
<TableCell className="text-right">{hideBalances ? maskAmount(transaction.amount) : transaction.amount.toLocaleString()}</TableCell>
{financialData.transactions.length === 0 ? (
/* Empty state: no trades/transactions in the selected period */
<FinancesEmptyState variant="trades" />
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Event</TableHead>
<TableHead>Type</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{financialData.transactions.map((transaction) => (
<TableRow key={transaction.id}>
<TableCell>{new Date(transaction.date).toLocaleDateString()}</TableCell>
<TableCell className="font-medium">{transaction.eventTitle}</TableCell>
<TableCell>{transaction.type === "platform_fee" ? "Platform Fee" : transaction.type}</TableCell>
<TableCell className="text-right">{hideBalances ? maskAmount(transaction.amount) : transaction.amount.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
Expand All @@ -253,24 +268,29 @@ export default function FinancesPage() {
<CardDescription>Breakdown of platform fees by prediction category</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Category</TableHead>
<TableHead>Percentage</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{financialData.feeDistribution.map((item) => (
<TableRow key={item.category}>
<TableCell className="font-medium">{item.category}</TableCell>
<TableCell>{item.percentage}%</TableCell>
<TableCell className="text-right">${item.amount.toLocaleString()}</TableCell>
{financialData.feeDistribution.length === 0 ? (
/* Empty state: no fee distribution data — prompt user to check claims */
<FinancesEmptyState variant="distribution" />
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Category</TableHead>
<TableHead>Percentage</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{financialData.feeDistribution.map((item) => (
<TableRow key={item.category}>
<TableCell className="font-medium">{item.category}</TableCell>
<TableCell>{item.percentage}%</TableCell>
<TableCell className="text-right">${item.amount.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
Expand Down
103 changes: 103 additions & 0 deletions components/empty-states/finances/FinancesEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use client';

/**
* FinancesEmptyState
*
* Category-aware empty state for the three /finances tabs:
* - "deposits" → no wallet activity yet → CTA: connect wallet
* - "trades" → no trade history yet → CTA: browse markets
* - "distribution" → no payouts/fees yet → CTA: view claims
*
* Design decisions
* ─────────────────
* • Uses design tokens (muted, foreground, primary) for dark-mode consistency.
* • Illustrations are decorative SVGs, aria-hidden; all meaning is in the text.
* • CTA buttons are plain <a> tags (asChild on Button) so they work as real
* deep-links without a JS router dependency — swap to <Link> if preferred.
* • WCAG 2.1 AA: minimum 4.5:1 contrast via token colours; focus-visible rings
* are inherited from the global Button styles.
*/

import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { NoDepositsIllustration } from './NoDepositsIllustration';
import { NoTradesIllustration } from './NoTradesIllustration';
import { NoPayoutsIllustration } from './NoPayoutsIllustration';

// ─── Types ───────────────────────────────────────────────────────────────────

export type FinancesEmptyVariant = 'deposits' | 'trades' | 'distribution';

interface FinancesEmptyStateProps {
/** Which finances tab this empty state belongs to */
variant: FinancesEmptyVariant;
className?: string;
}

// ─── Per-variant copy & routing config ───────────────────────────────────────

const CONFIG: Record<
FinancesEmptyVariant,
{
Illustration: React.ComponentType<{ className?: string }>;
title: string;
description: string;
cta: string;
href: string;
}
> = {
deposits: {
Illustration: NoDepositsIllustration,
title: 'No deposits yet',
description:
'Connect your wallet to fund your account and start participating in prediction markets.',
cta: 'Connect wallet',
href: '/dashboard', // wallet-connect flow is triggered from the dashboard
},
trades: {
Illustration: NoTradesIllustration,
title: 'No transactions yet',
description:
"You haven't made any trades in this period. Browse open markets to place your first prediction.",
cta: 'Browse markets',
href: '/events',
},
distribution: {
Illustration: NoPayoutsIllustration,
title: 'No fee distribution yet',
description:
'Platform fees are distributed once markets resolve. Check your pending claims to see what you may be owed.',
cta: 'View claims',
href: '/mypredictions',
},
};

// ─── Component ───────────────────────────────────────────────────────────────

export function FinancesEmptyState({ variant, className }: FinancesEmptyStateProps) {
const { Illustration, title, description, cta, href } = CONFIG[variant];

return (
<div
role="status"
aria-label={title}
className={cn(
'flex flex-col items-center justify-center gap-5 py-16 px-6 text-center',
className
)}
>
{/* 2× illustration — renders crisply on retina via SVG viewBox */}
<Illustration className="w-[120px] h-[80px] sm:w-[180px] sm:h-[120px]" />

<div className="space-y-2 max-w-xs">
<h3 className="text-base font-semibold text-foreground">{title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{description}</p>
</div>

<Button asChild size="sm">
<Link href={href}>{cta}</Link>
</Button>
</div>
);
}
79 changes: 79 additions & 0 deletions components/empty-states/finances/NoDepositsIllustration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* NoDepositsIllustration
*
* Decorative SVG illustration for the empty "deposits" state on /finances.
* Exported at a 2× logical size (240×160 viewBox) so it renders crisp on
* high-DPI screens without extra image assets.
*
* Accessibility: aria-hidden="true" — the surrounding empty-state component
* provides all necessary text context.
*/
export function NoDepositsIllustration({ className }: { className?: string }) {
return (
<svg
aria-hidden="true"
viewBox="0 0 240 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
{/* Wallet body */}
<rect
x="40"
y="50"
width="160"
height="90"
rx="12"
className="fill-muted stroke-border"
strokeWidth="2"
/>
{/* Wallet flap */}
<rect
x="40"
y="40"
width="160"
height="30"
rx="8"
className="fill-muted/60 stroke-border"
strokeWidth="2"
/>
{/* Card slot */}
<rect
x="130"
y="75"
width="55"
height="36"
rx="6"
className="fill-background stroke-border"
strokeWidth="1.5"
/>
{/* Coin stack — left */}
<ellipse cx="80" cy="118" rx="18" ry="6" className="fill-muted-foreground/20" />
<rect x="62" y="100" width="36" height="18" rx="4" className="fill-muted-foreground/20" />
<ellipse cx="80" cy="100" rx="18" ry="6" className="fill-muted-foreground/30" />
{/* Down-arrow indicating "no incoming" */}
<path
d="M120 22 L120 38 M113 31 L120 38 L127 31"
className="stroke-muted-foreground/40"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Dashed circle suggesting missing funds */}
<circle
cx="120"
cy="95"
r="14"
className="stroke-muted-foreground/25"
strokeWidth="1.5"
strokeDasharray="4 3"
/>
<path
d="M115 95 h10 M120 90 v10"
className="stroke-muted-foreground/30"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
61 changes: 61 additions & 0 deletions components/empty-states/finances/NoPayoutsIllustration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* NoPayoutsIllustration
*
* Decorative SVG illustration for the empty "fee distribution / payouts" state
* on /finances. A donut chart outline with an empty centre signals no
* distributed fees yet.
*
* Accessibility: aria-hidden="true" — surrounding text provides full context.
*/
export function NoPayoutsIllustration({ className }: { className?: string }) {
return (
<svg
aria-hidden="true"
viewBox="0 0 240 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
{/* Outer donut ring — dashed to signal "nothing here" */}
<circle
cx="120"
cy="80"
r="52"
className="stroke-muted-foreground/20"
strokeWidth="18"
strokeDasharray="6 4"
/>
{/* Inner cutout (white/bg fill gives donut shape without clip-path) */}
<circle cx="120" cy="80" r="34" className="fill-background" />

{/* Centre label — currency sign, greyed out */}
<text
x="120"
y="87"
textAnchor="middle"
fontSize="24"
fontWeight="700"
className="fill-muted-foreground/25"
fontFamily="system-ui"
>
$
</text>

{/* Three "slice" stubs that hint at categories but are empty */}
<line x1="120" y1="28" x2="120" y2="38" className="stroke-muted-foreground/15" strokeWidth="2" strokeLinecap="round" />
<line x1="165" y1="107" x2="158" y2="100" className="stroke-muted-foreground/15" strokeWidth="2" strokeLinecap="round" />
<line x1="75" y1="107" x2="82" y2="100" className="stroke-muted-foreground/15" strokeWidth="2" strokeLinecap="round" />

{/* Label dots at chart perimeter */}
<circle cx="120" cy="24" r="3" className="fill-muted-foreground/20" />
<circle cx="168" cy="112" r="3" className="fill-muted-foreground/20" />
<circle cx="72" cy="112" r="3" className="fill-muted-foreground/20" />

{/* Tiny legend rows — greyed out placeholders */}
<rect x="30" y="142" width="40" height="6" rx="3" className="fill-muted-foreground/15" />
<rect x="78" y="142" width="30" height="6" rx="3" className="fill-muted-foreground/10" />
<rect x="116" y="142" width="50" height="6" rx="3" className="fill-muted-foreground/15" />
<rect x="174" y="142" width="36" height="6" rx="3" className="fill-muted-foreground/10" />
</svg>
);
}
Loading