diff --git a/components/leaderboard/LeaderboardCards.tsx b/components/leaderboard/LeaderboardCards.tsx index 52718e78..f4e36c23 100644 --- a/components/leaderboard/LeaderboardCards.tsx +++ b/components/leaderboard/LeaderboardCards.tsx @@ -4,16 +4,60 @@ import React, { useRef } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; +import { LeaderboardEmptyState } from "./leaderboard-states"; import { LeaderboardUser } from "@/lib/leaderboard-data"; interface LeaderboardCardsProps { users: LeaderboardUser[]; onUserVisibilityChange?: (isVisible: boolean) => void; + isLoading?: boolean; + error?: string | null; + onRetry?: () => void; } -export function LeaderboardCards({ users, onUserVisibilityChange }: LeaderboardCardsProps) { +export function LeaderboardCards({ users, onUserVisibilityChange, isLoading, error, onRetry }: LeaderboardCardsProps) { const parentRef = useRef(null); + if (isLoading) { + return ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+ ); + } + + if (error) { + return ( + + ); + } + + if (users.length === 0) { + return ( + + ); + } + const rowVirtualizer = useVirtualizer({ count: users.length, getScrollElement: () => parentRef.current, @@ -54,7 +98,7 @@ export function LeaderboardCards({ users, onUserVisibilityChange }: LeaderboardC : "bg-slate-900/40 border-slate-800" )} style={{ - height: `72px`, // slightly less than estimate to allow gap + height: `72px`, transform: `translateY(${virtualRow.start}px)`, }} > diff --git a/components/leaderboard/LeaderboardPodium.tsx b/components/leaderboard/LeaderboardPodium.tsx index fcc84628..4eab2838 100644 --- a/components/leaderboard/LeaderboardPodium.tsx +++ b/components/leaderboard/LeaderboardPodium.tsx @@ -5,6 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { motion } from "framer-motion"; import { LeaderboardUser } from "@/lib/leaderboard-data"; import { useReducedMotion } from "@/hooks/useReducedMotion"; +import { LeaderboardEmptyState } from "./leaderboard-states"; interface LeaderboardPodiumProps { topThree: LeaderboardUser[]; @@ -24,6 +25,7 @@ interface LeaderboardPodiumProps { * - Responsive design with proper spacing and sizing * - Accessible: proper alt text, semantic structure * - Static fallback maintains visual hierarchy without motion + * - Gracefully handles empty or partial top-three data * * WCAG 2.1 AA Compliance: * - Respects prefers-reduced-motion for users with vestibular disorders @@ -38,6 +40,17 @@ export function LeaderboardPodium({ const prefersReducedMotion = useReducedMotion(); const reducedMotion = reducedMotionProp ?? prefersReducedMotion; + if (topThree.length === 0) { + return ( +
+ +
+ ); + } + // Static fallback component for reduced motion const StaticPodium = () => (
{/* 2nd Place */} -
-
- - - {second?.name?.[0]} - -
- 2 + {second ? ( +
+
+ + + {second.name?.[0]} + +
+ 2 +
+
+
+ {second.name} + +{second.profit}
-
- {second?.name} - +{second?.profit} -
-
+ ) :