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
27 changes: 16 additions & 11 deletions app/bounty/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -209,15 +210,15 @@ export default function BountiesPage() {
rewardRange[0] !== 0 ||
rewardRange[1] !== 5000 ||
statusFilter !== "open") && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="h-6 text-[10px] text-primary hover:text-primary/80 p-0 hover:bg-transparent"
>
Reset
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="h-6 text-[10px] text-primary hover:text-primary/80 p-0 hover:bg-transparent"
>
Reset
</Button>
)}
</div>

<div className="space-y-6">
Expand Down Expand Up @@ -423,6 +424,10 @@ export default function BountiesPage() {
</Accordion>
</div>
</div>

<div className="hidden lg:block">
<MiniLeaderboard className="w-full" />
</div>
</div>
</aside>

Expand Down Expand Up @@ -498,7 +503,7 @@ export default function BountiesPage() {
)}
</main>
</div>
</div>
</div>
</div >
</div >
);
}
5 changes: 5 additions & 0 deletions components/global-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -25,10 +26,14 @@ export function GlobalNavbar() {
<Link href="/projects" className={pathname?.startsWith('/projects') ? "text-black" : "text-gray-500 hover:text-black transition-colors"}>
Projects
</Link>
<Link href="/leaderboard" className={pathname?.startsWith('/leaderboard') ? "text-black" : "text-gray-500 hover:text-black transition-colors"}>
Leaderboard
</Link>
</div>
</div>

<div className="flex items-center gap-2">
<NavRankBadge userId="user-1" className="hidden sm:flex" /> {/* TODO: Replace with actual auth user ID */}
<SearchCommand />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/leaderboard/leaderboard-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
106 changes: 106 additions & 0 deletions components/leaderboard/mini-leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card className={cn("border-border/50 bg-background-card", className)}>
<CardContent className="py-6 text-center text-white/70 text-sm flex flex-col items-center gap-2">
<AlertCircle className="h-4 w-4" />
<span>Failed to load leaderboard</span>
</CardContent>
</Card>
);
}

return (
<Card className={cn("border-border/50 bg-background-card overflow-hidden", className)}>
<CardHeader className="pb-3 pt-4 px-4 flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-semibold text-white flex items-center gap-2">
<Trophy className="h-4 w-4 text-yellow-500" />
{title}
</CardTitle>
<Link href="/leaderboard" className="text-xs text-white/70 hover:text-white transition-colors flex items-center">
View All <ChevronRight className="h-3 w-3 ml-0.5" />
</Link>
</CardHeader>
<CardContent className="p-0">
{isLoading ? (
<div className="space-y-1 p-2">
{Array.from({ length: limit }).map((_, i) => (
<div key={i} className="flex items-center gap-3 p-2">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="space-y-1 flex-1">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-2 w-16" />
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col">
{contributors?.map((contributor, index) => (
<Link
key={contributor.id}
href={`/profile/${contributor.userId}`}
className="flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors border-b border-border/40 last:border-0 group"
>
<div className="flex-shrink-0 relative">
<Avatar className="h-9 w-9 border border-border/50">
<AvatarImage src={contributor.avatarUrl || undefined} />
<AvatarFallback>{contributor.displayName?.[0] ?? "?"}</AvatarFallback>
</Avatar>
<div className="absolute -top-1 -left-1 text-white/70 flex items-center justify-center w-4 h-4 rounded-full bg-background border border-border text-[10px] font-bold">
{index + 1}
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-white text-sm truncate group-hover:text-primary transition-colors">
{contributor.displayName}
</span>
</div>
<div className="flex items-center gap-2 mt-0.5">
<TierBadge tier={contributor.tier} className="h-4 text-[10px] px-1.5 py-0" />
<span className="text-[10px] text-white/70 font-mono">
{contributor.totalScore.toLocaleString()} pts
</span>
</div>
</div>
</Link>
))}
<div className="p-2">
<Button variant="ghost" className="w-full text-xs h-8 text-white/70 hover:text-black" asChild>
<Link href="/leaderboard">
See full rankings
</Link>
</Button>
</div>
</div>
)}
</CardContent>
</Card>
);
}
42 changes: 42 additions & 0 deletions components/leaderboard/nav-rank-badge.tsx
Original file line number Diff line number Diff line change
@@ -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 <Skeleton className="h-6 w-16 rounded-full" />;
}

if (!data || !data.rank) return null;

return (
<Link href="/leaderboard">
<Badge
variant="secondary"
className={cn(
"gap-1.5 pl-1.5 pr-2.5 py-0.5 hover:bg-secondary/80 transition-colors cursor-pointer",
className
)}
>
<div className="bg-yellow-500/10 text-yellow-500 rounded-full p-0.5">
<Trophy className="h-3 w-3" />
</div>
<span className="font-mono font-medium">#{data.rank}</span>
</Badge>
</Link>
);
}
6 changes: 3 additions & 3 deletions hooks/use-leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
});
};
Expand Down
3 changes: 2 additions & 1 deletion lib/api/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const leaderboardApi = {
return get<LeaderboardResponse>(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}`);
},

Expand Down
Loading