From 1dbe70423046e33b4831a664370e5ddc0f855aec Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Wed, 10 Dec 2025 21:40:34 +0700 Subject: [PATCH] feat: implement persistent personalized certificates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modified encodeCertificateId to include userId parameter - Updated decodeCertificateId to extract and return userId - Added backward compatibility for old 2-part certificate IDs - Certificate page now fetches and displays certificate owner's name - Submission page passes current user's ID when generating certificate - Fixed certificate template to use certificate owner's name instead of team leader - Certificates now show the encoded user's name regardless of who views them 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/app/certificate/[certId]/page.tsx | 20 +++++++++++-- .../app/teams/[teamId]/submission/page.tsx | 11 +++++-- apps/hackathon/src/utils/certificate.ts | 29 +++++++++++++------ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/apps/hackathon/src/app/certificate/[certId]/page.tsx b/apps/hackathon/src/app/certificate/[certId]/page.tsx index aaa23b5..144016b 100644 --- a/apps/hackathon/src/app/certificate/[certId]/page.tsx +++ b/apps/hackathon/src/app/certificate/[certId]/page.tsx @@ -5,6 +5,8 @@ import { decodeCertificateId } from '../../../utils/certificate'; import { useTeamById, useTeamSubmission, + useAuthStore, + useUserById, } from '@imphnen-frontend-service/service'; import QRCode from 'qrcode'; import html2canvas from 'html2canvas'; @@ -12,11 +14,13 @@ import html2canvas from 'html2canvas'; interface DecodedCert { teamId: string; submissionId: string; + userId: string; } const CertificatePage: FC = (): ReactElement => { const { certId } = useParams<{ certId: string }>(); const navigate = useNavigate(); + const { session } = useAuthStore(); const [decodedInfo, setDecodedInfo] = useState(null); const [error, setError] = useState(null); const teamNameRef = useRef(null); @@ -65,11 +69,21 @@ const CertificatePage: FC = (): ReactElement => { const { data: submissionData, isLoading: isLoadingSubmission } = useTeamSubmission(decodedInfo?.teamId || '', !!decodedInfo?.teamId); + // Fetch the certificate owner's user data + const { data: certificateUserData, isLoading: isLoadingCertUser } = useUserById( + decodedInfo?.userId || '', + !!decodedInfo?.userId + ); + const team = teamData?.data; const submission = submissionData?.data; + const certificateUser = certificateUserData?.data; const isLoading = - (!decodedInfo && !error) || isLoadingTeam || isLoadingSubmission; + (!decodedInfo && !error) || isLoadingTeam || isLoadingSubmission || isLoadingCertUser; + + // Certificate name: Use the user from the certificate ID, fallback to team leader + const certificateName = certificateUser?.fullname || team?.leader?.fullname; // Dynamic font sizing: shrink by 2px if height exceeds 80px useEffect(() => { @@ -98,7 +112,7 @@ const CertificatePage: FC = (): ReactElement => { }, 0); return () => clearTimeout(timer); - }, [team?.name, team?.leader?.fullname]); + }, [team?.name, certificateName]); // Generate certificate canvas screenshot useEffect(() => { @@ -335,7 +349,7 @@ const CertificatePage: FC = (): ReactElement => { margin: 0, }} > - {team?.leader?.fullname || 'N/A'} + {certificateName || 'N/A'} diff --git a/apps/hackathon/src/app/teams/[teamId]/submission/page.tsx b/apps/hackathon/src/app/teams/[teamId]/submission/page.tsx index 1009580..4a3e669 100644 --- a/apps/hackathon/src/app/teams/[teamId]/submission/page.tsx +++ b/apps/hackathon/src/app/teams/[teamId]/submission/page.tsx @@ -1,12 +1,13 @@ import { FC, ReactElement } from 'react'; import { Button } from '@imphnen-frontend-service/ui/atoms'; import { useNavigate, useParams } from 'react-router'; -import { useTeamById, useTeamSubmission } from '@imphnen-frontend-service/service'; +import { useTeamById, useTeamSubmission, useAuthStore } from '@imphnen-frontend-service/service'; import { encodeCertificateId } from '../../../../utils/certificate'; const SubmissionViewPage: FC = (): ReactElement => { const { teamId } = useParams<{ teamId: string }>(); const navigate = useNavigate(); + const { session } = useAuthStore(); const { data: teamData } = useTeamById(teamId || ''); const { data: submissionData, isLoading } = useTeamSubmission(teamId || '', !!teamId); @@ -113,13 +114,17 @@ const SubmissionViewPage: FC = (): ReactElement => { View Your Certificate

- Congratulations! Your certificate is ready to download and share. + Congratulations! Your personalized certificate is ready to download and share.