feat: update certificate verification and display components with improved layout and data handling
This commit is contained in:
@@ -24,7 +24,7 @@ export default function VerificationCert() {
|
|||||||
/>
|
/>
|
||||||
<h1 className="text-2xl font-bold text-red mb-2 animate-pulse">Maaf, ID Sertifikat anda masukkan tidak valid</h1>
|
<h1 className="text-2xl font-bold text-red mb-2 animate-pulse">Maaf, ID Sertifikat anda masukkan tidak valid</h1>
|
||||||
<p className="text-gray-500 text-[16px]">
|
<p className="text-gray-500 text-[16px]">
|
||||||
Sertifikat dengan UUID ini tidak dapat ditemukan atau sudah tidak berlaku.
|
Sertifikat dengan UUID ini tidak dapat ditemukan
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -38,20 +38,20 @@ export default function VerificationCert() {
|
|||||||
className="[240px] h-[240x] object-cover mb-14"
|
className="[240px] h-[240x] object-cover mb-14"
|
||||||
/>
|
/>
|
||||||
<h1 className="text-2xl font-bold text-green-600 mb-6 text-center">
|
<h1 className="text-2xl font-bold text-green-600 mb-6 text-center">
|
||||||
Sertifikat Valid
|
Validasi Berhasil!
|
||||||
</h1>
|
</h1>
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-6 text-sm p-12 bg-card shadow-xl rounded-2xl w-1/3">
|
||||||
<div>
|
<div className="flex justify-between items-start gap-4">
|
||||||
<span className="text-font">Nama Intern</span>
|
<span className="text-font">Nama Intern</span>
|
||||||
<p className="font-semibold">{verification.intern_name}</p>
|
<p className="font-semibold text-right">{verification.intern_name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex justify-between items-start gap-4">
|
||||||
<span className="text-font">Proyek</span>
|
<span className="text-font">Atas Proyek Humic</span>
|
||||||
<p className="font-semibold">{verification.project_name}</p>
|
<p className="font-semibold text-right">{verification.project.project_name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex justify-between items-start gap-4">
|
||||||
<span className="text-font">Tanggal Terbit</span>
|
<span className="text-font">Tanggal Terbit</span>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold text-right">
|
||||||
{new Date(verification.created_at).toLocaleDateString("id-ID", {
|
{new Date(verification.created_at).toLocaleDateString("id-ID", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
@@ -59,9 +59,19 @@ export default function VerificationCert() {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex justify-between items-start gap-4">
|
||||||
|
<span className="text-font">Projek Selesai</span>
|
||||||
|
<p className="font-semibold text-right">
|
||||||
|
{new Date(verification.project.end_date).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-start gap-4">
|
||||||
<span className="text-font">UUID</span>
|
<span className="text-font">UUID</span>
|
||||||
<p className="font-mono text-xs break-all">{verification.uuid}</p>
|
<p className="font-mono text-xs break-all text-right">{verification.uuid}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
|
|
||||||
const duration = formatDateRange(project?.start_date, project?.end_date)
|
const duration = formatDateRange(project?.start_date, project?.end_date)
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!templateUrl) return;
|
if (!templateUrl) return;
|
||||||
const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`;
|
const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`;
|
||||||
@@ -82,12 +81,17 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleShareLinkedIn = () => {
|
const handleShareLinkedIn = () => {
|
||||||
|
|
||||||
|
const issueDate = new Date(certificate.issued_at.replace(" ", "T"));
|
||||||
const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`;
|
const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
startTask: "CERTIFICATION_NAME",
|
startTask: "CERTIFICATION_NAME",
|
||||||
name: certificate.project.project_name,
|
name: "Penyelesaian Kerja Magang Instansi CoE HUMIC Engineering Proyek: " + certificate.project.project_name,
|
||||||
organizationName: "Internify",
|
issueYear: String(issueDate.getFullYear()),
|
||||||
|
issueMonth: String(issueDate.getMonth() + 1),
|
||||||
|
organizationName: "CoE Humic Engineering",
|
||||||
certUrl: verifyUrl,
|
certUrl: verifyUrl,
|
||||||
|
|
||||||
certId: certificate.certificate_no,
|
certId: certificate.certificate_no,
|
||||||
});
|
});
|
||||||
window.open(
|
window.open(
|
||||||
@@ -120,7 +124,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
|
|
||||||
{/* Program Summary */}
|
{/* Program Summary */}
|
||||||
<div className="grid grid-cols-2 gap-5">
|
<div className="grid grid-cols-2 gap-5">
|
||||||
<div className="bg-white rounded-2xl border border-card-outline shadow-sm p-5">
|
<div className="bg-box-primary rounded-2xl border border-box-border shadow-sm p-5">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<h3 className="font-semibold text-gray-800 text-sm">Ringkasan Program</h3>
|
<h3 className="font-semibold text-gray-800 text-sm">Ringkasan Program</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +132,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
{[
|
{[
|
||||||
{ label: "Nama Program", value: project?.project_name ?? "-" },
|
{ label: "Nama Program", value: project?.project_name ?? "-" },
|
||||||
{ label: "Durasi", value: duration },
|
{ label: "Durasi", value: duration },
|
||||||
{ label: "No. Sertifikat", value: certificate.certificate_no ?? "-" },
|
{ label: "No. Sertifikat", value: certificate.certificate_no ? certificate.certificate_no : "-" },
|
||||||
].map((row) => (
|
].map((row) => (
|
||||||
<div
|
<div
|
||||||
key={row.label}
|
key={row.label}
|
||||||
@@ -141,7 +145,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl border border-card-outline shadow-sm p-5">
|
<div className="bg-box-primary rounded-2xl border border-box-border shadow-sm p-5">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Unduh Sertifikat */}
|
{/* Unduh Sertifikat */}
|
||||||
<button
|
<button
|
||||||
@@ -160,7 +164,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl
|
|||||||
>
|
>
|
||||||
{/* LinkedIn icon */}
|
{/* LinkedIn icon */}
|
||||||
<svg className="w-4 h-4 fill-white" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg className="w-4 h-4 fill-white" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||||
</svg>
|
</svg>
|
||||||
Tambah ke Profil LinkedIn
|
Tambah ke Profil LinkedIn
|
||||||
</button>
|
</button>
|
||||||
@@ -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"
|
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={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`${window.location.origin}/verify-certificate/${certificate.uuid}`);
|
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");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link className="w-5 h-5" />
|
<Link className="w-5 h-5" />
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useMyCertificates } from "../../../../hooks/useCertificates";
|
|||||||
export default function SertificateHistory() {
|
export default function SertificateHistory() {
|
||||||
const { certificates, loading, error } = useMyCertificates();
|
const { certificates, loading, error } = useMyCertificates();
|
||||||
const displayed = certificates.slice(0, 3);
|
const displayed = certificates.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-box-primary rounded-2xl border border-box-border shadow-sm p-5">
|
<div className="bg-box-primary rounded-2xl border border-box-border shadow-sm p-5">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
@@ -36,7 +35,7 @@ export default function SertificateHistory() {
|
|||||||
{displayed.map((item: Certificate) => (
|
{displayed.map((item: Certificate) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="flex flex-col gap-3 p-3 bg-white border border-gray-200 shadow-sm rounded-xl hover:bg-gray-50 transition-colors cursor-pointer"
|
className="flex flex-col gap-3 p-3 bg-card border border-card-outline shadow-sm rounded-xl hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400">
|
||||||
@@ -48,7 +47,7 @@ export default function SertificateHistory() {
|
|||||||
<p className="text-sm font-semibold text-gray-800 leading-tight">
|
<p className="text-sm font-semibold text-gray-800 leading-tight">
|
||||||
{item.project.project_name}
|
{item.project.project_name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500">No: {item.certificate_no}</p>
|
<p className="text-sm text-gray-500">{item.certificate_no}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// CertificateNotAvailable.tsx
|
import { Clock, Lock } from "lucide-react";
|
||||||
import { Clock, Lock, Award } from "lucide-react";
|
import { FaAward } from "react-icons/fa";
|
||||||
|
|
||||||
interface CertificateNotAvailableProps {
|
interface CertificateNotAvailableProps {
|
||||||
progress: number;
|
progress: number;
|
||||||
@@ -19,9 +19,9 @@ export default function CertificateNotAvailable({
|
|||||||
<div className="w-34 h-34 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
<div className="w-34 h-34 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
||||||
<div className="w-26 h-26 flex items-center rounded-lg justify-center bg-gray-300">
|
<div className="w-26 h-26 flex items-center rounded-lg justify-center bg-gray-300">
|
||||||
{allTasksDone ? (
|
{allTasksDone ? (
|
||||||
<Award className="w-12 h-12 text-green-600" />
|
<FaAward className="w-12 h-12 text-green-400" />
|
||||||
) : (
|
) : (
|
||||||
<Lock className="w-12 h-12 text-red-700" />
|
<Lock className="w-12 h-12 text-red-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
|
<div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
|
||||||
|
|||||||
@@ -5,13 +5,6 @@ import CreateProjectModal from './components/CreateProjectDialog';
|
|||||||
import { useProjectsByRole } from '../../../hooks/useListProjects';
|
import { useProjectsByRole } from '../../../hooks/useListProjects';
|
||||||
import TeamPana from '../../../assets/Team-pana.svg';
|
import TeamPana from '../../../assets/Team-pana.svg';
|
||||||
|
|
||||||
function MentorProjectCardSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row bg-gray-300 rounded-xl overflow-hidden shadow-sm w-[180px] min-h-[270px] animate-pulse">
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MentorProjectsPage() {
|
export default function MentorProjectsPage() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const { projects, loading, error, refetch } = useProjectsByRole();
|
const { projects, loading, error, refetch } = useProjectsByRole();
|
||||||
@@ -35,7 +28,8 @@ export default function MentorProjectsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-3 w-full">
|
<div className="flex flex-row gap-3 w-full">
|
||||||
{[...Array(4)].map((_) => (
|
{[...Array(4)].map((_) => (
|
||||||
<MentorProjectCardSkeleton />
|
<div className="flex flex-row bg-gray-300 rounded-xl overflow-hidden shadow-sm w-[180px] min-h-[270px] animate-pulse">
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default function EligibleInternTable({
|
|||||||
id: intern.id_user,
|
id: intern.id_user,
|
||||||
id_project: project.id,
|
id_project: project.id,
|
||||||
id_user: intern.id_user,
|
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),
|
issued_at: new Date().toISOString().slice(0, 10),
|
||||||
uuid: "HANYA SEBATAS PREVIEW",
|
uuid: "HANYA SEBATAS PREVIEW",
|
||||||
user: {
|
user: {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { resolveFileUrl } from "./resolveFileFromUrl";
|
import { resolveFileUrl } from "./resolveFileFromUrl";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
export function loadImage(src: string): Promise<HTMLImageElement> {
|
//LOAD IMAGE TEMPLATE
|
||||||
|
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
if (!src.startsWith("data:")) img.crossOrigin = "anonymous";
|
if (!src.startsWith("data:")) img.crossOrigin = "anonymous";
|
||||||
@@ -11,7 +12,8 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureFontsLoaded(): Promise<void> {
|
//LOAD FONT UNTUK MENCEGAH FONT TERGANTI
|
||||||
|
async function ensureFontsLoaded(): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
document.fonts.load('105px "Great Vibes"'),
|
document.fonts.load('105px "Great Vibes"'),
|
||||||
document.fonts.load('54px "Grenze"'),
|
document.fonts.load('54px "Grenze"'),
|
||||||
@@ -19,16 +21,30 @@ export async function ensureFontsLoaded(): Promise<void> {
|
|||||||
await document.fonts.ready;
|
await document.fonts.ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LAYOUT DEFAULT SERTIFIKAT YANG AKAN DIGENERATE
|
||||||
const LAYOUT = {
|
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" },
|
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" },
|
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" },
|
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"},
|
certNoSmall: { x: 0.827, y: 0.92, size: 32, font: "Grenze", color: "#090909"},
|
||||||
qr: { x: 0.77, y: 0.715, size: 0.103 },
|
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(
|
export async function generateCertificate(
|
||||||
templateUrl: string,
|
templateUrl: string,
|
||||||
internName: string,
|
internName: string,
|
||||||
@@ -52,7 +68,8 @@ export async function generateCertificate(
|
|||||||
cfg: { x: number; y: number; size: number; font: string; color: string },
|
cfg: { x: number; y: number; size: number; font: string; color: string },
|
||||||
value: 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.fillStyle = cfg.color;
|
||||||
ctx.fillText(value, canvas.width * cfg.x, canvas.height * cfg.y);
|
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.projectName, projectName);
|
||||||
text(LAYOUT.duration, ProjectDuration);
|
text(LAYOUT.duration, ProjectDuration);
|
||||||
text(LAYOUT.certNo, certificateNo);
|
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 qrSize = Math.round(canvas.width * LAYOUT.qr.size);
|
||||||
const qrDataUrl = await QRCode.toDataURL(
|
const qrDataUrl = await QRCode.toDataURL(
|
||||||
`${window.location.origin}/verify-certificate/${verifyUrl}`,
|
`${window.location.origin}/verify-certificate/${verifyUrl}`,
|
||||||
|
|||||||
@@ -5,29 +5,48 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&family=Grenze:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #1a1a1a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid #ffffff55;
|
||||||
|
box-shadow: 0 0 0 1px #000, 0 4px 20px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="preview" style="max-width: 100%;"></canvas>
|
<canvas id="preview"></canvas>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const TEMPLATE_PATH = "../../assets/template.png";
|
const TEMPLATE_PATH = "../../assets/template.png";
|
||||||
|
|
||||||
// LAYOUT
|
// LAYOUT
|
||||||
const LAYOUT = {
|
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: 33, font: "Great Vibes", color: "#800000" },
|
internNameSmall: { x: 0.82, y: 0.889, size: 33, font: "Great Vibes", color: "#800000" },
|
||||||
projectName: { x: 0.815, y: 0.598, size: 46, font: "Grenze", color: "#090909" },
|
projectName: { x: 0.815, y: 0.598, size: 46, font: "Grenze", color: "#800000" },
|
||||||
duration: { x: 0.692, y: 0.645, size: 48, font: "Grenze", color: "#090909" },
|
duration: { x: 0.692, y: 0.645, size: 48, font: "Grenze", color: "#090909" },
|
||||||
certNo: { x: 0.6, 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.825, y: 0.91, size: 22, font: "Grenze", color: "#800000" },
|
certNoSmall: { x: 0.825, y: 0.91, size: 22, font: "Grenze", color: "#800000" },
|
||||||
qr: { x: 0.77, y: 0.715, size: 0.103 },
|
qr: { x: 0.77, y: 0.715, size: 0.103 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const DATA = {
|
const DATA = {
|
||||||
internName: "Jonathan Kristina",
|
internName: "Kemas M. Aryadary Rasyad",
|
||||||
projectName: "Internify Proyek Humic",
|
projectName: "Internify Proyek Humic",
|
||||||
duration: "20 April 2026 - 20 Juni 2026",
|
duration: "20 April 2026 - 20 Juni 2026",
|
||||||
certNo: "CERT/XXXXXX/1",
|
certNo: "No.2026/SPI3/CERT/XXXXXX/1",
|
||||||
verifyUrl: "dummy-uuid-preview",
|
verifyUrl: "dummy-uuid-preview",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,17 @@ export interface CertificateTemplate {
|
|||||||
certificate_template: string;
|
certificate_template: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface projectbysertif {
|
||||||
|
project_id: number
|
||||||
|
project_name: string;
|
||||||
|
end_date: string;
|
||||||
|
description: string;
|
||||||
|
certificate_template: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface CertificateVerifyResult {
|
export interface CertificateVerifyResult {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
intern_name: string;
|
intern_name: string;
|
||||||
project_name: string;
|
project: projectbysertif;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user