feat: implement certificate download functionality with image and PDF options
This commit is contained in:
@@ -52,16 +52,16 @@ export default function SertificateList() {
|
|||||||
{projects.map((item: Project) => (
|
{projects.map((item: Project) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow cursor-pointer group"
|
className="bg-white border border-card-outline rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow cursor-pointer group"
|
||||||
onClick={() => navigate(`/intern/certificates/${item.slug}`, { state: { project: item } })}
|
onClick={() => navigate(`/intern/certificates/${item.slug}`, { state: { project: item } })}
|
||||||
>
|
>
|
||||||
{/* Thumbnail */}
|
{/* Thumbnail */}
|
||||||
<div className="p-6 pb-0">
|
<div className="p-6 pb-0">
|
||||||
<div className="rounded-lg overflow-hidden">
|
<div className="rounded-lg overflow-hidden">
|
||||||
<div className="w-full h-32 bg-gradient-to-br from-red-400 to-red-600 flex items-center justify-center">
|
<div className="w-full h-32 bg-gray-300 flex items-center justify-center">
|
||||||
{(() => {
|
{(() => {
|
||||||
const Icon = getDynamicIcon(item.project_icon);
|
const Icon = getDynamicIcon(item.project_icon);
|
||||||
return <Icon className="w-10 h-10 text-white" />;
|
return <Icon className="w-10 h-10 text-primary" />;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// CertificateAvailable.tsx
|
// CertificateAvailable.tsx
|
||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { Download, Link, Printer } from "lucide-react";
|
import { Download, Link, Printer } from "lucide-react";
|
||||||
import type { ProjectDetail } from "../../../../types/project.types";
|
import type { ProjectDetail } from "../../../../types/project.types";
|
||||||
import type { Certificate } from "../../../../types/certificate.types";
|
import type { Certificate } from "../../../../types/certificate.types";
|
||||||
|
import { downloadCertificateAsImage, downloadCertificateAsPdf } from "../../../utils/DownloadImagetoDevice";
|
||||||
|
|
||||||
interface CertificateAvailableProps {
|
interface CertificateAvailableProps {
|
||||||
project: ProjectDetail | null;
|
project: ProjectDetail | null;
|
||||||
@@ -11,6 +12,9 @@ interface CertificateAvailableProps {
|
|||||||
|
|
||||||
export default function CertificateAvailable({ project, certificate }: CertificateAvailableProps) {
|
export default function CertificateAvailable({ project, certificate }: CertificateAvailableProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [downloadingImg, setDownloadingImg] = useState(false);
|
||||||
|
const [downloadingPdf, setDownloadingPdf] = useState(false);
|
||||||
|
const certificateRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
navigator.clipboard.writeText(window.location.href);
|
navigator.clipboard.writeText(window.location.href);
|
||||||
@@ -33,12 +37,24 @@ export default function CertificateAvailable({ project, certificate }: Certifica
|
|||||||
})()
|
})()
|
||||||
: "-";
|
: "-";
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownloadImage = async () => {
|
||||||
const a = document.createElement("a");
|
setDownloadingImg(true);
|
||||||
a.href = certificate.image_path;
|
await downloadCertificateAsImage(
|
||||||
a.download = `Sertifikat - ${certificate.certificate_no}.png`;
|
certificateRef.current,
|
||||||
a.target = "_blank";
|
`Sertifikat - ${certificate.certificate_no}`,
|
||||||
a.click();
|
"png",
|
||||||
|
certificate.image_path
|
||||||
|
);
|
||||||
|
setDownloadingImg(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadPdf = async () => {
|
||||||
|
setDownloadingPdf(true);
|
||||||
|
await downloadCertificateAsPdf(
|
||||||
|
certificateRef.current,
|
||||||
|
`Sertifikat - ${certificate.certificate_no}`
|
||||||
|
);
|
||||||
|
setDownloadingPdf(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -87,11 +103,12 @@ export default function CertificateAvailable({ project, certificate }: Certifica
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Unduh Sertifikat */}
|
{/* Unduh Sertifikat */}
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownloadPdf}
|
||||||
|
disabled={downloadingPdf}
|
||||||
className="flex items-center justify-center gap-2 w-full bg-red-700 hover:bg-red-800 active:scale-95 transition-all text-white font-semibold py-2 rounded-xl text-sm"
|
className="flex items-center justify-center gap-2 w-full bg-red-700 hover:bg-red-800 active:scale-95 transition-all text-white font-semibold py-2 rounded-xl text-sm"
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
Unduh Sertifikat
|
{downloadingPdf? "Memproses": "Unduh Sertifikat"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Bagikan ke LinkedIn */}
|
{/* Bagikan ke LinkedIn */}
|
||||||
@@ -101,9 +118,12 @@ export default function CertificateAvailable({ project, certificate }: Certifica
|
|||||||
|
|
||||||
{/* Cetak & Salin Link */}
|
{/* Cetak & Salin Link */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button 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">
|
<button
|
||||||
|
onClick={handleDownloadImage}
|
||||||
|
disabled={downloadingImg}
|
||||||
|
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">
|
||||||
<Printer className="w-5 h-5" />
|
<Printer className="w-5 h-5" />
|
||||||
Cetak
|
{downloadingImg? "Memproses" : "Cetak"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopyLink}
|
onClick={handleCopyLink}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user