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
19 changes: 19 additions & 0 deletions app/api/transparency/payouts/route.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an upper bound to limit before wiring in real DB queries.

Number(searchParams.get("limit")) || 10 accepts arbitrarily large values. A caller can pass limit=1000000, which will become a full-table scan once the TODO is resolved with a real query. Cap it explicitly:

🛡️ Proposed fix
-    const limit = Number(searchParams.get("limit")) || 10;
+    const raw = Number(searchParams.get("limit"));
+    const limit = raw > 0 && Number.isFinite(raw) ? Math.min(raw, 100) : 10;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/transparency/payouts/route.ts` at line 5, The limit parsed from
searchParams (const limit) must be constrained to a safe maximum to prevent very
large queries; update the parsing to coerce to an integer, handle NaN/negative
values, and clamp it with a defined max (e.g., const MAX_LIMIT = 100) using
Math.min/Math.max so limit = clamp(parsed, 1, MAX_LIMIT); apply this change
where const limit is defined in route.ts (the request handler that reads
searchParams.get("limit")).


// 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));
}
13 changes: 13 additions & 0 deletions app/api/transparency/stats/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}
230 changes: 230 additions & 0 deletions app/transparency/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-32" />
) : (
<p className="text-2xl font-bold text-foreground">{value}</p>
)}
</CardContent>
</Card>
);
}
Comment on lines +15 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check the actual file to see imports and full context
cat -n app/transparency/page.tsx | head -50

Repository: boundlessfi/bounties

Length of output: 2103


🏁 Script executed:

# Check tsconfig.json for jsx and allowUmdGlobalAccess settings
fd -e json "tsconfig" --max-depth 2 -x cat {}

Repository: boundlessfi/bounties

Length of output: 729


🏁 Script executed:

# Look for global.d.ts files that might define React globally
fd -e "d.ts" "global" --max-depth 3

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

# Search for other uses of React.ElementType in the codebase
rg "React\.(ElementType|FC|ReactNode)" --type=tsx -A 2 -B 2 | head -100

Repository: boundlessfi/bounties

Length of output: 91


🏁 Script executed:

# Search for React.ElementType or React. type references (using proper rg syntax)
rg "React\." app/transparency/page.tsx -A 1 -B 1

Repository: boundlessfi/bounties

Length of output: 135


🏁 Script executed:

# Look for how other components define icon/component props
rg "icon:|Icon" --type ts --type tsx -A 2 -B 2 | head -150

Repository: boundlessfi/bounties

Length of output: 91


🏁 Script executed:

# Check if React is imported anywhere in the codebase for type usage
rg "import.*React" --type tsx | grep -i "elementtype\|React\.FC\|React\.ReactNode" | head -20

Repository: boundlessfi/bounties

Length of output: 91


🏁 Script executed:

# Look for ElementType imports to see the pattern used elsewhere
rg "ElementType" --type tsx -B 3 | head -50

Repository: boundlessfi/bounties

Length of output: 91


🏁 Script executed:

# Search for React. references in tsx files using glob pattern
rg "React\." -g "*.tsx" -A 1 -B 1 | head -100

Repository: boundlessfi/bounties

Length of output: 4260


🏁 Script executed:

# Search for how icon/Icon props are typed in other components
rg "icon.*:" -g "*.tsx" -A 1 | head -100

Repository: boundlessfi/bounties

Length of output: 7006


🏁 Script executed:

# Check if any component file imports ElementType directly
rg "import.*ElementType" -g "*.tsx"

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

# Verify by checking if the file would compile - look at how components handle component types
rg "React\.FC\|React\.Component\|ElementType" -g "*.tsx" -B 2 | head -80

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

# Check tier-badge.tsx imports (it uses React.ElementType)
head -20 components/reputation/tier-badge.tsx

Repository: boundlessfi/bounties

Length of output: 839


🏁 Script executed:

# Check a component using React.ReactNode to see if they import React
head -20 components/ui/alert-dialog.tsx

Repository: boundlessfi/bounties

Length of output: 642


🏁 Script executed:

# Final check: look at full imports of app/transparency/page.tsx
head -15 app/transparency/page.tsx

Repository: boundlessfi/bounties

Length of output: 720


React.ElementType used without importing React — TypeScript compile error.

With the new JSX transform, you still need to import React "in order to use Hooks or other exports that React provides" — and the same applies to type namespace references. Line 23 references React.ElementType but there is no import React from 'react' or import type { ElementType } from 'react' in the file. Under the project's tsconfig ("jsx": "react-jsx", strict: true, no allowUmdGlobalAccess), this produces a TypeScript error and will fail next build.

Note: components/reputation/tier-badge.tsx has the same issue on line 12.

🐛 Proposed fix
+import type { ElementType } from 'react';
 
 function StatCard({
     title,
     value,
     icon: Icon,
     isLoading,
 }: {
     title: string;
     value: string;
-    icon: React.ElementType;
+    icon: ElementType;
     isLoading: boolean;
 }) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function StatCard({
title,
value,
icon: Icon,
isLoading,
}: {
title: string;
value: string;
icon: React.ElementType;
isLoading: boolean;
}) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-32" />
) : (
<p className="text-2xl font-bold text-foreground">{value}</p>
)}
</CardContent>
</Card>
);
}
function StatCard({
title,
value,
icon: Icon,
isLoading,
}: {
title: string;
value: string;
icon: ElementType;
isLoading: boolean;
}) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-32" />
) : (
<p className="text-2xl font-bold text-foreground">{value}</p>
)}
</CardContent>
</Card>
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/transparency/page.tsx` around lines 15 - 43, StatCard uses the React type
namespace (React.ElementType) but the file does not import React, causing a TS
error; fix by importing the type instead of the whole namespace—add an import
type { ElementType } from 'react' at the top and change the prop type to use
ElementType (or alternatively add import React from 'react' if you prefer), and
apply the same change to the other component that references React.ElementType
(the tier badge component) so both compile under the "react-jsx" tsconfig.


// Payout Row

function PayoutRow({ payout }: { payout: RecentPayout }) {
return (
<div className="flex items-center justify-between py-3 border-b border-border/50 last:border-0">
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarImage src={payout.contributorAvatar ?? undefined} />
<AvatarFallback>
{payout.contributorName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium text-foreground">
{payout.contributorName}
</p>
<p className="text-xs text-muted-foreground">{payout.projectName}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="font-mono">
{payout.amount.toLocaleString()} {payout.currency}
</Badge>
<span className="text-xs text-muted-foreground hidden sm:block">
{new Date(payout.paidAt).toLocaleDateString()}
</span>
</div>
</div>
);
}

// 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 (
<div className="min-h-screen bg-background pb-12">
{/* Hero Header */}
<div className="border-b border-border/40">
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl md:text-4xl font-bold text-foreground tracking-tight mb-3">
Transparency
</h1>
<p className="text-lg text-muted-foreground max-w-2xl">
A real-time look at platform funding activity, contributor payouts,
and ecosystem growth.
</p>
</div>
</div>

<div className="container mx-auto px-4 py-8 space-y-10">

{/* Stats Error */}
{statsError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription className="flex flex-col gap-2">
<p>
Failed to load platform stats.{" "}
{(statsErr as Error)?.message}
</p>
<Button
variant="outline"
size="sm"
onClick={() => refetchStats()}
className="w-fit bg-background text-foreground border-border hover:bg-muted"
>
Try Again
</Button>
</AlertDescription>
</Alert>
)}
Comment on lines +137 to +156
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

as Error cast is redundant in TanStack Query v5.

In @tanstack/react-query v5, error is typed as Error | null by default, so statsErr is already Error | null. The as Error at line 144 is unnecessary; use statsErr?.message directly.

♻️ Proposed fix
-                            {(statsErr as Error)?.message}
+                            {statsErr?.message}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{statsError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription className="flex flex-col gap-2">
<p>
Failed to load platform stats.{" "}
{(statsErr as Error)?.message}
</p>
<Button
variant="outline"
size="sm"
onClick={() => refetchStats()}
className="w-fit bg-background text-foreground border-border hover:bg-muted"
>
Try Again
</Button>
</AlertDescription>
</Alert>
)}
{statsError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription className="flex flex-col gap-2">
<p>
Failed to load platform stats.{" "}
{statsErr?.message}
</p>
<Button
variant="outline"
size="sm"
onClick={() => refetchStats()}
className="w-fit bg-background text-foreground border-border hover:bg-muted"
>
Try Again
</Button>
</AlertDescription>
</Alert>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/transparency/page.tsx` around lines 137 - 156, The code is casting
statsErr to Error redundantly; update the AlertDescription rendering to remove
the unnecessary cast and use the existing typed variable directly (replace
{(statsErr as Error)?.message} with statsErr?.message) in the component that
shows the error (inside the JSX block rendering Alert / AlertDescription),
ensuring you still handle possible null by using optional chaining; no other
changes to refetchStats or the Alert structure are required.


{/* Stats Grid */}
<section>
<h2 className="text-xl font-semibold text-foreground mb-4">
Platform Overview
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{statCards.map((card) => (
<StatCard
key={card.title}
title={card.title}
value={card.value}
icon={card.icon}
isLoading={statsLoading}
/>
))}
</div>
</section>

{/* Recent Payouts */}
<section>
<h2 className="text-xl font-semibold text-foreground mb-4">
Recent Payouts
</h2>

{payoutsError ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription className="flex flex-col gap-2">
<p>Failed to load recent payouts.</p>
<Button
variant="outline"
size="sm"
onClick={() => refetchPayouts()}
className="w-fit bg-background text-foreground border-border hover:bg-muted"
>
Try Again
</Button>
</AlertDescription>
</Alert>
) : (
<Card>
<CardContent className="pt-4">
{payoutsLoading ? (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="space-y-1 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-20" />
</div>
<Skeleton className="h-6 w-20" />
</div>
))}
</div>
) : !payouts || payouts.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
No payouts recorded yet.
</p>
) : (
payouts.map((payout) => (
<PayoutRow key={payout.id} payout={payout} />
))
)}
</CardContent>
</Card>
)}
</section>
</div>
</div>
);
}
10 changes: 10 additions & 0 deletions components/global-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export function GlobalNavbar() {
>
Leaderboard
</Link>
<Link
href="/transparency"
className={`transition-colors hover:text-foreground/80 ${
pathname.startsWith("/transparency")
? "text-foreground"
: "text-foreground/60"
}`}
>
Transparency
</Link>
<Link
href="/wallet"
className={`transition-colors hover:text-foreground/80 ${
Expand Down
24 changes: 24 additions & 0 deletions hooks/use-transparency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import { transparencyApi } from '@/lib/api/transparency';

export const TRANSPARENCY_KEYS = {
all: ['transparency'] as const,
stats: () => [...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,
});
};
33 changes: 33 additions & 0 deletions lib/api/transparency.ts
Original file line number Diff line number Diff line change
@@ -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<typeof platformStatsSchema>;
export type RecentPayout = z.infer<typeof recentPayoutSchema>;

const TRANSPARENCY_ENDPOINT = '/api/transparency';

export const transparencyApi = {
getStats: (): Promise<PlatformStats> =>
get<PlatformStats>(`${TRANSPARENCY_ENDPOINT}/stats`),

getRecentPayouts: (limit = 10): Promise<RecentPayout[]> =>
get<RecentPayout[]>(`${TRANSPARENCY_ENDPOINT}/payouts`, { params: { limit } }),
};
Comment on lines +27 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find where the get helper is imported from in transparency.ts
head -30 lib/api/transparency.ts | grep -E "^import|^export"

Repository: boundlessfi/bounties

Length of output: 372


🏁 Script executed:

# Search for the get function definition in lib/client
fd -type f -e ts lib/client

Repository: boundlessfi/bounties

Length of output: 234


🏁 Script executed:

# Also search for any client-related files
fd -type f -name "*client*" lib

Repository: boundlessfi/bounties

Length of output: 234


🏁 Script executed:

# Find client files in lib directory
find lib -name "*client*" -type f

Repository: boundlessfi/bounties

Length of output: 143


🏁 Script executed:

# Also check the directory structure of lib/api
ls -la lib/api/

Repository: boundlessfi/bounties

Length of output: 662


🏁 Script executed:

# Read the client.ts file to see the get() function implementation
cat -n lib/api/client.ts

Repository: boundlessfi/bounties

Length of output: 4964


Zod schemas are defined but never used for runtime validation.

get<PlatformStats>() and get<RecentPayout[]>() accept generic type parameters, not Zod schemas — the get helper in lib/api/client.ts performs no runtime validation and simply returns the axios response data. When the stub routes are replaced with real DB queries, unexpected field types or missing fields will surface as silent runtime bugs rather than Zod parse errors. Either call .parse() in the API methods, or extend the get helper to accept a validator argument.

♻️ Proposed refactor (parse at the call site)
 export const transparencyApi = {
-    getStats: (): Promise<PlatformStats> =>
-        get<PlatformStats>(`${TRANSPARENCY_ENDPOINT}/stats`),
+    getStats: async (): Promise<PlatformStats> => {
+        const data = await get(`${TRANSPARENCY_ENDPOINT}/stats`);
+        return platformStatsSchema.parse(data);
+    },

-    getRecentPayouts: (limit = 10): Promise<RecentPayout[]> =>
-        get<RecentPayout[]>(`${TRANSPARENCY_ENDPOINT}/payouts`, { params: { limit } }),
+    getRecentPayouts: async (limit = 10): Promise<RecentPayout[]> => {
+        const data = await get(`${TRANSPARENCY_ENDPOINT}/payouts`, { params: { limit } });
+        return z.array(recentPayoutSchema).parse(data);
+    },
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const transparencyApi = {
getStats: (): Promise<PlatformStats> =>
get<PlatformStats>(`${TRANSPARENCY_ENDPOINT}/stats`),
getRecentPayouts: (limit = 10): Promise<RecentPayout[]> =>
get<RecentPayout[]>(`${TRANSPARENCY_ENDPOINT}/payouts`, { params: { limit } }),
};
export const transparencyApi = {
getStats: async (): Promise<PlatformStats> => {
const data = await get(`${TRANSPARENCY_ENDPOINT}/stats`);
return platformStatsSchema.parse(data);
},
getRecentPayouts: async (limit = 10): Promise<RecentPayout[]> => {
const data = await get(`${TRANSPARENCY_ENDPOINT}/payouts`, { params: { limit } });
return z.array(recentPayoutSchema).parse(data);
},
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/transparency.ts` around lines 27 - 33, The transparencyApi methods
currently return axios data without runtime validation; update getStats and
getRecentPayouts to validate the response with the Zod schemas before returning
(e.g., import your PlatformStatsSchema and RecentPayoutSchema and call
PlatformStatsSchema.parse(...) on the result of get in getStats, and
RecentPayoutSchema.array().parse(...) in getRecentPayouts), or alternately
extend the get helper to accept a validator and run parse inside it; target the
transparencyApi.getStats and transparencyApi.getRecentPayouts calls to ensure
parsed/validated data (use the TRANSPARENCY_ENDPOINT and existing
PlatformStats/RecentPayout types as references).

Loading