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
2 changes: 1 addition & 1 deletion app/api/bounties/[id]/milestones/advance/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
return NextResponse.json({ error: 'Bounty not found' }, { status: 404 });
}

let updates: Partial<typeof participation> = {
const updates: Partial<typeof participation> = {
lastUpdatedAt: new Date().toISOString()
};

Expand Down Expand Up @@ -64,7 +64,7 @@

return NextResponse.json({ success: true, data: updated });

} catch (error) {

Check warning on line 67 in app/api/bounties/[id]/milestones/advance/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'error' is defined but never used
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
29 changes: 29 additions & 0 deletions app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { getMockLeaderboard } from '@/lib/mock-leaderboard';
import { LeaderboardResponse, ReputationTier } from '@/types/leaderboard';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const tier = searchParams.get('tier') as ReputationTier | null;

// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 300));

const { data, total } = getMockLeaderboard(page, limit, tier || undefined);

const response: LeaderboardResponse = {
entries: data.map((contributor, index) => ({
rank: (page - 1) * limit + index + 1,
previousRank: null, // Mock data doesn't track history yet
rankChange: 0,
contributor,
})),
totalCount: total,
currentUserRank: null, // Only relevant if user context is provided
lastUpdatedAt: new Date().toISOString(),
};

return NextResponse.json(response);
}
15 changes: 15 additions & 0 deletions app/api/leaderboard/top/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from 'next/server';
import { getMockLeaderboard } from '@/lib/mock-leaderboard';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const count = parseInt(searchParams.get('count') || '5');

// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 200));

// Get top N contributors
const { data } = getMockLeaderboard(1, count);

return NextResponse.json(data);
}
26 changes: 26 additions & 0 deletions app/api/leaderboard/user/[userId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from 'next/server';
import { getMockUserRank } from '@/lib/mock-leaderboard';

interface Params {
params: Promise<{ userId: string }>;
}

export async function GET(request: Request, { params }: Params) {
// Await params as per Next.js 15+ requirements if applicable, or good practice for future
const { userId } = await params;

// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 200));

const result = getMockUserRank(userId);

if (!result) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

return NextResponse.json({
rank: result.rank,
contributor: result.contributor,
paramsUserId: userId
});
}
64 changes: 64 additions & 0 deletions hooks/use-leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { leaderboardApi } from '@/lib/api/leaderboard';
import { LeaderboardFilters } from '@/types/leaderboard';

export const LEADERBOARD_KEYS = {
all: ['leaderboard'] as const,
lists: () => [...LEADERBOARD_KEYS.all, 'list'] as const,
list: (filters: LeaderboardFilters) => [...LEADERBOARD_KEYS.lists(), filters] as const,
user: (userId: string) => [...LEADERBOARD_KEYS.all, 'user', userId] as const,
top: (count: number) => [...LEADERBOARD_KEYS.all, 'top', count] as const,
};

export const useLeaderboard = (filters: LeaderboardFilters, limit: number = 20) => {
return useInfiniteQuery({
queryKey: LEADERBOARD_KEYS.list(filters),
queryFn: ({ pageParam = 1 }) =>
leaderboardApi.fetchLeaderboard(filters, { page: pageParam, limit }),
getNextPageParam: (lastPage, allPages) => {
const loadedCount = allPages.flatMap(p => p.entries).length;
if (loadedCount < lastPage.totalCount) {
return allPages.length + 1;
}
return undefined;
},
initialPageParam: 1,
staleTime: 1000 * 60 * 5, // 5 minutes
});
};

export const useUserRank = (userId?: string) => {
return useQuery({
queryKey: LEADERBOARD_KEYS.user(userId || ''),
queryFn: () => leaderboardApi.fetchUserRank(userId!),
enabled: !!userId,
});
};

export const useTopContributors = (count: number = 5) => {
return useQuery({
queryKey: LEADERBOARD_KEYS.top(count),
queryFn: () => leaderboardApi.fetchTopContributors(count),
staleTime: 1000 * 60 * 15, // 15 minutes
});
};

export const usePrefetchLeaderboardPage = () => {
const queryClient = useQueryClient();

return (filters: LeaderboardFilters, page: number, limit: number) => {
queryClient.prefetchInfiniteQuery({
queryKey: LEADERBOARD_KEYS.list(filters),
queryFn: ({ pageParam }) => leaderboardApi.fetchLeaderboard(filters, { page: pageParam as number, limit }),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
const loadedCount = allPages.flatMap(p => p.entries).length;
if (loadedCount < lastPage.totalCount) {
return allPages.length + 1;
}
return undefined;
},
pages: page // Prefetch up to this many pages if needed
});
};
};
34 changes: 34 additions & 0 deletions lib/api/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { get } from './client';
import {
LeaderboardResponse,
LeaderboardFilters,
LeaderboardPagination,
LeaderboardContributor
} from '@/types/leaderboard';

const LEADERBOARD_ENDPOINT = '/api/leaderboard';

export const leaderboardApi = {
fetchLeaderboard: async (
filters: LeaderboardFilters,
pagination: LeaderboardPagination
): Promise<LeaderboardResponse> => {
const params: Record<string, unknown> = {
page: pagination.page,
limit: pagination.limit,
};

if (filters.tier) params.tier = filters.tier;
if (filters.tags?.length) params.tags = filters.tags.join(',');

return get<LeaderboardResponse>(LEADERBOARD_ENDPOINT, { params });
},

fetchUserRank: async (userId: string): Promise<{ rank: number, contributor: LeaderboardContributor }> => {
return get<{ rank: number, contributor: LeaderboardContributor }>(`${LEADERBOARD_ENDPOINT}/user/${userId}`);
},

fetchTopContributors: async (count: number = 5): Promise<LeaderboardContributor[]> => {
return get<LeaderboardContributor[]>(`${LEADERBOARD_ENDPOINT}/top`, { params: { count } });
}
};
74 changes: 74 additions & 0 deletions lib/mock-leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { LeaderboardContributor, ReputationTier } from "@/types/leaderboard";

const generateMockContributor = (
id: string,
rank: number,
tier: ReputationTier,
score: number
): LeaderboardContributor => ({
id: `contributor-${id}`,
userId: `user-${id}`,
walletAddress: `0x${Math.random().toString(16).substr(2, 40)}`,
displayName: `Contributor ${id}`,
avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${id}`,
totalScore: score,
tier,
stats: {
totalCompleted: Math.floor(Math.random() * 50) + 1,
totalEarnings: Math.floor(Math.random() * 10000) + 100,
earningsCurrency: "USDC",
completionRate: 0.85 + Math.random() * 0.15,
averageCompletionTime: Math.floor(Math.random() * 72) + 24,
currentStreak: Math.floor(Math.random() * 5),
longestStreak: Math.floor(Math.random() * 10) + 5,
},
topTags: ["DeFi", "Smart Contracts", "Frontend", "Auditing"].sort(() => 0.5 - Math.random()).slice(0, 3),
lastActiveAt: new Date(Date.now() - Math.floor(Math.random() * 7) * 24 * 60 * 60 * 1000).toISOString(),
});

export const mockLeaderboardData: LeaderboardContributor[] = [
generateMockContributor("1", 1, "LEGEND", 15000),
generateMockContributor("2", 2, "LEGEND", 14500),
generateMockContributor("3", 3, "EXPERT", 12000),
generateMockContributor("4", 4, "EXPERT", 11500),
generateMockContributor("5", 5, "ESTABLISHED", 9000),
generateMockContributor("6", 6, "ESTABLISHED", 8500),
generateMockContributor("7", 7, "CONTRIBUTOR", 5000),
generateMockContributor("8", 8, "CONTRIBUTOR", 4500),
generateMockContributor("9", 9, "NEWCOMER", 1000),
generateMockContributor("10", 10, "NEWCOMER", 800),
// Generate some more for pagination testing
...Array.from({ length: 40 }).map((_, i) =>
generateMockContributor(`${i + 11}`, i + 11, "NEWCOMER", 500 - i * 10)
)
];

export const getMockLeaderboard = (
page: number = 1,
limit: number = 10,
filterTier?: ReputationTier
) => {
let filtered = [...mockLeaderboardData];

if (filterTier) {
filtered = filtered.filter(c => c.tier === filterTier);
}

const sorted = filtered.sort((a, b) => b.totalScore - a.totalScore);
const start = (page - 1) * limit;
const paginated = sorted.slice(start, start + limit);

return {
data: paginated,
total: filtered.length
};
};

export const getMockUserRank = (userId: string) => {
const index = mockLeaderboardData.findIndex(u => u.userId === userId);
if (index === -1) return null;
return {
rank: index + 1,
contributor: mockLeaderboardData[index]
};
};
2 changes: 1 addition & 1 deletion types/bounty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface Bounty {
// Let's add them as optional to be safe and backward compatible with existing components.
requirements?: string[]
scope?: string
milestones?: any[] // Optional milestone definition
milestones?: unknown[] // Optional milestone definition
}

export type BountyStatus = Bounty['status']
Expand Down
59 changes: 59 additions & 0 deletions types/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export type ReputationTier =
| 'NEWCOMER'
| 'CONTRIBUTOR'
| 'ESTABLISHED'
| 'EXPERT'
| 'LEGEND';

export type LeaderboardTimeframe =
| 'ALL_TIME'
| 'THIS_MONTH'
| 'THIS_WEEK';

export interface ContributorStats {
totalCompleted: number;
totalEarnings: number;
earningsCurrency: string;
completionRate: number;
averageCompletionTime: number; // in hours
currentStreak: number; // consecutive days/weeks depending on logic, usually completion streak
longestStreak: number;
}

export interface LeaderboardContributor {
id: string;
userId: string;
walletAddress: string | null;
displayName: string;
avatarUrl: string | null;
totalScore: number;
tier: ReputationTier;
stats: ContributorStats;
topTags: string[];
lastActiveAt: string; // ISO Date string
}

export interface LeaderboardEntry {
rank: number;
previousRank: number | null;
rankChange: number | null;
contributor: LeaderboardContributor;
}

export interface LeaderboardResponse {
entries: LeaderboardEntry[];
totalCount: number;
currentUserRank: number | null;
lastUpdatedAt: string; // ISO Date string
}

export interface LeaderboardFilters {
timeframe: LeaderboardTimeframe;
tier?: ReputationTier;
tags?: string[];
}

export interface LeaderboardPagination {
page: number;
limit: number;
}
Loading