[TEST DONE] Checking Sertification generation for mentor/admin and view on intern

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-03 20:18:52 +07:00
parent 508ffe5f84
commit 5b7ac30589
14 changed files with 189 additions and 99 deletions
+28 -12
View File
@@ -6,14 +6,17 @@ import { generateCertificate, resolveImageUrl } from "./SertificateGenerator";
// ── 1. Preview di tab baru ────────────────────────────────────────────────────
export const previewCertificate = async (
cert: Certificate,
templateUrl: string
templateUrl: string,
): Promise<void> => {
// Buka tab DULU sebelum await — supaya tidak diblokir popup blocker
const tab = window.open("", "_blank");
const blob = await generateCertificate(
resolveImageUrl(templateUrl),
cert.user.full_name
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
cert.uuid,
);
const url = URL.createObjectURL(blob);
@@ -68,12 +71,15 @@ export const previewCertificate = async (
// ── 2. Download PDF ───────────────────────────────────────────────────────────
export const downloadCertificatePdf = async (
cert: Certificate,
templateUrl: string
templateUrl: string,
): Promise<void> => {
const fileName = `Sertifikat - ${cert.user.full_name} - ${cert.certificate_no}`;
const blob = await generateCertificate(
resolveImageUrl(templateUrl),
cert.user.full_name
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
cert.uuid,
);
const dataUrl: string = await new Promise((resolve, reject) => {
@@ -91,7 +97,11 @@ export const downloadCertificatePdf = async (
});
const orientation = img.width > img.height ? "landscape" : "portrait";
const pdf = new jsPDF({ orientation, unit: "px", format: [img.width, img.height] });
const pdf = new jsPDF({
orientation,
unit: "px",
format: [img.width, img.height],
});
pdf.addImage(dataUrl, "PNG", 0, 0, img.width, img.height);
pdf.save(`${fileName}.pdf`);
};
@@ -100,12 +110,15 @@ export const downloadCertificatePdf = async (
export const downloadCertificateImage = async (
cert: Certificate,
templateUrl: string,
format: "png" | "jpg" = "png"
format: "png" | "jpg" = "png",
): Promise<void> => {
const fileName = `Sertifikat - ${cert.user.full_name} - ${cert.certificate_no}`;
const blob = await generateCertificate(
resolveImageUrl(templateUrl),
cert.user.full_name
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
cert.uuid,
);
let finalBlob = blob;
@@ -137,7 +150,7 @@ export const downloadCertificateImage = async (
canvas.toBlob(
(b) => (b ? resolve(b) : reject(new Error("toBlob gagal"))),
"image/jpeg",
0.95
0.95,
);
});
}
@@ -155,7 +168,7 @@ export const downloadAllCertificatesZip = async (
certificates: Certificate[],
templateUrl: string,
projectName: string,
onProgress?: (done: number, total: number) => void
onProgress?: (done: number, total: number) => void,
): Promise<void> => {
const zip = new JSZip();
const total = certificates.length;
@@ -164,11 +177,14 @@ export const downloadAllCertificatesZip = async (
certificates.map(async (cert, i) => {
const blob = await generateCertificate(
resolveImageUrl(templateUrl),
cert.user.full_name
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
cert.uuid,
);
zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
onProgress?.(i + 1, total);
})
}),
);
const zipBlob = await zip.generateAsync({ type: "blob" });
@@ -177,4 +193,4 @@ export const downloadAllCertificatesZip = async (
a.download = `Sertifikat - ${projectName}.zip`;
a.click();
URL.revokeObjectURL(a.href);
};
};
+60 -11
View File
@@ -1,8 +1,12 @@
import api from "../../lib/api";
import QRCode from "qrcode";
// Flag supaya font hanya di-load sekali (tidak berulang tiap generate)
let fontsLoaded = false;
// src/utils/certificateGenerator.ts
export function resolveImageUrl(path: string): string {
if (!path) return "";
if (path.startsWith("data:")) return path; // untuk QR data URL
if (path.startsWith("http://") || path.startsWith("https://")) {
return path;
}
@@ -11,7 +15,7 @@ export function resolveImageUrl(path: string): string {
return `${origin}${path.startsWith("/") ? "" : "/"}${path}`;
}
// ── Load Image dari URL ────────────────────────────────────────────────────────
// Load Image dari URL
export function loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
@@ -22,29 +26,74 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
});
}
// Load kedua font dari Google Fonts (hanya sekali)
export async function ensureFontsLoaded(): Promise<void> {
// Trigger & tunggu font yang sudah diload via <link> di index.html
await Promise.all([
document.fonts.load('105px "Great Vibes"'),
document.fonts.load('54px "Grenze"'),
]);
await document.fonts.ready;
}
// ── Generate satu sertifikat sebagai Blob ─────────────────────────────────────
export async function generateCertificate(
templateUrl: string,
internName: string,
projectName: string,
uuid: string,
barcodeUrl: string
certificateNo: string,
verifyUrl: string
): Promise<Blob> {
const img = await loadImage(templateUrl);
// Pastikan font siap sebelum menggambar teks
await ensureFontsLoaded();
const img = await loadImage(templateUrl);
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, 0, 0);
// Tulis nama di tengah horizontal, posisi Y dari konstanta
ctx.font = `bold 48px Georgia, serif`;
ctx.fillStyle = "#800000";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(internName, canvas.width / 2, canvas.height * 0.55);
// Nama intern — besar (Great Vibes)
ctx.font = `105px "Great Vibes", cursive`;
ctx.fillStyle = "#800000";
ctx.fillText(internName, canvas.width * 0.57, canvas.height * 0.48);
// Nama intern — versi kecil di pojok bawah
ctx.font = `30px "Great Vibes", cursive`;
ctx.fillStyle = "#800000";
ctx.fillText(internName, canvas.width * 0.825, canvas.height * 0.895);
// Nama project (Grenze)
ctx.font = `400 54px "Grenze", serif`;
ctx.fillStyle = "#090909";
ctx.fillText(projectName, canvas.width * 0.57, canvas.height * 0.615);
// Certificate no — besar (Grenze)
ctx.font = `50px "Grenze", serif`;
ctx.fillStyle = "#090909";
ctx.fillText(certificateNo, canvas.width * 0.666, canvas.height * 0.305);
// Certificate no — kecil di pojok bawah
ctx.font = `20px "Grenze", serif`;
ctx.fillStyle = "#800000";
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
// );
// }
return new Promise((resolve, reject) => {
canvas.toBlob(
+50 -28
View File
@@ -1,39 +1,61 @@
<!-- test-cert.html — buka langsung di browser -->
<!DOCTYPE html>
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&family=Grenze:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
</head>
<body>
<canvas id="preview"></canvas>
<canvas id="preview" style="max-width: 100%;"></canvas>
<script>
// GANTI INI UNTUK MENGGUNAKAN TEMPLATE YANG BERBEDA
const templateUrl = "http://localhost:9000/uploads/template.png"; // ganti URL template
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);
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);
// Nama
ctx.font = "bold 48px Georgia, serif";
ctx.fillStyle = "#800000";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Rafi Athallah", canvas.width / 2, canvas.height * 0.55);
// 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);
// Project
ctx.font = "24px Georgia, serif";
ctx.fillStyle = "#444444";
ctx.fillText("Internify Platform Dev", canvas.width / 2, canvas.height * 0.65);
// 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);
// UUID
ctx.font = "16px monospace";
ctx.fillStyle = "#888888";
ctx.fillText("CERT/20260609/PRJ2/USR5", canvas.width / 2, canvas.height * 0.88);
};
img.src = templateUrl;
// 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);
// 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);
// 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);
};
img.src = "http://localhost:9000/uploads/1783080498359-420570422.png"; // ganti URL template kamu
}
draw();
</script>
</body>
</html>