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
@@ -9,7 +9,6 @@ import {
ChevronLeft,
ChevronRight,
Sparkles,
ArrowLeft,
} from "lucide-react";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -67,7 +66,6 @@ function Avatar({ initials }: { initials: string }) {
);
}
// ─── Progress Bar ─────────────────────────────────────────────────────────────
function ProgressBar({ value, total }: { value: number; total: number }) {
const pct = Math.round((value / total) * 100);
const isComplete = value === total;
@@ -86,7 +84,6 @@ function ProgressBar({ value, total }: { value: number; total: number }) {
);
}
// ─── Status Badge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: InternStatus }) {
const isEligible = status === "ELIGIBLE";
return (
@@ -99,7 +96,6 @@ function StatusBadge({ status }: { status: InternStatus }) {
);
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function CertificateDetail() {
const navigate = useNavigate();
@@ -135,26 +131,14 @@ export default function CertificateDetail() {
return (
<div>
{/* Breadcrumb */}
<div
className="mb-1 text-xs font-medium text-[#B30000] cursor-pointer w-fit"
onClick={() => navigate("/mentor/certificates/all")}
>
Certificates &rsaquo;
</div>
{/* Page Header */}
<div className="mb-6">
<h1 className="page-title">Certificates</h1>
<p className="page-title-desc">
Administrate, issue, and track professional certifications for all interns.
</p>
</div>
{/* ── Two-column layout ── */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* ── LEFT: Project Info ── */}
<div className="space-y-5">
{/* Status badge */}
<div className="flex items-center gap-1.5 text-xs font-extrabold text-green-600 tracking-wide">
@@ -162,18 +146,15 @@ export default function CertificateDetail() {
COMPLETED
</div>
{/* Title */}
<h2 className="text-3xl font-extrabold text-gray-900 leading-tight">
{mockProject.title}
</h2>
{/* Period */}
<div className="flex items-center gap-2 text-sm text-gray-400 font-medium">
<CalendarDays className="w-4 h-4 flex-shrink-0" />
{mockProject.period}
</div>
{/* Project Scope */}
<div className="border border-gray-200 rounded-xl p-5">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-3">
Project Scope
@@ -183,7 +164,6 @@ export default function CertificateDetail() {
</p>
</div>
{/* Stats: Total & Eligible */}
<div className="grid grid-cols-2 gap-4">
<div className="border border-gray-200 rounded-xl p-4">
<p className="text-2xl font-extrabold text-[#B30000]">{mockProject.totalInterns}</p>
@@ -196,10 +176,8 @@ export default function CertificateDetail() {
</div>
</div>
{/* ── RIGHT: Intern Roster ── */}
<div className="border border-gray-200 rounded-2xl overflow-hidden bg-white shadow-sm">
{/* Roster Header */}
<div className="flex items-start justify-between px-5 py-4 border-b border-gray-100">
<div>
<h3 className="text-base font-bold text-gray-900">Intern Roster</h3>
@@ -207,7 +185,6 @@ export default function CertificateDetail() {
Confirm completion status before final certificate generation.
</p>
</div>
{/* Select All button */}
<button
onClick={toggleSelectAll}
className="flex items-center gap-2 border border-gray-200 rounded-xl px-4 py-2.5 bg-white hover:bg-gray-50 transition-colors flex-shrink-0 ml-4"
@@ -340,14 +317,7 @@ export default function CertificateDetail() {
</div>
{/* ── Bottom Action Bar ── */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-100">
<button
onClick={() => navigate("/mentor/certificates/all")}
className="flex items-center gap-2 text-sm font-bold text-gray-500 hover:text-gray-700 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Project Selection
</button>
<div className="flex flex-col sm:flex-row items-center justify-end gap-4 mt-6 pt-4">
<button
onClick={() => navigate("/mentor/certificates/result")}
className="flex items-center gap-2 px-8 py-3.5 bg-[#B30000] hover:bg-[#990000] text-white font-bold text-sm rounded-xl shadow-md transition-colors"
@@ -1,6 +1,5 @@
// src/pages/mentor/CertificateResult.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
CheckCircle2,
FolderArchive,
@@ -55,7 +54,6 @@ function Avatar({ initials }: { initials: string }) {
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function CertificateResult() {
const navigate = useNavigate();
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
@@ -75,12 +73,6 @@ export default function CertificateResult() {
return (
<div>
{/* Breadcrumb */}
<div className="mb-1 text-xs font-medium text-[#B30000] cursor-pointer w-fit"
onClick={() => navigate("/mentor/certificates/all")}>
Certificates &rsaquo;
</div>
{/* Page Header */}
<div className="mb-8">
<h1 className="page-title">Certificates</h1>
@@ -1,127 +1,18 @@
// src/pages/mentor/CertificateList.tsx
// src/pages/mentor/CertificateListPage.tsx
import { useNavigate } from "react-router-dom";
import {CalendarDays} from "lucide-react";
// ─── Types ───────────────────────────────────────────────────────────────────
type CertStatus = "COMPLETED" | "PENDING CERTS";
interface Project {
id: number;
title: string;
period: string;
internCount: number;
status: CertStatus;
}
// ─── Mock Data ────────────────────────────────────────────────────────────────
const mockProjects: Project[] = [
{
id: 1,
title: "Autonomous Navigation Systems",
period: "Jan 2024 - Apr 2024",
internCount: 14,
status: "COMPLETED",
},
{
id: 2,
title: "Full-Stack Fintech Solution",
period: "Feb 2024 - June 2024",
internCount: 8,
status: "PENDING CERTS",
},
{
id: 3,
title: "User Experience Audit 2024",
period: "Mar 2024 - May 2024",
internCount: 5,
status: "PENDING CERTS",
},
{
id: 4,
title: "Edge Computing Framework",
period: "Dec 2023 - Mar 2024",
internCount: 21,
status: "COMPLETED",
},
];
// ─── Sub Components ───────────────────────────────────────────────────────────
/** Badge status: COMPLETED (hijau) vs PENDING CERTS (kuning) */
function StatusBadge({ status }: { status: CertStatus }) {
const isCompleted = status === "COMPLETED";
return (
<span
className={`text-[10px] font-extrabold tracking-wider px-2.5 py-1 rounded-full border ${
isCompleted
? "bg-green-50 text-green-600 border-green-200"
: "bg-yellow-50 text-yellow-600 border-yellow-200"
}`}
>
{status}
</span>
);
}
/** Satu card project */
function ProjectCard({
project,
onClick,
}: {
project: Project;
onClick: () => void;
}) {
return (
<div
onClick={onClick}
className="bg-white border border-gray-200 rounded-2xl p-5 cursor-pointer hover:shadow-md hover:border-gray-300 transition-all duration-200 flex flex-col gap-4"
>
{/* Top: icon + status */}
<div className="flex items-start justify-between">
<StatusBadge status={project.status} />
</div>
{/* Title + period */}
<div>
<h3 className="text-sm font-bold text-gray-900 leading-snug mb-1.5">
{project.title}
</h3>
<div className="flex items-center gap-1.5 text-[11px] text-gray-400 font-medium">
<CalendarDays className="w-3.5 h-3.5 flex-shrink-0" />
<span>{project.period}</span>
</div>
</div>
<hr className="border-gray-100" />
{/* Intern count + avatars */}
<div className="flex items-center justify-between">
<div>
<p className="text-[10px] text-gray-400 font-medium mb-0.5">Interns</p>
<p className="text-sm font-extrabold text-[#B30000]">
{project.internCount} {project.internCount === 1 ? "intern" : "Interns"}
</p>
</div>
</div>
</div>
);
}
import CertificateProjectCard from "./components/CertificateProjectCard";
import { useProjects } from "../../../hooks/useProjects";
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function CertificateList() {
const navigate = useNavigate();
const { projects, loading, error } = useProjects();
const handleCardClick = (id: number) => {
navigate(`/mentor/certificates/${id}`);
const handleCardClick = (slug: string) => {
navigate(`/mentor/certificates/${slug}`);
};
return (
<div>
{/* Breadcrumb */}
<div className="mb-1 text-xs font-medium text-[#B30000]">
Certificates &rsaquo;
</div>
{/* Page Header */}
<div className="mb-6">
<h1 className="page-title">Certificates</h1>
@@ -135,11 +26,11 @@ export default function CertificateList() {
<h2 className="text-base font-bold text-gray-900 mb-4">List Projects</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
{mockProjects.map((project) => (
<ProjectCard
{projects.map((project) => (
<CertificateProjectCard
key={project.id}
project={project}
onClick={() => handleCardClick(project.id)}
onClick={() => handleCardClick(project.slug)}
/>
))}
</div>
+62 -25
View File
@@ -1,32 +1,55 @@
import { useState, useMemo } from "react";
import { Search, SlidersHorizontal, Plus, Trash2 } from "lucide-react";
import { mockInternsData } from "../../../lib/mockData";
import { useAllInterns, useRemoveMember, useProjects } from "../../../hooks/useProjects";
import AddInternsModal from "./components/AddInternsToProjects";
export default function InternsManagement() {
// State Filter & Search
const [searchEmail, setSearchEmail] = useState("");
const [selectedProject, setSelectedProject] = useState("All Projects");
const [isModalOpen, setIsModalOpen] = useState(false);
// Handler Aksi Kursor/Tombol
const handleAddAssignment = (id: number) => {
alert(`Assign/Add action clicked for intern ID: ${id}`);
};
const handleDeleteIntern = (id: number) => {
alert(`Deleted intern ID: ${id}`);
};
const { interns, loading, error, refetch } = useAllInterns();
const { remove, loading: removing } = useRemoveMember();
const { projects: activeProjects } = useProjects("active");
// Logic Filtering Data menggunakan useMemo
// Filter data berdasarkan email dan nama project
const filteredInterns = useMemo(() => {
return mockInternsData.filter((intern) => {
return interns.filter((intern) => {
const matchesEmail = intern.email.toLowerCase().includes(searchEmail.toLowerCase());
const matchesProject =
selectedProject === "All Projects" ||
intern.projectName.toLowerCase() === selectedProject.toLowerCase();
(intern.projectName || "Unassigned").toLowerCase() === selectedProject.toLowerCase();
return matchesEmail && matchesProject;
});
}, [searchEmail, selectedProject]);
}, [interns, searchEmail, selectedProject]);
// Hitung statistik dari data asli
const totalInterns = interns.length;
// Active assignments = total member dari semua project yang statusnya active
const activeAssignments = activeProjects.length;
// Handler hapus intern dari project
// Catatan: useRemoveMember memerlukan id_project & id_user.
// Karena InternDetail tidak menyimpan id_project, aksi ini saat ini hanya placeholder.
// Untuk implementasi penuh, perlu endpoint khusus atau data id_project dari InternDetail.
const handleDeleteIntern = async (internId: number) => {
const confirmed = window.confirm("Yakin ingin menghapus intern ini dari project?");
if (!confirmed) return;
alert(`Remove intern ID ${internId} — perlu id_project dari backend untuk dilanjutkan.`);
// Contoh penggunaan jika id_project tersedia:
// const res = await remove({ id_project: intern.id_project, id_user: internId });
// if (res !== null) refetch();
};
if (loading) return (
<div className="flex flex-col gap-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-gray-100 rounded-lg animate-pulse" />
))}
</div>
);
if (error) return <p className="text-red-500 text-sm">{error}</p>;
return (
<div className="">
@@ -64,8 +87,6 @@ export default function InternsManagement() {
className="w-full pl-3 pr-8 py-2 bg-gray-100 border border-transparent rounded-lg text-sm text-gray-700 font-medium appearance-none focus:outline-none focus:bg-white focus:border-gray-200"
>
<option value="All Projects">All Projects</option>
<option value="Internify Project">Internify Project</option>
<option value="MBG Project">MBG Project</option>
<option value="Unassigned">Unassigned</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500">
@@ -81,17 +102,21 @@ export default function InternsManagement() {
</button>
</div>
{/* Right Info Box Stats */}
{/* Right Info Box Stats — data real dari API */}
<div className="bg-white border border-gray-100 rounded-xl px-6 py-2.5 flex items-center justify-around text-center shadow-[0_2px_10px_rgba(0,0,0,0.01)] w-full lg:max-w-sm ml-auto">
<div>
<span className="block text-xl font-bold text-red-600 leading-none">124</span>
<span className="block text-xl font-bold text-red-600 leading-none">
{totalInterns}
</span>
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase mt-1 block">
TOTAL INTERNS
</span>
</div>
<div className="h-8 w-px bg-gray-100 mx-2"></div>
<div>
<span className="block text-xl font-bold text-green-600 leading-none">92</span>
<span className="block text-xl font-bold text-green-600 leading-none">
{activeAssignments}
</span>
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase mt-1 block">
ACTIVE ASSIGNMENTS
</span>
@@ -100,7 +125,10 @@ export default function InternsManagement() {
</div>
<div className="flex justify-end">
<button className="inline-flex items-center gap-5 px-3 py-1 bg-red-700 text-white rounded-xl shadow-sm hover:bg-red-800 transition-colors focus:outline-none focus:ring-2 focus:ring-red-300" onClick={() => setIsModalOpen(true)}>
<button
className="inline-flex items-center gap-2 px-3 py-1 bg-red-700 text-white rounded-xl shadow-sm hover:bg-red-800 transition-colors focus:outline-none focus:ring-2 focus:ring-red-300"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-4 h-4" />
Add
</button>
@@ -120,7 +148,7 @@ export default function InternsManagement() {
<div className="divide-y divide-gray-100">
{filteredInterns.length > 0 ? (
filteredInterns.map((intern, index) => {
const isUnassigned = intern.projectName === "Unassigned";
const isUnassigned = !intern.projectName || intern.projectName === "Unassigned";
return (
<div key={`${intern.id}-${index}`} className="px-6 py-4 flex items-center hover:bg-gray-50/60 transition-colors">
@@ -147,7 +175,7 @@ export default function InternsManagement() {
{/* Column 2: Current Project Title & Role Accent */}
<div className="w-1/3">
<h5 className={`text-sm font-semibold leading-tight ${isUnassigned ? 'text-gray-500 italic' : 'text-gray-900'}`}>
{intern.projectName}
{intern.projectName || "Unassigned"}
</h5>
<p className={`text-xs font-bold mt-0.5 ${isUnassigned ? 'text-red-500' : 'text-red-600'}`}>
{intern.role}
@@ -156,15 +184,20 @@ export default function InternsManagement() {
{/* Column 3: Action Buttons Block Right aligned */}
<div className="w-1/6 flex items-center justify-end gap-3.5 pr-2">
{/* Tombol Add: membuka modal assign member */}
<button
onClick={() => handleAddAssignment(intern.id)}
onClick={() => setIsModalOpen(true)}
className="p-1.5 bg-[#B30000] hover:bg-[#990000] text-white rounded-lg transition-colors shadow-sm"
title="Assign ke project"
>
<Plus className="w-4 h-4 stroke-[3]" />
</button>
{/* Tombol Delete: remove intern dari project */}
<button
onClick={() => handleDeleteIntern(intern.id)}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
disabled={removing}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50"
title="Hapus dari project"
>
<Trash2 className="w-4 h-4 stroke-[2]" />
</button>
@@ -183,7 +216,7 @@ export default function InternsManagement() {
{/* Red Table Pagination Bar Footer */}
<div className="bg-[#B30000] text-white px-6 py-3.5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs font-medium">
<div>
Showing <span className="font-bold">{filteredInterns.length}</span> of 124 interns
Showing <span className="font-bold">{filteredInterns.length}</span> of {totalInterns} interns
</div>
<div className="flex items-center gap-2 self-end sm:self-auto">
@@ -196,10 +229,14 @@ export default function InternsManagement() {
</div>
</div>
</div>
{isModalOpen &&
<AddInternsModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onClose={() => {
setIsModalOpen(false);
refetch(); // Refresh data setelah modal assign ditutup
}}
/>
}
</div>
@@ -1,6 +1,7 @@
import { useRef, useEffect, useState } from "react";
import { X } from "lucide-react";
import { LuUserPlus, LuMail, LuBriefcase, LuFolderOpen } from "react-icons/lu";
import { LuUserPlus, LuMail, LuFolderOpen } from "react-icons/lu";
import { useAssignMember, useProjects } from "../../../../hooks/useProjects";
interface AddInternsModalProps {
isOpen: boolean;
@@ -12,8 +13,11 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
// Form States
const [email, setEmail] = useState("");
const [position, setPosition] = useState("");
const [projectName, setProjectName] = useState("");
const [selectedProjectId, setSelectedProjectId] = useState<string>("");
// Hooks
const { assign, loading: assigning, error: assignError } = useAssignMember();
const { projects, loading: loadingProjects } = useProjects("active");
useEffect(() => {
const dialog = dialogRef.current;
@@ -39,11 +43,29 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
};
}, [isOpen, onClose]);
const handleSubmit = (e: React.FormEvent) => {
// Reset form saat modal dibuka kembali
useEffect(() => {
if (isOpen) {
setEmail("");
setSelectedProjectId("");
}
}, [isOpen]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const payload = { email, position, projectName };
console.log("Adding Intern:", payload);
alert("Intern Added Successfully!");
// Catatan: useAssignMember memerlukan id_user (number), bukan email.
// Endpoint /project-api/assign-member menerima { id_project: number, id_user: number }.
// Untuk implementasi berbasis email, perlu endpoint tambahan atau lookup user by email.
// Saat ini form ini dikomen karena tidak ada lookup email → id_user di service.
// Contoh penggunaan jika id_user sudah diketahui:
// const res = await assign({ id_project: Number(selectedProjectId), id_user: userId });
// if (res !== null) {
// onClose();
// }
alert(`Assign intern "${email}" ke project ID ${selectedProjectId} — perlu lookup id_user dari email via backend.`);
onClose();
};
@@ -69,7 +91,7 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
{/* Form Body */}
<form onSubmit={handleSubmit} className="bg-gray-100 px-6 py-6 space-y-4 overflow-y-auto flex-1">
{/* Email Address */}
<div className="space-y-1.5">
<label className="text-xs font-bold tracking-wide text-foreground">
@@ -90,29 +112,30 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
</div>
</div>
{/* Position */}
{/* Project Selection */}
<div className="space-y-1.5">
<label className="text-xs font-bold tracking-wide text-foreground">
Position
Project
</label>
<div className="relative">
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-border">
<LuBriefcase className="w-4 h-4 stroke-[2]" />
<LuFolderOpen className="w-4 h-4 stroke-[2]" />
</span>
<select
required
value={position}
onChange={(e) => setPosition(e.target.value)}
className={`w-full pl-10 pr-9 py-2.5 bg-white text-sm font-medium appearance-none cursor-pointer border border-border rounded-radius focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all text-foreground`}
value={selectedProjectId}
onChange={(e) => setSelectedProjectId(e.target.value)}
disabled={loadingProjects}
className="w-full pl-10 pr-9 py-2.5 bg-white text-sm font-medium appearance-none cursor-pointer border border-border rounded-radius focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all text-foreground disabled:opacity-60"
>
<option value="" disabled>Select position</option>
<option value="frontend">Frontend Developer</option>
<option value="backend">Backend Developer</option>
<option value="fullstack">Fullstack Developer</option>
<option value="designer">UI/UX Designer</option>
<option value="mobile">Mobile Developer</option>
<option value="devops">DevOps Engineer</option>
<option value="qa">QA Engineer</option>
<option value="" disabled>
{loadingProjects ? "Memuat projects..." : "Pilih project"}
</option>
{projects.map((project) => (
<option key={project.id} value={project.id}>
{project.project_name}
</option>
))}
</select>
<span className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-border">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
@@ -122,33 +145,15 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
</div>
</div>
{/* Project Name */}
<div className="space-y-1.5">
<label className="text-xs font-bold tracking-wide text-foreground">
Project Name
</label>
<div className="relative">
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-border">
<LuFolderOpen className="w-4 h-4 stroke-[2]" />
</span>
<select
required
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
className={`w-full pl-10 pr-9 py-2.5 bg-white text-sm font-medium appearance-none cursor-pointer border border-border rounded-radius focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all text-foreground`}
>
<option value="" disabled>Select project assignment</option>
<option value="internify">Internify Project</option>
<option value="rekrutmen">Rekrutmen Digital Platform</option>
<option value="ecommerce">E-Commerce Analytics Dashboard</option>
</select>
<span className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-border">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</span>
</div>
</div>
{/* Field Position dikomentari — tidak ada dalam payload assign-member */}
{/* <div className="space-y-1.5">
<label className="text-xs font-bold tracking-wide text-foreground">Position</label>
<select>...</select>
</div> */}
{assignError && (
<p className="text-xs text-red-600 font-medium">{assignError}</p>
)}
<div className="h-px bg-border/30" />
@@ -163,10 +168,11 @@ export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProp
</button>
<button
type="submit"
className="flex-1 py-2.5 text-sm font-bold text-white bg-primary hover:bg-danger rounded-radius shadow-md flex items-center justify-center gap-2 transition-colors"
disabled={assigning}
className="flex-1 py-2.5 text-sm font-bold text-white bg-primary hover:bg-danger rounded-radius shadow-md flex items-center justify-center gap-2 transition-colors disabled:opacity-60"
>
<LuUserPlus className="w-4 h-4 stroke-[2.5]" />
Add Member
{assigning ? "Assigning..." : "Add Member"}
</button>
</div>
@@ -0,0 +1,62 @@
import { CalendarDays } from "lucide-react";
import type { Project } from "../../../../types/project.types";
// ─── StatusBadge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: string }) {
const isCompleted = status === "COMPLETED";
return (
<span
className={`text-[10px] font-extrabold tracking-wider px-2.5 py-1 rounded-full border ${
isCompleted
? "bg-green-50 text-green-600 border-green-200"
: "bg-yellow-50 text-yellow-600 border-yellow-200"
}`}
>
{status}
</span>
);
}
// ─── ProjectCard ──────────────────────────────────────────────────────────────
export default function CertificateProjectCard({
project,
onClick,
}: {
project: Project;
onClick: () => void;
}) {
return (
<div
onClick={onClick}
className="bg-white border border-gray-200 rounded-2xl p-5 cursor-pointer hover:shadow-md hover:border-gray-300 transition-all duration-200 flex flex-col gap-4"
>
{/* Top: status badge */}
<div className="flex items-start justify-between">
<StatusBadge status={project.status} />
</div>
{/* Title + period */}
<div>
<h3 className="text-sm font-bold text-gray-900 leading-snug mb-1.5">
{project.project_name}
</h3>
<div className="flex items-center gap-1.5 text-[11px] text-gray-400 font-medium">
<CalendarDays className="w-3.5 h-3.5 flex-shrink-0" />
<span>{project.start_date} - {project.end_date}</span>
</div>
</div>
<hr className="border-gray-100" />
{/* Intern count */}
<div className="flex items-center justify-between">
<div>
<p className="text-[10px] text-gray-400 font-medium mb-0.5">Interns</p>
<p className="text-sm font-extrabold text-[#B30000]">
{project.total_members} interns
</p>
</div>
</div>
</div>
);
}
@@ -1,6 +1,6 @@
import { useRef, useEffect, useState } from "react";
import { X } from "lucide-react";
import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../..//../lib/ProjectIcons";
import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../../../lib/ProjectIcons";
import { LuSendHorizontal, LuMail } from "react-icons/lu";
import { useCreateProject } from "../../../../hooks/useProjects";