feat: implement persistent personalized certificates

- 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 <noreply@anthropic.com>
This commit is contained in:
Maulana Sodiqin
2025-12-10 21:40:34 +07:00
co-authored by Claude
parent 59c1bb0366
commit 1dbe704230
3 changed files with 45 additions and 15 deletions
@@ -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<DecodedCert | null>(null);
const [error, setError] = useState<string | null>(null);
const teamNameRef = useRef<HTMLHeadingElement>(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'}
</h3>
</div>