diff --git a/app/api/transparency/payouts/route.ts b/app/api/transparency/payouts/route.ts new file mode 100644 index 0000000..d74dc48 --- /dev/null +++ b/app/api/transparency/payouts/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const limit = Number(searchParams.get("limit")) || 10; + + // TODO: Replace with real DB queries when backend is ready + const payouts: { + id: string; + contributorName: string; + contributorAvatar: string | null; + amount: number; + currency: string; + projectName: string; + paidAt: string; + }[] = []; + + return NextResponse.json(payouts.slice(0, limit)); +} \ No newline at end of file diff --git a/app/api/transparency/stats/route.ts b/app/api/transparency/stats/route.ts new file mode 100644 index 0000000..baf83b3 --- /dev/null +++ b/app/api/transparency/stats/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + // TODO: Replace with real DB queries when backend is ready + const stats = { + totalFundsDistributed: 0, + totalContributorsPaid: 0, + totalProjectsFunded: 0, + averagePayoutTimeDays: 0, + }; + + return NextResponse.json(stats); +} \ No newline at end of file diff --git a/app/transparency/page.tsx b/app/transparency/page.tsx new file mode 100644 index 0000000..d75abd6 --- /dev/null +++ b/app/transparency/page.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { usePlatformStats, useRecentPayouts } from "@/hooks/use-transparency"; +import { AlertCircle, DollarSign, Users, FolderOpen, Clock } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import type { RecentPayout } from "@/lib/api/transparency"; + +// Stat Card + +function StatCard({ + title, + value, + icon: Icon, + isLoading, +}: { + title: string; + value: string; + icon: React.ElementType; + isLoading: boolean; +}) { + return ( + + + + {title} + + + + + {isLoading ? ( + + ) : ( +

{value}

+ )} +
+
+ ); +} + +// Payout Row + +function PayoutRow({ payout }: { payout: RecentPayout }) { + return ( +
+
+ + + + {payout.contributorName.slice(0, 2).toUpperCase()} + + +
+

+ {payout.contributorName} +

+

{payout.projectName}

+
+
+
+ + {payout.amount.toLocaleString()} {payout.currency} + + + {new Date(payout.paidAt).toLocaleDateString()} + +
+
+ ); +} + +// Page + +export default function TransparencyPage() { + const { + data: stats, + isLoading: statsLoading, + isError: statsError, + error: statsErr, + refetch: refetchStats, + } = usePlatformStats(); + + const { + data: payouts, + isLoading: payoutsLoading, + isError: payoutsError, + refetch: refetchPayouts, + } = useRecentPayouts(10); + + const statCards = [ + { + title: "Total Funds Distributed", + value: stats + ? `$${stats.totalFundsDistributed.toLocaleString()}` + : "$0", + icon: DollarSign, + }, + { + title: "Contributors Paid", + value: stats ? stats.totalContributorsPaid.toLocaleString() : "0", + icon: Users, + }, + { + title: "Projects Funded", + value: stats ? stats.totalProjectsFunded.toLocaleString() : "0", + icon: FolderOpen, + }, + { + title: "Avg. Payout Time", + value: stats ? `${stats.averagePayoutTimeDays} days` : "0 days", + icon: Clock, + }, + ]; + + return ( +
+ {/* Hero Header */} +
+
+

+ Transparency +

+

+ A real-time look at platform funding activity, contributor payouts, + and ecosystem growth. +

+
+
+ +
+ + {/* Stats Error */} + {statsError && ( + + + Error + +

+ Failed to load platform stats.{" "} + {(statsErr as Error)?.message} +

+ +
+
+ )} + + {/* Stats Grid */} +
+

+ Platform Overview +

+
+ {statCards.map((card) => ( + + ))} +
+
+ + {/* Recent Payouts */} +
+

+ Recent Payouts +

+ + {payoutsError ? ( + + + Error + +

Failed to load recent payouts.

+ +
+
+ ) : ( + + + {payoutsLoading ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ +
+ + +
+ +
+ ))} +
+ ) : !payouts || payouts.length === 0 ? ( +

+ No payouts recorded yet. +

+ ) : ( + payouts.map((payout) => ( + + )) + )} +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/global-navbar.tsx b/components/global-navbar.tsx index df91580..ef50d75 100644 --- a/components/global-navbar.tsx +++ b/components/global-navbar.tsx @@ -57,6 +57,16 @@ export function GlobalNavbar() { > Leaderboard + + Transparency + [...TRANSPARENCY_KEYS.all, 'stats'] as const, + payouts: (limit: number) => [...TRANSPARENCY_KEYS.all, 'payouts', limit] as const, +}; + +export const usePlatformStats = () => { + return useQuery({ + queryKey: TRANSPARENCY_KEYS.stats(), + queryFn: () => transparencyApi.getStats(), + staleTime: 1000 * 60 * 10, // 10 minutes + }); +}; + +export const useRecentPayouts = (limit = 10) => { + return useQuery({ + queryKey: TRANSPARENCY_KEYS.payouts(limit), + queryFn: () => transparencyApi.getRecentPayouts(limit), + staleTime: 1000 * 60 * 5, + }); +}; \ No newline at end of file diff --git a/lib/api/transparency.ts b/lib/api/transparency.ts new file mode 100644 index 0000000..114d04d --- /dev/null +++ b/lib/api/transparency.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { get } from './client'; + +// Schema +export const platformStatsSchema = z.object({ + totalFundsDistributed: z.number(), + totalContributorsPaid: z.number(), + totalProjectsFunded: z.number(), + averagePayoutTimeDays: z.number(), +}); + +export const recentPayoutSchema = z.object({ + id: z.string(), + contributorName: z.string(), + contributorAvatar: z.string().nullable(), + amount: z.number(), + currency: z.string(), + projectName: z.string(), + paidAt: z.string(), +}); + +export type PlatformStats = z.infer; +export type RecentPayout = z.infer; + +const TRANSPARENCY_ENDPOINT = '/api/transparency'; + +export const transparencyApi = { + getStats: (): Promise => + get(`${TRANSPARENCY_ENDPOINT}/stats`), + + getRecentPayouts: (limit = 10): Promise => + get(`${TRANSPARENCY_ENDPOINT}/payouts`, { params: { limit } }), +}; \ No newline at end of file