update Task Submission Page: Submission, aftersubmission, etc
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ›
|
||||
</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 ›
|
||||
</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 ›
|
||||
</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">
|
||||
|
||||
@@ -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">→</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user