diff --git a/src/components/bounty-card.tsx b/src/components/bounty-card.tsx index ba1c467..12e4848 100644 --- a/src/components/bounty-card.tsx +++ b/src/components/bounty-card.tsx @@ -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 = { @@ -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 ( -
+

{title}

@@ -27,7 +43,7 @@ export function BountyCard({ title, reward, tags, difficulty, progress }: Bounty
-
${reward}
+
{formatReward(reward)}
{difficulty} @@ -40,8 +56,8 @@ export function BountyCard({ title, reward, tags, difficulty, progress }: Bounty
diff --git a/src/components/bounty-filter.tsx b/src/components/bounty-filter.tsx index a945c38..36e7a74 100644 --- a/src/components/bounty-filter.tsx +++ b/src/components/bounty-filter.tsx @@ -1,27 +1,40 @@ "use client"; -import { useState } from "react"; - -// BUG: Filter state resets on page refresh -// FIX: Persist to URL query params or localStorage +import { useState, useEffect } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; type FilterProps = { onFilterChange: (filters: { difficulty: string; minReward: number }) => void; }; export function BountyFilter({ onFilterChange }: FilterProps) { - // BUG: State is lost on refresh - not persisted - const [difficulty, setDifficulty] = useState("all"); - const [minReward, setMinReward] = useState(0); + const searchParams = useSearchParams(); + const router = useRouter(); + + // Initialize from URL params or default + const [difficulty, setDifficulty] = useState(searchParams.get("difficulty") || "all"); + const [minReward, setMinReward] = useState(Number(searchParams.get("minReward")) || 0); + + // Update URL when filters change + useEffect(() => { + const params = new URLSearchParams(); + if (difficulty !== "all") params.set("difficulty", difficulty); + if (minReward > 0) params.set("minReward", minReward.toString()); + + const newUrl = params.toString() + ? `?${params.toString()}` + : window.location.pathname; + + router.replace(newUrl, { scroll: false }); + onFilterChange({ difficulty, minReward }); + }, [difficulty, minReward, router, onFilterChange]); const handleDifficultyChange = (value: string) => { setDifficulty(value); - onFilterChange({ difficulty: value, minReward }); }; const handleMinRewardChange = (value: number) => { setMinReward(value); - onFilterChange({ difficulty, minReward: value }); }; return ( @@ -31,7 +44,7 @@ export function BountyFilter({ onFilterChange }: FilterProps) {
-
handleMinRewardChange(Number(e.target.value))} - className="w-24 rounded-lg border border-slate-200 px-3 py-2 text-sm" - placeholder="0" + className="w-32" /> -
- -
- (Bug: refresh the page - filters reset!) + ${minReward}+
); diff --git a/src/components/leaderboard.tsx b/src/components/leaderboard.tsx index 684fe7a..f22c202 100644 --- a/src/components/leaderboard.tsx +++ b/src/components/leaderboard.tsx @@ -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; @@ -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 (

Top Earners

- {sorted.map((entry, index) => ( + {rankedEntries.map((entry) => (
- {/* BUG: Rank is just index+1, doesn't handle ties */} - - {index + 1} + + {entry.rank} ))}
- -
-

- Bug hint: Notice how users with $35.00 and $20.00 might appear in different orders on page refresh. - Also, shouldn't tied users have the same rank? -

-
); }