11import React from "react" ;
22import { UserRoundSearch , Flame } from "lucide-react" ;
33
4- // ── Renders either an <img> or emoji depending on the avatar value ──
54const Avatar = ( { avatar, name } ) => {
65 const isImagePath =
76 typeof avatar === "string" &&
87 ( avatar . startsWith ( "/" ) ||
98 avatar . startsWith ( "http" ) ||
109 avatar . match ( / \. ( p n g | j p g | j p e g | g i f | w e b p | s v g ) $ / i) ) ;
10+
1111 if ( isImagePath ) {
1212 return (
1313 < img
1414 src = { avatar }
1515 alt = { name }
16- style = { { width : 32 , height : 32 , borderRadius : "50%" , objectFit : "cover" ,
17- border : "2px solid #334155" , flexShrink : 0 } }
18- onError = { ( e ) => { e . target . replaceWith ( Object . assign ( document . createElement ( "span" ) , { textContent : "🕵️" } ) ) ; } }
16+ style = { {
17+ width : 32 , height : 32 , borderRadius : "50%" , objectFit : "cover" ,
18+ border : "2px solid var(--border)" , flexShrink : 0 ,
19+ } }
20+ onError = { ( e ) => { e . target . style . display = "none" ; } }
1921 />
2022 ) ;
2123 }
24+
2225 const isEmoji = typeof avatar === "string" && / \p{ Emoji} / u. test ( avatar ) ;
23- return < span className = "text-xl" > { isEmoji ? avatar : < UserRoundSearch className = "w-6 h-6" /> } </ span > ;
26+ return (
27+ < span style = { { fontSize : "1.3rem" } } >
28+ { isEmoji ? avatar : < UserRoundSearch style = { { width : 24 , height : 24 } } /> }
29+ </ span >
30+ ) ;
2431} ;
2532
33+ const RANK_MEDAL = { 1 : "🥇" , 2 : "🥈" , 3 : "🥉" } ;
34+
2635const LeaderboardTable = ( { data = [ ] } ) => {
2736 return (
28- < div className = "table-container" style = { { width : "100%" } } >
37+ < div
38+ style = { {
39+ width : "100%" ,
40+ borderRadius : "1rem" ,
41+ border : "1px solid var(--border)" ,
42+ background : "var(--surface)" ,
43+ overflow : "hidden" ,
44+ marginTop : "2rem" ,
45+ } }
46+ >
2947 < table style = { { width : "100%" , tableLayout : "fixed" , borderSpacing : 0 , borderCollapse : "collapse" } } >
48+
49+ { /* Header */ }
3050 < thead >
31- < tr >
32- < th style = { { width : "6%" , padding : "18px 20px" , textAlign : "left" } } > Rank</ th >
33- < th style = { { width : "20%" , padding : "18px 20px" , textAlign : "left" } } > Detective</ th >
34- < th style = { { width : "14%" , padding : "18px 20px" , textAlign : "left" } } > Title</ th >
35- < th style = { { width : "18%" , padding : "18px 20px" , textAlign : "left" } } > Specialization</ th >
36- < th style = { { width : "16%" , padding : "18px 20px" , textAlign : "left" } } > Investigation Points</ th >
37- < th style = { { width : "13%" , padding : "18px 20px" , textAlign : "left" } } > Cases Solved</ th >
38- < th style = { { width : "13%" , padding : "18px 20px" , textAlign : "left" } } > Streak</ th >
51+ < tr style = { { borderBottom : "1px solid var(--border)" } } >
52+ { [
53+ { label : "Rank" , width : "7%" } ,
54+ { label : "Detective" , width : "22%" } ,
55+ { label : "Title" , width : "13%" } ,
56+ { label : "Specialization" , width : "16%" } ,
57+ { label : "Investigation Points" , width : "17%" } ,
58+ { label : "Cases Solved" , width : "13%" } ,
59+ { label : "Streak" , width : "12%" } ,
60+ ] . map ( ( { label, width } ) => (
61+ < th
62+ key = { label }
63+ style = { {
64+ width,
65+ padding : "14px 20px" ,
66+ textAlign : "left" ,
67+ fontSize : "0.7rem" ,
68+ fontWeight : 700 ,
69+ letterSpacing : "0.08em" ,
70+ textTransform : "uppercase" ,
71+ color : "var(--muted)" ,
72+ } }
73+ >
74+ { label }
75+ </ th >
76+ ) ) }
3977 </ tr >
4078 </ thead >
79+
80+ { /* Body */ }
4181 < tbody >
42- { data . map ( ( user ) => (
43- < tr key = { user . rank } style = { { borderTop : "1px solid rgba(255,255,255,0.06)" } } >
44- < td style = { { padding : "20px 20px" } } > #{ user . rank } </ td >
45- < td style = { { padding : "20px 20px" } } >
46- < div style = { { display : "flex" , alignItems : "center" , gap : "12px" } } >
47- < Avatar avatar = { user . avatar } name = { user . name } />
48- < span > { user . name } </ span >
49- </ div >
50- </ td >
51- < td style = { { padding : "20px 20px" } } > { user . title } </ td >
52- < td style = { { padding : "20px 20px" } } > { user . specialization } </ td >
53- < td style = { { padding : "20px 20px" } } className = "green" > { user . points . toLocaleString ( ) } </ td >
54- < td style = { { padding : "20px 20px" } } > { user . cases } </ td >
55- < td style = { { padding : "20px 20px" } } className = "orange" >
56- < div className = "flex items-center gap-1" >
57- { user . streak } < Flame className = "w-4 h-4" />
58- </ div >
59- </ td >
60- </ tr >
61- ) ) }
82+ { data . map ( ( user , i ) => {
83+ const isTop3 = user . rank <= 3 ;
84+ return (
85+ < tr
86+ key = { user . rank }
87+ style = { {
88+ borderTop : i === 0 ? "none" : "1px solid var(--border)" ,
89+ background : isTop3 ? "rgba(var(--brand-rgb, 202,138,4), 0.04)" : "transparent" ,
90+ transition : "background 0.15s" ,
91+ } }
92+ onMouseEnter = { ( e ) => ( e . currentTarget . style . background = "rgba(128,128,128,0.07)" ) }
93+ onMouseLeave = { ( e ) => ( e . currentTarget . style . background = isTop3 ? "rgba(202,138,4,0.04)" : "transparent" ) }
94+ >
95+ { /* Rank */ }
96+ < td style = { { padding : "18px 20px" , fontWeight : 700 , color : "var(--text)" } } >
97+ < span style = { { display : "flex" , alignItems : "center" , gap : 6 } } >
98+ #{ user . rank }
99+ { RANK_MEDAL [ user . rank ] && (
100+ < span style = { { fontSize : "1rem" } } > { RANK_MEDAL [ user . rank ] } </ span >
101+ ) }
102+ </ span >
103+ </ td >
104+
105+ { /* Detective */ }
106+ < td style = { { padding : "18px 20px" } } >
107+ < div style = { { display : "flex" , alignItems : "center" , gap : 10 } } >
108+ < Avatar avatar = { user . avatar } name = { user . name } />
109+ < span style = { { fontWeight : 600 , color : "var(--text)" } } > { user . name } </ span >
110+ </ div >
111+ </ td >
112+
113+ { /* Title badge */ }
114+ < td style = { { padding : "18px 20px" } } >
115+ < span
116+ style = { {
117+ display : "inline-block" ,
118+ padding : "3px 10px" ,
119+ borderRadius : 999 ,
120+ fontSize : "0.72rem" ,
121+ fontWeight : 700 ,
122+ letterSpacing : "0.05em" ,
123+ textTransform : "uppercase" ,
124+ border : "1px solid var(--border)" ,
125+ color : "var(--brand)" ,
126+ background : "rgba(202,138,4,0.08)" ,
127+ } }
128+ >
129+ { user . title }
130+ </ span >
131+ </ td >
132+
133+ { /* Specialization */ }
134+ < td style = { { padding : "18px 20px" , color : "var(--muted)" } } >
135+ { user . specialization }
136+ </ td >
137+
138+ { /* Investigation Points */ }
139+ < td style = { { padding : "18px 20px" , fontWeight : 700 , color : "var(--brand)" } } >
140+ { user . points . toLocaleString ( ) }
141+ </ td >
142+
143+ { /* Cases Solved */ }
144+ < td style = { { padding : "18px 20px" , color : "var(--muted)" } } >
145+ { user . cases }
146+ </ td >
147+
148+ { /* Streak */ }
149+ < td style = { { padding : "18px 20px" } } >
150+ < div style = { { display : "flex" , alignItems : "center" , gap : 4 } } >
151+ < Flame style = { { width : 16 , height : 16 , color : "#ea580c" } } />
152+ < span style = { { fontWeight : 600 , color : "#ea580c" } } > { user . streak } </ span >
153+ </ div >
154+ </ td >
155+ </ tr >
156+ ) ;
157+ } ) }
62158 </ tbody >
63159 </ table >
64160 </ div >
65161 ) ;
66162} ;
67163
68- export default LeaderboardTable ;
164+ export default LeaderboardTable ;
0 commit comments