diff --git a/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx b/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx index 3af716b..1df25b9 100644 --- a/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx +++ b/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx @@ -1,15 +1,16 @@ import { FC, ReactElement, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Icon } from '@iconify/react'; import { decodeWinnerCertificateId } from '../../../../utils/certificate'; import { useAuthStore, useMyTeams, useTeamById, + useWinners, } from '@imphnen-frontend-service/service'; import QRCode from 'qrcode'; import html2canvas from 'html2canvas'; -import winnerData from '../../winner-data.json'; type WinnerEntry = { team_id: string; @@ -20,9 +21,20 @@ type WinnerEntry = { }; }; -type WinnerDataFile = { - message: string; - data: WinnerEntry[]; +const formatOrdinalRank = (rank: number): string => { + const mod100 = rank % 100; + if (mod100 >= 11 && mod100 <= 13) return `${rank}th`; + + switch (rank % 10) { + case 1: + return `${rank}st`; + case 2: + return `${rank}nd`; + case 3: + return `${rank}rd`; + default: + return `${rank}th`; + } }; const CERT_WIDTH = 1000; @@ -33,6 +45,11 @@ const CertificateWinnerPage: FC = (): ReactElement => { const navigate = useNavigate(); const { session } = useAuthStore(); const { data: myTeamsData } = useMyTeams(); + const { + data: winnersResponse, + isLoading: isLoadingWinners, + isError: isWinnersError, + } = useWinners(); const [decodedTeamId, setDecodedTeamId] = useState(''); const [error, setError] = useState(null); @@ -51,12 +68,20 @@ const CertificateWinnerPage: FC = (): ReactElement => { .catch(() => setError('Invalid certificate ID')); }, [certId]); - const winners = (winnerData as unknown as WinnerDataFile).data; + const winners = useMemo( + () => (winnersResponse?.data || []) as WinnerEntry[], + [winnersResponse?.data] + ); const winnerEntry = useMemo(() => { if (!decodedTeamId) return undefined; return winners.find((w) => w.team_id === decodedTeamId); }, [decodedTeamId, winners]); + const rankLabel = useMemo(() => { + if (!winnerEntry?.rank) return ''; + return formatOrdinalRank(winnerEntry.rank); + }, [winnerEntry?.rank]); + const { data: teamData, isLoading: isLoadingTeam } = useTeamById( decodedTeamId, !!decodedTeamId @@ -202,6 +227,33 @@ const CertificateWinnerPage: FC = (): ReactElement => { ); } + if (isLoadingWinners) { + return ( +
+
+
+
+ Loading winners... +
+
+
+ ); + } + + if (isWinnersError) { + return ( +
+

+ Unable to Load Winners +

+

+ Please try again later. +

+ +
+ ); + } + if (!isWinnerTeam) { return (
@@ -245,7 +297,10 @@ const CertificateWinnerPage: FC = (): ReactElement => {

{isTeamMember && ( - )} @@ -253,7 +308,10 @@ const CertificateWinnerPage: FC = (): ReactElement => { -
+
{/* Hidden Template for Canvas Generation */}
{ } `} - {/* Team Name (below "Diberikan Kepada") */} + {/* Team Name */}
{
{ id="winner-members" style={{ margin: 0, - paddingLeft: '18px', fontFamily: 'Poppins, sans-serif', fontSize: '18px', lineHeight: '1.35', - color: '#323232', + color: '#59bef5', }} > - {(memberNames.length ? memberNames : ['(Members unavailable)']).map((name) => ( -
  • {name}
  • + {(memberNames.length + ? memberNames + : ['(Members unavailable)'] + ).map((name) => ( +
  • • {name}
  • ))}
    @@ -348,10 +408,42 @@ const CertificateWinnerPage: FC = (): ReactElement => { > Diberikan sebagai penghargaan atas pencapaian meraih
    - JUARA {winnerEntry.rank} pada Hackathon IMPHNEN x Kolosal.ai + JUARA {winnerEntry.rank} pada Hackathon IMPHNEN x + Kolosal.ai

    + {/* Rank badge */} + {!!rankLabel && ( +
    +
    + {rankLabel} +
    +
    + )} + {/* QR Code (same placement as existing certificate page) */}
    { {/* Actions */} {isTeamMember && ( -
    +
    @@ -233,7 +236,8 @@ const DashboardPage: FC = (): ReactElement => {

    - Team features are closed. You can no longer accept invitations. + Team features are closed. You can no longer accept + invitations.

    )} @@ -302,7 +306,10 @@ const DashboardPage: FC = (): ReactElement => { /> ) : (
    - +
    )}
    @@ -312,19 +319,33 @@ const DashboardPage: FC = (): ReactElement => {
    {team.has_submission && ( - + Submitted )} {team.city && ( - + {team.city} )} - {team.member_count || team.members?.length || 0} member{(team.member_count || team.members?.length || 0) !== 1 ? 's' : ''} + {team.member_count || + team.members?.length || + 0}{' '} + member + {(team.member_count || + team.members?.length || + 0) !== 1 + ? 's' + : ''}