update Task Submission Page: Submission, aftersubmission, etc
This commit is contained in:
+25
-22
@@ -45,6 +45,8 @@ import SettingsContent from "./Pages/Internify/SettingsPage";
|
||||
import TaskSubmission from "./Pages/Internify/intern/TaskSubmission";
|
||||
import SertificateList from "./Pages/Internify/intern/SertificateList";
|
||||
import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage";
|
||||
import ViewInternSubmission from "./Pages/Internify/mentor/ViewInternSubmission";
|
||||
import SettingsPage from "./Pages/Internify/mentor/MentorSettingsPage";
|
||||
import ProtectedRoutes from "./Pages/utils/ProtectedRoute";
|
||||
|
||||
function App() {
|
||||
@@ -86,29 +88,30 @@ function App() {
|
||||
|
||||
<Route path="/login-internify" element={<InternifyLogin />} />
|
||||
|
||||
<Route path="/intern" element={<InternLayout />}>
|
||||
<Route index element={<HomeInternPage />} />
|
||||
<Route path="projects" element={<ProjectsPage />} />
|
||||
<Route path="certificates" element={<SertificatePage />} />
|
||||
<Route path="certificates/all" element={<SertificateList />} />
|
||||
<Route path="projects/:slug" element={<ProjectDetailPage />} />
|
||||
<Route path="projects/:slug/:TaskSlug" element={<TaskSubmission />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
<Route path="/intern" element={<InternLayout />}>
|
||||
<Route index element={<HomeInternPage />} />
|
||||
<Route path="projects" element={<ProjectsPage />} />
|
||||
<Route path="certificates" element={<SertificatePage />} />
|
||||
<Route path="certificates/all" element={<SertificateList />} />
|
||||
<Route path="projects/:slug" element={<ProjectDetailPage />} />
|
||||
<Route path="projects/:slug/:TaskSlug" element={<TaskSubmission />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/mentor" element={<MentorLayout />}>
|
||||
<Route index element={<HomeMentorPage />} />
|
||||
<Route path="projects" element={<MentorProjectsPage />} />
|
||||
<Route path="certificates" element={<MentorCertificatePage />} />
|
||||
<Route path="intern" element={<MentorInternPage />} />
|
||||
<Route path="projects/:slug" element={<MentorDetailProject />} />
|
||||
<Route path="projects/:slug/:taskSlug" element={<MentorProjectsDetailPage />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
<Route path="/mentor" element={<MentorLayout />}>
|
||||
<Route index element={<HomeMentorPage />} />
|
||||
<Route path="projects" element={<MentorProjectsPage />} />
|
||||
<Route path="certificates" element={<MentorCertificatePage />} />
|
||||
<Route path="intern" element={<MentorInternPage />} />
|
||||
<Route path="projects/:slug" element={<MentorDetailProject />} />
|
||||
<Route path="projects/:slug/:taskSlug" element={<MentorProjectsDetailPage />} />
|
||||
<Route path="projects/:slug/:taskSlug/:nameIntern" element={<ViewInternSubmission />}/>
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ export default function Header({ toggleSidebar }: { toggleSidebar: () => void })
|
||||
return (
|
||||
<header className='sticky top-0 flex w-full justify-between items-center py-2 px-12 bg-white border-b border-gray-300 z-10'>
|
||||
<div className='flex items-center gap-3'>
|
||||
{/* Tombol Burger Menu: Hanya muncul di layar < md */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="p-1 text-gray-600 hover:bg-gray-100 rounded-md md:hidden"
|
||||
@@ -15,7 +14,7 @@ export default function Header({ toggleSidebar }: { toggleSidebar: () => void })
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
<button onClick={()=>nav('/intern')}>
|
||||
<button>
|
||||
<img src={humiclogo} alt="Humic Logo" className="w-[100px] cursor-pointer" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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,12 +1,14 @@
|
||||
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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+7
-7
@@ -14,12 +14,16 @@
|
||||
|
||||
@theme {
|
||||
/* Dasar Warna Gelap */
|
||||
--color-background: #e9e3e3;
|
||||
--color-background: #fdfdfd;
|
||||
--color-foreground: #1e282c;
|
||||
|
||||
--color-card: #f3f3f3;
|
||||
--color-card-hover: #d5d5d5;
|
||||
|
||||
--color-box-primary: #ebebeb;
|
||||
--color-box-secondary: #f3f4f6 ;
|
||||
--color-box-border: #d8d9db;
|
||||
|
||||
--color-primary: #ad2b19;
|
||||
--color-primary-foreground: #ffffff;
|
||||
|
||||
@@ -36,7 +40,7 @@
|
||||
min-width: 0; /* penting! mencegah overflow */
|
||||
padding-inline: calc(var(--spacing) * 14);
|
||||
padding-block: calc(var(--spacing) * 12);
|
||||
background-color: var(--color-gray-50);
|
||||
background-color: var(--color-background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 13px;
|
||||
@@ -116,15 +120,11 @@ dialog.custom-dialog::backdrop {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-bottom: 2px solid #f3f4f6; /* border-gray-100 */
|
||||
border-bottom: 2px solid #b3b8c0; /* border-gray-100 */
|
||||
}
|
||||
|
||||
/* Breakpoint Medium (md:) - Aktif saat lebar layar >= 768px */
|
||||
@media (min-width: 768px) {
|
||||
.box {
|
||||
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
/* Perubahan padding untuk layar desktop (md:px-8 md:py-5) */
|
||||
padding-left: 2rem; /* 32px */
|
||||
|
||||
+145
-69
@@ -1,21 +1,19 @@
|
||||
import type { Deadline } from "./mockDeadline";
|
||||
|
||||
export interface Submission {
|
||||
internEmail: string;
|
||||
submittedAt?: string;
|
||||
status: 'Done' | 'Pending' | 'Overdue';
|
||||
submittedAt: string;
|
||||
status: "Done" | "Pending" | "Overdue";
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
deadline: Deadline;
|
||||
type: 'file' | 'link';
|
||||
type: "file" | "link";
|
||||
deadline: { date: Date; label: string; time: string };
|
||||
isSubmitted: boolean;
|
||||
submittedCount: number;
|
||||
totalInterns: number;
|
||||
submissions: Submission[];
|
||||
isSubmitted: boolean; // Kembalikan untuk komponen lain
|
||||
submittedCount: number; // Kembalikan untuk komponen lain
|
||||
totalInterns: number; // Kembalikan untuk komponen lain
|
||||
}
|
||||
|
||||
export interface Intern {
|
||||
@@ -31,122 +29,200 @@ export interface Project {
|
||||
tasks: Task[];
|
||||
interns: Intern[];
|
||||
mentor: string;
|
||||
avatarUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
progress: number;
|
||||
tasksDone: number;
|
||||
tasksTotal: number;
|
||||
}
|
||||
|
||||
// ─── Project 1 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const project1Interns: Intern[] = [
|
||||
{ name: "Jonathan Kristina", role: "Backend", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Anisa Rahmawati", role: "Backend", email: "anisarahmawati@gmail.com" },
|
||||
{ name: "Budi Santoso", role: "Frontend", email: "budisantoso@gmail.com" },
|
||||
{ name: "Cindy Permata", role: "Frontend", email: "cindypermata@gmail.com" },
|
||||
{ name: "Dewa Aditya", role: "UI/UX", email: "dewaaditya@gmail.com" },
|
||||
{ name: "Eka Pratiwi", role: "UI/UX", email: "ekapratiwi@gmail.com" },
|
||||
];
|
||||
|
||||
// ─── Project 2 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const project2Interns: Intern[] = [
|
||||
{ name: "Fajar Nugroho", role: "Backend", email: "fajarnugroho@gmail.com" },
|
||||
{ name: "Gita Lestari", role: "Backend", email: "gitalestari@gmail.com" },
|
||||
{ name: "Hendra Wijaya", role: "Frontend", email: "hendrawijaya@gmail.com" },
|
||||
{ name: "Indah Permata", role: "Frontend", email: "indahpermata@gmail.com" },
|
||||
{ name: "Joko Susilo", role: "UI/UX", email: "jokosusilo@gmail.com" },
|
||||
{ name: "Kartika Dewi", role: "UI/UX", email: "kartikadewi@gmail.com" },
|
||||
];
|
||||
|
||||
// ─── Project 3 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const project3Interns: Intern[] = [
|
||||
{ name: "Maya Anggraeni", role: "Backend", email: "mayaanggraeni@gmail.com" },
|
||||
{ name: "Nanda Pratama", role: "Backend", email: "nandapratama@gmail.com" },
|
||||
{ name: "Oscar Firmansyah",role: "Frontend", email: "oscarfirmansyah@gmail.com"},
|
||||
{ name: "Putri Handayani", role: "Frontend", email: "putrihandayani@gmail.com" },
|
||||
{ name: "Rizky Maulana", role: "UI/UX", email: "rizkymaulana@gmail.com" },
|
||||
{ name: "Sari Utami", role: "UI/UX", email: "sariutami@gmail.com" },
|
||||
];
|
||||
|
||||
// ─── mock data ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const mockProjects: Project[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Internify Project",
|
||||
description:
|
||||
"Internify merupakan website Human Capital (HUMC) yang dirancang sebagai platform pendaftaran dan pengelolaan program magang secara digital. Sistem ini dibuat untuk membantu proses rekrutmen peserta magang mulai dari pendaftaran, seleksi, hingga monitoring aktivitas peserta selama program berlangsung.\n\nFokus utama dari pengembangan Internify adalah pembuatan Learning Management System (LMS) yang digunakan untuk mendukung kegiatan pembelajaran dan pengelolaan peserta magang. LMS pada Internify menyediakan fitur seperti akses materi pembelajaran, pengumpulan tugas, monitoring progress peserta, absensi, sertifikat, hingga komunikasi antara mentor dan peserta magang.",
|
||||
interns: project1Interns,
|
||||
mentor: "Mas A",
|
||||
progress: 12,
|
||||
tasksDone: 1,
|
||||
tasksTotal: 8,
|
||||
tasks: [
|
||||
{
|
||||
id: 2,
|
||||
title: "Laporan tugas 1",
|
||||
description: "Berisi design uiux, dan user flow dari aplikasi internify.",
|
||||
type: 'file',
|
||||
description:
|
||||
"Pada tugas pertama ini, Anda diminta untuk melakukan analisis mendalam terhadap kebutuhan pengguna untuk sistem Smart Campus Utility. Analisis ini akan menjadi fondasi bagi perancangan antarmuka pada fase berikutnya.",
|
||||
type: "file",
|
||||
deadline: { date: new Date(2026, 4, 14), label: "May 14, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 2,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "jonathankristina@gmail.com", submittedAt: "May 12, 2026", status: "Done" },
|
||||
{ internEmail: "anisarahmawati@gmail.com", submittedAt: "May 13, 2026", status: "Done" },
|
||||
{ internEmail: "budisantoso@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "cindypermata@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "dewaaditya@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "ekapratiwi@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
]
|
||||
{ internEmail: "jonathankristina@gmail.com", submittedAt: "May 12, 2026", status: "Done" },
|
||||
{ internEmail: "anisarahmawati@gmail.com", submittedAt: "May 13, 2026", status: "Done" },
|
||||
{ internEmail: "budisantoso@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "cindypermata@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "dewaaditya@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "ekapratiwi@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Laporan tugas 2",
|
||||
description: "Berisi design uiux, dan user flow dari aplikasi internify. Pastikan semua link Figma dapat diakses oleh mentor dan mencakup setidaknya 15 screen utama beserta prototype interaktifnya.",
|
||||
type: 'link',
|
||||
description:
|
||||
"Berisi design UI/UX dan user flow dari aplikasi Internify. Pastikan semua link Figma dapat diakses oleh mentor dan mencakup setidaknya 15 screen utama beserta prototype interaktifnya.",
|
||||
type: "link",
|
||||
deadline: { date: new Date(2026, 4, 28), label: "May 28, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 3,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "jonathankristina@gmail.com", submittedAt: "May 25, 2026", status: "Done" },
|
||||
{ internEmail: "anisarahmawati@gmail.com", submittedAt: "May 26, 2026", status: "Pending" },
|
||||
{ internEmail: "budisantoso@gmail.com", submittedAt: "May 26, 2026", status: "Done" },
|
||||
{ internEmail: "cindypermata@gmail.com", submittedAt: "May 27, 2026", status: "Pending" },
|
||||
{ internEmail: "dewaaditya@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
{ internEmail: "ekapratiwi@gmail.com", submittedAt: "May 28, 2026", status: "Done" },
|
||||
]
|
||||
{ internEmail: "jonathankristina@gmail.com", submittedAt: "May 25, 2026", status: "Done" },
|
||||
{ internEmail: "anisarahmawati@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "budisantoso@gmail.com", submittedAt: "May 26, 2026", status: "Done" },
|
||||
{ internEmail: "cindypermata@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "dewaaditya@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
{ internEmail: "ekapratiwi@gmail.com", submittedAt: "May 28, 2026", status: "Done" },
|
||||
],
|
||||
},
|
||||
],
|
||||
interns: [
|
||||
{ name: "Alex Rivera", role: "Backend", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Sarah Jenkins", role: "Backend", email: "anisarahmawati@gmail.com" },
|
||||
{ name: "Marcus Lee", role: "Frontend", email: "budisantoso@gmail.com" },
|
||||
{ name: "Tanya Hudson", role: "Frontend", email: "cindypermata@gmail.com" },
|
||||
{ name: "Ben King", role: "UI/UX", email: "dewaaditya@gmail.com" },
|
||||
{ name: "Eka Pratiwi", role: "UI/UX", email: "ekapratiwi@gmail.com" },
|
||||
],
|
||||
mentor: "jonathankristina@gmail.com",
|
||||
progress: 12,
|
||||
tasksDone: 1,
|
||||
tasksTotal: 8,
|
||||
},
|
||||
|
||||
{
|
||||
id: 2,
|
||||
name: "Rekrutmen Digital Platform",
|
||||
description: "Platform digital yang memudahkan proses rekrutmen karyawan baru secara end-to-end.",
|
||||
description:
|
||||
"Platform digital yang memudahkan proses rekrutmen karyawan baru secara end-to-end, mulai dari posting lowongan, seleksi berkas, hingga penjadwalan wawancara.",
|
||||
interns: project2Interns,
|
||||
mentor: "mas B",
|
||||
progress: 37,
|
||||
tasksDone: 3,
|
||||
tasksTotal: 8,
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Analisis Kebutuhan Sistem",
|
||||
description: "Dokumentasi kebutuhan platform rekrutmen",
|
||||
type: 'file',
|
||||
description:
|
||||
"Dokumentasikan kebutuhan fungsional dan non-fungsional platform rekrutmen. Sertakan diagram use case dan daftar fitur prioritas.",
|
||||
type: "file",
|
||||
deadline: { date: new Date(2026, 4, 5), label: "May 5, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 6,
|
||||
submittedCount: 4,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "fajarnugroho@gmail.com", submittedAt: "May 4, 2026", status: "Done" },
|
||||
]
|
||||
}
|
||||
{ internEmail: "fajarnugroho@gmail.com", submittedAt: "May 4, 2026", status: "Done" },
|
||||
{ internEmail: "gitalestari@gmail.com", submittedAt: "May 4, 2026", status: "Done" },
|
||||
{ internEmail: "hendrawijaya@gmail.com", submittedAt: "May 5, 2026", status: "Done" },
|
||||
{ internEmail: "indahpermata@gmail.com", submittedAt: "May 5, 2026", status: "Done" },
|
||||
{ internEmail: "jokosusilo@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
{ internEmail: "kartikadewi@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Desain Wireframe Aplikasi",
|
||||
description:
|
||||
"Buat wireframe low-fidelity untuk seluruh alur utama rekrutmen menggunakan Figma. Sertakan link yang dapat diakses oleh mentor.",
|
||||
type: "link",
|
||||
deadline: { date: new Date(2026, 4, 15), label: "May 15, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 3,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "fajarnugroho@gmail.com", submittedAt: "May 13, 2026", status: "Done" },
|
||||
{ internEmail: "gitalestari@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "hendrawijaya@gmail.com", submittedAt: "May 14, 2026", status: "Done" },
|
||||
{ internEmail: "indahpermata@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "jokosusilo@gmail.com", submittedAt: "May 15, 2026", status: "Done" },
|
||||
{ internEmail: "kartikadewi@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
],
|
||||
interns: [
|
||||
{ name: "Fajar Nugroho", role: "Backend", email: "fajarnugroho@gmail.com" },
|
||||
],
|
||||
mentor: "seniormHR@company.com",
|
||||
progress: 37,
|
||||
tasksDone: 3,
|
||||
tasksTotal: 8,
|
||||
},
|
||||
|
||||
{
|
||||
id: 3,
|
||||
name: "E-Commerce Analytics Dashboard",
|
||||
description: "Dashboard analitik komprehensif untuk platform e-commerce.",
|
||||
description:
|
||||
"Dashboard analitik komprehensif untuk platform e-commerce, mencakup visualisasi data penjualan, perilaku pengguna, dan performa produk secara real-time.",
|
||||
interns: project3Interns,
|
||||
mentor: "Mas C",
|
||||
progress: 50,
|
||||
tasksDone: 4,
|
||||
tasksTotal: 8,
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup Pipeline Data",
|
||||
description: "Konfigurasi ETL pipeline dari database transaksi ke data warehouse",
|
||||
type: 'link',
|
||||
description:
|
||||
"Konfigurasi ETL pipeline dari database transaksi ke data warehouse. Dokumentasikan arsitektur pipeline dan sertakan screenshot hasil pengujian.",
|
||||
type: "link",
|
||||
deadline: { date: new Date(2026, 4, 3), label: "May 3, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 6,
|
||||
submittedCount: 5,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "mayaanggraeni@gmail.com", submittedAt: "May 2, 2026", status: "Done" },
|
||||
]
|
||||
}
|
||||
{ internEmail: "mayaanggraeni@gmail.com", submittedAt: "May 2, 2026", status: "Done" },
|
||||
{ internEmail: "nandapratama@gmail.com", submittedAt: "May 2, 2026", status: "Done" },
|
||||
{ internEmail: "oscarfirmansyah@gmail.com", submittedAt: "May 3, 2026", status: "Done" },
|
||||
{ internEmail: "putrihandayani@gmail.com", submittedAt: "May 3, 2026", status: "Done" },
|
||||
{ internEmail: "rizkymaulana@gmail.com", submittedAt: "May 3, 2026", status: "Done" },
|
||||
{ internEmail: "sariutami@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implementasi Dashboard UI",
|
||||
description:
|
||||
"Implementasikan tampilan dashboard menggunakan React dan library chart pilihan. Pastikan responsive di desktop dan tablet.",
|
||||
type: "file",
|
||||
deadline: { date: new Date(2026, 4, 20), label: "May 20, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 2,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "mayaanggraeni@gmail.com", submittedAt: "May 18, 2026", status: "Done" },
|
||||
{ internEmail: "nandapratama@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "oscarfirmansyah@gmail.com", submittedAt: "May 19, 2026", status: "Done" },
|
||||
{ internEmail: "putrihandayani@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "rizkymaulana@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "sariutami@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
],
|
||||
interns: [
|
||||
{ name: "Maya Anggraeni", role: "Backend", email: "mayaanggraeni@gmail.com" },
|
||||
],
|
||||
mentor: "leadanalyst@company.com",
|
||||
progress: 50,
|
||||
tasksDone: 4,
|
||||
tasksTotal: 8,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user