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';