diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx
index ae98942..d730c54 100644
--- a/app/leaderboard/page.tsx
+++ b/app/leaderboard/page.tsx
@@ -5,6 +5,7 @@ import { LeaderboardTable } from "@/components/leaderboard/leaderboard-table";
import { LeaderboardFilters } from "@/components/leaderboard/leaderboard-filters";
import { UserRankSidebar } from "@/components/leaderboard/user-rank-sidebar";
import { LeaderboardFilters as FiltersType, ReputationTier } from "@/types/leaderboard";
+import { LeaderboardTimeframe } from "@/lib/graphql/generated";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { TIMEFRAMES, TIERS } from "@/components/leaderboard/leaderboard-filters";
@@ -22,7 +23,7 @@ export default function LeaderboardPage() {
const initialTimeframe = TIMEFRAMES.some(t => t.value === rawTimeframe)
? (rawTimeframe as FiltersType["timeframe"])
- : "ALL_TIME";
+ : LeaderboardTimeframe.AllTime;
const initialTier = TIERS.some(t => t.value === rawTier)
? (rawTier as ReputationTier)
@@ -65,7 +66,7 @@ export default function LeaderboardPage() {
// Sync debounced filters to URL
useEffect(() => {
const params = new URLSearchParams();
- if (debouncedFilters.timeframe !== "ALL_TIME") params.set("timeframe", debouncedFilters.timeframe);
+ if (debouncedFilters.timeframe !== LeaderboardTimeframe.AllTime) params.set("timeframe", debouncedFilters.timeframe);
if (debouncedFilters.tier) params.set("tier", debouncedFilters.tier);
if (debouncedFilters.tags && debouncedFilters.tags.length > 0) {
params.set("tags", debouncedFilters.tags.join(","));
diff --git a/components/bounty-detail/bounty-detail-submissions-card.tsx b/components/bounty-detail/bounty-detail-submissions-card.tsx
index 40f95a4..8fdd99e 100644
--- a/components/bounty-detail/bounty-detail-submissions-card.tsx
+++ b/components/bounty-detail/bounty-detail-submissions-card.tsx
@@ -57,7 +57,7 @@ export function BountyDetailSubmissionsCard({
const [prUrl, setPrUrl] = useState("");
const [submitComments, setSubmitComments] = useState("");
const [reviewComments, setReviewComments] = useState("");
- const [reviewStatus, setReviewStatus] = useState("APPROVED");
+ const reviewStatus = "APPROVED";
const [transactionHash, setTransactionHash] = useState("");
const submitToBounty = useSubmitToBounty();
diff --git a/components/leaderboard/leaderboard-filters.tsx b/components/leaderboard/leaderboard-filters.tsx
index fee53c4..43df8ae 100644
--- a/components/leaderboard/leaderboard-filters.tsx
+++ b/components/leaderboard/leaderboard-filters.tsx
@@ -9,10 +9,9 @@ import {
SelectValue,
} from "@/components/ui/select";
import {
- LeaderboardFilters as FiltersType,
- LeaderboardTimeframe,
- ReputationTier
+ LeaderboardFilters as FiltersType
} from "@/types/leaderboard";
+import { LeaderboardTimeframe, ReputationTier } from "@/lib/graphql/generated";
import { FilterX } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
@@ -38,17 +37,17 @@ interface LeaderboardFiltersProps {
}
export const TIMEFRAMES: { value: LeaderboardTimeframe; label: string }[] = [
- { value: "ALL_TIME", label: "All Time" },
- { value: "THIS_MONTH", label: "This Month" },
- { value: "THIS_WEEK", label: "This Week" },
+ { value: LeaderboardTimeframe.AllTime, label: "All Time" },
+ { value: LeaderboardTimeframe.ThisMonth, label: "This Month" },
+ { value: LeaderboardTimeframe.ThisWeek, label: "This Week" },
];
export const TIERS: { value: ReputationTier; label: string }[] = [
- { value: "LEGEND", label: "Legend" },
- { value: "EXPERT", label: "Expert" },
- { value: "ESTABLISHED", label: "Established" },
- { value: "CONTRIBUTOR", label: "Contributor" },
- { value: "NEWCOMER", label: "Newcomer" },
+ { value: ReputationTier.Legend, label: "Legend" },
+ { value: ReputationTier.Expert, label: "Expert" },
+ { value: ReputationTier.Established, label: "Established" },
+ { value: ReputationTier.Contributor, label: "Contributor" },
+ { value: ReputationTier.Newcomer, label: "Newcomer" },
];
// Mock available tags for filter - in real app could be passed as prop
@@ -72,13 +71,13 @@ export function LeaderboardFilters({ filters, onFilterChange }: LeaderboardFilte
const clearFilters = () => {
onFilterChange({
- timeframe: "ALL_TIME",
+ timeframe: LeaderboardTimeframe.AllTime,
tier: undefined,
tags: [],
});
};
- const hasActiveFilters = filters.timeframe !== "ALL_TIME" || filters.tier || (filters.tags?.length || 0) > 0;
+ const hasActiveFilters = filters.timeframe !== LeaderboardTimeframe.AllTime || filters.tier || (filters.tags?.length || 0) > 0;
return (
diff --git a/components/leaderboard/mini-leaderboard.tsx b/components/leaderboard/mini-leaderboard.tsx
index 3f4d4d3..c4e0e48 100644
--- a/components/leaderboard/mini-leaderboard.tsx
+++ b/components/leaderboard/mini-leaderboard.tsx
@@ -9,6 +9,7 @@ import { TierBadge } from "@/components/reputation/tier-badge";
import { Trophy, ChevronRight, AlertCircle } from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
+import { LeaderboardContributor } from "@/types/leaderboard";
interface MiniLeaderboardProps {
className?: string;
@@ -61,7 +62,7 @@ export function MiniLeaderboard({
) : (
- {contributors?.map((contributor, index) => (
+ {contributors?.map((contributor: LeaderboardContributor, index: number) => (
{
return useInfiniteQuery({
queryKey: LEADERBOARD_KEYS.list(filters),
- queryFn: ({ pageParam = 1 }) =>
- leaderboardApi.fetchLeaderboard(filters, { page: pageParam, limit }),
- getNextPageParam: (lastPage, allPages) => {
- // Optimization: Use simple math instead of iterating all entries
- if (allPages.length * limit < lastPage.totalCount) {
+ queryFn: ({ pageParam = 1 }) => {
+ return fetcher
(
+ LeaderboardDocument,
+ { filters, pagination: { page: pageParam as number, limit } }
+ )().then(data => data.leaderboard);
+ },
+ getNextPageParam: (lastPage: LeaderboardResponse, allPages: LeaderboardResponse[]) => {
+ // Use actual loaded entries count instead of optimistic calculation
+ const loadedCount = allPages.flatMap(p => p.entries).length;
+ if (loadedCount < lastPage.totalCount) {
return allPages.length + 1;
}
return undefined;
@@ -30,7 +46,13 @@ export const useLeaderboard = (filters: LeaderboardFilters, limit: number = 20)
export const useUserRank = (userId?: string) => {
return useQuery({
queryKey: LEADERBOARD_KEYS.user(userId || ''),
- queryFn: () => leaderboardApi.fetchUserRank(userId),
+ queryFn: () => {
+ if (!userId) return null;
+ return fetcher(
+ UserLeaderboardRankDocument,
+ { userId }
+ )().then(data => data.userLeaderboardRank);
+ },
enabled: !!userId,
});
};
@@ -38,7 +60,12 @@ export const useUserRank = (userId?: string) => {
export const useTopContributors = (count: number = 5) => {
return useQuery({
queryKey: LEADERBOARD_KEYS.top(count),
- queryFn: () => leaderboardApi.fetchTopContributors(count),
+ queryFn: () => {
+ return fetcher(
+ TopContributorsDocument,
+ { count }
+ )().then(data => data.topContributors);
+ },
staleTime: 1000 * 60 * 15, // 15 minutes
});
};
@@ -46,13 +73,18 @@ export const useTopContributors = (count: number = 5) => {
export const usePrefetchLeaderboardPage = () => {
const queryClient = useQueryClient();
- return (filters: LeaderboardFilters, page: number, limit: number) => {
- queryClient.prefetchInfiniteQuery({
+ return (filters: LeaderboardFilters, page: number, limit: number): Promise => {
+ return queryClient.prefetchInfiniteQuery({
queryKey: LEADERBOARD_KEYS.list(filters),
- queryFn: ({ pageParam }) => leaderboardApi.fetchLeaderboard(filters, { page: pageParam as number, limit }),
+ queryFn: ({ pageParam }: { pageParam: number }) => {
+ return fetcher(
+ LeaderboardDocument,
+ { filters, pagination: { page: pageParam, limit } }
+ )().then(data => data.leaderboard);
+ },
initialPageParam: 1,
- getNextPageParam: (lastPage, allPages) => {
- const loadedCount = allPages.flatMap(p => p.entries).length;
+ getNextPageParam: (lastPage: LeaderboardResponse, allPages: LeaderboardResponse[]) => {
+ const loadedCount = allPages.flatMap((p: LeaderboardResponse) => p.entries).length;
if (loadedCount < lastPage.totalCount) {
return allPages.length + 1;
}
diff --git a/lib/api/leaderboard.ts b/lib/api/leaderboard.ts
deleted file mode 100644
index 85023ad..0000000
--- a/lib/api/leaderboard.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-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 => {
- const params: Record = {
- page: pagination.page,
- limit: pagination.limit,
- };
-
- if (filters.tier) params.tier = filters.tier;
- if (filters.tags?.length) params.tags = filters.tags.join(',');
-
- return get(LEADERBOARD_ENDPOINT, { params });
- },
-
- fetchUserRank: async (userId?: string): Promise<{ rank: number, contributor: LeaderboardContributor } | null> => {
- if (!userId) return null;
- return get<{ rank: number, contributor: LeaderboardContributor }>(`${LEADERBOARD_ENDPOINT}/user/${userId}`);
- },
-
- fetchTopContributors: async (count: number = 5): Promise => {
- return get(`${LEADERBOARD_ENDPOINT}/top`, { params: { count } });
- }
-};
diff --git a/lib/graphql/generated.ts b/lib/graphql/generated.ts
index 8fb0acb..a9b6335 100644
--- a/lib/graphql/generated.ts
+++ b/lib/graphql/generated.ts
@@ -757,6 +757,19 @@ export type ContactOrganizersInput = {
subject: Scalars['String']['input'];
};
+export type ContributorStats = {
+ __typename?: 'ContributorStats';
+ averageCompletionTime: Scalars['Float']['output'];
+ completionRate: Scalars['Float']['output'];
+ currentStreak: Scalars['Int']['output'];
+ currentTierPoints?: Maybe;
+ earningsCurrency: Scalars['String']['output'];
+ longestStreak: Scalars['Int']['output'];
+ nextTierThreshold?: Maybe;
+ totalCompleted: Scalars['Int']['output'];
+ totalEarnings: Scalars['Float']['output'];
+};
+
export type CreateBlogPostDto = {
categories?: InputMaybe>;
content: Scalars['String']['input'];
@@ -849,6 +862,53 @@ export enum HackathonStatus {
Published = 'PUBLISHED'
}
+export type LeaderboardContributor = {
+ __typename?: 'LeaderboardContributor';
+ avatarUrl?: Maybe;
+ displayName: Scalars['String']['output'];
+ id: Scalars['ID']['output'];
+ lastActiveAt: Scalars['DateTime']['output'];
+ stats: ContributorStats;
+ tier: ReputationTier;
+ topTags: Array;
+ totalScore: Scalars['Float']['output'];
+ userId: Scalars['String']['output'];
+ walletAddress?: Maybe;
+};
+
+export type LeaderboardEntry = {
+ __typename?: 'LeaderboardEntry';
+ contributor: LeaderboardContributor;
+ previousRank?: Maybe;
+ rank: Scalars['Int']['output'];
+ rankChange?: Maybe;
+};
+
+export type LeaderboardFilters = {
+ tags?: InputMaybe>;
+ tier?: InputMaybe;
+ timeframe: LeaderboardTimeframe;
+};
+
+export type LeaderboardPagination = {
+ limit: Scalars['Int']['input'];
+ page: Scalars['Int']['input'];
+};
+
+export type LeaderboardResponse = {
+ __typename?: 'LeaderboardResponse';
+ currentUserRank?: Maybe;
+ entries: Array;
+ lastUpdatedAt: Scalars['DateTime']['output'];
+ totalCount: Scalars['Int']['output'];
+};
+
+export enum LeaderboardTimeframe {
+ AllTime = 'ALL_TIME',
+ ThisMonth = 'THIS_MONTH',
+ ThisWeek = 'THIS_WEEK'
+}
+
export enum MilestoneReviewStatusEnum {
Approved = 'APPROVED',
Pending = 'PENDING',
@@ -1159,10 +1219,16 @@ export type Query = {
bounties: PaginatedBounties;
/** Get a single bounty by ID */
bounty: Bounty;
+ /** Get leaderboard with filtering and pagination */
+ leaderboard: LeaderboardResponse;
/** Get bounties for a specific organization */
organizationBounties: Array;
/** Get bounties for a specific project */
projectBounties: Array;
+ /** Get top contributors */
+ topContributors: Array;
+ /** Get user's rank in leaderboard */
+ userLeaderboardRank?: Maybe;
};
@@ -1285,6 +1351,12 @@ export type QueryBountyArgs = {
};
+export type QueryLeaderboardArgs = {
+ filters: LeaderboardFilters;
+ pagination: LeaderboardPagination;
+};
+
+
export type QueryOrganizationBountiesArgs = {
organizationId: Scalars['ID']['input'];
};
@@ -1294,11 +1366,29 @@ export type QueryProjectBountiesArgs = {
projectId: Scalars['ID']['input'];
};
+
+export type QueryTopContributorsArgs = {
+ count?: InputMaybe;
+};
+
+
+export type QueryUserLeaderboardRankArgs = {
+ userId: Scalars['ID']['input'];
+};
+
export type RejectRewardDistributionDto = {
adminNote?: InputMaybe;
reason: Scalars['String']['input'];
};
+export enum ReputationTier {
+ Contributor = 'CONTRIBUTOR',
+ Established = 'ESTABLISHED',
+ Expert = 'EXPERT',
+ Legend = 'LEGEND',
+ Newcomer = 'NEWCOMER'
+}
+
export type ReviewSubmissionInput = {
reviewComments?: InputMaybe;
status: Scalars['String']['input'];
@@ -1351,6 +1441,12 @@ export type UpdateHackathonStatusInput = {
status: HackathonStatus;
};
+export type UserLeaderboardRankResponse = {
+ __typename?: 'UserLeaderboardRankResponse';
+ contributor: LeaderboardContributor;
+ rank: Scalars['Int']['output'];
+};
+
export type CreateBountyMutationVariables = Exact<{
input: CreateBountyInput;
}>;
@@ -1411,6 +1507,28 @@ export type SubmissionFieldsFragment = { __typename?: 'BountySubmissionType', id
export type SubmissionFieldsWithContactFragment = { __typename?: 'BountySubmissionType', id: string, bountyId: string, submittedBy: string, githubPullRequestUrl?: string | null, status: string, createdAt: string, updatedAt: string, reviewedAt?: string | null, reviewedBy?: string | null, reviewComments?: string | null, paidAt?: string | null, rewardTransactionHash?: string | null, submittedByUser?: { __typename?: 'BountySubmissionUser', email?: string | null, id: string, name?: string | null, image?: string | null } | null, reviewedByUser?: { __typename?: 'BountySubmissionUser', email?: string | null, id: string, name?: string | null, image?: string | null } | null };
+export type LeaderboardQueryVariables = Exact<{
+ filters: LeaderboardFilters;
+ pagination: LeaderboardPagination;
+}>;
+
+
+export type LeaderboardQuery = { __typename?: 'Query', leaderboard: { __typename?: 'LeaderboardResponse', totalCount: number, currentUserRank?: number | null, lastUpdatedAt: string, entries: Array<{ __typename?: 'LeaderboardEntry', rank: number, previousRank?: number | null, rankChange?: number | null, contributor: { __typename?: 'LeaderboardContributor', id: string, userId: string, walletAddress?: string | null, displayName: string, avatarUrl?: string | null, totalScore: number, tier: ReputationTier, topTags: Array, lastActiveAt: string, stats: { __typename?: 'ContributorStats', totalCompleted: number, totalEarnings: number, earningsCurrency: string, completionRate: number, averageCompletionTime: number, currentStreak: number, longestStreak: number, nextTierThreshold?: number | null, currentTierPoints?: number | null } } }> } };
+
+export type UserLeaderboardRankQueryVariables = Exact<{
+ userId: Scalars['ID']['input'];
+}>;
+
+
+export type UserLeaderboardRankQuery = { __typename?: 'Query', userLeaderboardRank?: { __typename?: 'UserLeaderboardRankResponse', rank: number, contributor: { __typename?: 'LeaderboardContributor', id: string, userId: string, walletAddress?: string | null, displayName: string, avatarUrl?: string | null, totalScore: number, tier: ReputationTier, topTags: Array, lastActiveAt: string, stats: { __typename?: 'ContributorStats', totalCompleted: number, totalEarnings: number, earningsCurrency: string, completionRate: number, averageCompletionTime: number, currentStreak: number, longestStreak: number, nextTierThreshold?: number | null, currentTierPoints?: number | null } } } | null };
+
+export type TopContributorsQueryVariables = Exact<{
+ count?: InputMaybe;
+}>;
+
+
+export type TopContributorsQuery = { __typename?: 'Query', topContributors: Array<{ __typename?: 'LeaderboardContributor', id: string, userId: string, walletAddress?: string | null, displayName: string, avatarUrl?: string | null, totalScore: number, tier: ReputationTier, topTags: Array, lastActiveAt: string, stats: { __typename?: 'ContributorStats', totalCompleted: number, totalEarnings: number, earningsCurrency: string, completionRate: number, averageCompletionTime: number, currentStreak: number, longestStreak: number, nextTierThreshold?: number | null, currentTierPoints?: number | null } }> };
+
export type SubmitToBountyMutationVariables = Exact<{
input: CreateSubmissionInput;
}>;
@@ -1711,6 +1829,154 @@ export const useProjectBountiesQuery = <
useProjectBountiesQuery.getKey = (variables: ProjectBountiesQueryVariables) => ['ProjectBounties', variables];
+export const LeaderboardDocument = `
+ query Leaderboard($filters: LeaderboardFilters!, $pagination: LeaderboardPagination!) {
+ leaderboard(filters: $filters, pagination: $pagination) {
+ entries {
+ rank
+ previousRank
+ rankChange
+ contributor {
+ id
+ userId
+ walletAddress
+ displayName
+ avatarUrl
+ totalScore
+ tier
+ stats {
+ totalCompleted
+ totalEarnings
+ earningsCurrency
+ completionRate
+ averageCompletionTime
+ currentStreak
+ longestStreak
+ nextTierThreshold
+ currentTierPoints
+ }
+ topTags
+ lastActiveAt
+ }
+ }
+ totalCount
+ currentUserRank
+ lastUpdatedAt
+ }
+}
+ `;
+
+export const useLeaderboardQuery = <
+ TData = LeaderboardQuery,
+ TError = unknown
+ >(
+ variables: LeaderboardQueryVariables,
+ options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] }
+ ) => {
+
+ return useQuery(
+ {
+ queryKey: ['Leaderboard', variables],
+ queryFn: fetcher(LeaderboardDocument, variables),
+ ...options
+ }
+ )};
+
+useLeaderboardQuery.getKey = (variables: LeaderboardQueryVariables) => ['Leaderboard', variables];
+
+export const UserLeaderboardRankDocument = `
+ query UserLeaderboardRank($userId: ID!) {
+ userLeaderboardRank(userId: $userId) {
+ rank
+ contributor {
+ id
+ userId
+ walletAddress
+ displayName
+ avatarUrl
+ totalScore
+ tier
+ stats {
+ totalCompleted
+ totalEarnings
+ earningsCurrency
+ completionRate
+ averageCompletionTime
+ currentStreak
+ longestStreak
+ nextTierThreshold
+ currentTierPoints
+ }
+ topTags
+ lastActiveAt
+ }
+ }
+}
+ `;
+
+export const useUserLeaderboardRankQuery = <
+ TData = UserLeaderboardRankQuery,
+ TError = unknown
+ >(
+ variables: UserLeaderboardRankQueryVariables,
+ options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] }
+ ) => {
+
+ return useQuery(
+ {
+ queryKey: ['UserLeaderboardRank', variables],
+ queryFn: fetcher(UserLeaderboardRankDocument, variables),
+ ...options
+ }
+ )};
+
+useUserLeaderboardRankQuery.getKey = (variables: UserLeaderboardRankQueryVariables) => ['UserLeaderboardRank', variables];
+
+export const TopContributorsDocument = `
+ query TopContributors($count: Int = 5) {
+ topContributors(count: $count) {
+ id
+ userId
+ walletAddress
+ displayName
+ avatarUrl
+ totalScore
+ tier
+ stats {
+ totalCompleted
+ totalEarnings
+ earningsCurrency
+ completionRate
+ averageCompletionTime
+ currentStreak
+ longestStreak
+ nextTierThreshold
+ currentTierPoints
+ }
+ topTags
+ lastActiveAt
+ }
+}
+ `;
+
+export const useTopContributorsQuery = <
+ TData = TopContributorsQuery,
+ TError = unknown
+ >(
+ variables?: TopContributorsQueryVariables,
+ options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] }
+ ) => {
+
+ return useQuery(
+ {
+ queryKey: variables === undefined ? ['TopContributors'] : ['TopContributors', variables],
+ queryFn: fetcher(TopContributorsDocument, variables),
+ ...options
+ }
+ )};
+
+useTopContributorsQuery.getKey = (variables?: TopContributorsQueryVariables) => variables === undefined ? ['TopContributors'] : ['TopContributors', variables];
+
export const SubmitToBountyDocument = `
mutation SubmitToBounty($input: CreateSubmissionInput!) {
submitToBounty(input: $input) {
diff --git a/lib/graphql/operations/leaderboard-queries.graphql b/lib/graphql/operations/leaderboard-queries.graphql
new file mode 100644
index 0000000..3d73714
--- /dev/null
+++ b/lib/graphql/operations/leaderboard-queries.graphql
@@ -0,0 +1,87 @@
+query Leaderboard($filters: LeaderboardFilters!, $pagination: LeaderboardPagination!) {
+ leaderboard(filters: $filters, pagination: $pagination) {
+ entries {
+ rank
+ previousRank
+ rankChange
+ contributor {
+ id
+ userId
+ walletAddress
+ displayName
+ avatarUrl
+ totalScore
+ tier
+ stats {
+ totalCompleted
+ totalEarnings
+ earningsCurrency
+ completionRate
+ averageCompletionTime
+ currentStreak
+ longestStreak
+ nextTierThreshold
+ currentTierPoints
+ }
+ topTags
+ lastActiveAt
+ }
+ }
+ totalCount
+ currentUserRank
+ lastUpdatedAt
+ }
+}
+
+query UserLeaderboardRank($userId: ID!) {
+ userLeaderboardRank(userId: $userId) {
+ rank
+ contributor {
+ id
+ userId
+ walletAddress
+ displayName
+ avatarUrl
+ totalScore
+ tier
+ stats {
+ totalCompleted
+ totalEarnings
+ earningsCurrency
+ completionRate
+ averageCompletionTime
+ currentStreak
+ longestStreak
+ nextTierThreshold
+ currentTierPoints
+ }
+ topTags
+ lastActiveAt
+ }
+ }
+}
+
+query TopContributors($count: Int = 5) {
+ topContributors(count: $count) {
+ id
+ userId
+ walletAddress
+ displayName
+ avatarUrl
+ totalScore
+ tier
+ stats {
+ totalCompleted
+ totalEarnings
+ earningsCurrency
+ completionRate
+ averageCompletionTime
+ currentStreak
+ longestStreak
+ nextTierThreshold
+ currentTierPoints
+ }
+ topTags
+ lastActiveAt
+ }
+}
diff --git a/lib/graphql/schema.graphql b/lib/graphql/schema.graphql
index 9266b9b..cdfb4a8 100644
--- a/lib/graphql/schema.graphql
+++ b/lib/graphql/schema.graphql
@@ -995,6 +995,15 @@ type Query {
"""Get bounties for a specific project"""
projectBounties(projectId: ID!): [Bounty!]!
+
+ """Get leaderboard with filtering and pagination"""
+ leaderboard(filters: LeaderboardFilters!, pagination: LeaderboardPagination!): LeaderboardResponse!
+
+ """Get user's rank in leaderboard"""
+ userLeaderboardRank(userId: ID!): UserLeaderboardRankResponse
+
+ """Get top contributors"""
+ topContributors(count: Int = 5): [LeaderboardContributor!]!
}
input RejectRewardDistributionDto {
@@ -1052,4 +1061,74 @@ input UpdateBountyInput {
input UpdateHackathonStatusInput {
status: HackathonStatus!
+}
+
+# Leaderboard Types
+enum ReputationTier {
+ NEWCOMER
+ CONTRIBUTOR
+ ESTABLISHED
+ EXPERT
+ LEGEND
+}
+
+enum LeaderboardTimeframe {
+ ALL_TIME
+ THIS_MONTH
+ THIS_WEEK
+}
+
+type ContributorStats {
+ totalCompleted: Int!
+ totalEarnings: Float!
+ earningsCurrency: String!
+ completionRate: Float!
+ averageCompletionTime: Float!
+ currentStreak: Int!
+ longestStreak: Int!
+ nextTierThreshold: Float
+ currentTierPoints: Float
+}
+
+type LeaderboardContributor {
+ id: ID!
+ userId: String!
+ walletAddress: String
+ displayName: String!
+ avatarUrl: String
+ totalScore: Float!
+ tier: ReputationTier!
+ stats: ContributorStats!
+ topTags: [String!]!
+ lastActiveAt: DateTime!
+}
+
+type LeaderboardEntry {
+ rank: Int!
+ previousRank: Int
+ rankChange: Int
+ contributor: LeaderboardContributor!
+}
+
+type LeaderboardResponse {
+ entries: [LeaderboardEntry!]!
+ totalCount: Int!
+ currentUserRank: Int
+ lastUpdatedAt: DateTime!
+}
+
+input LeaderboardFilters {
+ timeframe: LeaderboardTimeframe!
+ tier: ReputationTier
+ tags: [String!]
+}
+
+input LeaderboardPagination {
+ page: Int!
+ limit: Int!
+}
+
+type UserLeaderboardRankResponse {
+ rank: Int!
+ contributor: LeaderboardContributor!
}
\ No newline at end of file
diff --git a/lib/mock-leaderboard.ts b/lib/mock-leaderboard.ts
index 7c93149..2f5d546 100644
--- a/lib/mock-leaderboard.ts
+++ b/lib/mock-leaderboard.ts
@@ -27,19 +27,19 @@ const generateMockContributor = (
});
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),
+ generateMockContributor("1", 1, ReputationTier.Legend, 15000),
+ generateMockContributor("2", 2, ReputationTier.Legend, 14500),
+ generateMockContributor("3", 3, ReputationTier.Expert, 12000),
+ generateMockContributor("4", 4, ReputationTier.Expert, 11500),
+ generateMockContributor("5", 5, ReputationTier.Established, 9000),
+ generateMockContributor("6", 6, ReputationTier.Established, 8500),
+ generateMockContributor("7", 7, ReputationTier.Contributor, 5000),
+ generateMockContributor("8", 8, ReputationTier.Contributor, 4500),
+ generateMockContributor("9", 9, ReputationTier.Newcomer, 1000),
+ generateMockContributor("10", 10, ReputationTier.Newcomer, 800),
// Generate some more for pagination testing
...Array.from({ length: 40 }).map((_, i) =>
- generateMockContributor(`${i + 11}`, i + 11, "NEWCOMER", 500 - i * 10)
+ generateMockContributor(`${i + 11}`, i + 11, ReputationTier.Newcomer, 500 - i * 10)
)
];
diff --git a/package-lock.json b/package-lock.json
index 3e7e788..2014dce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -70,9 +70,9 @@
"zod": "^4.3.5"
},
"devDependencies": {
- "@graphql-codegen/cli": "^6.1.1",
- "@graphql-codegen/typescript": "^5.0.7",
- "@graphql-codegen/typescript-operations": "^5.0.7",
+ "@graphql-codegen/cli": "^6.1.2",
+ "@graphql-codegen/typescript": "^5.0.8",
+ "@graphql-codegen/typescript-operations": "^5.0.8",
"@graphql-codegen/typescript-react-query": "^6.1.1",
"@next/swc-wasm-nodejs": "^16.1.6",
"@tailwindcss/postcss": "^4",
@@ -2089,9 +2089,9 @@
"license": "0BSD"
},
"node_modules/@graphql-codegen/cli": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.1.1.tgz",
- "integrity": "sha512-Ni8UdZ6D/UTvLvDtPb6PzshI0lTqtLDnmv/2t1w2SYP92H0MMEdAzxB/ujDWwIXm2LzVPvvrGvzzCTMsyXa+mA==",
+ "version": "6.1.2",
+ "resolved": "https://registry.npmmirror.com/@graphql-codegen/cli/-/cli-6.1.2.tgz",
+ "integrity": "sha512-BQ49LF0jnQNL12rU1RucTemoX1bHx8slR4B11nOrp4k5NTojhcc1A1czzU5wXCK/1+ezNHrVGONWg3jxZUy08w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2325,7 +2325,7 @@
},
"node_modules/@graphql-codegen/typescript": {
"version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.8.tgz",
+ "resolved": "https://registry.npmmirror.com/@graphql-codegen/typescript/-/typescript-5.0.8.tgz",
"integrity": "sha512-lUW6ari+rXP6tz5B0LXjmV9rEMOphoCZAkt+SJGObLQ6w6544ZsXSsRga/EJiSvZ1fRfm9yaFoErOZ56IVThyg==",
"dev": true,
"license": "MIT",
@@ -2345,7 +2345,7 @@
},
"node_modules/@graphql-codegen/typescript-operations": {
"version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.8.tgz",
+ "resolved": "https://registry.npmmirror.com/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.8.tgz",
"integrity": "sha512-5H58DnDIy59Q+wcPRu13UnAS7fkMCW/vPI1+g8rHBmxuV9YGyGlVL9lE/fmJ06181hI7G9YGuUaoFYMJFU6bxQ==",
"dev": true,
"license": "MIT",
@@ -2378,7 +2378,7 @@
},
"node_modules/@graphql-codegen/typescript-react-query": {
"version": "6.1.1",
- "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-react-query/-/typescript-react-query-6.1.1.tgz",
+ "resolved": "https://registry.npmmirror.com/@graphql-codegen/typescript-react-query/-/typescript-react-query-6.1.1.tgz",
"integrity": "sha512-knSlUFmq7g7G2DIa5EGjOnwWtNfpU4k+sXWJkxdwJ7lU9nrw6pnDizJcjHCqKelRmk2xwfspVNzu0KoXP7LLsg==",
"dev": true,
"license": "MIT",
diff --git a/package.json b/package.json
index 912e431..113b99a 100644
--- a/package.json
+++ b/package.json
@@ -77,9 +77,9 @@
"zod": "^4.3.5"
},
"devDependencies": {
- "@graphql-codegen/cli": "^6.1.1",
- "@graphql-codegen/typescript": "^5.0.7",
- "@graphql-codegen/typescript-operations": "^5.0.7",
+ "@graphql-codegen/cli": "^6.1.2",
+ "@graphql-codegen/typescript": "^5.0.8",
+ "@graphql-codegen/typescript-operations": "^5.0.8",
"@graphql-codegen/typescript-react-query": "^6.1.1",
"@next/swc-wasm-nodejs": "^16.1.6",
"@tailwindcss/postcss": "^4",
diff --git a/types/leaderboard.ts b/types/leaderboard.ts
index f1930fd..e6b5ac1 100644
--- a/types/leaderboard.ts
+++ b/types/leaderboard.ts
@@ -1,61 +1,15 @@
-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;
- currentStreak: number;
- longestStreak: number;
- nextTierThreshold?: number;
- currentTierPoints?: 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;
-}
-
-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;
-}
-
-export interface LeaderboardFilters {
- timeframe: LeaderboardTimeframe;
- tier?: ReputationTier;
- tags?: string[];
-}
-
-export interface LeaderboardPagination {
- page: number;
- limit: number;
-}
+// Re-export GraphQL generated types for convenience
+export {
+ ReputationTier,
+ LeaderboardTimeframe
+} from '@/lib/graphql/generated';
+
+export type {
+ ContributorStats,
+ LeaderboardContributor,
+ LeaderboardEntry,
+ LeaderboardResponse,
+ LeaderboardFilters,
+ LeaderboardPagination,
+ UserLeaderboardRankResponse
+} from '@/lib/graphql/generated';