feat: update project task card to use new task type and improve navigation

fix: update project card to reflect actual tasks done from project data

feat: enhance task form components to accept project ID for submissions

refactor: clean up certificate detail page by removing unused elements

chore: streamline certificate generation page by removing unnecessary navigation

refactor: update certificate list page to use dynamic project data

feat: improve interns management page with API integration and better state handling

feat: add modal for assigning interns to projects with project selection

feat: create a new component for displaying project cards in certificate list

fix: correct import paths in create project dialog component

refactor: enhance project hooks to include task filtering and refetch capabilities

fix: update mock certificate data to reflect correct status

feat: extend certificate types to include new fields for better tracking

feat: add task_done field to project types for improved task management
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-28 08:12:56 +07:00
parent 6ec9620f1f
commit ad26a53f21
26 changed files with 587 additions and 347 deletions
@@ -1,10 +1,13 @@
import { useState } from "react";
import { Download, Link, Printer } from "lucide-react";
import { type CertificateData } from "../../../../lib/mockSertificates";
import type { ProjectDetail } from "../../../../types/project.types";
import certificateImg from "../../../../assets/certificate.png";
export default function CertificateAvailable({ data }: { data: CertificateData }) {
const { certificate } = data;
interface CertificateAvailableProps {
project: ProjectDetail | null;
}
export default function CertificateAvailable({ project }: CertificateAvailableProps) {
const [copied, setCopied] = useState(false);
const handleCopyLink = () => {
@@ -13,10 +16,25 @@ export default function CertificateAvailable({ data }: { data: CertificateData }
setTimeout(() => setCopied(false), 2000);
};
// Hitung durasi dari start_date - end_date
const duration = project
? (() => {
const start = new Date(project.start_date);
const end = new Date(project.end_date);
const diffMs = end.getTime() - start.getTime();
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
const months = Math.floor(diffDays / 30);
const days = diffDays % 30;
if (months > 0 && days > 0) return `${months} Bulan ${days} Hari`;
if (months > 0) return `${months} Bulan`;
return `${diffDays} Hari`;
})()
: "-";
return (
<div className="flex flex-col gap-4">
{/* Certificate Card */}
<div className="flex rounded-xl p-5 mb-1 border border-gray-300 shadow-md items-center justify-center">
<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"
@@ -32,13 +50,14 @@ export default function CertificateAvailable({ data }: { data: CertificateData }
</div>
<div className="space-y-3">
{[
{ label: "Nama Program", value: certificate.programName },
{ label: "Durasi", value: certificate.duration },
{ 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">
VERIFIED
UNVERIFIED
</span>
),
},
@@ -1,38 +1,69 @@
import { useNavigate } from "react-router-dom";
import { History } from "lucide-react";
import { type CertificateHistory, history as certificateHistory } from "../../../../lib/mockSertificates";
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";
export default function SertificateHistory() {
const navigate = useNavigate();
const { certificates, loading, error } = useMyCertificates();
const displayed = certificates.slice(0, 2);
return (
<div className="bg-white rounded-2xl border border-gray-200 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">
<History className="w-5 h-5 text-red-700" />
<h3 className="font-semibold text-gray-800 text-lg">Certificate History</h3>
</div>
<div className="space-y-3">
{certificateHistory.slice(0, 2).map((item: CertificateHistory) => (
<div
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"
>
<div className="w-full flex justify-center">
<img
src={certificateImg}
alt="Certificate"
className="rounded-xl h-[150px] w-[150px] w-full object-cover"
/>
{loading && (
<div className="flex items-center justify-center py-8 text-gray-400 gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span className="text-sm">Loading certificates...</span>
</div>
)}
{error && !loading && (
<div className="flex items-center gap-2 py-6 text-red-500">
<AlertCircle className="w-4 h-4" />
<span className="text-sm">{error}</span>
</div>
)}
{!loading && !error && displayed.length === 0 && (
<p className="text-sm text-gray-400 py-6 text-center">No certificates yet.</p>
)}
{!loading && !error && displayed.length > 0 && (
<div className="space-y-3">
{displayed.map((item: Certificate) => (
<div
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"
>
<div className="w-full flex justify-center">
<img
src={certificateImg}
alt="Certificate"
className="rounded-xl h-[150px] w-full object-cover"
/>
</div>
<div className="w-full">
<p className="text-xs text-gray-400">
{new Date(item.issued_at).toLocaleDateString("id-ID", {
year: "numeric",
month: "long",
})}
</p>
<p className="text-sm font-semibold text-gray-800 leading-tight">
{item.project.project_name}
</p>
<p className="text-sm text-gray-500">No: {item.certificate_no}</p>
</div>
</div>
<div className="w-full">
<p className="text-xs text-gray-400">{item.month}</p>
<p className="text-sm font-semibold text-gray-800 leading-tight">{item.title}</p>
<p className="text-sm text-gray-500">Grade: {item.grade}</p>
</div>
</div>
))}
</div>
))}
</div>
)}
<div className="w-full my-3 flex justify-end">
<button
@@ -1,43 +1,104 @@
import { Lock, Clock } from "lucide-react";
import { Lock, Award, Loader2 } from "lucide-react";
export default function CertificateNotAvailable({ progress, remainingTasks }: { progress: number; remainingTasks: number }) {
interface CertificateNotAvailableProps {
progress: number;
remainingTasks: number;
allTasksDone?: boolean;
onClaim?: () => void;
claiming?: boolean;
}
export default function CertificateNotAvailable({
progress,
remainingTasks,
allTasksDone = false,
onClaim,
claiming = false,
}: CertificateNotAvailableProps) {
return (
<div className="bg-white w-full h-full rounded-2xl border border-gray-100 shadow-sm px-24 flex flex-col items-center justify-center">
<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 */}
<div className="relative mb-6">
<div className=" w-45 h-45 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
<div className="w-30 h-30 flex items-center rounded-lg justify-center bg-gray-300">
<Lock className="w-16 h-16 text-red-700" />
</div>
<div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
<Clock className="w-5 h-5 text-yellow-800" />
<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">
{allTasksDone ? (
<Award className="w-12 h-12 text-green-600" />
) : (
<Lock className="w-12 h-12 text-red-700" />
)}
</div>
{/* <div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
<Clock className="w-4 h-4 text-yellow-800" />
</div> */}
</div>
</div>
<h2 className="text-[37px] font-bold text-gray-900 mb-3">Sertifikat Belum Tersedia</h2>
<p className="text-gray-500 text-[17px] mb-6">
Anda harus menyelesaikan <span className="font-bold text-red-600">semua tugas proyek</span> sebelum dapat mengunduh sertifikat kelulusan.
Silakan periksa kembali progres tugas Anda di menu Proyek.
</p>
{allTasksDone ? (
<>
<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.
</p>
{/* Progress Bar */}
<div className="w-1/3">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
<span className="text-sm font-bold text-red-700">{progress}%</span>
</div>
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-red-700 rounded-full transition-all duration-700"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-2 text-left">
Selesaikan {remainingTasks} tugas lagi untuk membuka akses.
</p>
</div>
{/* Progress Bar — 100% */}
<div className="w-1/3 mb-6">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
<span className="text-sm font-bold text-green-600">100%</span>
</div>
<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!</p>
</div>
{/* Tombol Claim */}
<button
onClick={onClaim}
disabled={claiming}
className="flex items-center justify-center gap-2 px-8 py-3 bg-red-700 hover:bg-red-800 disabled:opacity-60 active:scale-95 transition-all text-white font-semibold rounded-xl text-sm"
>
{claiming ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Mengklaim...
</>
) : (
<>
<Award className="w-4 h-4" />
Claim Certificate
</>
)}
</button>
</>
) : (
<>
<h2 className="text-[28px] font-bold text-gray-900 mb-3">Sertifikat Belum Tersedia</h2>
<p className="text-gray-500 text-[15px] mb-6 text-center">
Anda harus menyelesaikan <span className="font-bold text-red-600">semua tugas proyek</span> sebelum dapat mengunduh sertifikat kelulusan.
Silakan periksa kembali progres tugas Anda di menu Proyek.
</p>
{/* Progress Bar */}
<div className="w-1/3">
<p className="text-xs text-font mt-2">
Selesaikan {remainingTasks} tugas lagi untuk membuka akses.
</p>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
<span className="text-sm font-bold text-red-700">{progress}%</span>
</div>
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-red-700 rounded-full transition-all duration-700"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import { useNavigate, useParams } from "react-router-dom";
import type { ProjectTask } from "../../../../types/task.types";
import { useNavigate } from "react-router-dom";
import type { InternTaskItem } from "../../../../types/project.types";
const formatDate = (dateStr: string) =>
new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
@@ -7,37 +7,36 @@ const formatDate = (dateStr: string) =>
const formatTime = (dateStr: string) =>
new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
export default function TaskCard({ task }: { task: ProjectTask }) {
export default function TaskCard({ task }: { task: InternTaskItem }) {
const navigate = useNavigate();
const { slug } = useParams<{ slug: string }>();
return (
<div
className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3"
onClick={() => navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`)}
className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3 cursor-pointer hover:bg-gray-50 transition-colors"
onClick={() => navigate(`/intern/projects/${task.project_slug}/${task.slug}?type=${task.submission_type}`)}
>
<div className="flex-1 min-w-0">
<div className=" text-[11px] text-font mb-1">
<div className="text-[11px] text-font mb-1">
Deadline : {formatDate(task.deadline_at)} at {formatTime(task.deadline_at)}
</div>
<div className=" font-bold text-[15px] text-[#1a1a1a] mb-1">
<div className="font-bold text-[15px] text-[#1a1a1a] mb-1">
{task.title}
</div>
<div className=" text-[13px] text-font">
<div className="text-[13px] text-font">
{task.description}
</div>
<div className=" text-[13px] text-font">
<div className="text-[13px] text-font mt-0.5">
Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"}
</div>
</div>
<div className="block md:hidden h-[10px] bg-gray-600"/>
<div className="shrink-0 hidden md:block">
<button
className="bg-white text-font-shade border-[1.5px] border-card-outline font-semibold text-xs px-3.5 py-[5px] rounded-md cursor-pointer whitespace-nowrap transition-colors duration-150"
<div className="shrink-0 hidden md:flex items-center">
<span
className={`font-semibold text-xs px-3.5 py-[5px] rounded-md whitespace-nowrap bg-gray-50`}
>
Add Submission
</button>
{task.submission_status}
</span>
</div>
</div>
);
@@ -14,7 +14,7 @@ const ProjectCard = (project: Project) => {
const Icon = getDynamicIcon(project.project_icon);
const tasksDone = 1;
const tasksDone = project.task_done;
const tasksTotal = project.total_tasks;
return (
@@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types";
interface TaskFormFileProps {
taskId: string;
projectId?: string;
deadline?: Date;
initialSubmission?: TaskSubmissionData | null;
}
export default function TaskFormFile({ taskId, deadline, initialSubmission }: TaskFormFileProps) {
const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission);
export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission }: TaskFormFileProps) {
const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission, projectId);
const [files, setFiles] = useState<File[]>([]);
const [isDragActive, setIsDragActive] = useState(false);
const [isEditing, setIsEditing] = useState(false);
@@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types";
interface TaskFormLinkProps {
taskId: string;
projectId?: string;
deadline?: Date;
initialSubmission?: TaskSubmissionData | null;
}
export default function TaskFormLink({ taskId, deadline, initialSubmission }: TaskFormLinkProps) {
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission);
export default function TaskFormLink({ taskId, projectId, deadline, initialSubmission }: TaskFormLinkProps) {
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission, projectId);
const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? "");
const [isEditing, setIsEditing] = useState(false);