diff --git a/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx b/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx index ac9860d66..91518cd5a 100644 --- a/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx +++ b/app/(landing)/hackathons/[slug]/HackathonPageClient.tsx @@ -159,6 +159,7 @@ export default function HackathonPageClient() { tabs.push({ id: 'winners', label: 'Winners', + badge: winners.length, }); } diff --git a/components/hackathons/winners/WinnersTab.tsx b/components/hackathons/winners/WinnersTab.tsx index cf9e946ae..602e13779 100644 --- a/components/hackathons/winners/WinnersTab.tsx +++ b/components/hackathons/winners/WinnersTab.tsx @@ -4,15 +4,28 @@ import React from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { HackathonWinner } from '@/lib/api/hackathons'; -import { Trophy, Medal, Award } from 'lucide-react'; +import { Trophy, ArrowUpRight } from 'lucide-react'; import { motion } from 'motion/react'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import Ribbon from '@/components/svg/Ribbon'; +import { + getRibbonColors, + getRibbonText, +} from '@/components/organization/hackathons/rewards/winnersUtils'; interface WinnersTabProps { winners: HackathonWinner[]; - hackathonSlug?: string; + hackathonSlug?: string; // Intentionally retained for API consistency/future use } -export const WinnersTab = ({ winners, hackathonSlug }: WinnersTabProps) => { +export const WinnersTab = ({ winners }: WinnersTabProps) => { if (!winners || winners.length === 0) { return (
@@ -28,146 +41,230 @@ export const WinnersTab = ({ winners, hackathonSlug }: WinnersTabProps) => { ); } - // Sort by rank just in case + // Sort by rank const sortedWinners = [...winners].sort((a, b) => a.rank - b.rank); + // Split into podium (1-3) and others + const podiumWinners = sortedWinners.filter(w => w.rank <= 3); + const otherWinners = sortedWinners.filter(w => w.rank > 3); + + // Reorder podium to 2, 1, 3 for visual hierarchy + const getPodiumOrder = (winners: HackathonWinner[]) => { + if (winners.length < 2) return winners; + const first = winners.find(w => w.rank === 1); + const second = winners.find(w => w.rank === 2); + const third = winners.find(w => w.rank === 3); + + return [second, first, third].filter( + (w): w is HackathonWinner => w !== undefined + ); + }; + + const podiumToDisplay = getPodiumOrder(podiumWinners); + + const getPodiumGridCols = (count: number) => { + if (count === 1) return 'mx-auto max-w-sm grid-cols-1'; + if (count === 2) return 'mx-auto max-w-2xl grid-cols-1 md:grid-cols-2'; + return 'grid-cols-1 md:grid-cols-3'; + }; + return ( -
-
- {sortedWinners.map((winner, index) => ( - - ))} -
+
+ {/* Podium Section */} + {podiumToDisplay.length > 0 && ( +
+ {podiumToDisplay.map(winner => ( + + ))} +
+ )} + + {/* Grid Section for Ranks 4+ */} + {otherWinners.length > 0 && ( +
+ {otherWinners.map(winner => ( + + ))} +
+ )}
); }; const WinnerCard = ({ winner, - index, - hackathonSlug, + isPodium = false, }: { winner: HackathonWinner; - index: number; - hackathonSlug?: string; + isPodium?: boolean; }) => { - const getRankIcon = (rank: number) => { - switch (rank) { - case 1: - return ; - case 2: - return ; - case 3: - return ; - default: - return ; - } + const getScaleClass = () => { + if (!isPodium) return ''; + if (winner.rank === 1) return 'md:scale-110 z-10'; + return 'md:scale-95 opacity-90'; }; - const getRankColor = (rank: number) => { - switch (rank) { - case 1: - return 'border-yellow-500/50 bg-yellow-500/5 hover:border-yellow-500'; - case 2: - return 'border-gray-300/50 bg-gray-300/5 hover:border-gray-300'; - case 3: - return 'border-amber-600/50 bg-amber-600/5 hover:border-amber-600'; - default: - return 'border-white/10 bg-white/5 hover:border-white/20'; - } - }; + const projectUrl = winner.submissionId + ? `/projects/${winner.submissionId}?type=submission` + : winner.slug + ? `/projects/${winner.slug}` + : winner.projectId + ? `/projects/${winner.projectId}` + : null; - const CardContent = ( -
-
-
- {getRankIcon(winner.rank)} -
-
- Rank #{winner.rank} -
-
+ const { primaryColor, secondaryColor } = getRibbonColors(winner.rank); -

- {winner.projectName} -

- -
-
- {winner.participants.map((p, i) => ( -
- {p.avatar ? ( - {p.username} - ) : ( -
- {p.username.charAt(0).toUpperCase()} -
- )} -
- ))} + const ProjectContent = ( +
+
+
+ {/* Fallback to trophy if no project image is available in HackathonWinner type */} +
+ +
- - {winner.teamName || - (winner.participants.length === 1 - ? winner.participants[0].username - : 'Team')} - -
- -
-
- Prize - {winner.prize} +
+ + +

+ {winner.projectName} +

+
+ +

{winner.projectName}

+
+
+ + Project + +
+
+ Submission +
+
); - if (winner.projectId) { - return ( - - - {CardContent} - - - ); - } - return ( - {CardContent} + {/* Prize Header */} +
+
+ + + {(() => { + const prize = winner.prize || ''; + const match = prize.match( + /^(?:USDC)?\s*(\d+(?:\.\d+)?)\s*(?:USDC)?$/i + ); + return match ? `${match[1]} USDC` : prize; + })()} + +
+
+ + {/* Ranks/Participants */} +
+
+ {winner.participants.map((p, i) => { + const profileUrl = p.username ? `/profile/${p.username}` : null; + const AvatarElement = ( + + + + {p.username?.charAt(0) || '?'} + + + ); + + return profileUrl ? ( + + {AvatarElement} + + ) : ( +
+ {AvatarElement} +
+ ); + })} +
+ + {/* Ribbon */} +
+ +
+ {getRibbonText(winner.rank)} +
+
+ + {/* Team/Participant Name */} +
+

+ {winner.teamName || + (winner.participants.length === 1 && + winner.participants[0].username ? ( + + {winner.participants[0].username} + + ) : ( + 'Team' + ))} +

+
+
+ + {/* Project Link */} + {projectUrl ? ( + + {ProjectContent} + + ) : ( +
{ProjectContent}
+ )}
); };