update Task Submission Page: Submission, aftersubmission, etc

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-23 12:43:25 +07:00
parent dc64fadfdd
commit 9e5a97ef10
20 changed files with 1485 additions and 280 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ const breadcrumbRoutes = [
{ path: '/mentor/projects', breadcrumb: 'List Projects' },
{ path: '/mentor/projects/:slug', breadcrumb: ProjectNameBreadcrumb },
{ path: '/mentor/projects/:slug/:taskSlug', breadcrumb: TaskNameBreadcrumb },
{ path: '/mentor/certificates', breadcrumb: 'Approve Certificates' },
{ path: '/mentor/certificates', breadcrumb: 'Certificates' },
{ path: '/mentor/intern', breadcrumb: 'List Intern' },
{ path: '/mentor/faq', breadcrumb: 'FAQ' },
{ path: '/mentor/notifications', breadcrumb: 'Notifications' },
+18 -22
View File
@@ -8,12 +8,12 @@ const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
export default function TaskSubmission() {
const [searchParams] = useSearchParams();
const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>();
const { slug, TaskSlug } = useParams<{ slug: string; TaskSlug: string }>();
const submissionType = (searchParams.get("type") ?? "file") as "file" | "link";
const project = mockProjects.find((p) => toSlug(p.name) === slug);
const task = project?.tasks.find((t) => toSlug(t.title) === taskSlug);
const task = project?.tasks.find((t) => toSlug(t.title) === TaskSlug);
const handleFileSubmit = (files: File[]) => {
const fileNames = files.map(f => f.name).join(', ');
console.log(`Submitting Files for Project ${slug}:`, fileNames);
@@ -28,47 +28,43 @@ export default function TaskSubmission() {
return (
<div className="space-y-4">
{/* Header Card */}
<div className="w-full bg-white rounded-2xl border border-gray-100 p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)]">
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 ">
<div className="flex justify-between items-start">
<div>
<div className="space-y-2">
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">
{task?.title ?? "Submit Task"}
{task?.title}
</h1>
<span className="text-sm font-bold text-gray-600">
Due {task?.deadline.label} at {task?.deadline.time}
</span>
</div>
<span className={`text-xs font-bold px-3 py-1.5 rounded-lg ${submissionType === "file"
? "bg-blue-50 text-blue-700 border border-blue-100"
: "bg-purple-50 text-purple-700 border border-purple-100"
}`}>
{submissionType === "file" ? "📄 File Upload" : "🔗 Link Submission"}
</span>
</div>
</div>
{/* Task Description Card */}
<div className="w-full bg-white rounded-2xl border border-gray-100 p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)] space-y-3">
<h2 className="text-base font-bold text-gray-800 tracking-tight">Task Description</h2>
<p className="text-sm text-gray-500 font-medium leading-relaxed">
{task?.description ?? "Kerjakan dan kirimkan tugas sesuai dengan instruksi yang diberikan."}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-3">
<h2 className="text-xl text-gray-800 tracking-tight">Task Description</h2>
<p className="text-sm text-foreground font-light leading-relaxed">
{task?.description}
</p>
</div>
{/* Submit Area Card */}
<div className="w-full bg-white rounded-2xl border border-gray-100 p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)] space-y-5">
<div className="flex items-center gap-2 border-b border-gray-50 pb-2">
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-5">
<div className="flex items-center gap-2 pb-2">
<FileText className="w-4 h-4 text-[#B30000] stroke-[2.5]" />
<h2 className="text-base font-bold text-gray-800 tracking-tight">Submit Task</h2>
<h2 className="text-base font-bold text-secondary tracking-tight">Save Changes</h2>
</div>
{/* Render form sesuai tipe dari query param */}
{submissionType === "file" ? (
<TaskFormFile taskId={slug} onSubmitSuccess={handleFileSubmit} />
<TaskFormFile onSubmitSuccess={handleFileSubmit} deadline={task?.deadline.date}/>
) : (
<TaskFormLink taskId={slug} onSubmitSuccess={handleLinkSubmit} />
<TaskFormLink onSubmitSuccess={handleLinkSubmit} deadline={task?.deadline.date}/>
)}
{/* Info Box */}
<div className="w-full bg-gray-50 border border-gray-100 rounded-lg p-3 flex items-start gap-2.5">
<div className="w-full border border-box-border rounded-lg p-3 flex items-start gap-2.5">
<Info className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
<p className="text-[11px] text-gray-400 font-medium leading-relaxed">
Once submitted, your work will be locked for mentor review. You can request a re-submission if needed.
@@ -0,0 +1,145 @@
import { useState } from "react";
import { FileText, FileUp, Link } from "lucide-react";
interface SubmitStatusTableProps {
type: "file" | "link";
files?: File[];
link?: string;
submittedAt?: Date;
deadline?: Date;
onEdit: () => void;
onDelete: () => void;
}
function getTimeEarly(submittedAt: Date, deadline: Date): string {
const diffMs = deadline.getTime() - submittedAt.getTime();
if (diffMs <= 0) return "Submitted after deadline";
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const days = Math.floor(diffHours / 24);
const hours = diffHours % 24;
const parts = [];
if (days > 0) parts.push(`${days} day${days > 1 ? "s" : ""}`);
if (hours > 0) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
return `Assignment was submitted ${parts.join(" ")} early`;
}
export default function SubmitStatusTable({
type,
files = [],
link = "",
submittedAt = new Date(),
deadline,
onEdit,
onDelete,
}: SubmitStatusTableProps) {
const [confirmDelete, setConfirmDelete] = useState(false);
const timeLabel = deadline
? getTimeEarly(submittedAt, deadline)
: "Assignment was submitted early";
const rows = [
{ label: "Submission status", value: "Submitted for grading" },
{ label: "Grading status", value: "Not graded" },
{ label: "Time Remaining", value: timeLabel },
];
return (
<div className="bg-box-secondary border border-box-border rounded-2xl overflow-hidden">
{/* Header */}
<div className="flex justify-between items-center px-4 py-3 border-b border-box-border">
<div className="flex items-center gap-2">
{confirmDelete ? (
<span className="text-xs font-medium text-gray-600">
Are you sure you want to remove your submission?
</span>
) : (
<>
<FileUp className="w-5 h-5 text-red-700 stroke-[2]" />
<span className="font-bold text-sm text-gray-800">Submit Status</span>
</>
)}
</div>
{confirmDelete ? (
/* Confirm state */
<div className="flex items-center gap-3">
<button
onClick={() => setConfirmDelete(false)}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
onClick={() => {
setConfirmDelete(false);
onDelete();
}}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-primary text-white hover:bg-red-800 transition-colors"
>
Continue
</button>
</div>
) : (
/* Default state */
<div className="flex gap-2">
<button
onClick={onEdit}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
>
Edit Submission
</button>
<button
onClick={() => setConfirmDelete(true)}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 hover:text-red-700 hover:bg-red-50 hover:border-red-200 transition-colors"
>
Delete Submission
</button>
</div>
)}
</div>
{/* Rows */}
<div>
{rows.map((row) => (
<div
key={row.label}
className="grid grid-cols-[180px_1fr] border-b border-box-border"
>
<div className="px-4 py-3 text-sm text-gray-500 border-r border-box-border">
{row.label}
</div>
<div className="px-4 py-3 text-sm text-gray-800">{row.value}</div>
</div>
))}
{/* File / Link row */}
<div className="grid grid-cols-[180px_1fr]">
<div className="px-4 py-3 text-sm text-gray-500 border-r border-box-border">
{type === "file" ? "File submission" : "Link submission"}
</div>
<div className="px-4 py-3 flex flex-col gap-2">
{type === "file" ? (
files.map((f, i) => (
<div key={i} className="flex items-center gap-2 text-sm text-gray-800">
<FileText className="w-4 h-4 text-red-700 stroke-[2.5]" />
{f.name}
</div>
))
) : (
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-blue-600 hover:underline break-all"
>
<Link className="w-3.5 h-3.5 shrink-0" />
{link}
</a>
)}
</div>
</div>
</div>
</div>
);
}
@@ -1,13 +1,14 @@
import { useState, useRef } from "react";
import { FileText, UploadCloud, X, FileUp } from "lucide-react";
import { FileText, UploadCloud, X } from "lucide-react";
import { LuSendHorizontal } from "react-icons/lu";
import SubmitStatusTable from "./SubmissionStatusTable";
interface TaskFormFileProps {
taskId: string | undefined;
onSubmitSuccess: (files: File[]) => void;
deadline?: Date;
}
export default function TaskFormFile({ taskId, onSubmitSuccess }: TaskFormFileProps) {
export default function TaskFormFile({ onSubmitSuccess, deadline }: TaskFormFileProps) {
const [files, setFiles] = useState<File[]>([]);
const [isDragActive, setIsDragActive] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
@@ -88,77 +89,19 @@ export default function TaskFormFile({ taskId, onSubmitSuccess }: TaskFormFilePr
}
};
if (isSubmitted) {
return (
<div className="border border-gray-200 rounded-xl bg-white shadow-sm overflow-hidden">
{/* Header Action */}
<div className="flex flex-col sm:flex-row justify-between items-center bg-gray-50/50 p-4 border-b border-gray-200 gap-4 sm:gap-0">
<div className="flex items-center gap-2">
<FileUp className="w-5 h-5 text-red-700 stroke-[2]" />
<h3 className="font-bold text-gray-800 tracking-tight">Submit Status</h3>
</div>
<div className="flex gap-2 w-full sm:w-auto">
<button
onClick={handleEditSubmission}
className="flex-1 sm:flex-none bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold text-xs py-2 px-4 rounded-lg transition-colors"
>
Edit Submission
</button>
<button
onClick={handleDeleteSubmission}
className="flex-1 sm:flex-none bg-gray-200 hover:bg-red-100 text-gray-800 hover:text-red-700 font-bold text-xs py-2 px-4 rounded-lg transition-colors"
>
Delete Submission
</button>
</div>
</div>
{/* Status Table */}
<div className="grid grid-cols-1 divide-y divide-gray-100">
<div className="grid grid-cols-1 sm:grid-cols-[1fr_2fr] hover:bg-gray-50/30 transition-colors">
<div className="p-4 text-sm font-medium text-gray-500 bg-gray-50 sm:bg-transparent sm:border-r border-gray-100">
Submission status
</div>
<div className="p-4 text-sm font-medium text-gray-800">
Submitted for grading
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-[1fr_2fr] hover:bg-gray-50/30 transition-colors">
<div className="p-4 text-sm font-medium text-gray-500 bg-gray-50 sm:bg-transparent sm:border-r border-gray-100">
Grading status
</div>
<div className="p-4 text-sm font-medium text-gray-800">
Not graded
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-[1fr_2fr] hover:bg-gray-50/30 transition-colors">
<div className="p-4 text-sm font-medium text-gray-500 bg-gray-50 sm:bg-transparent sm:border-r border-gray-100">
Time Remaining
</div>
<div className="p-4 text-sm font-medium text-gray-800">
Assignment was submitted [waktu submitted]
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-[1fr_2fr] hover:bg-gray-50/30 transition-colors">
<div className="p-4 text-sm font-medium text-gray-500 bg-gray-50 sm:bg-transparent sm:border-r border-gray-100">
File submission
</div>
<div className="p-4 flex flex-col gap-2">
{files.map((f, i) => (
<div key={i} className="flex items-center gap-2 text-sm font-medium text-gray-800">
<FileText className="w-4 h-4 text-red-700 stroke-[2.5]" />
{f.name}
</div>
))}
</div>
</div>
</div>
</div>
);
}
// ganti if (isSubmitted) { return (...) }
if (isSubmitted) {
return (
<SubmitStatusTable
type="file"
files={files}
submittedAt={new Date()}
deadline={deadline} // opsional, kirim dari parent kalau ada
onEdit={handleEditSubmission}
onDelete={handleDeleteSubmission}
/>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
@@ -177,7 +120,7 @@ export default function TaskFormFile({ taskId, onSubmitSuccess }: TaskFormFilePr
onDragLeave={handleDrag}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`w-full min-h-[120px] border border-dashed rounded-xl p-4 bg-white cursor-pointer transition-all ${isDragActive ? "border-[#B30000] bg-red-50/40" : "border-gray-200 hover:border-gray-300"
className={`w-full min-h-[120px] border border-dashed rounded-xl p-4 bg-box-secondary cursor-pointer transition-all ${isDragActive ? "border-[#B30000] bg-red-50/40" : "border-foreground hover:border-gray-500"
}`}
>
{files.length > 0 ? (
@@ -1,20 +1,21 @@
import { useState } from "react";
import { Link, FileText } from "lucide-react";
import { LuSendHorizontal } from "react-icons/lu";
import SubmitStatusTable from "./SubmissionStatusTable";
interface TaskFormLinkProps {
taskId: string | undefined;
onSubmitSuccess: (link: string) => void;
deadline?: Date;
}
export default function TaskFormLink({ taskId, onSubmitSuccess }: TaskFormLinkProps) {
export default function TaskFormLink({ onSubmitSuccess, deadline }: TaskFormLinkProps) {
const [submissionLink, setSubmissionLink] = useState("");
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!submissionLink.trim()) {
alert("Silakan masukkan link tugas Anda terlebih dahulu.");
// alert("Silakan masukkan link tugas Anda terlebih dahulu.");
return;
}
setIsSubmitted(true); // ini hilang
@@ -33,62 +34,17 @@ export default function TaskFormLink({ taskId, onSubmitSuccess }: TaskFormLinkPr
};
if (isSubmitted) {
return (
<div className="border border-gray-200 rounded-xl bg-white overflow-hidden">
{/* Header */}
<div className="flex justify-between items-center bg-gray-50 p-4 border-b border-gray-200">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-red-700 stroke-[2]" />
<h3 className="font-bold text-gray-800 text-sm">Submit Status</h3>
</div>
<div className="flex gap-2">
<button
onClick={handleEditSubmission}
className="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold text-xs py-1.5 px-3 rounded-lg transition-colors"
>
Edit Submission
</button>
<button
onClick={handleDeleteSubmission}
className="bg-gray-200 hover:bg-red-100 text-gray-800 hover:text-red-700 font-bold text-xs py-1.5 px-3 rounded-lg transition-colors"
>
Delete Submission
</button>
</div>
</div>
{/* Table */}
<div className="divide-y divide-gray-100">
<div className="grid grid-cols-[1fr_2fr]">
<div className="p-4 text-sm text-gray-500 font-medium bg-gray-50 border-r border-gray-100">Submission status</div>
<div className="p-4 text-sm text-gray-800 font-medium">Submitted for grading</div>
</div>
<div className="grid grid-cols-[1fr_2fr]">
<div className="p-4 text-sm text-gray-500 font-medium bg-gray-50 border-r border-gray-100">Grading status</div>
<div className="p-4 text-sm text-gray-800 font-medium">pc: Not graded</div>
</div>
<div className="grid grid-cols-[1fr_2fr]">
<div className="p-4 text-sm text-gray-500 font-medium bg-gray-50 border-r border-gray-100">Time Remaining</div>
<div className="p-4 text-sm text-gray-800 font-medium">pc: Assignment was submitted 2 days 15 hours early</div>
</div>
<div className="grid grid-cols-[1fr_2fr]">
<div className="p-4 text-sm text-gray-500 font-medium bg-gray-50 border-r border-gray-100">Link submission</div>
<div className="p-4 text-sm font-medium">
<a
href={submissionLink}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline break-all flex items-center gap-1.5"
>
<Link className="w-3.5 h-3.5 shrink-0" />
{submissionLink}
</a>
</div>
</div>
</div>
</div >
);
}
return (
<SubmitStatusTable
type="link"
link={submissionLink}
submittedAt={new Date()}
deadline={deadline}
onEdit={handleEditSubmission}
onDelete={handleDeleteSubmission}
/>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-5">
+2 -2
View File
@@ -11,7 +11,7 @@ export const InternLayout = () => {
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
const closeSidebar = () => setIsSidebarOpen(false);
return (
<div className="min-h-screen bg-gray-50 flex flex-col relative">
<div className="min-h-screen flex flex-col relative">
<Header toggleSidebar={toggleSidebar}/>
<div className="flex flex-1 relative">
<SidebarIntern isOpen={isSidebarOpen} closeSidebar={closeSidebar} />
@@ -36,7 +36,7 @@ export const MentorLayout = () => {
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
const closeSidebar = () => setIsSidebarOpen(false);
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
<div className="min-h-screenflex flex-col">
<Header toggleSidebar={toggleSidebar}/>
<div className="flex flex-1">
<SidebarMentor isOpen={isSidebarOpen} closeSidebar={closeSidebar} />
@@ -0,0 +1,361 @@
// src/pages/mentor/CertificateDetail.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
CalendarDays,
CheckCircle2,
AlertCircle,
Eye,
ChevronLeft,
ChevronRight,
Sparkles,
ArrowLeft,
} from "lucide-react";
// ─── Types ────────────────────────────────────────────────────────────────────
type InternStatus = "ELIGIBLE" | "INCOMPLETE";
interface Intern {
id: number;
name: string;
role: string;
initials: string;
taskProgress: number;
taskTotal: number;
status: InternStatus;
}
// ─── Mock Data ────────────────────────────────────────────────────────────────
const mockProject = {
title: "Edge Computing Framework",
period: "Dec 2023 — Mar 2024",
status: "COMPLETED" as const,
scope:
"Advanced research and implementation of low-latency computing nodes at the network edge, focusing on real-time data processing for autonomous systems.",
totalInterns: 21,
eligibleInterns: 18,
};
const mockInterns: Intern[] = [
{ id: 1, name: "Marcus Rodriguez", role: "Software Engineer Intern", initials: "MR", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" },
{ id: 2, name: "Priya Sharma", role: "Frontend Developer Intern", initials: "PS", taskProgress: 8, taskTotal: 10, status: "INCOMPLETE" },
{ id: 3, name: "James Smith", role: "QA Engineering Intern", initials: "JS", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" },
{ id: 4, name: "Nina Castillo", role: "Mobile Development Intern", initials: "NC", taskProgress: 9, taskTotal: 10, status: "ELIGIBLE" },
{ id: 5, name: "Derek Lim", role: "DevOps & Cloud Intern", initials: "DL", taskProgress: 7, taskTotal: 10, status: "INCOMPLETE" },
{ id: 6, name: "Alex Johnson", role: "Backend Engineering Intern", initials: "AJ", taskProgress: 10, taskTotal: 10, status: "ELIGIBLE" },
];
const TOTAL_ENTRIES = 1284;
const PER_PAGE = 3;
// ─── Avatar ───────────────────────────────────────────────────────────────────
const avatarColors: Record<string, string> = {
MR: "bg-blue-100 text-blue-700",
PS: "bg-purple-100 text-purple-700",
JS: "bg-green-100 text-green-700",
NC: "bg-pink-100 text-pink-700",
DL: "bg-teal-100 text-teal-700",
AJ: "bg-orange-100 text-orange-700",
};
function Avatar({ initials }: { initials: string }) {
const color = avatarColors[initials] ?? "bg-gray-100 text-gray-500";
return (
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-xs font-black flex-shrink-0 ${color}`}>
{initials}
</div>
);
}
// ─── Progress Bar ─────────────────────────────────────────────────────────────
function ProgressBar({ value, total }: { value: number; total: number }) {
const pct = Math.round((value / total) * 100);
const isComplete = value === total;
return (
<div className="flex items-center gap-2">
<div className="w-16 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${isComplete ? "bg-[#B30000]" : "bg-gray-400"}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className={`text-xs font-bold ${isComplete ? "text-[#B30000]" : "text-gray-500"}`}>
{value}/{total}
</span>
</div>
);
}
// ─── Status Badge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: InternStatus }) {
const isEligible = status === "ELIGIBLE";
return (
<div className={`flex items-center gap-1 text-[11px] font-extrabold tracking-wide ${isEligible ? "text-green-600" : "text-orange-500"}`}>
{isEligible
? <CheckCircle2 className="w-3.5 h-3.5" />
: <AlertCircle className="w-3.5 h-3.5" />}
{status}
</div>
);
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function CertificateDetail() {
const navigate = useNavigate();
// Selected interns (checked)
const initialSelected = new Set(
mockInterns.filter((i) => i.status === "ELIGIBLE").map((i) => i.id)
);
const [selected, setSelected] = useState<Set<number>>(initialSelected);
const [page, setPage] = useState(1);
const totalPages = Math.ceil(mockInterns.length / PER_PAGE);
const paginated = mockInterns.slice((page - 1) * PER_PAGE, page * PER_PAGE);
const eligibleIds = mockInterns.filter((i) => i.status === "ELIGIBLE").map((i) => i.id);
const allEligibleSelected = eligibleIds.every((id) => selected.has(id));
const toggleSelectAll = () => {
if (allEligibleSelected) {
setSelected(new Set());
} else {
setSelected(new Set(eligibleIds));
}
};
const toggleOne = (id: number, status: InternStatus) => {
if (status === "INCOMPLETE") return; // tidak bisa dipilih
setSelected((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
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">
<CheckCircle2 className="w-4 h-4" />
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
</p>
<p className="text-sm text-gray-600 leading-relaxed">
{mockProject.scope}
</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>
<p className="text-xs text-gray-400 font-medium mt-0.5">Total Interns</p>
</div>
<div className="border border-gray-200 rounded-xl p-4">
<p className="text-2xl font-extrabold text-gray-900">{mockProject.eligibleInterns}</p>
<p className="text-xs text-gray-400 font-medium mt-0.5">Eligible</p>
</div>
</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>
<p className="text-xs text-gray-400 mt-0.5 leading-snug max-w-xs">
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"
>
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-colors ${
allEligibleSelected ? "bg-[#B30000] border-[#B30000]" : "border-gray-300"
}`}>
{allEligibleSelected && (
<svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-xs font-bold text-gray-700 whitespace-nowrap">
Select All Eligible ({mockProject.eligibleInterns})
</span>
</button>
</div>
{/* Column Headers */}
<div className="grid grid-cols-[auto_1fr_auto_auto_auto] items-center gap-3 px-5 py-2.5 bg-gray-50/70 border-b border-gray-100">
<div className="w-4" />
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Intern Name</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider w-20 text-center">Task Progress</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider w-24 text-center">Status</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider w-16 text-center">Action</span>
</div>
{/* Rows */}
<div className="divide-y divide-gray-50">
{paginated.map((intern) => {
const isChecked = selected.has(intern.id);
const isIncomplete = intern.status === "INCOMPLETE";
return (
<div
key={intern.id}
className="grid grid-cols-[auto_1fr_auto_auto_auto] items-center gap-3 px-5 py-3.5 hover:bg-gray-50/40 transition-colors"
>
{/* Checkbox */}
<button
onClick={() => toggleOne(intern.id, intern.status)}
disabled={isIncomplete}
className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-colors ${
isIncomplete
? "border-gray-200 bg-gray-50 cursor-not-allowed"
: isChecked
? "bg-[#B30000] border-[#B30000]"
: "border-gray-300 hover:border-gray-400"
}`}
>
{isChecked && !isIncomplete && (
<svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
</button>
{/* Name + role */}
<div className="flex items-center gap-3 min-w-0">
<Avatar initials={intern.initials} />
<div className="min-w-0">
<p className="text-sm font-bold text-gray-900 leading-tight truncate">{intern.name}</p>
<p className="text-[11px] text-gray-400 mt-0.5 truncate">{intern.role}</p>
</div>
</div>
{/* Task Progress */}
<div className="w-20 flex justify-center">
<ProgressBar value={intern.taskProgress} total={intern.taskTotal} />
</div>
{/* Status */}
<div className="w-24 flex justify-center">
<StatusBadge status={intern.status} />
</div>
{/* Action */}
<div className="w-16 flex justify-center">
{isIncomplete ? (
<button className="text-[11px] font-extrabold text-[#B30000] hover:underline whitespace-nowrap">
Nag Intern
</button>
) : (
<button className="p-1.5 text-gray-400 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors" title="View">
<Eye className="w-4 h-4" />
</button>
)}
</div>
</div>
);
})}
</div>
{/* Pagination Footer */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 px-5 py-3.5 border-t border-gray-100 bg-gray-50/30">
<p className="text-xs text-gray-400 font-medium">
Showing {(page - 1) * PER_PAGE + 1} to {Math.min(page * PER_PAGE, mockInterns.length)} of {TOTAL_ENTRIES.toLocaleString()} entries
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50 disabled:opacity-40 transition-colors"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
<button
key={n}
onClick={() => setPage(n)}
className={`w-7 h-7 flex items-center justify-center rounded-md text-xs font-bold transition-colors ${
n === page
? "bg-[#B30000] text-white shadow-sm"
: "bg-white border border-gray-200 text-gray-600 hover:bg-gray-50"
}`}
>
{n}
</button>
))}
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50 disabled:opacity-40 transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
</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>
<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"
>
Generate Certificates
<Sparkles className="w-4 h-4" />
</button>
</div>
</div>
);
}
@@ -0,0 +1,240 @@
// src/pages/mentor/CertificateResult.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
CheckCircle2,
FolderArchive,
Search,
Eye,
Download,
Share2,
ChevronLeft,
ChevronRight,
} from "lucide-react";
// ─── Types ────────────────────────────────────────────────────────────────────
interface InternCert {
id: number;
name: string;
role: string;
initials: string;
credentialId: string;
}
// ─── Mock Data ────────────────────────────────────────────────────────────────
const mockCerts: InternCert[] = [
{ id: 1, name: "Alex Johnson", role: "Backend Engineering", initials: "AJ", credentialId: "CERT-2023-0045" },
{ id: 2, name: "Sarah Lin", role: "UI/UX Design Specialist",initials: "SL", credentialId: "CERT-2023-0046" },
{ id: 3, name: "Marcus Kane", role: "Data Analytics", initials: "MK", credentialId: "CERT-2023-0047" },
{ id: 4, name: "Priya Sharma", role: "Frontend Engineering", initials: "PS", credentialId: "CERT-2023-0048" },
{ id: 5, name: "Derek Lim", role: "DevOps & Cloud", initials: "DL", credentialId: "CERT-2023-0049" },
{ id: 6, name: "Nina Castillo", role: "Mobile Development", initials: "NC", credentialId: "CERT-2023-0050" },
];
const TOTAL_CERTS = 18;
const PER_PAGE = 3;
// ─── Avatar ───────────────────────────────────────────────────────────────────
const avatarColors: Record<string, string> = {
AJ: "bg-blue-100 text-blue-700",
SL: "bg-purple-100 text-purple-700",
MK: "bg-orange-100 text-orange-700",
PS: "bg-teal-100 text-teal-700",
DL: "bg-green-100 text-green-700",
NC: "bg-pink-100 text-pink-700",
};
function Avatar({ initials }: { initials: string }) {
const color = avatarColors[initials] ?? "bg-gray-100 text-gray-600";
return (
<div className={`w-9 h-9 rounded-full flex items-center justify-center text-[11px] font-black flex-shrink-0 ${color}`}>
{initials}
</div>
);
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function CertificateResult() {
const navigate = useNavigate();
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
// Filter + paginate
const filtered = mockCerts.filter(
(c) =>
c.name.toLowerCase().includes(search.toLowerCase()) ||
c.role.toLowerCase().includes(search.toLowerCase())
);
const totalPages = Math.ceil(filtered.length / PER_PAGE);
const paginated = filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
setPage(1);
};
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>
<p className="page-title-desc">
Administrate, issue, and track professional certifications for all interns.
</p>
</div>
{/* ── Success Hero ── */}
<div className="flex flex-col items-center text-center mb-8 px-4">
{/* Icon circle */}
<div className="w-16 h-16 rounded-full bg-[#B30000] flex items-center justify-center shadow-lg mb-5">
<CheckCircle2 className="w-8 h-8 text-white" strokeWidth={2.5} />
</div>
{/* Headline */}
<h2 className="text-3xl md:text-4xl font-extrabold text-[#B30000] mb-3 leading-tight">
{TOTAL_CERTS} Certificates Successfully Generated!
</h2>
{/* Subtext */}
<p className="text-sm text-gray-500 max-w-md leading-relaxed mb-6">
The validation engine has processed all engineering credentials. You can now
publish them to the public registry or download the batch for offline distribution.
</p>
{/* Download CTA */}
<button className="flex items-center gap-2 px-6 py-2.5 border border-gray-200 rounded-xl text-sm font-bold text-[#B30000] bg-white hover:bg-red-50 hover:border-red-200 shadow-sm transition-colors">
<FolderArchive className="w-4 h-4" />
Download All (ZIP)
</button>
</div>
{/* ── Bottom Section: Table + Preview Card ── */}
<div className="flex flex-col xl:flex-row gap-4 items-start">
{/* Recipient Directory Table */}
<div className="flex-1 min-w-0 border border-gray-200 rounded-2xl overflow-hidden bg-white shadow-sm">
{/* Table Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100">
<h3 className="text-sm font-bold text-gray-900">Recipient Directory</h3>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-400" />
<input
type="text"
placeholder="Filter interns..."
value={search}
onChange={handleSearch}
className="pl-8 pr-3 py-1.5 text-xs border border-gray-200 rounded-lg bg-white text-gray-700 placeholder-gray-400 focus:outline-none focus:border-red-400 focus:ring-1 focus:ring-red-400 w-44 transition-colors"
/>
</div>
</div>
{/* Column Headers */}
<div className="grid grid-cols-[2fr_1.5fr_auto] px-5 py-2.5 bg-gray-50 border-b border-gray-100">
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Intern Name</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Credential ID</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Actions</span>
</div>
{/* Rows */}
<div className="divide-y divide-gray-50">
{paginated.length === 0 ? (
<div className="px-5 py-10 text-center text-sm text-gray-400">
No interns match your search.
</div>
) : (
paginated.map((cert) => (
<div
key={cert.id}
className="grid grid-cols-[2fr_1.5fr_auto] items-center px-5 py-3.5 hover:bg-gray-50/60 transition-colors"
>
{/* Name + role */}
<div className="flex items-center gap-3">
<Avatar initials={cert.initials} />
<div>
<p className="text-sm font-bold text-gray-900 leading-tight">{cert.name}</p>
<p className="text-[11px] text-gray-400 mt-0.5">{cert.role}</p>
</div>
</div>
{/* Credential ID */}
<p className="text-xs font-mono text-gray-500 tracking-wide">{cert.credentialId}</p>
{/* Actions */}
<div className="flex items-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">
<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 className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors" title="Share">
<Share2 className="w-4 h-4" />
</button>
</div>
</div>
))
)}
</div>
{/* Pagination Footer */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 px-5 py-3.5 border-t border-gray-100 bg-gray-50/30">
<p className="text-xs text-gray-400 font-medium">
Showing {filtered.length === 0 ? 0 : (page - 1) * PER_PAGE + 1} to{" "}
{Math.min(page * PER_PAGE, filtered.length)} of {TOTAL_CERTS} interns
</p>
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50 disabled:opacity-40 transition-colors"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
<button
key={n}
onClick={() => setPage(n)}
className={`w-7 h-7 flex items-center justify-center rounded-md text-xs font-bold transition-colors ${
n === page
? "bg-[#B30000] text-white shadow-sm"
: "bg-white border border-gray-200 text-gray-600 hover:bg-gray-50"
}`}
>
{n}
</button>
))}
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages || totalPages === 0}
className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50 disabled:opacity-40 transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
</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="mt-auto">
<p className="text-white font-extrabold text-base leading-snug">
Institutional Excellence
</p>
<p className="text-red-200 text-xs mt-1">
Certified via Mentor Portal v2.4
</p>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,149 @@
// src/pages/mentor/CertificateList.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>
);
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function CertificateList() {
const navigate = useNavigate();
const handleCardClick = (id: number) => {
navigate(`/mentor/certificates/${id}`);
};
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>
<p className="page-title-desc">
Administrate, issue, and track professional certifications for all interns.
</p>
</div>
{/* Section: List Projects */}
<div>
<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
key={project.id}
project={project}
onClick={() => handleCardClick(project.id)}
/>
))}
</div>
</div>
</div>
);
}
@@ -58,10 +58,9 @@ const MentorCertificatePage = () => {
</div>
{/* Main Grid Content */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
{/* Kiri: Form Issue New Certificate (4 Kolom) */}
<div className="lg:col-span-5 xl:col-span-4 bg-white border border-gray-100 rounded-2xl p-6 md:p-8 shadow-sm">
<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>
@@ -143,7 +142,7 @@ const MentorCertificatePage = () => {
</div>
{/* Kanan: Issued Certificates History (8 Kolom) */}
<div className="lg:col-span-7 lg:col-span-8 bg-white border border-gray-100 rounded-2xl shadow-sm overflow-hidden flex flex-col justify-between">
<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">
+2 -3
View File
@@ -41,8 +41,7 @@ export default function InternsManagement() {
{/* Filter Row & Stats Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 items-center mb-6">
{/* Left Control Inputs */}
<div className="lg:col-span-2 flex flex-col sm:flex-row items-center gap-3">
<div className="lg:col-span-2 flex flex-col sm:flex-row items-center gap-3 border border-gray-300 p-4 rounded-xl">
{/* Search Box Input */}
<div className="relative w-full sm:max-w-xs">
<span className="absolute inset-y-0 left-3 flex items-center text-gray-400">
@@ -101,7 +100,7 @@ export default function InternsManagement() {
</div>
<div className="flex justify-end">
<button className="inline-flex items-center gap-2 p-3 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-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)}>
<Plus className="w-4 h-4" />
Add
</button>
@@ -1,19 +1,21 @@
import { useParams } from "react-router-dom";
import { useParams, useNavigate } from "react-router-dom";
import { useState } from "react";
import { mockProjects } from "../../../lib/mockProjects";
import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react";
import EditSubmissionModal from "./components/EditSubmissionTask";
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
export default function MentorProjectsDetailPage() {
const navigate = useNavigate();
const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>();
const targetedProject = mockProjects.find((p) => toSlug(p.name) === slug);
if (!targetedProject) {
return <div className="p-10 text-[#888]">Project tidak ditemukan.</div>;
}
const targetedTask = targetedProject.tasks.find((t) => toSlug(t.title) === taskSlug);
if (!targetedTask) {
return <div className="p-10 text-[#888]">Tugas tidak ditemukan.</div>;
@@ -36,7 +38,7 @@ export default function MentorProjectsDetailPage() {
</div>
{/* Kotak Informasi Detail & Deadline */}
<div className="border border-gray-300 rounded-xl p-6 bg-white flex flex-col md:flex-row gap-6 mb-8 shadow-sm">
<div className="border border-gray-300 rounded-xl p-6 bg-white flex flex-col lg:flex-row gap-6 mb-8 shadow-sm">
<div className="flex-1 md:border-r border-gray-200 md:pr-6">
<h2 className="text-xs font-sans text-gray-400 tracking-wider uppercase mb-2">Description</h2>
<p className="text-sm text-gray-700 leading-relaxed whitespace-pre-line">
@@ -94,7 +96,7 @@ export default function MentorProjectsDetailPage() {
<tr key={idx} className="hover:bg-gray-50/50 transition-colors">
{/* Kolom Nama & Profil */}
<td className="px-6 py-4 flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-red-800 text-gray-500 font-semibold text-xs flex items-center justify-center border border-gray-300"/>
<div className="w-9 h-9 rounded-full bg-red-800 text-gray-500 font-semibold text-xs flex items-center justify-center border border-gray-300" />
<span className="font-semibold text-sm text-gray-800">{internProfile.name}</span>
</td>
@@ -124,7 +126,15 @@ export default function MentorProjectsDetailPage() {
{/* Kolom Tombol Aksi */}
<td className="px-6 py-4 text-right">
<button className="text-sm font-bold text-red-800 hover:text-red-900 transition-colors inline-flex items-center gap-1">
<button
className="text-sm font-bold text-red-800 hover:text-red-900 transition-colors inline-flex items-center gap-1"
onClick={() =>
navigate(
`/mentor/projects/${toSlug(targetedProject.name)}/${toSlug(targetedTask.title)}/${sub.internEmail.split("@")[0]}`,
{ state: { status: sub.status === "Done" ? "submitted" : sub.status === "Overdue" ? "overdue" : "pending" } }
)
}
>
View Submission <span className="text-xs">&rarr;</span>
</button>
</td>
@@ -0,0 +1,183 @@
import { useState, useRef } from "react";
const mockProfileData = {
fullName: "JonathanKristina",
email: "JonathanKristina@gmail.com",
bio: "UIUX holic"
};
export default function MentorSettingsPage() {
const [formData, setFormData] = useState(mockProfileData);
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSaveChanges = (e: React.FormEvent) => {
e.preventDefault();
alert("Changes saved successfully!");
console.log("Saved Data:", formData);
};
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => setPhotoPreview(reader.result as string);
reader.readAsDataURL(file);
}
};
const handleRemovePhoto = () => {
setPhotoPreview(null);
if (fileInputRef.current) fileInputRef.current.value = "";
};
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
return (
<div>
{/* Header Title */}
<div className="mb-6">
<h1 className="page-title">Settings</h1>
<p className="page-title-desc">
Manage and edit your profile and account information.
</p>
</div>
{/* White Card Container */}
<div className="w-full bg-white rounded-xl border border-box-border shadow-lg overflow-hidden">
{/* Card Header */}
<div className="p-6 border-b border-box-border">
<h2 className="text-[20px] font-bold text-foreground">Profile Information</h2>
<p className="text-xs text-foreground mt-0.5">
Update your photo and personal details.
</p>
</div>
{/* Card Body & Form */}
<form onSubmit={handleSaveChanges} className="p-6 space-y-6">
{/* Profile Photo Row */}
<div className="flex items-center gap-5 py-2">
{/* Avatar dengan badge edit */}
<div className="relative flex-shrink-0">
<div className="w-20 h-20 rounded-xl border-2 border-red-100 overflow-hidden bg-gray-100 flex items-center justify-center shadow-sm">
{photoPreview ? (
<img src={photoPreview} alt="Profile" className="w-full h-full object-cover" />
) : (
<span className="text-3xl font-black text-gray-900">{initialLetter}</span>
)}
</div>
{/* Badge edit icon */}
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="absolute -bottom-1.5 -right-1.5 w-6 h-6 bg-[#B30000] rounded-full flex items-center justify-center shadow border-2 border-white hover:bg-[#990000] transition-colors"
title="Edit photo"
>
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M15.232 5.232l3.536 3.536M9 13l6.586-6.586a2 2 0 112.828 2.828L11.828 15.828a2 2 0 01-1.414.586H9v-2a2 2 0 01.586-1.414z" />
</svg>
</button>
</div>
{/* Info & Action Buttons */}
<div>
<h3 className="text-sm font-bold text-gray-900 mb-0.5">Profile Photo</h3>
<p className="text-xs text-gray-400 mb-3">
JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K.
</p>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors"
>
Upload New
</button>
<button
type="button"
onClick={handleRemovePhoto}
className="px-4 py-1.5 bg-white border border-box-border text-gray-600 hover:bg-gray-50 text-xs font-bold rounded-lg transition-colors"
>
Remove
</button>
</div>
</div>
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
accept="image/png, image/jpeg, image/gif"
className="hidden"
onChange={handlePhotoUpload}
/>
</div>
<hr className="border-t border-dashed border-box-border" />
{/* Grid Inputs */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
Full Name
</label>
<input
type="text"
name="fullName"
value={formData.fullName}
onChange={handleChange}
className="w-full px-3 py-2 border border-box-border rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 transition-colors bg-white font-medium"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
Email Address
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border border-box-border rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 transition-colors bg-white font-medium"
/>
</div>
</div>
{/* Professional Bio */}
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
Professional Bio
</label>
<textarea
name="bio"
rows={4}
value={formData.bio}
onChange={handleChange}
className="w-full px-3 py-2 border border-box-border rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 transition-colors bg-white font-medium resize-none"
/>
<p className="text-[11px] text-gray-500 font-normal pt-0.5">
Brief description for your profile. URLs and @mentions are allowed.
</p>
</div>
{/* Footer Action */}
<div className="flex justify-end pt-2">
<button
type="submit"
className="px-6 py-2 bg-[#B30000] hover:bg-[#990000] text-white font-bold text-xs rounded-lg shadow-sm transition-colors uppercase tracking-wider"
>
Save Changes
</button>
</div>
</form>
</div>
</div>
);
}
@@ -1,5 +1,5 @@
import { useState } from 'react';
import type { Project, Intern } from '../../../lib/mockData';
import type { Project, Intern } from '../../../lib/mockProjects';
import { Plus } from 'lucide-react';
import ManageInternsModal from './components/AddInternsDialog';
@@ -51,6 +51,11 @@ export default function InternsTab({ project }: { project: Project }) {
<span className=" text-[13px] text-[#555] font-semibold">
{intern.role}
</span>
<div>
<button className='flex items-center gap-1 text-[14px] bg-red-600 text-white p-1 rounded-xl'>
<Plus className='w-4 h-4'/>Delete
</button>
</div>
</div>
))}
</div>
@@ -0,0 +1,144 @@
import { useParams, useLocation } from "react-router-dom";
import { FileText, Download, Clock, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react";
import { mockProjects } from "../../../lib/mockProjects";
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, "-");
type SubmissionStatus = "submitted" | "pending" | "overdue";
// ─── status badge ───────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: SubmissionStatus }) {
if (status === "submitted")
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-green-700 bg-green-50 border border-green-200 px-2.5 py-1 rounded-full">
<CheckCircle2 className="w-3.5 h-3.5" />
Submitted for grading
</span>
);
if (status === "overdue")
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-red-700 bg-red-50 border border-red-200 px-2.5 py-1 rounded-full">
<AlertCircle className="w-3.5 h-3.5" />
Overdue
</span>
);
return (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 px-2.5 py-1 rounded-full">
<MinusCircle className="w-3.5 h-3.5" />
Not submitted
</span>
);
}
// ─── status table ────────────────────────────────────────────────────────────
function StatusTable({ status }: { status: SubmissionStatus }) {
const rows: { label: string; value: React.ReactNode }[] = [
{
label: "Submission status",
value: <StatusBadge status={status} />,
},
{
label: "Grading status",
value: <span className="text-sm text-gray-400">-</span>,
},
{
label: "Time Remaining",
value:
status === "overdue" ? (
<span className="text-sm text-red-600 font-medium">Overdue</span>
) : (
<span className="text-sm text-gray-400">-</span>
),
},
{
label: "File submission",
value:
status === "submitted" ? (
<div className="flex items-center gap-2 text-sm text-gray-800">
<FileText className="w-4 h-4 text-red-700 stroke-[2.5]" />
<span className="text-gray-400 italic">-</span>
</div>
) : (
<span className="text-sm text-gray-400">-</span>
),
},
];
return (
<div className="bg-box-secondary border border-box-border rounded-2xl overflow-hidden">
<div className="divide-y divide-box-border">
{rows.map((row) => (
<div key={row.label} className="grid grid-cols-[180px_1fr]">
<div className="px-4 py-3 text-sm text-gray-500 border-r border-box-border bg-gray-50/50">
{row.label}
</div>
<div className="px-4 py-3">{row.value}</div>
</div>
))}
</div>
</div>
);
}
// ─── main page ───────────────────────────────────────────────────────────────
export default function ViewInternSubmission() {
const { slug, taskSlug, internName } = useParams<{ slug: string; taskSlug: string; internName: string }>();
const location = useLocation();
const status: SubmissionStatus = location.state?.status ?? "pending";
const project = mockProjects.find((p) => toSlug(p.name) === slug);
const task = project?.tasks.find((t) => toSlug(t.title) === taskSlug);
if (!task) {
return (
<div className="flex flex-col items-center justify-center h-64 gap-3 text-gray-400">
<AlertCircle className="w-10 h-10" />
<p className="text-sm font-medium">Task not found.</p>
</div>
);
}
return (
<div className="space-y-4">
{/* Header */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6">
<div className="flex items-start justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">
{task.title}
</h1>
<p className="text-sm text-gray-500 flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
Due {task.deadline.label} at {task.deadline.time}
</p>
</div>
</div>
</div>
{/* Task description */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-3">
<h2 className="text-base font-bold text-gray-800">Task Description</h2>
<p className="text-sm text-foreground font-medium leading-relaxed">
{task.description}
</p>
</div>
{/* Status table */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-bold text-gray-800">Submission Detail</h2>
{status === "submitted" && (
<button className="flex items-center gap-2 bg-primary text-white text-xs font-bold px-4 py-2 rounded-xl hover:bg-red-800 transition-colors">
<Download className="w-4 h-4" />
Download
</button>
)}
</div>
<StatusTable status={status} />
</div>
</div>
);
}
@@ -50,7 +50,7 @@ export default function EditSubmissionModal({ isOpen, onClose }: EditTaskModalPr
};
return (
<dialog ref={dialogRef} className="custom-dialog p-[1.5rem]">
<dialog ref={dialogRef} className="custom-dialog">
{/* Header */}
<div className="dialog-header">
@@ -64,7 +64,7 @@ export default function EditSubmissionModal({ isOpen, onClose }: EditTaskModalPr
</div>
{/* Form Body */}
<form onSubmit={handleSubmit} className="px-6 pb-6 space-y-4 overflow-y-auto flex-1">
<form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto flex-1">
{/* Title */}
<div className="space-y-1.5">
@@ -103,11 +103,8 @@ export default function EditSubmissionModal({ isOpen, onClose }: EditTaskModalPr
required
value={deadlineDate}
onChange={(e) => setDeadlineDate(e.target.value)}
className="w-full pl-3 pr-9 py-2.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] appearance-none"
className="w-full p-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] appearance-none"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
<LuCalendar className="w-4 h-4" />
</span>
</div>
</div>