feat: add progress bar component and integrate into certificate detail page
- Implemented ProgressBar component for visual task completion representation. - Created SertificatePage to display certificate details and handle claiming logic. - Updated CertificateAvailable component to support dynamic certificate data. - Enhanced CertificateNotAvailable component with error handling and claim functionality. - Refactored CertificateHistory component to use updated hooks for fetching certificates. - Modified CertificateForm to utilize new hooks for uploading certificate templates. - Introduced CertificateResult component for displaying generated certificates with pagination. - Added EligibleInternTable component for managing eligible interns and generating certificates. - Developed utility functions for generating certificates as images. - Created hooks for managing certificate-related API interactions. - Updated project and certificate types to include new properties for templates and images.
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
// CertificateAvailable.tsx
|
||||
import { useState } from "react";
|
||||
import { Download, Link, Printer } from "lucide-react";
|
||||
import type { ProjectDetail } from "../../../../types/project.types";
|
||||
import certificateImg from "../../../../assets/certificate.png";
|
||||
import type { Certificate } from "../../../../types/certificate.types";
|
||||
|
||||
interface CertificateAvailableProps {
|
||||
project: ProjectDetail | null;
|
||||
certificate: Certificate;
|
||||
}
|
||||
|
||||
export default function CertificateAvailable({ project }: CertificateAvailableProps) {
|
||||
export default function CertificateAvailable({ project, certificate }: CertificateAvailableProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
@@ -31,80 +33,89 @@ export default function CertificateAvailable({ project }: CertificateAvailablePr
|
||||
})()
|
||||
: "-";
|
||||
|
||||
const handleDownload = () => {
|
||||
const a = document.createElement("a");
|
||||
a.href = certificate.image_path;
|
||||
a.download = `Sertifikat - ${certificate.certificate_no}.png`;
|
||||
a.target = "_blank";
|
||||
a.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Certificate Card */}
|
||||
<div className="flex rounded-xl p-5 mb-1 border bg-box-primary border-box-border shadow-md items-center justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt="Certificate"
|
||||
className="rounded-xl w-[550px] h-[350px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex rounded-xl p-5 mb-1 border bg-box-primary border-box-border shadow-md items-center justify-center">
|
||||
<img
|
||||
src={certificate.image_path}
|
||||
alt="Certificate"
|
||||
className="rounded-xl w-[550px] h-[350px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Program Summary */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h3 className="font-semibold text-gray-800 text-sm">Ringkasan Program</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: "Nama Program", value: project?.project_name ?? "-" },
|
||||
{ label: "Durasi", value: duration },
|
||||
{ label: "Total Tugas", value: project ? `${project.total_tasks} Tugas` : "-" },
|
||||
{
|
||||
label: "Status",
|
||||
value: (
|
||||
<span className="bg-green-100 text-green-700 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
UNVERIFIED
|
||||
</span>
|
||||
),
|
||||
},
|
||||
].map((row) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className="flex justify-between items-center text-sm border-b border-gray-50 pb-2 last:border-0"
|
||||
>
|
||||
<span className="text-gray-500">{row.label}</span>
|
||||
<span className="text-gray-800 font-medium">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Program Summary */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="bg-white rounded-2xl border border-card-outline shadow-sm p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h3 className="font-semibold text-gray-800 text-sm">Ringkasan Program</h3>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
{/* Unduh Sertifikat */}
|
||||
<button 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" />
|
||||
Unduh Sertifikat (PDF)
|
||||
</button>
|
||||
|
||||
{/* Bagikan ke LinkedIn */}
|
||||
<button className="flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 active:scale-95 transition-all text-white font-semibold py-2 rounded-xl text-sm">
|
||||
Bagikan ke LinkedIn
|
||||
</button>
|
||||
|
||||
{/* Cetak & Salin Link */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button className="flex flex-col items-center justify-center gap-1.5 border border-gray-200 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" />
|
||||
Cetak
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="flex flex-col items-center justify-center gap-1.5 border border-gray-200 hover:bg-gray-50 active:scale-95 transition-all text-red-700 font-medium py-3.5 rounded-xl text-sm"
|
||||
>
|
||||
<Link className="w-5 h-5" />
|
||||
{copied ? "Copied!" : "Salin Link"}
|
||||
</button>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: "Nama Program", value: project?.project_name ?? "-" },
|
||||
{ label: "Durasi", value: duration },
|
||||
{ label: "Total Tugas", value: project ? `${project.total_tasks} Tugas` : "-" },
|
||||
{
|
||||
label: "Status",
|
||||
value: (
|
||||
<span className="bg-green-100 text-green-700 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
{certificate.certificate_status.toUpperCase()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
].map((row) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className="flex justify-between items-center text-sm border-b border-card-outline pb-2 last:border-0"
|
||||
>
|
||||
<span className="text-gray-500">{row.label}</span>
|
||||
<span className="text-gray-800 font-medium">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-card-outline shadow-sm p-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Unduh Sertifikat */}
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
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" />
|
||||
Unduh Sertifikat
|
||||
</button>
|
||||
|
||||
{/* Bagikan ke LinkedIn */}
|
||||
<button className="flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 active:scale-95 transition-all text-white font-semibold py-2 rounded-xl text-sm">
|
||||
Bagikan ke LinkedIn
|
||||
</button>
|
||||
|
||||
{/* Cetak & Salin Link */}
|
||||
<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">
|
||||
<Printer className="w-5 h-5" />
|
||||
Cetak
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
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"
|
||||
>
|
||||
<Link className="w-5 h-5" />
|
||||
{copied ? "Copied!" : "Salin Link"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { History, Loader2, AlertCircle } from "lucide-react";
|
||||
import type { Certificate } from "../../../../types/certificate.types";
|
||||
import certificateImg from "../../../../assets/certificate.png";
|
||||
import { useMyCertificates } from "../../../../hooks/useSertificates";
|
||||
import certificateImg from "../../../../assets/certificate.png"; //Placeholder
|
||||
import { useMyCertificates } from "../../../../hooks/useCertificates";
|
||||
|
||||
export default function SertificateHistory() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
// CertificateNotAvailable.tsx
|
||||
import { Clock, Lock, Award } from "lucide-react";
|
||||
import { useClaimCertificate } from "../../../../hooks/useSertificates";
|
||||
|
||||
interface CertificateNotAvailableProps {
|
||||
progress: number;
|
||||
remainingTasks: number;
|
||||
allTasksDone?: boolean;
|
||||
project_id?: number;
|
||||
onClaim: () => void;
|
||||
claiming: boolean;
|
||||
claimError: string | null;
|
||||
}
|
||||
|
||||
export default function CertificateNotAvailable({
|
||||
progress,
|
||||
remainingTasks,
|
||||
allTasksDone = false,
|
||||
project_id
|
||||
onClaim,
|
||||
claiming,
|
||||
claimError,
|
||||
}: CertificateNotAvailableProps) {
|
||||
|
||||
const { claim, loading, error } = useClaimCertificate();
|
||||
|
||||
const handleClaim = async () => {
|
||||
if (!project_id) return;
|
||||
await claim(project_id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-box-primary w-full h-full rounded-2xl border border-box-border shadow-sm px-24 py-12 flex flex-col items-center justify-center">
|
||||
{/* Icon */}
|
||||
@@ -60,12 +56,13 @@ export default function CertificateNotAvailable({
|
||||
<p className="text-xs text-gray-400 mt-2 text-left">Semua tugas telah diselesaikan! Silahkan tunggu untuk mendapatkan sertifikat</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClaim}
|
||||
disabled={loading}
|
||||
onClick={onClaim}
|
||||
disabled={claiming}
|
||||
className="mt-0 px-6 py-2.5 bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors duration-200"
|
||||
>
|
||||
{loading ? "Memproses..." : "Klaim Sertifikat"}
|
||||
{claiming ? "Memproses..." : "Klaim Sertifikat"}
|
||||
</button>
|
||||
{claimError && <p className="text-red-500 text-sm mt-2">{claimError}</p>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -91,7 +88,6 @@ export default function CertificateNotAvailable({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user