mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 17:11:12 -05:00
Add types, hook, UI components, and integration for leaderboard showing top users with badges
147 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|