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, useTeamSubmission, useWinners, } from '@imphnen-frontend-service/service'; import QRCode from 'qrcode'; import html2canvas from 'html2canvas'; type WinnerEntry = { team_id: string; rank: number; team?: { id: string; name: string; }; }; 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`; } }; // Keep the template size aligned with the SVG native size (842x595) using an integer multiplier. // This reduces sub-pixel scaling artifacts (blur) on thin lines when rasterizing with html2canvas. const CERT_WIDTH = 842 * 2; const CERT_HEIGHT = 595 * 2; // Balance between output sharpness and file size. // Output resolution will be (CERT_WIDTH * EXPORT_SCALE) x (CERT_HEIGHT * EXPORT_SCALE). const EXPORT_SCALE = 2; // The original layout was tuned around a ~1000px-wide template. // We keep the same visual proportions by scaling fixed pixel values. const LAYOUT_BASE_WIDTH = 1000; const LAYOUT_SCALE = CERT_WIDTH / LAYOUT_BASE_WIDTH; const s = (px: number) => Math.round(px * LAYOUT_SCALE); const CertificateWinnerPage: FC = (): ReactElement => { const { certId } = useParams<{ certId: string }>(); 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); const [qrCodeUrl, setQrCodeUrl] = useState(''); const certificateRef = useRef(null); const [isGenerating, setIsGenerating] = useState(false); const [certificateImage, setCertificateImage] = useState(''); const [showTemplate, setShowTemplate] = useState(true); useEffect(() => { if (!certId) return; decodeWinnerCertificateId(certId) .then((decoded) => setDecodedTeamId(decoded.teamId)) .catch(() => setError('Invalid certificate ID')); }, [certId]); 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 ); const { data: submissionData, isLoading: isLoadingSubmission } = useTeamSubmission(decodedTeamId, !!decodedTeamId); const team = teamData?.data; const submission = submissionData?.data; const submissionName = submission?.project_name || '(Submission unavailable)'; const memberNames = useMemo(() => { const members = team?.members || []; return members .map((m) => m.user?.fullname) .filter((name): name is string => !!name); }, [team?.members]); const isWinnerTeam = !!winnerEntry; const isTeamMember = !!session?.user?.id && !!decodedTeamId && (myTeamsData?.data || []).some( (t) => (t as { id?: string } | null | undefined)?.id === decodedTeamId ); // Generate QR Code (public link) useEffect(() => { if (!certId) return; const encodedCertId = encodeURIComponent(certId); const certificateUrl = `${window.location.origin}/certificate/winner/${encodedCertId}`; QRCode.toDataURL(certificateUrl, { width: s(200), margin: 1, color: { dark: '#000000', light: '#ffffff', }, }) .then(setQrCodeUrl) .catch((err) => console.error('QR Code generation failed:', err)); }, [certId]); // Generate certificate canvas screenshot useEffect(() => { const generateCertificate = async () => { if (!certificateRef.current) return; if (!team?.name) return; if (!qrCodeUrl) return; if (!winnerEntry?.rank) return; if (isLoadingSubmission) return; setIsGenerating(true); try { setShowTemplate(true); await new Promise((resolve) => setTimeout(resolve, 1500)); const canvas = await html2canvas(certificateRef.current, { scale: EXPORT_SCALE, useCORS: true, backgroundColor: '#ffffff', logging: false, width: CERT_WIDTH, height: CERT_HEIGHT, allowTaint: true, imageTimeout: 0, removeContainer: true, }); const imageUrl = canvas.toDataURL('image/png', 1.0); setCertificateImage(imageUrl); setShowTemplate(false); } catch (e) { console.error('Failed to generate certificate:', e); } finally { setIsGenerating(false); } }; generateCertificate(); }, [ team?.name, memberNames, qrCodeUrl, winnerEntry?.rank, isLoadingSubmission, submissionName, ]); const handleDownloadCertificate = () => { if (!certificateImage) return; const link = document.createElement('a'); link.href = certificateImage; link.download = `winner-certificate-${team?.name || 'hackathon'}.png`; link.click(); }; const handlePrintCertificate = () => { if (!certificateImage) return; const printWindow = window.open('', '_blank'); if (!printWindow) return; printWindow.document.write(` Certificate - ${team?.name} `); printWindow.document.close(); printWindow.onload = () => { printWindow.print(); }; }; if (error || !certId) { return (

Invalid Certificate

{error || 'The certificate ID is invalid or malformed.'}

); } if (!decodedTeamId || isLoadingTeam) { return (
Loading certificate...
); } if (isLoadingWinners) { return (
Loading winners...
); } if (isWinnersError) { return (

Unable to Load Winners

Please try again later.

); } if (!isWinnerTeam) { return (

Certificate Not Found

This team is not listed as a hackathon winner.

); } if (!team?.name) { return (

Team Not Found

The team associated with this certificate could not be loaded.

); } return (
{/* Header */}

Winner Certificate

{team.name}

{isTeamMember && ( )}
{/* Hidden Template for Canvas Generation */}
{/* Team Name */}

{team.name}

{/* Members list */}
    {(memberNames.length ? memberNames : ['(Members unavailable)'] ).map((name) => (
  • • {name}
  • ))}
{/* Award text */}

Diberikan sebagai penghargaan atas pencapaian meraih
JUARA {winnerEntry.rank} pada Hackathon IMPHNEN x Kolosal.ai
dengan nama project: {submissionName}

{/* Rank badge */} {!!rankLabel && (
{rankLabel}
)} {/* QR Code (same placement as existing certificate page) */}
{qrCodeUrl && ( Certificate QR Code )}
{/* Display Certificate Image */}
{isGenerating && (
Generating certificate...
)} {certificateImage && !isGenerating && ( Winner Certificate )} {/* Actions */} {isTeamMember && (
)}
{/* Info Box */}

Certificate Information

This certificate is a digital record of your hackathon achievement.

); }; export default CertificateWinnerPage;