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}`);
},