[UNTEST] CERTIFICATE GENERATION AND BACKEND ENDPOINT IMPLEMENTATION ON CERTIFICATES (CLAIM, GENERATE, FETCH DATA)
- Updated AddInternsModal to allow selection of interns via dropdown instead of email input. - Introduced CertResultTable component for displaying certificate results with search and pagination. - Modified CreateProjectDialog to accept an onSuccess callback for better project creation handling. - Integrated certificate generation functionality in EligibleInternTable with improved error handling. - Enhanced HomeMentorProject and MentorProjectCard to dynamically display project icons and statuses. - Refactored SidebarMentor to improve active route detection. - Added utility functions for certificate preview and download in Certificates.ts. - Removed unused DownloadImagetoDevice utility and related hooks. - Implemented useProjectsByRole hook for fetching projects based on user role. - Updated types for certificates and projects to reflect recent changes in the data structure.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import jsPDF from "jspdf";
|
||||
import JSZip from "jszip";
|
||||
import type { Certificate } from "../../types/certificate.types";
|
||||
import { generateCertificate, resolveImageUrl } from "./SertificateGenerator";
|
||||
|
||||
// ── 1. Preview di tab baru ────────────────────────────────────────────────────
|
||||
export const previewCertificate = async (
|
||||
cert: Certificate,
|
||||
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
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
if (!tab) {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_blank";
|
||||
a.click();
|
||||
return;
|
||||
}
|
||||
|
||||
tab.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Sertifikat - ${cert.user.full_name}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #1a1a1a;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
p { color: #aaa; font-size: 13px; letter-spacing: 0.05em; }
|
||||
small { color: #555; font-size: 11px; font-family: monospace; }
|
||||
img {
|
||||
max-width: 90vw;
|
||||
max-height: 82vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.6);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>${cert.user.full_name.toUpperCase()}</p>
|
||||
<small>${cert.certificate_no}</small>
|
||||
<img src="${url}" alt="Sertifikat ${cert.user.full_name}"
|
||||
onload="URL.revokeObjectURL(this.src)" />
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
tab.document.close();
|
||||
};
|
||||
|
||||
// ── 2. Download PDF ───────────────────────────────────────────────────────────
|
||||
export const downloadCertificatePdf = async (
|
||||
cert: Certificate,
|
||||
templateUrl: string
|
||||
): Promise<void> => {
|
||||
const fileName = `Sertifikat - ${cert.user.full_name} - ${cert.certificate_no}`;
|
||||
const blob = await generateCertificate(
|
||||
resolveImageUrl(templateUrl),
|
||||
cert.user.full_name
|
||||
);
|
||||
|
||||
const dataUrl: string = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
const img = new Image();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = reject;
|
||||
img.src = dataUrl;
|
||||
});
|
||||
|
||||
const orientation = img.width > img.height ? "landscape" : "portrait";
|
||||
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`);
|
||||
};
|
||||
|
||||
// ── 3. Download PNG atau JPG ──────────────────────────────────────────────────
|
||||
export const downloadCertificateImage = async (
|
||||
cert: Certificate,
|
||||
templateUrl: string,
|
||||
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
|
||||
);
|
||||
|
||||
let finalBlob = blob;
|
||||
|
||||
if (format === "jpg") {
|
||||
const dataUrl: string = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
const img = new Image();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = reject;
|
||||
img.src = dataUrl;
|
||||
});
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
finalBlob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(b) => (b ? resolve(b) : reject(new Error("toBlob gagal"))),
|
||||
"image/jpeg",
|
||||
0.95
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(finalBlob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${fileName}.${format}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// ── 4. Download All ZIP ───────────────────────────────────────────────────────
|
||||
export const downloadAllCertificatesZip = async (
|
||||
certificates: Certificate[],
|
||||
templateUrl: string,
|
||||
projectName: string,
|
||||
onProgress?: (done: number, total: number) => void
|
||||
): Promise<void> => {
|
||||
const zip = new JSZip();
|
||||
const total = certificates.length;
|
||||
|
||||
await Promise.all(
|
||||
certificates.map(async (cert, i) => {
|
||||
const blob = await generateCertificate(
|
||||
resolveImageUrl(templateUrl),
|
||||
cert.user.full_name
|
||||
);
|
||||
zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
|
||||
onProgress?.(i + 1, total);
|
||||
})
|
||||
);
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(zipBlob);
|
||||
a.download = `Sertifikat - ${projectName}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
// utils/certificateDownload.ts
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
|
||||
/**
|
||||
* Helper: capture sebuah HTMLElement menjadi canvas
|
||||
*/
|
||||
export const captureCertificateElement = async (
|
||||
element: HTMLElement
|
||||
): Promise<HTMLCanvasElement> => {
|
||||
return html2canvas(element, {
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
backgroundColor: "#ffffff",
|
||||
scale: 2, // hasil lebih tajam
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Download elemen sertifikat sebagai PNG/JPG
|
||||
*/
|
||||
export const downloadCertificateAsImage = async (
|
||||
element: HTMLElement | null,
|
||||
fileName: string,
|
||||
format: "png" | "jpg" = "png",
|
||||
fallbackImageUrl?: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!element) throw new Error("Elemen sertifikat tidak ditemukan");
|
||||
|
||||
const canvas = await captureCertificateElement(element);
|
||||
|
||||
const mimeType = format === "jpg" ? "image/jpeg" : "image/png";
|
||||
const quality = format === "jpg" ? 0.95 : 1;
|
||||
const dataUrl = canvas.toDataURL(mimeType, quality);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = dataUrl;
|
||||
a.download = `${fileName}.${format}`;
|
||||
a.click();
|
||||
} catch (error) {
|
||||
console.error("Gagal mengunduh sertifikat sebagai gambar:", error);
|
||||
|
||||
// fallback: download langsung dari image_path kalau html2canvas gagal (mis. CORS)
|
||||
if (fallbackImageUrl) {
|
||||
const a = document.createElement("a");
|
||||
a.href = fallbackImageUrl;
|
||||
a.download = `${fileName}.png`;
|
||||
a.target = "_blank";
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Download elemen sertifikat sebagai PDF
|
||||
*/
|
||||
export const downloadCertificateAsPdf = async (
|
||||
element: HTMLElement | null,
|
||||
fileName: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!element) throw new Error("Elemen sertifikat tidak ditemukan");
|
||||
|
||||
const canvas = await captureCertificateElement(element);
|
||||
const imgData = canvas.toDataURL("image/png", 1.0);
|
||||
|
||||
const imgWidth = canvas.width;
|
||||
const imgHeight = canvas.height;
|
||||
|
||||
// Tentukan orientasi PDF berdasarkan rasio gambar
|
||||
const orientation = imgWidth > imgHeight ? "landscape" : "portrait";
|
||||
|
||||
const pdf = new jsPDF({
|
||||
orientation,
|
||||
unit: "px",
|
||||
format: [imgWidth, imgHeight],
|
||||
});
|
||||
|
||||
pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
|
||||
pdf.save(`${fileName}.pdf`);
|
||||
} catch (error) {
|
||||
console.error("Gagal mengunduh sertifikat sebagai PDF:", error);
|
||||
}
|
||||
};
|
||||
@@ -39,12 +39,10 @@ const ProtectedRoutes = ({ allowedRoles }: PrivateRouteProps) => {
|
||||
}
|
||||
|
||||
if (allowedRoles && !allowedRoles.includes(userRole)) {
|
||||
// Jika role tidak diizinkan, kembalikan ke base dashboard masing-masing role
|
||||
if (userRole === "admin" || userRole === "mentor") return <Navigate to="/mentor" replace />;
|
||||
if (userRole === "intern") return <Navigate to="/intern" replace />;
|
||||
}
|
||||
|
||||
// Jika lolos seleksi, render komponen di dalamnya
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import api from "../../lib/api";
|
||||
|
||||
// ── Konstanta posisi nama di template ─────────────────────────────────────────
|
||||
const NAME_Y_RATIO = 0.55; // posisi Y = 55% dari tinggi canvas
|
||||
const NAME_FONT_SIZE = 48; // px
|
||||
const NAME_FONT_FAMILY = "Georgia, serif";
|
||||
const NAME_COLOR = "#1a1a1a";
|
||||
|
||||
// src/utils/certificateGenerator.ts
|
||||
export function resolveImageUrl(path: string): string {
|
||||
if (!path) return "";
|
||||
@@ -31,7 +25,10 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
// ── Generate satu sertifikat sebagai Blob ─────────────────────────────────────
|
||||
export async function generateCertificate(
|
||||
templateUrl: string,
|
||||
internName: string
|
||||
internName: string,
|
||||
projectName: string,
|
||||
uuid: string,
|
||||
barcodeUrl: string
|
||||
): Promise<Blob> {
|
||||
const img = await loadImage(templateUrl);
|
||||
|
||||
@@ -43,11 +40,11 @@ export async function generateCertificate(
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// Tulis nama di tengah horizontal, posisi Y dari konstanta
|
||||
ctx.font = `bold ${NAME_FONT_SIZE}px ${NAME_FONT_FAMILY}`;
|
||||
ctx.fillStyle = NAME_COLOR;
|
||||
ctx.font = `bold 48px Georgia, serif`;
|
||||
ctx.fillStyle = "#800000";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(internName, canvas.width / 2, canvas.height * NAME_Y_RATIO);
|
||||
ctx.fillText(internName, canvas.width / 2, canvas.height * 0.55);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- test-cert.html — buka langsung di browser -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<canvas id="preview"></canvas>
|
||||
<script>
|
||||
// GANTI INI UNTUK MENGGUNAKAN TEMPLATE YANG BERBEDA
|
||||
const templateUrl = "http://localhost:9000/uploads/template.png"; // ganti URL template
|
||||
|
||||
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);
|
||||
|
||||
// Project
|
||||
ctx.font = "24px Georgia, serif";
|
||||
ctx.fillStyle = "#444444";
|
||||
ctx.fillText("Internify Platform Dev", canvas.width / 2, canvas.height * 0.65);
|
||||
|
||||
// UUID
|
||||
ctx.font = "16px monospace";
|
||||
ctx.fillStyle = "#888888";
|
||||
ctx.fillText("CERT/20260609/PRJ2/USR5", canvas.width / 2, canvas.height * 0.88);
|
||||
};
|
||||
img.src = templateUrl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user