feat: update certificate handling and project management features
- Refactored CertificateHistory component to display dynamic certificate images. - Modified CertificateNotAvailable component text for clarity on certificate publication. - Enhanced CertificateForm to include file preview and clear functionality for uploaded images. - Removed obsolete CertificatePage component to streamline mentor interface. - Updated CertificateResult to handle project details and loading states more effectively. - Improved HomeMentorPage structure by adjusting imports for better organization. - Enhanced EligibleInternTable with navigation to results and improved error handling for certificate generation. - Updated HomeMentorProject styles for consistency with design system. - Added utility function to resolve image URLs for certificates. - Introduced new hooks for generating certificates and managing notifications. - Expanded project service functions to include project completion and assignable interns retrieval. - Updated types for certificates and projects to reflect new data structures. - Added user types for better type safety in user management.
This commit is contained in:
+12
-20
@@ -1,8 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useMyTasks } from '../../../../hooks/useProjects';
|
||||
import { useCurrentUser } from '../../../../hooks/useUser';
|
||||
import { useAllMentorTasks } from '../../../../hooks/useTasks';
|
||||
import { useDeadlines } from '../../hooks/useTasks';
|
||||
|
||||
const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
|
||||
const MONTH_NAMES = [
|
||||
@@ -20,21 +18,7 @@ function getFirstDayOfMonth(year: number, month: number) {
|
||||
|
||||
export default function HomeCalendar() {
|
||||
const [today, setToday] = useState(new Date());
|
||||
const { user } = useCurrentUser();
|
||||
|
||||
const isIntern = user?.role === "intern";
|
||||
const { tasks: internTasks } = useMyTasks();
|
||||
const { tasks: mentorTasks } = useAllMentorTasks(!isIntern && user !== null);
|
||||
|
||||
const DEADLINES = isIntern
|
||||
? internTasks.map(task => ({
|
||||
date: new Date(task.deadline_at),
|
||||
label: task.title,
|
||||
}))
|
||||
: mentorTasks.map(task => ({
|
||||
date: new Date(task.deadline_at),
|
||||
label: `[${task.project_name}] ${task.title}`,
|
||||
}));
|
||||
const { deadlines, loading } = useDeadlines();
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
@@ -74,13 +58,13 @@ export default function HomeCalendar() {
|
||||
currentYear === today.getFullYear()
|
||||
|
||||
const hasDeadline = (day: number) =>
|
||||
DEADLINES.some(d =>
|
||||
deadlines.some(d =>
|
||||
d.date.getDate() === day &&
|
||||
d.date.getMonth() === currentMonth &&
|
||||
d.date.getFullYear() === currentYear
|
||||
)
|
||||
|
||||
const upcomingDeadlines = DEADLINES.filter(d => {
|
||||
const upcomingDeadlines = deadlines.filter(d => {
|
||||
return (
|
||||
d.date.getMonth() === currentMonth &&
|
||||
d.date.getFullYear() === currentYear
|
||||
@@ -104,6 +88,14 @@ export default function HomeCalendar() {
|
||||
return d === 0 || d === 6 // Sun or Sat
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="px-13 py-5 border border-card-outline rounded-2xl">
|
||||
<p className="text-[11px] text-font-shade">Loading calendar...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className=" px-13 py-5 border border-card-outline rounded-2xl">
|
||||
{/* Header */}
|
||||
@@ -1,6 +1,6 @@
|
||||
import HomeHeader from "./components/HomeHeader";
|
||||
import HomeProjectList from "./components/HomeProjectList";
|
||||
import HomeCalendar from "./components/HomeCalendar";
|
||||
import HomeCalendar from "../HomeCalendar";
|
||||
import TaskTimeline from "./components/TaskTimeline";
|
||||
|
||||
export default function HomeInternPage() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Download, Link, Printer } from "lucide-react";
|
||||
import type { ProjectDetail } from "../../../../types/project.types";
|
||||
import type { Certificate } from "../../../../types/certificate.types";
|
||||
import { downloadCertificateAsImage, downloadCertificateAsPdf } from "../../../utils/DownloadImagetoDevice";
|
||||
import { resolveImageUrl } from "../../../utils/SertificateGenerator";
|
||||
|
||||
interface CertificateAvailableProps {
|
||||
project: ProjectDetail | null;
|
||||
@@ -62,7 +63,7 @@ export default function CertificateAvailable({ project, certificate }: Certifica
|
||||
{/* 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={certificate.image_path}
|
||||
src={resolveImageUrl(certificate.image_path)}
|
||||
alt="Certificate"
|
||||
className="rounded-xl w-[550px] h-[350px] object-contain"
|
||||
/>
|
||||
@@ -83,7 +84,7 @@ export default function CertificateAvailable({ project, certificate }: Certifica
|
||||
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()}
|
||||
{certificate.certificate_status}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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"; //Placeholder
|
||||
import { useMyCertificates } from "../../../../hooks/useCertificates";
|
||||
|
||||
export default function SertificateHistory() {
|
||||
@@ -43,7 +42,7 @@ export default function SertificateHistory() {
|
||||
>
|
||||
<div className="w-full flex justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
src={item.image_path}
|
||||
alt="Certificate"
|
||||
className="rounded-xl h-[150px] w-full object-cover"
|
||||
/>
|
||||
|
||||
@@ -40,8 +40,8 @@ export default function CertificateNotAvailable({
|
||||
<>
|
||||
<h2 className="text-[37px] font-bold text-gray-900 mb-3">Selamat!</h2>
|
||||
<p className="text-gray-500 text-[17px] mb-6 text-center">
|
||||
Semua tugas proyek telah selesai. Klik tombol di bawah untuk{" "}
|
||||
<span className="font-bold text-green-600">mengklaim sertifikat</span> kelulusan Anda.
|
||||
Semua tugas proyek telah selesai. Silahkan tunggu Mentor untuk{" "}
|
||||
<span className="font-bold text-green-600">Mempublish Sertifikat</span> kelulusan Anda.
|
||||
</p>
|
||||
|
||||
{/* Progress Bar — 100% */}
|
||||
@@ -53,7 +53,6 @@ export default function CertificateNotAvailable({
|
||||
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-green-500 rounded-full transition-all duration-700 w-full" />
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2 text-left">Semua tugas telah diselesaikan! Silahkan tunggu untuk mendapatkan sertifikat</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClaim}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { CalendarDays, UploadCloud, CheckCircle2, XCircle, Loader2 } from "lucid
|
||||
import { useUploadCertificateTemplate } from "../../../hooks/useCertificates";
|
||||
import { useProjectDetail } from "../../../hooks/useProjects";
|
||||
import EligibleInternTable from "./components/EligibleInternTable";
|
||||
import { useProjectInternProgress } from "../../../hooks/useInternProgress"
|
||||
import { useProjectInternProgress } from "../../../hooks/useInternProgress";
|
||||
import { resolveImageUrl } from "../../utils/SertificateGenerator";
|
||||
|
||||
export default function CertificateDetail() {
|
||||
const navigate = useNavigate();
|
||||
@@ -17,12 +18,17 @@ export default function CertificateDetail() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [uploadSuccess, setUploadSuccess] = useState(false);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setSelectedFile(file);
|
||||
setUploadSuccess(false);
|
||||
|
||||
// Revoke object URL lama biar tidak memory leak
|
||||
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(URL.createObjectURL(file));
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
@@ -35,6 +41,16 @@ export default function CertificateDetail() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearFile = () => {
|
||||
setSelectedFile(null);
|
||||
setUploadSuccess(false);
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20 text-gray-400 text-sm">
|
||||
@@ -85,30 +101,48 @@ export default function CertificateDetail() {
|
||||
</label>
|
||||
|
||||
{/* Drop Zone */}
|
||||
<div
|
||||
className="border-2 border-dashed border-card-outline bg-primary-foreground rounded-xl p-6 text-center flex flex-col items-center justify-center cursor-pointer hover:bg-red-50/20 transition-colors group"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mb-2 group-hover:scale-105 transition-transform">
|
||||
<UploadCloud className="w-5 h-5 text-red" />
|
||||
<div className="relative">
|
||||
<div
|
||||
className="border-2 border-dashed border-card-outline bg-primary-foreground rounded-xl p-6 text-center flex flex-col items-center justify-center cursor-pointer hover:bg-red-50/20 transition-colors group overflow-hidden"
|
||||
onClick={() => !selectedFile && fileInputRef.current?.click()}
|
||||
>
|
||||
{previewUrl || project.certificate_template ? (
|
||||
<>
|
||||
<img
|
||||
src={previewUrl || resolveImageUrl(project.certificate_template)}
|
||||
alt="Preview template"
|
||||
className="w-full h-32 object-contain rounded-lg mb-2"
|
||||
/>
|
||||
<p className="text-xs font-bold text-gray-800 truncate max-w-[180px]">
|
||||
{selectedFile?.name}
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
{selectedFile && (selectedFile.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mb-2 group-hover:scale-105 transition-transform">
|
||||
<UploadCloud className="w-5 h-5 text-red" />
|
||||
</div>
|
||||
<p className="text-xs font-bold text-gray-800">Upload Certificate Image</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">PNG, JPG up to 5MB</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<p className="text-xs font-bold text-gray-800 truncate max-w-[180px]">
|
||||
{selectedFile.name}
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-xs font-bold text-gray-800">Upload Certificate Image</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">PNG, JPG up to 5MB</p>
|
||||
</>
|
||||
|
||||
{/* Tombol Clear */}
|
||||
{selectedFile && (
|
||||
<button
|
||||
onClick={handleClearFile}
|
||||
type="button"
|
||||
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-white border border-gray-200 shadow-sm flex items-center justify-center text-gray-500 hover:text-red-600 hover:border-red-200 transition-colors"
|
||||
title="Hapus file"
|
||||
>
|
||||
<XCircle className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@@ -151,25 +185,25 @@ export default function CertificateDetail() {
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Project Scope ── */}
|
||||
<div className="border border-card-outline rounded-xl p-5">
|
||||
<p className="text-[13px] font-bold text-gray-400 uppercase tracking-widest mb-3">
|
||||
Project Scope
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">{project.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Stats: Total Intern dalam project dan jumlah intern eligible */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="border border-card-outline rounded-xl p-4">
|
||||
<p className="text-2xl font-extrabold text-[#B30000]">{project.total_members}</p>
|
||||
<p className="text-xs text-font font-medium mt-0.5">Total Interns</p>
|
||||
{/* ── Project Scope ── */}
|
||||
<div className="border border-card-outline rounded-xl p-5">
|
||||
<p className="text-[13px] font-bold text-gray-400 uppercase tracking-widest mb-3">
|
||||
Project Scope
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">{project.description}</p>
|
||||
</div>
|
||||
<div className="border border-card-outline rounded-xl p-4">
|
||||
<p className="text-2xl font-extrabold text-gray-900">{eligibleCount}</p>
|
||||
<p className="text-xs text-font font-medium mt-0.5">Eligible</p>
|
||||
|
||||
{/* Stats: Total Intern dalam project dan jumlah intern eligible */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="border border-card-outline rounded-xl p-4">
|
||||
<p className="text-2xl font-extrabold text-[#B30000]">{project.total_members}</p>
|
||||
<p className="text-xs text-font font-medium mt-0.5">Total Interns</p>
|
||||
</div>
|
||||
<div className="border border-card-outline rounded-xl p-4">
|
||||
<p className="text-2xl font-extrabold text-gray-900">{eligibleCount}</p>
|
||||
<p className="text-xs text-font font-medium mt-0.5">Eligible</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
PlusCircle,
|
||||
UploadCloud,
|
||||
Eye,
|
||||
Download,
|
||||
Trash2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
DownloadCloud
|
||||
} from 'lucide-react';
|
||||
import { LuHistory, LuSendHorizontal } from 'react-icons/lu';
|
||||
|
||||
const initialHistoryData = [
|
||||
{ id: 1, name: "Jonathan Kristina", email: "JonathanKristina@gmail.com", project: "Internify Project", date: "24 Oct 2024", initials: "JK" },
|
||||
{ id: 2, name: "Ahmad Faisal", email: "ahmad.faisal@gmail.com", project: "Web Dev Fundamentals", date: "15 Sep 2024", initials: "AF" },
|
||||
{ id: 3, name: "Siti Rahma", email: "siti.rahma@gmail.com", project: "UI/UX Essentials", date: "10 Aug 2024", initials: "SR" },
|
||||
{ id: 4, name: "David Beckham", email: "david.b@gmail.com", project: "Engineering Intern 101", date: "05 Aug 2024", initials: "DB" },
|
||||
];
|
||||
|
||||
const MentorCertificatePage = () => {
|
||||
const [selectedIntern, setSelectedIntern] = useState("JonathanKristina@gmail.com");
|
||||
const [selectedProject, setSelectedProject] = useState("Internify Project (Web Development)");
|
||||
const [issueDate, setIssueDate] = useState("2024-10-24");
|
||||
const [expiryDate, setExpiryDate] = useState("");
|
||||
|
||||
// State Data Tabel
|
||||
const [historyData, setHistoryData] = useState(initialHistoryData);
|
||||
|
||||
const handleIssueCertificate = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const payload = {
|
||||
intern: selectedIntern,
|
||||
project: selectedProject,
|
||||
issueDate,
|
||||
expiryDate
|
||||
};
|
||||
console.log("Issuing Certificate:", payload);
|
||||
alert("Certificate Issued Successfully!");
|
||||
};
|
||||
|
||||
const handleDeleteHistory = (id: number) => {
|
||||
if(confirm("Are you sure you want to delete this certificate log?")) {
|
||||
setHistoryData(historyData.filter(item => item.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 tracking-tight">Certificates</h1>
|
||||
<p className="text-sm md:text-base text-gray-500 mt-1">
|
||||
Administrate, issue, and track professional certifications for all interns.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
|
||||
<div className="bg-white border border-gray-100 rounded-2xl p-6 md:p-8 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<PlusCircle className="w-6 h-6 text-[#B30000]" />
|
||||
<h2 className="text-xl font-bold text-gray-900 tracking-tight">Issue New Certificate</h2>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleIssueCertificate} className="space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Select Intern</label>
|
||||
<select
|
||||
value={selectedIntern}
|
||||
onChange={(e) => setSelectedIntern(e.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||
>
|
||||
<option value="JonathanKristina@gmail.com">JonathanKristina@gmail.com</option>
|
||||
<option value="ahmad.faisal@gmail.com">ahmad.faisal@gmail.com</option>
|
||||
<option value="siti.rahma@gmail.com">siti.rahma@gmail.com</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Select Project / Course</label>
|
||||
<select
|
||||
value={selectedProject}
|
||||
onChange={(e) => setSelectedProject(e.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||
>
|
||||
<option value="Internify Project (Web Development)">Internify Project (Web Development)</option>
|
||||
<option value="Web Dev Fundamentals">Web Dev Fundamentals</option>
|
||||
<option value="UI/UX Essentials">UI/UX Essentials</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Certificate Image</label>
|
||||
<div className="border-2 border-dashed border-red-200/60 bg-red-50/10 rounded-xl p-6 text-center flex flex-col items-center justify-center cursor-pointer hover:bg-red-50/20 transition-colors group">
|
||||
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mb-2 group-hover:scale-105 transition-transform">
|
||||
<UploadCloud className="w-5 h-5 text-[#B30000]" />
|
||||
</div>
|
||||
<p className="text-xs font-bold text-gray-800">Upload Certificate Image</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">PNG, JPG up to 5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Issue Date</label>
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
value={issueDate}
|
||||
onChange={(e) => setIssueDate(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-500 tracking-wide">Expiry (Optional)</label>
|
||||
<input
|
||||
type="date"
|
||||
value={expiryDate}
|
||||
onChange={(e) => setExpiryDate(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 mt-4 bg-[#B30000] hover:bg-[#990000] text-white text-sm font-bold rounded-xl transition-colors shadow-sm flex items-center justify-center gap-2"
|
||||
>
|
||||
<LuSendHorizontal className="w-4 h-4" />
|
||||
Issue Certificate
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Kanan: Issued Certificates History (8 Kolom) */}
|
||||
<div className="bg-white border border-gray-100 rounded-2xl shadow-sm overflow-hidden flex flex-col justify-between h-full">
|
||||
<div>
|
||||
{/* Header Table */}
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-gray-50">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<LuHistory className="w-5 h-5 text-red-600 stroke-[2.5]" />
|
||||
<h2 className="text-lg font-bold text-gray-900 tracking-tight">Issued Certificates History</h2>
|
||||
</div>
|
||||
<button className="text-xs font-bold text-[#B30000] hover:text-[#990000] flex items-center gap-1.5 bg-red-50/50 px-3 py-1.5 rounded-lg border border-red-100 transition-colors">
|
||||
<span>Export CSV</span>
|
||||
<DownloadCloud className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Responsive Table Wrapper */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50/70 border-b border-gray-100">
|
||||
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Intern Name</th>
|
||||
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Project</th>
|
||||
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Issue Date</th>
|
||||
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{historyData.map((row) => (
|
||||
<tr key={row.id} className="hover:bg-gray-50/40 transition-colors">
|
||||
{/* Kolom Nama Intern + Avatar Ringkas */}
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 border border-gray-200/60 flex items-center justify-center text-xs font-bold text-gray-500 flex-shrink-0">
|
||||
{row.initials}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-gray-900 leading-tight">{row.name}</p>
|
||||
<p className="text-[11px] text-gray-400 mt-0.5">{row.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* Kolom Nama Proyek */}
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-sm text-gray-600 font-medium">{row.project}</p>
|
||||
</td>
|
||||
{/* Kolom Tanggal Terbit */}
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-sm text-gray-500 font-medium">{row.date}</p>
|
||||
</td>
|
||||
{/* Kolom Deretan Aksi */}
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button className="p-1.5 text-gray-400 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors" title="View Certificate">
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors" title="Download">
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteHistory(row.id)}
|
||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
||||
title="Delete Log"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bagian Footer Tabel & Paginasi */}
|
||||
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50/30 flex flex-col sm:flex-row gap-3 items-center justify-between text-xs font-medium text-gray-500">
|
||||
<p>Showing 1 to {historyData.length} of 1,284 entries</p>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<button className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50 disabled:opacity-50">
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-[#B30000] text-white font-bold shadow-sm">1</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-white border border-gray-200 text-gray-600 hover:bg-gray-50">2</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-white border border-gray-200 text-gray-600 hover:bg-gray-50">3</button>
|
||||
<button className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MentorCertificatePage;
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { useProjectCertificates } from "../../../hooks/useCertificates";
|
||||
import { useProjectCertificates } from "../../../hooks/useCertificates"; //Problemnya cuma role "admin" yang bisa
|
||||
import { useProjectDetail } from "../../../hooks/useProjects";
|
||||
import type { Certificate } from "../../../types/certificate.types";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
@@ -97,10 +98,17 @@ function CertificateResultSkeleton() {
|
||||
|
||||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||||
export default function CertificateResult() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const {certificates, loading, error} = useProjectCertificates(Number(id));
|
||||
|
||||
const { project, loading: projectLoading, error: projectError } = useProjectDetail(slug ?? "");
|
||||
const { certificates, loading: certLoading, error: certError } = useProjectCertificates(
|
||||
project?.id ?? 0
|
||||
);
|
||||
|
||||
const loading = projectLoading || certLoading;
|
||||
const error = projectError || certError;
|
||||
|
||||
if (loading) return <CertificateResultSkeleton />;
|
||||
if (error) return <div className="text-sm text-red-500 p-4">Gagal memuat data sertifikat.</div>;
|
||||
@@ -262,7 +270,7 @@ export default function CertificateResult() {
|
||||
</div>
|
||||
|
||||
{/* Preview Card (placeholder — gambar belum diimplementasi) */}
|
||||
<div className="xl:w-64 w-full flex-shrink-0 rounded-2xl overflow-hidden bg-gradient-to-br from-gray-700 via-[#7a1010] to-[#B30000] shadow-md min-h-[200px] flex flex-col justify-end p-5">
|
||||
{/* <div className="xl:w-64 w-full flex-shrink-0 rounded-2xl overflow-hidden bg-gradient-to-br from-gray-700 via-[#7a1010] to-[#B30000] shadow-md min-h-[200px] flex flex-col justify-end p-5">
|
||||
<div className="mt-auto">
|
||||
<p className="text-white font-extrabold text-base leading-snug">
|
||||
Institutional Excellence
|
||||
@@ -271,7 +279,7 @@ export default function CertificateResult() {
|
||||
Certified via Mentor Portal v2.4
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import HomeHeader from '../intern/components/HomeHeader'
|
||||
import HomeCalendar from '../intern/components/HomeCalendar'
|
||||
import HomeCalendar from '../HomeCalendar'
|
||||
import HomeMentorProject from './components/HomeMentorProject'
|
||||
|
||||
const HomeMentorPage = () => {
|
||||
|
||||
@@ -78,7 +78,8 @@ export default function EligibleInternTable({
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!project?.template_sertificate) {
|
||||
// NOTE: DI RESPONSE GAK DIKASIH TAHU DIMANA SIMPAN TEMPLATE SERTIF NYA
|
||||
if (!project?.certificate_template) {
|
||||
setGenerateError("Template sertifikat belum diupload.");
|
||||
return;
|
||||
}
|
||||
@@ -98,7 +99,7 @@ export default function EligibleInternTable({
|
||||
await Promise.all(
|
||||
selectedInterns.map(async (intern) => {
|
||||
const blob = await generateCertificate(
|
||||
project.template_sertificate,
|
||||
project.certificate_template,
|
||||
intern.full_name
|
||||
);
|
||||
// Nama file: "Sertifikat - Nama Intern.png"
|
||||
@@ -286,7 +287,12 @@ export default function EligibleInternTable({
|
||||
)}
|
||||
|
||||
{/* Bottom Action Bar */}
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="text-gray-700 hover:underline"
|
||||
onClick={()=> navigate(`/mentor/certificates/${project?.slug}/result`)}>
|
||||
Ke Hasil
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating || selected.size === 0}
|
||||
@@ -304,6 +310,7 @@ export default function EligibleInternTable({
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { mockProjects } from '../../../../lib/mockData'
|
||||
import { useState } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import CreateProjectModal from './CreateProjectDialog';
|
||||
@@ -20,9 +19,9 @@ const HomeMentorProject = () => {
|
||||
<div className='flex flex-row justify-between mb-3 px-5 items-center'>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Projects</h2>
|
||||
<button
|
||||
className='bg-gray-300 p-1.5 rounded-lg font-bold hover:bg-gray-400'
|
||||
className='bg-card p-1.5 rounded-lg font-bold hover:bg-card-hover border border-card-outline'
|
||||
onClick={openDialog} >
|
||||
<span className='text-sm flex flex-row justify-between items-center gap-3.5'>
|
||||
<span className='text-sm flex flex-row px-1 justify-between items-center gap-3.5'>
|
||||
<Plus className='w-4 h-4' />
|
||||
Add
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user