Generate Certificates: Adding QR with UUID of certificates to verify

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-04 02:06:02 +07:00
parent 5b7ac30589
commit c4ca23e869
11 changed files with 280 additions and 131 deletions
+17 -19
View File
@@ -1,9 +1,6 @@
import api from "../../lib/api";
import QRCode from "qrcode";
// Flag supaya font hanya di-load sekali (tidak berulang tiap generate)
let fontsLoaded = false;
export function resolveImageUrl(path: string): string {
if (!path) return "";
if (path.startsWith("data:")) return path; // untuk QR data URL
@@ -36,13 +33,13 @@ export async function ensureFontsLoaded(): Promise<void> {
await document.fonts.ready;
}
// ── Generate satu sertifikat sebagai Blob ─────────────────────────────────────
// Generate satu sertifikat sebagai Blob
export async function generateCertificate(
templateUrl: string,
internName: string,
projectName: string,
certificateNo: string,
verifyUrl: string
verifyUrl: string,
): Promise<Blob> {
// Pastikan font siap sebelum menggambar teks
await ensureFontsLoaded();
@@ -82,23 +79,24 @@ export async function generateCertificate(
ctx.fillText(certificateNo, canvas.width * 0.825, canvas.height * 0.918);
// QR code — kalau verifyUrl diisi
// if (verifyUrl) {
// const qrDataUrl = await QRCode.toDataURL(verifyUrl, { width: 200, margin: 1 });
// const qrImg = await loadImage(qrDataUrl);
// const qrSize = canvas.width * 0.08;
// ctx.drawImage(
// qrImg,
// canvas.width * 0.1 - qrSize / 2, // posisi X — sesuaikan
// canvas.height * 0.88 - qrSize / 2, // posisi Y — sesuaikan
// qrSize,
// qrSize
// );
// }
const qrDataUrl = await QRCode.toDataURL(verifyUrl, {
width: 200,
margin: 1,
});
const qrImg = await loadImage(qrDataUrl);
const qrSize = canvas.width * 0.103;
ctx.drawImage(
qrImg,
canvas.width * 0.77 - qrSize / 2, // posisi X — sesuaikan
canvas.height * 0.715 - qrSize / 2, // posisi Y — sesuaikan
qrSize,
qrSize,
);
return new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => (blob ? resolve(blob) : reject(new Error("toBlob gagal"))),
"image/png"
"image/png",
);
});
}
}
+47 -36
View File
@@ -7,52 +7,63 @@
</head>
<body>
<canvas id="preview" style="max-width: 100%;"></canvas>
<script>
// Helper: bungkus loading image jadi Promise, biar bisa di-await
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
async function draw() {
// Tunggu semua font dari Google Fonts selesai load
await document.fonts.ready;
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.getElementById("preview");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// Load template sertifikat & QR dummy secara paralel
const [templateImg, qrImg] = await Promise.all([
loadImage("http://localhost:9000/uploads/1783080498359-420570422.png"),
loadImage("https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=dummy-uuid-preview")
]);
// Nama intern — pakai Great Vibes (font kaligrafi/script)
ctx.font = `105px "Great Vibes", cursive`;
ctx.fillStyle = "#800000";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Jonathan Kristina", canvas.width * 0.57, canvas.height * 0.48);
const canvas = document.getElementById("preview");
canvas.width = templateImg.naturalWidth;
canvas.height = templateImg.naturalHeight;
const ctx = canvas.getContext("2d");
// Nama intern — versi kecil
ctx.font = `30px "Great Vibes", cursive`;
ctx.fillStyle = "#800000";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Jonathan Kristina", canvas.width * 0.825, canvas.height * 0.895);
// 1. Gambar template dulu (paling belakang)
ctx.drawImage(templateImg, 0, 0);
// Nama project — pakai Grenze
ctx.font = `400 54px "Grenze", serif`;
ctx.fillStyle = "#090909";
ctx.fillText("Internify Platform Dev", canvas.width * 0.57, canvas.height * 0.615);
// 2. Nama intern — Great Vibes
ctx.font = `105px "Great Vibes", cursive`;
ctx.fillStyle = "#800000";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Jonathan Kristina", canvas.width * 0.57, canvas.height * 0.48);
// Certificate no — monospace biasa
ctx.font = `50px Grenze`;
ctx.fillStyle = "#090909";
ctx.fillText("CERT/20260609/PRJ2/USR5", canvas.width * 0.666, canvas.height * 0.305);
ctx.font = `30px "Great Vibes", cursive`;
ctx.fillText("Jonathan Kristina", canvas.width * 0.825, canvas.height * 0.895);
// Certificate no — monospace biasa
ctx.font = `20px Grenze`;
ctx.fillStyle = "#800000";
ctx.fillText("CERT/20260609/PRJ2/USR5",canvas.width * 0.825, canvas.height * 0.918);
};
// 3. Nama project — Grenze
ctx.font = `400 54px "Grenze", serif`;
ctx.fillStyle = "#090909";
ctx.fillText("Internify Platform Dev", canvas.width * 0.57, canvas.height * 0.615);
img.src = "http://localhost:9000/uploads/1783080498359-420570422.png"; // ganti URL template kamu
// 4. Certificate no
ctx.font = `50px Grenze`;
ctx.fillText("CERT/20260609/PRJ2/USR5", canvas.width * 0.665, canvas.height * 0.305);
ctx.font = `20px Grenze`;
ctx.fillStyle = "#800000";
ctx.fillText("CERT/20260609/PRJ2/USR5", canvas.width * 0.825, canvas.height * 0.918);
// 5. QR code — posisi & ukuran pakai persentase, sama seperti elemen lain
const qrSize = canvas.width * 0.103; // atur besar-kecil di sini
const qrX = canvas.width * 0.77; // titik kiri-atas QR (bukan tengah!)
const qrY = canvas.height * 0.715;
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
}
draw();