diff --git a/app/bounty/page.tsx b/app/bounty/page.tsx index 0db3e8b..f55de18 100644 --- a/app/bounty/page.tsx +++ b/app/bounty/page.tsx @@ -25,6 +25,7 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Search, Filter } from "lucide-react"; +import { MiniLeaderboard } from "@/components/leaderboard/mini-leaderboard"; export default function BountiesPage() { const { data, isLoading, isError, error, refetch } = useBounties(); @@ -209,15 +210,15 @@ export default function BountiesPage() { rewardRange[0] !== 0 || rewardRange[1] !== 5000 || statusFilter !== "open") && ( - - )} + + )}
@@ -423,6 +424,10 @@ export default function BountiesPage() {
+ +
+ +
@@ -498,7 +503,7 @@ export default function BountiesPage() { )} - - + + ); } diff --git a/components/global-navbar.tsx b/components/global-navbar.tsx index 7cb9f55..2751825 100644 --- a/components/global-navbar.tsx +++ b/components/global-navbar.tsx @@ -3,6 +3,7 @@ import Link from "next/link" import { SearchCommand } from "@/components/search-command" import { usePathname } from "next/navigation" +import { NavRankBadge } from "@/components/leaderboard/nav-rank-badge" export function GlobalNavbar() { const pathname = usePathname() @@ -25,10 +26,14 @@ export function GlobalNavbar() { Projects + + Leaderboard +
+ {/* TODO: Replace with actual auth user ID */}
diff --git a/components/leaderboard/leaderboard-filters.tsx b/components/leaderboard/leaderboard-filters.tsx index 03ae4e8..3483053 100644 --- a/components/leaderboard/leaderboard-filters.tsx +++ b/components/leaderboard/leaderboard-filters.tsx @@ -63,7 +63,7 @@ export function LeaderboardFilters({ filters, onFilterChange }: LeaderboardFilte }; const handleTagToggle = (tag: string) => { - const currentTags = filters.tags || []; + const currentTags = filters.tags ?? []; const newTags = currentTags.includes(tag) ? currentTags.filter((t) => t !== tag) : [...currentTags, tag]; diff --git a/components/leaderboard/mini-leaderboard.tsx b/components/leaderboard/mini-leaderboard.tsx new file mode 100644 index 0000000..79316b2 --- /dev/null +++ b/components/leaderboard/mini-leaderboard.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useTopContributors } from "@/hooks/use-leaderboard"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { TierBadge } from "./tier-badge"; +import { Trophy, ChevronRight, AlertCircle } from "lucide-react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +interface MiniLeaderboardProps { + className?: string; + limit?: number; + title?: string; +} + +export function MiniLeaderboard({ + className, + limit = 5, + title = "Top Contributors" +}: MiniLeaderboardProps) { + const { data: contributors, isLoading, error } = useTopContributors(limit); + + if (error) { + // Quiet failure for sidebars - or minimal error state + return ( + + + + Failed to load leaderboard + + + ); + } + + return ( + + + + + {title} + + + View All + + + + {isLoading ? ( +
+ {Array.from({ length: limit }).map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+ ) : ( +
+ {contributors?.map((contributor, index) => ( + +
+ + + {contributor.displayName?.[0] ?? "?"} + +
+ {index + 1} +
+
+
+
+ + {contributor.displayName} + +
+
+ + + {contributor.totalScore.toLocaleString()} pts + +
+
+ + ))} +
+ +
+
+ )} +
+
+ ); +} diff --git a/components/leaderboard/nav-rank-badge.tsx b/components/leaderboard/nav-rank-badge.tsx new file mode 100644 index 0000000..8941eb1 --- /dev/null +++ b/components/leaderboard/nav-rank-badge.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useUserRank } from "@/hooks/use-leaderboard"; +import { Badge } from "@/components/ui/badge"; +import { Trophy } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +interface NavRankBadgeProps { + userId?: string; + className?: string; +} + +export function NavRankBadge({ userId, className }: NavRankBadgeProps) { + const { data, isLoading } = useUserRank(userId); + + if (!userId) return null; + + if (isLoading) { + return ; + } + + if (!data || !data.rank) return null; + + return ( + + +
+ +
+ #{data.rank} +
+ + ); +} diff --git a/hooks/use-leaderboard.ts b/hooks/use-leaderboard.ts index 2532507..7d75394 100644 --- a/hooks/use-leaderboard.ts +++ b/hooks/use-leaderboard.ts @@ -16,8 +16,8 @@ export const useLeaderboard = (filters: LeaderboardFilters, limit: number = 20) queryFn: ({ pageParam = 1 }) => leaderboardApi.fetchLeaderboard(filters, { page: pageParam, limit }), getNextPageParam: (lastPage, allPages) => { - const loadedCount = allPages.flatMap(p => p.entries).length; - if (loadedCount < lastPage.totalCount) { + // Optimization: Use simple math instead of iterating all entries + if (allPages.length * limit < lastPage.totalCount) { return allPages.length + 1; } return undefined; @@ -30,7 +30,7 @@ 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: () => leaderboardApi.fetchUserRank(userId), enabled: !!userId, }); }; diff --git a/lib/api/leaderboard.ts b/lib/api/leaderboard.ts index 51e17df..85023ad 100644 --- a/lib/api/leaderboard.ts +++ b/lib/api/leaderboard.ts @@ -24,7 +24,8 @@ export const leaderboardApi = { return get(LEADERBOARD_ENDPOINT, { params }); }, - fetchUserRank: async (userId: string): Promise<{ rank: number, contributor: LeaderboardContributor }> => { + fetchUserRank: async (userId?: string): Promise<{ rank: number, contributor: LeaderboardContributor } | null> => { + if (!userId) return null; return get<{ rank: number, contributor: LeaderboardContributor }>(`${LEADERBOARD_ENDPOINT}/user/${userId}`); },