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.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
+
+
+
);
- 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}
+ )}
);
};