feat: integrate new certificate endpoint from backend

- Updated certificate page to use useCertificatePublicData hook
- Removed old multi-API-call approach
- Now uses single /certificates/{userId} endpoint
- Simplified data fetching for user, team, submission data
- Certificate now works for unauthenticated users
- Removed duplicate certificate hook file

🤖 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-11 00:16:29 +07:00
co-authored by Claude
parent afaaff4a7f
commit f5882e3cdc
@@ -3,10 +3,7 @@ import { useParams, useNavigate } from 'react-router';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { decodeCertificateId } from '../../../utils/certificate'; import { decodeCertificateId } from '../../../utils/certificate';
import { import {
useTeamById, useCertificatePublicData,
useTeamSubmission,
useAuthStore,
useUserById,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
@@ -20,7 +17,6 @@ interface DecodedCert {
const CertificatePage: FC = (): ReactElement => { const CertificatePage: FC = (): ReactElement => {
const { certId } = useParams<{ certId: string }>(); const { certId } = useParams<{ certId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { session } = useAuthStore();
const [decodedInfo, setDecodedInfo] = useState<DecodedCert | null>(null); const [decodedInfo, setDecodedInfo] = useState<DecodedCert | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const teamNameRef = useRef<HTMLHeadingElement>(null); const teamNameRef = useRef<HTMLHeadingElement>(null);
@@ -43,6 +39,12 @@ const CertificatePage: FC = (): ReactElement => {
} }
}, [certId]); }, [certId]);
// Fetch certificate data using the new endpoint
const { data: certificateData, isLoading: isLoadingCertificate } = useCertificatePublicData(
decodedInfo?.userId || '',
!!decodedInfo?.userId
);
// Generate QR Code // Generate QR Code
useEffect(() => { useEffect(() => {
if (certId) { if (certId) {
@@ -62,28 +64,15 @@ const CertificatePage: FC = (): ReactElement => {
} }
}, [certId]); }, [certId]);
const { data: teamData, isLoading: isLoadingTeam } = useTeamById( const certificate = certificateData?.data;
decodedInfo?.teamId || '', const team = certificate?.team;
!!decodedInfo?.teamId const submission = certificate?.submission;
); const certificateUser = certificate?.user;
const { data: submissionData, isLoading: isLoadingSubmission } =
useTeamSubmission(decodedInfo?.teamId || '', !!decodedInfo?.teamId);
// Fetch the certificate owner's user data const isLoading = (!decodedInfo && !error) || isLoadingCertificate;
const { data: certificateUserData, isLoading: isLoadingCertUser } = useUserById(
decodedInfo?.userId || '',
!!decodedInfo?.userId
);
const team = teamData?.data; // Certificate name from the user data
const submission = submissionData?.data; const certificateName = certificateUser?.fullname;
const certificateUser = certificateUserData?.data;
const isLoading =
(!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 // Dynamic font sizing: shrink by 2px if height exceeds 80px
useEffect(() => { useEffect(() => {
@@ -216,7 +205,7 @@ const CertificatePage: FC = (): ReactElement => {
); );
} }
if (!submission || submission.id !== decodedInfo?.submissionId) { if (!certificateUser) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-6xl mb-4">📄</div> <div className="text-6xl mb-4">📄</div>
@@ -224,7 +213,7 @@ const CertificatePage: FC = (): ReactElement => {
Certificate Not Found Certificate Not Found
</h2> </h2>
<p className="text-gray-600 dark:text-gray-400 mb-6"> <p className="text-gray-600 dark:text-gray-400 mb-6">
The submission associated with this certificate could not be found. The user associated with this certificate could not be found.
</p> </p>
<Button onClick={() => navigate('/')}>Back to Home</Button> <Button onClick={() => navigate('/')}>Back to Home</Button>
</div> </div>
@@ -292,14 +281,16 @@ const CertificatePage: FC = (): ReactElement => {
{team?.name} {team?.name}
</p> </p>
</div> </div>
<Button {team && (
variant="secondary" <Button
onClick={() => variant="secondary"
navigate(`/teams/${decodedInfo?.teamId}/submission`) onClick={() =>
} navigate(`/teams/${team.id}/submission`)
> }
Back to Submission >
</Button> Back to Submission
</Button>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -445,16 +436,14 @@ const CertificatePage: FC = (): ReactElement => {
margin: 0, margin: 0,
}} }}
> >
{submission.submitted_at {new Date().toLocaleDateString(
? new Date(submission.submitted_at).toLocaleDateString( 'id-ID',
'id-ID', {
{ day: 'numeric',
day: 'numeric', month: 'long',
month: 'long', year: 'numeric',
year: 'numeric', }
} )}
)
: 'N/A'}
</p> </p>
</div> </div>
</div> </div>
@@ -500,15 +489,17 @@ const CertificatePage: FC = (): ReactElement => {
> >
{isGenerating ? '⏳ Generating...' : '🖨️ Print'} {isGenerating ? '⏳ Generating...' : '🖨️ Print'}
</Button> </Button>
<Button {team && (
onClick={() => <Button
navigate(`/teams/${decodedInfo?.teamId}/submission`) onClick={() =>
} navigate(`/teams/${team.id}/submission`)
variant="secondary" }
className="col-span-2 flex items-center gap-2 xl:col-span-1" variant="secondary"
> className="col-span-2 flex items-center gap-2 xl:col-span-1"
View Submission >
</Button> View Submission
</Button>
)}
</div> </div>
</div> </div>