@@ -5,6 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
55import { motion } from "framer-motion" ;
66import { LeaderboardUser } from "@/lib/leaderboard-data" ;
77import { useReducedMotion } from "@/hooks/useReducedMotion" ;
8+ import { LeaderboardEmptyState } from "./leaderboard-states" ;
89
910interface LeaderboardPodiumProps {
1011 topThree : LeaderboardUser [ ] ;
@@ -24,6 +25,7 @@ interface LeaderboardPodiumProps {
2425 * - Responsive design with proper spacing and sizing
2526 * - Accessible: proper alt text, semantic structure
2627 * - Static fallback maintains visual hierarchy without motion
28+ * - Gracefully handles empty or partial top-three data
2729 *
2830 * WCAG 2.1 AA Compliance:
2931 * - Respects prefers-reduced-motion for users with vestibular disorders
@@ -38,6 +40,17 @@ export function LeaderboardPodium({
3840 const prefersReducedMotion = useReducedMotion ( ) ;
3941 const reducedMotion = reducedMotionProp ?? prefersReducedMotion ;
4042
43+ if ( topThree . length === 0 ) {
44+ return (
45+ < div className = "flex items-center justify-center py-12" >
46+ < LeaderboardEmptyState
47+ title = "No Rankings Yet"
48+ description = "The leaderboard is empty. Be the first to start predicting and claim the top spot."
49+ />
50+ </ div >
51+ ) ;
52+ }
53+
4154 // Static fallback component for reduced motion
4255 const StaticPodium = ( ) => (
4356 < div
@@ -47,24 +60,26 @@ export function LeaderboardPodium({
4760 aria-label = "Leaderboard top 3 positions"
4861 >
4962 { /* 2nd Place */ }
50- < div className = "flex flex-col items-center" >
51- < div className = "relative mb-2" >
52- < Avatar className = "h-16 w-16 border-2 border-slate-300 ring-4 ring-slate-300/20" >
53- < AvatarImage src = { second ?. avatarUrl } alt = { second ?. name } />
54- < AvatarFallback > { second ?. name ?. [ 0 ] } </ AvatarFallback >
55- </ Avatar >
56- < div
57- className = "absolute -top-2 -right-2 bg-slate-300 text-slate-900 text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
58- aria-label = "2nd place"
59- >
60- 2
63+ { second ? (
64+ < div className = "flex flex-col items-center" >
65+ < div className = "relative mb-2" >
66+ < Avatar className = "h-16 w-16 border-2 border-slate-300 ring-4 ring-slate-300/20" >
67+ < AvatarImage src = { second . avatarUrl } alt = { second . name } />
68+ < AvatarFallback > { second . name ?. [ 0 ] } </ AvatarFallback >
69+ </ Avatar >
70+ < div
71+ className = "absolute -top-2 -right-2 bg-slate-300 text-slate-900 text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
72+ aria-label = "2nd place"
73+ >
74+ 2
75+ </ div >
76+ </ div >
77+ < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-24 flex flex-col items-center justify-center p-2 text-center" >
78+ < span className = "text-xs font-semibold text-white truncate w-full" > { second . name } </ span >
79+ < span className = "text-[10px] text-cyan-400 tabular-nums" > +{ second . profit } </ span >
6180 </ div >
6281 </ div >
63- < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-24 flex flex-col items-center justify-center p-2 text-center" >
64- < span className = "text-xs font-semibold text-white truncate w-full" > { second ?. name } </ span >
65- < span className = "text-[10px] text-cyan-400 tabular-nums" > +{ second ?. profit } </ span >
66- </ div >
67- </ div >
82+ ) : < div className = "w-20 h-40" aria-hidden = "true" /> }
6883
6984 { /* 1st Place */ }
7085 < div className = "flex flex-col items-center -mt-8" >
@@ -91,24 +106,26 @@ export function LeaderboardPodium({
91106 </ div >
92107
93108 { /* 3rd Place */ }
94- < div className = "flex flex-col items-center" >
95- < div className = "relative mb-2" >
96- < Avatar className = "h-16 w-16 border-2 border-amber-600 ring-4 ring-amber-600/20" >
97- < AvatarImage src = { third ?. avatarUrl } alt = { third ?. name } />
98- < AvatarFallback > { third ?. name ?. [ 0 ] } </ AvatarFallback >
99- </ Avatar >
100- < div
101- className = "absolute -top-2 -right-2 bg-amber-600 text-white text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
102- aria-label = "3rd place"
103- >
104- 3
109+ { third ? (
110+ < div className = "flex flex-col items-center" >
111+ < div className = "relative mb-2" >
112+ < Avatar className = "h-16 w-16 border-2 border-amber-600 ring-4 ring-amber-600/20" >
113+ < AvatarImage src = { third . avatarUrl } alt = { third . name } />
114+ < AvatarFallback > { third . name ?. [ 0 ] } </ AvatarFallback >
115+ </ Avatar >
116+ < div
117+ className = "absolute -top-2 -right-2 bg-amber-600 text-white text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
118+ aria-label = "3rd place"
119+ >
120+ 3
121+ </ div >
122+ </ div >
123+ < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-20 flex flex-col items-center justify-center p-2 text-center" >
124+ < span className = "text-xs font-semibold text-white truncate w-full" > { third . name } </ span >
125+ < span className = "text-[10px] text-cyan-400 tabular-nums" > +{ third . profit } </ span >
105126 </ div >
106127 </ div >
107- < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-20 flex flex-col items-center justify-center p-2 text-center" >
108- < span className = "text-xs font-semibold text-white truncate w-full" > { third ?. name } </ span >
109- < span className = "text-[10px] text-cyan-400 tabular-nums" > +{ third ?. profit } </ span >
110- </ div >
111- </ div >
128+ ) : < div className = "w-20 h-20" aria-hidden = "true" /> }
112129 </ div >
113130 ) ;
114131
@@ -126,29 +143,31 @@ export function LeaderboardPodium({
126143 aria-label = "Leaderboard top 3 positions"
127144 >
128145 { /* 2nd Place */ }
129- < motion . div
130- initial = { { opacity : 0 , y : 20 } }
131- animate = { { opacity : 1 , y : 0 } }
132- transition = { { delay : 0.2 , duration : 0.6 , ease : "easeOut" } }
133- className = "flex flex-col items-center"
134- >
135- < div className = "relative mb-2" >
136- < Avatar className = "h-16 w-16 border-2 border-slate-300 ring-4 ring-slate-300/20" >
137- < AvatarImage src = { second ?. avatarUrl } alt = { second ?. name } />
138- < AvatarFallback > { second ?. name ?. [ 0 ] } </ AvatarFallback >
139- </ Avatar >
140- < div
141- className = "absolute -top-2 -right-2 bg-slate-300 text-slate-900 text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
142- aria-label = "2nd place"
143- >
144- 2
146+ { second ? (
147+ < motion . div
148+ initial = { { opacity : 0 , y : 20 } }
149+ animate = { { opacity : 1 , y : 0 } }
150+ transition = { { delay : 0.2 , duration : 0.6 , ease : "easeOut" } }
151+ className = "flex flex-col items-center"
152+ >
153+ < div className = "relative mb-2" >
154+ < Avatar className = "h-16 w-16 border-2 border-slate-300 ring-4 ring-slate-300/20" >
155+ < AvatarImage src = { second . avatarUrl } alt = { second . name } />
156+ < AvatarFallback > { second . name ?. [ 0 ] } </ AvatarFallback >
157+ </ Avatar >
158+ < div
159+ className = "absolute -top-2 -right-2 bg-slate-300 text-slate-900 text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
160+ aria-label = "2nd place"
161+ >
162+ 2
163+ </ div >
145164 </ div >
146- </ div >
147- < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-24 flex flex-col items-center justify-center p-2 text-center" >
148- < span className = "text-xs font-semibold text-white truncate w-full" > { second ?. name } </ span >
149- < span className = "text-[10px] text-cyan-400 tabular-nums" > + { second ?. profit } </ span >
150- </ div >
151- </ motion . div >
165+ < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-24 flex flex-col items-center justify-center p-2 text-center" >
166+ < span className = "text-xs font-semibold text-white truncate w-full" > { second . name } </ span >
167+ < span className = "text-[10px] text-cyan-400 tabular-nums" > + { second . profit } </ span >
168+ </ div >
169+ </ motion . div >
170+ ) : < div className = "w-20 h-40" aria-hidden = "true" /> }
152171
153172 { /* 1st Place */ }
154173 < motion . div
@@ -180,29 +199,31 @@ export function LeaderboardPodium({
180199 </ motion . div >
181200
182201 { /* 3rd Place */ }
183- < motion . div
184- initial = { { opacity : 0 , y : 20 } }
185- animate = { { opacity : 1 , y : 0 } }
186- transition = { { delay : 0.4 , duration : 0.6 , ease : "easeOut" } }
187- className = "flex flex-col items-center"
188- >
189- < div className = "relative mb-2" >
190- < Avatar className = "h-16 w-16 border-2 border-amber-600 ring-4 ring-amber-600/20" >
191- < AvatarImage src = { third ?. avatarUrl } alt = { third ?. name } />
192- < AvatarFallback > { third ?. name ?. [ 0 ] } </ AvatarFallback >
193- </ Avatar >
194- < div
195- className = "absolute -top-2 -right-2 bg-amber-600 text-white text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
196- aria-label = "3rd place"
197- >
198- 3
202+ { third ? (
203+ < motion . div
204+ initial = { { opacity : 0 , y : 20 } }
205+ animate = { { opacity : 1 , y : 0 } }
206+ transition = { { delay : 0.4 , duration : 0.6 , ease : "easeOut" } }
207+ className = "flex flex-col items-center"
208+ >
209+ < div className = "relative mb-2" >
210+ < Avatar className = "h-16 w-16 border-2 border-amber-600 ring-4 ring-amber-600/20" >
211+ < AvatarImage src = { third . avatarUrl } alt = { third . name } />
212+ < AvatarFallback > { third . name ?. [ 0 ] } </ AvatarFallback >
213+ </ Avatar >
214+ < div
215+ className = "absolute -top-2 -right-2 bg-amber-600 text-white text-xs font-bold w-6 h-6 rounded-full flex items-center justify-center"
216+ aria-label = "3rd place"
217+ >
218+ 3
219+ </ div >
199220 </ div >
200- </ div >
201- < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-20 flex flex-col items-center justify-center p-2 text-center" >
202- < span className = "text-xs font-semibold text-white truncate w-full" > { third ?. name } </ span >
203- < span className = "text-[10px] text-cyan-400 tabular-nums" > + { third ?. profit } </ span >
204- </ div >
205- </ motion . div >
221+ < div className = "bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-t-lg w-20 h-20 flex flex-col items-center justify-center p-2 text-center" >
222+ < span className = "text-xs font-semibold text-white truncate w-full" > { third . name } </ span >
223+ < span className = "text-[10px] text-cyan-400 tabular-nums" > + { third . profit } </ span >
224+ </ div >
225+ </ motion . div >
226+ ) : < div className = "w-20 h-20" aria-hidden = "true" /> }
206227 </ div >
207228 ) ;
208229}
0 commit comments