Files
thrilltrack-explorer/src/components/contributors/LeaderboardEntry.tsx
gpt-engineer-app[bot] 9b1c2415b0 Add contributor leaderboard
Add types, hook, UI components, and integration for leaderboard showing top users with badges
2025-11-11 17:51:15 +00:00

147 lines
5.5 KiB
TypeScript

import { Card } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { LeaderboardContributor } from '@/types/contributor';
import { AchievementBadge, SpecialBadge } from './AchievementBadge';
import { Trophy, TrendingUp, Calendar } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
interface LeaderboardEntryProps {
contributor: LeaderboardContributor;
showPeriodStats?: boolean;
}
export function LeaderboardEntry({ contributor, showPeriodStats = false }: LeaderboardEntryProps) {
const periodStats = contributor.stats;
const allTimeStats = contributor.total_stats;
const totalContributions = showPeriodStats
? contributor.contribution_score
: contributor.total_score;
const getRankColor = (rank: number) => {
if (rank === 1) return 'text-yellow-500';
if (rank === 2) return 'text-gray-400';
if (rank === 3) return 'text-orange-600';
return 'text-muted-foreground';
};
const getRankIcon = (rank: number) => {
if (rank <= 3) {
return <Trophy className={`w-6 h-6 ${getRankColor(rank)}`} />;
}
return null;
};
return (
<Card className="p-4 hover:shadow-lg transition-shadow">
<div className="flex items-start gap-4">
{/* Rank */}
<div className="flex flex-col items-center justify-center min-w-[60px]">
{getRankIcon(contributor.rank)}
<span className={`text-2xl font-bold ${getRankColor(contributor.rank)}`}>
#{contributor.rank}
</span>
</div>
{/* Avatar & Info */}
<div className="flex-1 min-w-0">
<div className="flex items-start gap-3 mb-3">
<Avatar className="w-12 h-12">
<AvatarImage src={contributor.avatar_url || undefined} />
<AvatarFallback>
{(contributor.display_name || contributor.username).slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-lg truncate">
{contributor.display_name || contributor.username}
</h3>
<AchievementBadge level={contributor.achievement_level} />
</div>
<div className="flex items-center gap-1 text-sm text-muted-foreground mb-2">
<Calendar className="w-3 h-3" />
<span>
Joined {formatDistanceToNow(new Date(contributor.join_date), { addSuffix: true })}
</span>
</div>
{/* Special Badges */}
{contributor.special_badges.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
{contributor.special_badges.map((badge) => (
<SpecialBadge key={badge} badge={badge} />
))}
</div>
)}
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{showPeriodStats ? (
<>
{periodStats.parks_added > 0 && (
<StatCard label="Parks" value={periodStats.parks_added} />
)}
{periodStats.rides_added > 0 && (
<StatCard label="Rides" value={periodStats.rides_added} />
)}
{periodStats.photos_added > 0 && (
<StatCard label="Photos" value={periodStats.photos_added} />
)}
{periodStats.reviews_added > 0 && (
<StatCard label="Reviews" value={periodStats.reviews_added} />
)}
{periodStats.edits_made > 0 && (
<StatCard label="Edits" value={periodStats.edits_made} />
)}
</>
) : (
<>
{allTimeStats.total_parks > 0 && (
<StatCard label="Parks" value={allTimeStats.total_parks} />
)}
{allTimeStats.total_rides > 0 && (
<StatCard label="Rides" value={allTimeStats.total_rides} />
)}
{allTimeStats.total_photos > 0 && (
<StatCard label="Photos" value={allTimeStats.total_photos} />
)}
{allTimeStats.total_reviews > 0 && (
<StatCard label="Reviews" value={allTimeStats.total_reviews} />
)}
{allTimeStats.total_edits > 0 && (
<StatCard label="Edits" value={allTimeStats.total_edits} />
)}
</>
)}
</div>
{/* Total Score */}
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<TrendingUp className="w-4 h-4" />
<span>Contribution Score</span>
</div>
<Badge variant="secondary" className="text-base font-bold">
{totalContributions.toLocaleString()} pts
</Badge>
</div>
</div>
</div>
</Card>
);
}
function StatCard({ label, value }: { label: string; value: number }) {
return (
<div className="bg-muted/50 rounded-lg p-2 text-center">
<div className="text-xs text-muted-foreground mb-1">{label}</div>
<div className="text-lg font-bold">{value.toLocaleString()}</div>
</div>
);
}