@@ -4,15 +4,28 @@ import React from 'react';
44import Image from 'next/image' ;
55import Link from 'next/link' ;
66import { HackathonWinner } from '@/lib/api/hackathons' ;
7- import { Trophy , Medal , Award } from 'lucide-react' ;
7+ import { Trophy , ArrowUpRight } from 'lucide-react' ;
88import { motion } from 'motion/react' ;
9+ import { Avatar , AvatarFallback , AvatarImage } from '@/components/ui/avatar' ;
10+ import { Badge } from '@/components/ui/badge' ;
11+ import {
12+ Tooltip ,
13+ TooltipTrigger ,
14+ TooltipContent ,
15+ } from '@/components/ui/tooltip' ;
16+ import { cn } from '@/lib/utils' ;
17+ import Ribbon from '@/components/svg/Ribbon' ;
18+ import {
19+ getRibbonColors ,
20+ getRibbonText ,
21+ } from '@/components/organization/hackathons/rewards/winnersUtils' ;
922
1023interface WinnersTabProps {
1124 winners : HackathonWinner [ ] ;
12- hackathonSlug ?: string ;
25+ hackathonSlug ?: string ; // Intentionally retained for API consistency/future use
1326}
1427
15- export const WinnersTab = ( { winners, hackathonSlug } : WinnersTabProps ) => {
28+ export const WinnersTab = ( { winners } : WinnersTabProps ) => {
1629 if ( ! winners || winners . length === 0 ) {
1730 return (
1831 < div className = 'flex min-h-[400px] flex-col items-center justify-center text-center' >
@@ -28,146 +41,230 @@ export const WinnersTab = ({ winners, hackathonSlug }: WinnersTabProps) => {
2841 ) ;
2942 }
3043
31- // Sort by rank just in case
44+ // Sort by rank
3245 const sortedWinners = [ ...winners ] . sort ( ( a , b ) => a . rank - b . rank ) ;
3346
47+ // Split into podium (1-3) and others
48+ const podiumWinners = sortedWinners . filter ( w => w . rank <= 3 ) ;
49+ const otherWinners = sortedWinners . filter ( w => w . rank > 3 ) ;
50+
51+ // Reorder podium to 2, 1, 3 for visual hierarchy
52+ const getPodiumOrder = ( winners : HackathonWinner [ ] ) => {
53+ if ( winners . length < 2 ) return winners ;
54+ const first = winners . find ( w => w . rank === 1 ) ;
55+ const second = winners . find ( w => w . rank === 2 ) ;
56+ const third = winners . find ( w => w . rank === 3 ) ;
57+
58+ return [ second , first , third ] . filter (
59+ ( w ) : w is HackathonWinner => w !== undefined
60+ ) ;
61+ } ;
62+
63+ const podiumToDisplay = getPodiumOrder ( podiumWinners ) ;
64+
65+ const getPodiumGridCols = ( count : number ) => {
66+ if ( count === 1 ) return 'mx-auto max-w-sm grid-cols-1' ;
67+ if ( count === 2 ) return 'mx-auto max-w-2xl grid-cols-1 md:grid-cols-2' ;
68+ return 'grid-cols-1 md:grid-cols-3' ;
69+ } ;
70+
3471 return (
35- < div className = 'space-y-8' >
36- < div className = 'grid gap-6 md:grid-cols-2 lg:grid-cols-3' >
37- { sortedWinners . map ( ( winner , index ) => (
38- < WinnerCard
39- key = { `${ winner . rank } -${ winner . projectName } ` }
40- winner = { winner }
41- index = { index }
42- hackathonSlug = { hackathonSlug }
43- />
44- ) ) }
45- </ div >
72+ < div className = 'space-y-12 py-8' >
73+ { /* Podium Section */ }
74+ { podiumToDisplay . length > 0 && (
75+ < div
76+ className = { cn (
77+ 'grid gap-6 md:gap-8' ,
78+ getPodiumGridCols ( podiumToDisplay . length )
79+ ) }
80+ >
81+ { podiumToDisplay . map ( winner => (
82+ < WinnerCard
83+ key = { `${ winner . submissionId } -${ winner . rank } ` }
84+ winner = { winner }
85+ isPodium
86+ />
87+ ) ) }
88+ </ div >
89+ ) }
90+
91+ { /* Grid Section for Ranks 4+ */ }
92+ { otherWinners . length > 0 && (
93+ < div className = 'grid gap-6 sm:grid-cols-2 lg:grid-cols-3' >
94+ { otherWinners . map ( winner => (
95+ < WinnerCard
96+ key = { `${ winner . submissionId } -${ winner . rank } ` }
97+ winner = { winner }
98+ />
99+ ) ) }
100+ </ div >
101+ ) }
46102 </ div >
47103 ) ;
48104} ;
49105
50106const WinnerCard = ( {
51107 winner,
52- index,
53- hackathonSlug,
108+ isPodium = false ,
54109} : {
55110 winner : HackathonWinner ;
56- index : number ;
57- hackathonSlug ?: string ;
111+ isPodium ?: boolean ;
58112} ) => {
59- const getRankIcon = ( rank : number ) => {
60- switch ( rank ) {
61- case 1 :
62- return < Trophy className = 'h-6 w-6 text-yellow-500' /> ;
63- case 2 :
64- return < Medal className = 'h-6 w-6 text-gray-300' /> ;
65- case 3 :
66- return < Medal className = 'h-6 w-6 text-amber-600' /> ;
67- default :
68- return < Award className = 'h-6 w-6 text-blue-500' /> ;
69- }
113+ const getScaleClass = ( ) => {
114+ if ( ! isPodium ) return '' ;
115+ if ( winner . rank === 1 ) return 'md:scale-110 z-10' ;
116+ return 'md:scale-95 opacity-90' ;
70117 } ;
71118
72- const getRankColor = ( rank : number ) => {
73- switch ( rank ) {
74- case 1 :
75- return 'border-yellow-500/50 bg-yellow-500/5 hover:border-yellow-500' ;
76- case 2 :
77- return 'border-gray-300/50 bg-gray-300/5 hover:border-gray-300' ;
78- case 3 :
79- return 'border-amber-600/50 bg-amber-600/5 hover:border-amber-600' ;
80- default :
81- return 'border-white/10 bg-white/5 hover:border-white/20' ;
82- }
83- } ;
119+ const projectUrl = winner . submissionId
120+ ? `/projects/${ winner . submissionId } ?type=submission`
121+ : winner . slug
122+ ? `/projects/${ winner . slug } `
123+ : winner . projectId
124+ ? `/projects/${ winner . projectId } `
125+ : null ;
84126
85- const CardContent = (
86- < div
87- className = { `group relative h-full overflow-hidden rounded-xl border p-6 transition-all duration-300 ${ getRankColor (
88- winner . rank
89- ) } `}
90- >
91- < div className = 'mb-4 flex items-start justify-between' >
92- < div className = 'flex h-12 w-12 items-center justify-center rounded-full bg-white/10' >
93- { getRankIcon ( winner . rank ) }
94- </ div >
95- < div className = 'rounded-full bg-white/10 px-3 py-1 text-sm font-medium text-white' >
96- Rank #{ winner . rank }
97- </ div >
98- </ div >
127+ const { primaryColor, secondaryColor } = getRibbonColors ( winner . rank ) ;
99128
100- < h3 className = 'mb-2 text-xl font-bold text-white group-hover:text-blue-400' >
101- { winner . projectName }
102- </ h3 >
103-
104- < div className = 'mb-4 flex items-center gap-2' >
105- < div className = 'flex -space-x-2' >
106- { winner . participants . map ( ( p , i ) => (
107- < div
108- key = { i }
109- className = 'relative h-8 w-8 overflow-hidden rounded-full border-2 border-black bg-gray-800'
110- title = { p . username }
111- >
112- { p . avatar ? (
113- < Image
114- src = { p . avatar }
115- alt = { p . username }
116- fill
117- className = 'object-cover'
118- />
119- ) : (
120- < div className = 'flex h-full w-full items-center justify-center text-xs text-white' >
121- { p . username . charAt ( 0 ) . toUpperCase ( ) }
122- </ div >
123- ) }
124- </ div >
125- ) ) }
129+ const ProjectContent = (
130+ < div className = 'flex items-center justify-between rounded-lg border border-gray-900 bg-black/20 p-2 transition-colors hover:bg-black/40' >
131+ < div className = 'grid grid-cols-[44px_1fr] grid-rows-2 gap-x-2' >
132+ < div className = 'row-span-2 h-11 w-11 overflow-hidden rounded bg-gray-800' >
133+ { /* Fallback to trophy if no project image is available in HackathonWinner type */ }
134+ < div className = 'flex h-full w-full items-center justify-center p-2' >
135+ < Trophy className = 'h-6 w-6 text-yellow-500/20' />
136+ </ div >
126137 </ div >
127- < span className = 'text-sm text-gray-400' >
128- { winner . teamName ||
129- ( winner . participants . length === 1
130- ? winner . participants [ 0 ] . username
131- : 'Team' ) }
132- </ span >
133- </ div >
134-
135- < div className = 'mt-auto border-t border-white/10 pt-4' >
136- < div className = 'flex items-center justify-between' >
137- < span className = 'text-sm text-gray-400' > Prize</ span >
138- < span className = 'font-semibold text-white' > { winner . prize } </ span >
138+ < div className = 'flex items-center gap-1 overflow-hidden' >
139+ < Tooltip >
140+ < TooltipTrigger asChild >
141+ < p className = 'line-clamp-1 cursor-help text-sm font-medium text-white' >
142+ { winner . projectName }
143+ </ p >
144+ </ TooltipTrigger >
145+ < TooltipContent side = 'top' className = 'max-w-xs' >
146+ < p className = 'break-words' > { winner . projectName } </ p >
147+ </ TooltipContent >
148+ </ Tooltip >
149+ < Badge className = 'bg-office-brown border-office-brown-darker text-office-brown-darker shrink-0 rounded-[4px] border px-1 py-0.5 text-[10px] font-medium' >
150+ Project
151+ </ Badge >
152+ </ div >
153+ < div className = 'flex items-center gap-2 text-[10px] text-gray-500' >
154+ < span > Submission</ span >
155+ < div className = 'h-2 w-px bg-gray-900' />
156+ < ArrowUpRight className = 'h-3 w-3' />
139157 </ div >
140158 </ div >
141159 </ div >
142160 ) ;
143161
144- if ( winner . projectId ) {
145- return (
146- < Link
147- href = { `/projects/${ winner . projectId } ?type=submission` }
148- target = '_blank'
149- className = 'block h-full no-underline'
150- >
151- < motion . div
152- initial = { { opacity : 0 , y : 20 } }
153- animate = { { opacity : 1 , y : 0 } }
154- transition = { { duration : 0.3 , delay : index * 0.1 } }
155- className = 'h-full'
156- >
157- { CardContent }
158- </ motion . div >
159- </ Link >
160- ) ;
161- }
162-
163162 return (
164163 < motion . div
165164 initial = { { opacity : 0 , y : 20 } }
166- animate = { { opacity : 1 , y : 0 } }
167- transition = { { duration : 0.3 , delay : index * 0.1 } }
168- className = 'h-full'
165+ whileInView = { { opacity : 1 , y : 0 } }
166+ viewport = { { once : true } }
167+ transition = { { duration : 0.4 } }
168+ className = { cn (
169+ 'bg-background-card relative w-full overflow-hidden rounded-xl border border-white/5 p-5 transition-all duration-300 hover:border-white/10' ,
170+ getScaleClass ( )
171+ ) }
169172 >
170- { CardContent }
173+ { /* Prize Header */ }
174+ < div className = 'mb-4 flex items-center justify-center gap-2' >
175+ < div className = 'flex items-center gap-1.5 rounded-full border border-[#2775CA]/20 bg-[#2775CA]/10 px-3 py-1' >
176+ < Trophy className = 'h-4 w-4 text-yellow-500' />
177+ < span className = 'text-sm font-bold text-white uppercase' >
178+ { ( ( ) => {
179+ const prize = winner . prize || '' ;
180+ const match = prize . match (
181+ / ^ (?: U S D C ) ? \s * ( \d + (?: \. \d + ) ? ) \s * (?: U S D C ) ? $ / i
182+ ) ;
183+ return match ? `${ match [ 1 ] } USDC` : prize ;
184+ } ) ( ) }
185+ </ span >
186+ </ div >
187+ </ div >
188+
189+ { /* Ranks/Participants */ }
190+ < div className = 'mb-4 flex flex-col items-center justify-center gap-3' >
191+ < div className = 'flex -space-x-3' >
192+ { winner . participants . map ( ( p , i ) => {
193+ const profileUrl = p . username ? `/profile/${ p . username } ` : null ;
194+ const AvatarElement = (
195+ < Avatar
196+ className = { cn (
197+ 'border-background h-16 w-16 border-2 shadow-xl' ,
198+ profileUrl ? 'transition-transform hover:scale-105' : ''
199+ ) }
200+ >
201+ < AvatarImage src = { p . avatar } alt = { p . username || 'Participant' } />
202+ < AvatarFallback className = 'bg-gray-800 text-lg text-white uppercase' >
203+ { p . username ?. charAt ( 0 ) || '?' }
204+ </ AvatarFallback >
205+ </ Avatar >
206+ ) ;
207+
208+ return profileUrl ? (
209+ < Link
210+ key = { `${ p . username } -${ i } ` }
211+ href = { profileUrl }
212+ className = 'z-[1]'
213+ >
214+ { AvatarElement }
215+ </ Link >
216+ ) : (
217+ < div key = { `${ p . username } -${ i } ` } className = 'z-[1]' >
218+ { AvatarElement }
219+ </ div >
220+ ) ;
221+ } ) }
222+ </ div >
223+
224+ { /* Ribbon */ }
225+ < div className = 'relative flex items-center justify-center py-2' >
226+ < Ribbon
227+ primaryColor = { primaryColor }
228+ secondaryColor = { secondaryColor }
229+ className = 'w-48'
230+ />
231+ < div className = 'absolute inset-0 flex items-center justify-center pb-2 pl-[1px] text-[10px] font-black tracking-tight text-white uppercase' >
232+ { getRibbonText ( winner . rank ) }
233+ </ div >
234+ </ div >
235+
236+ { /* Team/Participant Name */ }
237+ < div className = 'text-center' >
238+ < h3 className = 'text-sm font-semibold text-white' >
239+ { winner . teamName ||
240+ ( winner . participants . length === 1 &&
241+ winner . participants [ 0 ] . username ? (
242+ < Link
243+ href = { `/profile/${ winner . participants [ 0 ] . username } ` }
244+ className = 'transition-colors hover:text-blue-400'
245+ >
246+ { winner . participants [ 0 ] . username }
247+ </ Link >
248+ ) : (
249+ 'Team'
250+ ) ) }
251+ </ h3 >
252+ </ div >
253+ </ div >
254+
255+ { /* Project Link */ }
256+ { projectUrl ? (
257+ < Link
258+ href = { projectUrl }
259+ target = '_blank'
260+ rel = 'noopener noreferrer'
261+ className = 'block no-underline'
262+ >
263+ { ProjectContent }
264+ </ Link >
265+ ) : (
266+ < div className = 'block' > { ProjectContent } </ div >
267+ ) }
171268 </ motion . div >
172269 ) ;
173270} ;
0 commit comments