From 09ddb2115998d7c67501d34db3ba51771623f332 Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:17:46 +0700 Subject: [PATCH] feat: update certificate verification and display components with improved layout and data handling --- src/Pages/internify/VerificationCert.tsx | 74 +++++++++++-------- .../intern/components/CertificateAvailabe.tsx | 20 +++-- .../intern/components/CertificateHistory.tsx | 5 +- .../components/CertificateNotAvailable.tsx | 8 +- .../internify/mentor/BaseProjectsPage.tsx | 10 +-- .../mentor/components/EligibleInternTable.tsx | 2 +- src/Pages/utils/SertificateGenerator.ts | 30 ++++++-- src/Pages/utils/testlayout.html | 31 ++++++-- src/types/certificate.types.ts | 10 ++- 9 files changed, 120 insertions(+), 70 deletions(-) diff --git a/src/Pages/internify/VerificationCert.tsx b/src/Pages/internify/VerificationCert.tsx index b0e79a6..c44a256 100644 --- a/src/Pages/internify/VerificationCert.tsx +++ b/src/Pages/internify/VerificationCert.tsx @@ -24,7 +24,7 @@ export default function VerificationCert() { />

Maaf, ID Sertifikat anda masukkan tidak valid

- Sertifikat dengan UUID ini tidak dapat ditemukan atau sudah tidak berlaku. + Sertifikat dengan UUID ini tidak dapat ditemukan

); @@ -32,38 +32,48 @@ export default function VerificationCert() { return (
- Humic Internify -

- Sertifikat Valid -

-
-
- Nama Intern -

{verification.intern_name}

-
-
- Proyek -

{verification.project_name}

-
-
- Tanggal Terbit -

- {new Date(verification.created_at).toLocaleDateString("id-ID", { - day: "numeric", - month: "long", - year: "numeric", - })} -

-
-
- UUID -

{verification.uuid}

-
+ Humic Internify +

+ Validasi Berhasil! +

+
+
+ Nama Intern +

{verification.intern_name}

+
+ Atas Proyek Humic +

{verification.project.project_name}

+
+
+ Tanggal Terbit +

+ {new Date(verification.created_at).toLocaleDateString("id-ID", { + day: "numeric", + month: "long", + year: "numeric", + })} +

+
+
+ Projek Selesai +

+ {new Date(verification.project.end_date).toLocaleDateString("id-ID", { + day: "numeric", + month: "long", + year: "numeric", + })} +

+
+
+ UUID +

{verification.uuid}

+
+
); } \ No newline at end of file diff --git a/src/Pages/internify/intern/components/CertificateAvailabe.tsx b/src/Pages/internify/intern/components/CertificateAvailabe.tsx index e3a3c7c..fbceb0d 100644 --- a/src/Pages/internify/intern/components/CertificateAvailabe.tsx +++ b/src/Pages/internify/intern/components/CertificateAvailabe.tsx @@ -34,7 +34,6 @@ export default function CertificateAvailable({ project, certificate, templateUrl const duration = formatDateRange(project?.start_date, project?.end_date) - useEffect(() => { if (!templateUrl) return; const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`; @@ -82,12 +81,17 @@ export default function CertificateAvailable({ project, certificate, templateUrl }; const handleShareLinkedIn = () => { + + const issueDate = new Date(certificate.issued_at.replace(" ", "T")); const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`; const params = new URLSearchParams({ startTask: "CERTIFICATION_NAME", - name: certificate.project.project_name, - organizationName: "Internify", + name: "Penyelesaian Kerja Magang Instansi CoE HUMIC Engineering Proyek: " + certificate.project.project_name, + issueYear: String(issueDate.getFullYear()), + issueMonth: String(issueDate.getMonth() + 1), + organizationName: "CoE Humic Engineering", certUrl: verifyUrl, + certId: certificate.certificate_no, }); window.open( @@ -120,7 +124,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl {/* Program Summary */}
-
+

Ringkasan Program

@@ -128,7 +132,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl {[ { label: "Nama Program", value: project?.project_name ?? "-" }, { label: "Durasi", value: duration }, - { label: "No. Sertifikat", value: certificate.certificate_no ?? "-" }, + { label: "No. Sertifikat", value: certificate.certificate_no ? certificate.certificate_no : "-" }, ].map((row) => (
-
+
{/* Unduh Sertifikat */} @@ -179,7 +183,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl className="flex flex-col items-center justify-center gap-1.5 border border-card-outline hover:bg-gray-50 active:scale-95 transition-all text-red-700 font-medium py-3.5 rounded-xl text-sm" onClick={() => { navigator.clipboard.writeText(`${window.location.origin}/verify-certificate/${certificate.uuid}`); - customToast.success("Link berhasil disalin!"); + customToast.success("Link berhasil disalin!", "Link sudah disalin ke clipboard"); }} > diff --git a/src/Pages/internify/intern/components/CertificateHistory.tsx b/src/Pages/internify/intern/components/CertificateHistory.tsx index 26c3ea9..5cea127 100644 --- a/src/Pages/internify/intern/components/CertificateHistory.tsx +++ b/src/Pages/internify/intern/components/CertificateHistory.tsx @@ -5,7 +5,6 @@ import { useMyCertificates } from "../../../../hooks/useCertificates"; export default function SertificateHistory() { const { certificates, loading, error } = useMyCertificates(); const displayed = certificates.slice(0, 3); - return (
@@ -36,7 +35,7 @@ export default function SertificateHistory() { {displayed.map((item: Certificate) => (

@@ -48,7 +47,7 @@ export default function SertificateHistory() {

{item.project.project_name}

-

No: {item.certificate_no}

+

{item.certificate_no}

))} diff --git a/src/Pages/internify/intern/components/CertificateNotAvailable.tsx b/src/Pages/internify/intern/components/CertificateNotAvailable.tsx index 2ca3845..c1e9faa 100644 --- a/src/Pages/internify/intern/components/CertificateNotAvailable.tsx +++ b/src/Pages/internify/intern/components/CertificateNotAvailable.tsx @@ -1,5 +1,5 @@ -// CertificateNotAvailable.tsx -import { Clock, Lock, Award } from "lucide-react"; +import { Clock, Lock } from "lucide-react"; +import { FaAward } from "react-icons/fa"; interface CertificateNotAvailableProps { progress: number; @@ -19,9 +19,9 @@ export default function CertificateNotAvailable({
{allTasksDone ? ( - + ) : ( - + )}
diff --git a/src/Pages/internify/mentor/BaseProjectsPage.tsx b/src/Pages/internify/mentor/BaseProjectsPage.tsx index 0baa0be..b671553 100644 --- a/src/Pages/internify/mentor/BaseProjectsPage.tsx +++ b/src/Pages/internify/mentor/BaseProjectsPage.tsx @@ -5,13 +5,6 @@ import CreateProjectModal from './components/CreateProjectDialog'; import { useProjectsByRole } from '../../../hooks/useListProjects'; import TeamPana from '../../../assets/Team-pana.svg'; -function MentorProjectCardSkeleton() { - return ( -
-
- ) -} - export default function MentorProjectsPage() { const [isModalOpen, setIsModalOpen] = useState(false); const { projects, loading, error, refetch } = useProjectsByRole(); @@ -35,7 +28,8 @@ export default function MentorProjectsPage() { return (
{[...Array(4)].map((_) => ( - +
+
))}
); diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx index 5cbe202..686b414 100644 --- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx +++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx @@ -118,7 +118,7 @@ export default function EligibleInternTable({ id: intern.id_user, id_project: project.id, id_user: intern.id_user, - certificate_no: "CERT/yyddmm/X", + certificate_no: "No.2025/SPI3/CERT/yyddmm/X", issued_at: new Date().toISOString().slice(0, 10), uuid: "HANYA SEBATAS PREVIEW", user: { diff --git a/src/Pages/utils/SertificateGenerator.ts b/src/Pages/utils/SertificateGenerator.ts index f1469b9..bf307fb 100644 --- a/src/Pages/utils/SertificateGenerator.ts +++ b/src/Pages/utils/SertificateGenerator.ts @@ -1,7 +1,8 @@ import { resolveFileUrl } from "./resolveFileFromUrl"; import QRCode from "qrcode"; -export function loadImage(src: string): Promise { +//LOAD IMAGE TEMPLATE +function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); if (!src.startsWith("data:")) img.crossOrigin = "anonymous"; @@ -11,7 +12,8 @@ export function loadImage(src: string): Promise { }); } -export async function ensureFontsLoaded(): Promise { +//LOAD FONT UNTUK MENCEGAH FONT TERGANTI +async function ensureFontsLoaded(): Promise { await Promise.all([ document.fonts.load('105px "Great Vibes"'), document.fonts.load('54px "Grenze"'), @@ -19,16 +21,30 @@ export async function ensureFontsLoaded(): Promise { await document.fonts.ready; } +// LAYOUT DEFAULT SERTIFIKAT YANG AKAN DIGENERATE const LAYOUT = { - internName: { x: 0.57, y: 0.48, size: 140, font: "Great Vibes", color: "#800000" }, + internName: { x: 0.54, y: 0.5, size: 130, font: "Great Vibes", color: "#800000" }, internNameSmall: { x: 0.82, y: 0.889, size: 47, font: "Great Vibes", color: "#090909" }, projectName: { x: 0.815, y: 0.598, size: 46, font: "Grenze", color: "#800000" }, duration: { x: 0.7, y: 0.645, size: 48, font: "Grenze", color: "#090909" }, - certNo: { x: 0.597, y: 0.305, size: 50, font: "Grenze", color: "#090909" }, + certNo: { x: 0.56, y: 0.305, size: 50, font: "Grenze", color: "#090909" }, certNoSmall: { x: 0.827, y: 0.92, size: 32, font: "Grenze", color: "#090909"}, qr: { x: 0.77, y: 0.715, size: 0.103 }, }; +// HELPER: kurangi ukuran font 25% jika nama melebihi 22 karakter (termasuk spasi) +function fitNameText(name: string, baseSize: number): number { + const SAFE_LENGTH = 24; + const REDUCTION_PER_CHAR = 0.032; + const MIN_SCALE = 0.75; + + const length = name.length; + if (length <= SAFE_LENGTH) return baseSize; + const scale = Math.max(1 - (length - SAFE_LENGTH) * REDUCTION_PER_CHAR, MIN_SCALE); + return baseSize * scale; +} + +//MAIN FUNCTION: generasi sertifikat intern formatted export async function generateCertificate( templateUrl: string, internName: string, @@ -52,7 +68,8 @@ export async function generateCertificate( cfg: { x: number; y: number; size: number; font: string; color: string }, value: string, ) => { - ctx.font = `400 ${cfg.size}px "${cfg.font}", serif`; + const size = cfg === LAYOUT.internName ? fitNameText(value, cfg.size) : cfg.size; + ctx.font = `400 ${size}px "${cfg.font}", serif`; ctx.fillStyle = cfg.color; ctx.fillText(value, canvas.width * cfg.x, canvas.height * cfg.y); }; @@ -62,9 +79,8 @@ export async function generateCertificate( text(LAYOUT.projectName, projectName); text(LAYOUT.duration, ProjectDuration); text(LAYOUT.certNo, certificateNo); - text(LAYOUT.certNoSmall, "No.2026/SPI3/" + certificateNo); + text(LAYOUT.certNoSmall, certificateNo); - // QR code — nanti akan diganti ke alamat domain const qrSize = Math.round(canvas.width * LAYOUT.qr.size); const qrDataUrl = await QRCode.toDataURL( `${window.location.origin}/verify-certificate/${verifyUrl}`, diff --git a/src/Pages/utils/testlayout.html b/src/Pages/utils/testlayout.html index d55f2bc..36127b1 100644 --- a/src/Pages/utils/testlayout.html +++ b/src/Pages/utils/testlayout.html @@ -5,29 +5,48 @@ + + - +