Skip to content
Open
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
26 changes: 21 additions & 5 deletions src/components/bounty-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ type BountyCardProps = {
tags: string[];
difficulty: "Easy" | "Medium" | "Hard";
progress: number;
onClick?: () => void;
};

// Format currency with comma separators
const formatReward = (amount: number): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};

const difficultyStyles = {
Expand All @@ -12,9 +23,14 @@ const difficultyStyles = {
Hard: "bg-rose-50 text-rose-700 border-rose-200",
};

export function BountyCard({ title, reward, tags, difficulty, progress }: BountyCardProps) {
export function BountyCard({ title, reward, tags, difficulty, progress, onClick }: BountyCardProps) {
return (
<div className="card p-4 sm:p-5 hover:shadow-md transition">
<div
className="card p-4 sm:p-5 hover:shadow-lg transition-all duration-200 cursor-pointer hover:-translate-y-0.5"
onClick={onClick}
role={onClick ? "button" : undefined}
tabIndex={onClick ? 0 : undefined}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<h3 className="text-base sm:text-lg font-semibold leading-snug break-words">{title}</h3>
Expand All @@ -27,7 +43,7 @@ export function BountyCard({ title, reward, tags, difficulty, progress }: Bounty
</div>
</div>
<div className="text-right shrink-0">
<div className="text-xl sm:text-xl font-bold">${reward}</div>
<div className="text-xl sm:text-xl font-bold">{formatReward(reward)}</div>
<span className={`mt-1 inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] sm:text-xs font-semibold whitespace-nowrap ${difficultyStyles[difficulty]}`}>
{difficulty}
</span>
Expand All @@ -40,8 +56,8 @@ export function BountyCard({ title, reward, tags, difficulty, progress }: Bounty
</div>
<div className="mt-1.5 h-2 w-full rounded-full bg-slate-100">
<div
className="h-2 rounded-full bg-brand-600"
style={{ width: `${progress}%` }}
className="h-2 rounded-full bg-brand-600 transition-all duration-300"
style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}
/>
</div>
</div>
Expand Down
55 changes: 30 additions & 25 deletions src/components/leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
"use client";

// BUG: Sorting algorithm doesn't handle ties correctly
// When two users have the same earnings, their relative order is inconsistent
// FIX: Add secondary sort key (e.g., by name or join date)

type LeaderboardEntry = {
id: string;
name: string;
Expand All @@ -12,36 +8,52 @@ type LeaderboardEntry = {
bounties_completed: number;
};

// Mock data with intentional ties in earnings
// Mock data with ties in earnings
const mockLeaderboard: LeaderboardEntry[] = [
{ id: "1", name: "alice_dev", avatar: "https://github.com/alice.png", earned: 5000, bounties_completed: 10 },
{ id: "2", name: "bob_coder", avatar: "https://github.com/bob.png", earned: 3500, bounties_completed: 7 },
{ id: "3", name: "charlie_eng", avatar: "https://github.com/charlie.png", earned: 3500, bounties_completed: 8 }, // TIE with bob
{ id: "3", name: "charlie_eng", avatar: "https://github.com/charlie.png", earned: 3500, bounties_completed: 8 },
{ id: "4", name: "diana_dev", avatar: "https://github.com/diana.png", earned: 2000, bounties_completed: 4 },
{ id: "5", name: "eve_hacker", avatar: "https://github.com/eve.png", earned: 2000, bounties_completed: 5 }, // TIE with diana
{ id: "6", name: "frank_dev", avatar: "https://github.com/frank.png", earned: 2000, bounties_completed: 3 }, // TIE with diana and eve
{ id: "5", name: "eve_hacker", avatar: "https://github.com/eve.png", earned: 2000, bounties_completed: 5 },
{ id: "6", name: "frank_dev", avatar: "https://github.com/frank.png", earned: 2000, bounties_completed: 3 },
];

export function Leaderboard() {
// BUG: This sort is unstable - tied entries will have inconsistent ordering
// The sort only compares by earned, but when earned values are equal,
// the result depends on the browser's sort implementation (which may vary)
const sorted = [...mockLeaderboard].sort((a, b) => b.earned - a.earned);
// Sort by earned (descending), then by bounties_completed (descending) as tiebreaker
const sorted = [...mockLeaderboard].sort((a, b) => {
if (b.earned !== a.earned) {
return b.earned - a.earned;
}
return b.bounties_completed - a.bounties_completed;
});

// Calculate ranks handling ties properly
let currentRank = 0;
let previousEarned = -1;
const rankedEntries = sorted.map((entry) => {
if (entry.earned !== previousEarned) {
currentRank = sorted.indexOf(entry) + 1;
previousEarned = entry.earned;
}
return { ...entry, rank: currentRank };
});

// BUG: Rank calculation doesn't account for ties properly
// Users with the same earnings should have the same rank
return (
<div className="card p-6">
<h3 className="text-lg font-semibold mb-4">Top Earners</h3>
<div className="space-y-3">
{sorted.map((entry, index) => (
{rankedEntries.map((entry) => (
<div
key={entry.id}
className="flex items-center gap-3 p-3 rounded-lg bg-slate-50 hover:bg-slate-100 transition"
>
{/* BUG: Rank is just index+1, doesn't handle ties */}
<span className="w-8 h-8 flex items-center justify-center rounded-full bg-slate-200 text-sm font-bold">
{index + 1}
<span className={`w-8 h-8 flex items-center justify-center rounded-full text-sm font-bold ${
entry.rank === 1 ? 'bg-yellow-100 text-yellow-700' :
entry.rank === 2 ? 'bg-gray-100 text-gray-700' :
entry.rank === 3 ? 'bg-orange-100 text-orange-700' :
'bg-slate-200'
}`}>
{entry.rank}
</span>
<img
src={entry.avatar}
Expand All @@ -63,13 +75,6 @@ export function Leaderboard() {
</div>
))}
</div>

<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-xs text-amber-700">
<strong>Bug hint:</strong> Notice how users with $35.00 and $20.00 might appear in different orders on page refresh.
Also, shouldn&apos;t tied users have the same rank?
</p>
</div>
</div>
);
}