Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | 1x 7x 7x 9x 2x 2x 2x 3x 7x 1x 6x 2x 4x 9x | // src/components/Leaderboard.tsx
import React from 'react';
import { useLeaderboardQuery } from '../hooks/queries/useLeaderboardQuery';
import { Award, Crown, ShieldAlert } from 'lucide-react';
/**
* Leaderboard component displaying top users by points.
*
* Refactored to use TanStack Query (ADR-0005 Phase 8).
*/
export const Leaderboard: React.FC = () => {
const { data: leaderboard = [], isLoading, error } = useLeaderboardQuery(10);
const getRankIcon = (rank: string) => {
switch (rank) {
case '1':
return <Crown className="w-6 h-6 text-yellow-400" />;
case '2':
return <Crown className="w-6 h-6 text-gray-400" />;
case '3':
return <Crown className="w-6 h-6 text-yellow-600" />;
default:
return <span className="font-semibold text-gray-500 dark:text-gray-400">{rank}</span>;
}
};
if (isLoading) {
return <div className="text-center p-8">Loading Leaderboard...</div>;
}
if (error) {
return (
<div
className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-md"
role="alert"
>
<div className="flex items-center">
<ShieldAlert className="h-6 w-6 mr-3" />
<p className="font-bold">Error: {error.message}</p>
</div>
</div>
);
}
return (
<div className="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
<Award className="w-6 h-6 mr-2 text-blue-500" />
Top Users
</h2>
{leaderboard.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400">
The leaderboard is currently empty. Be the first to earn points!
</p>
) : (
<ol className="space-y-4">
{leaderboard.map((user) => (
<li
key={user.user_id}
className="flex items-center space-x-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg transition hover:bg-gray-100 dark:hover:bg-gray-600"
>
<div className="shrink-0 w-8 text-center">{getRankIcon(user.rank)}</div>
<img
src={
user.avatar_url ||
`https://api.dicebear.com/8.x/initials/svg?seed=${user.full_name || user.user_id}`
}
alt={user.full_name || 'User Avatar'}
className="w-12 h-12 rounded-full object-cover"
/>
<div className="flex-1">
<p className="font-semibold text-gray-800 dark:text-gray-100">
{user.full_name || 'Anonymous User'}
</p>
</div>
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">
{user.points} pts
</div>
</li>
))}
</ol>
)}
</div>
);
};
export default Leaderboard;
|