feat: implement mentor dashboard features including project task management, submission tracking, and modal-based task creation.

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-26 18:05:54 +07:00
parent 53b270921d
commit 60aba4180e
19 changed files with 471 additions and 275 deletions
+2 -2
View File
@@ -71,8 +71,8 @@ export default function InternifyLogin() {
HUMIC LMS PORTAL
</p>
</div>
<div className="max-w-md bg-white rounded-2xl border border-box-border p-8 md:p-12">
{/* MAIN CONTENT */}
<div className="max-w-md bg-box-primary rounded-2xl border border-box-border p-8 md:p-12 shadow-sm">
<h2 className="text-xl font-bold text-gray-900 mb-1">Welcome back</h2>
<p className="text-sm text-gray-500 mb-6">Access your dashboard with your credentials.</p>
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useMyTasks } from '../../../../hooks/useProjects';
//SEMENTARA USERSERVICES BELUM KUBUAT
import api from '../../../../lib/api';
import { useCurrentUser } from '../../../../hooks/useUser';
import { useAllMentorTasks } from '../../../../hooks/useTasks';
const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
const MONTH_NAMES = [
@@ -18,24 +19,23 @@ function getFirstDayOfMonth(year: number, month: number) {
return new Date(year, month, 1).getDay()
}
interface roleUser {
role: "mentor" | "intern" | "admin";
}
export default function HomeCalendar() {
const [today, setToday] = useState(new Date());
const [role, setRole] = useState<"mentor" | "intern" | "admin" | null>(null);
const { tasks } = useMyTasks();
const { user } = useCurrentUser();
useEffect(() => {
api.get("/auth-api/me")
.then((res) => setRole(res.data.data.role))
.catch((err) => console.error("Failed to fetch user:", err));
}, []);
const isIntern = user?.role === "intern";
const { tasks: internTasks } = useMyTasks();
const { tasks: mentorTasks } = useAllMentorTasks(!isIntern && user !== null);
const DEADLINES = role === "intern"
? tasks.map(task => ({ date: new Date(task.deadline_at), label: task.title, }))
: [];
const DEADLINES = isIntern
? internTasks.map(task => ({
date: new Date(task.deadline_at),
label: task.title,
}))
: mentorTasks.map(task => ({
date: new Date(task.deadline_at),
label: `[${task.project_name}] ${task.title}`,
}));
useEffect(() => {
const interval = setInterval(() => {
@@ -1,33 +1,41 @@
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 { Calendar, Edit } from "lucide-react";
import EditSubmissionModal from "./components/EditSubmissionTask";
import { useTaskSubmissions } from "../../../hooks/useTasks";
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>;
}
const { task, loading, error } = useTaskSubmissions(taskSlug!, slug);
const [openModal, setOpenModal] = useState(false);
if (loading) return (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-32 bg-box-secondary rounded-2xl animate-pulse" />
))}
</div>
);
if (error) return <p className="text-red-500 text-sm">{error}</p>;
if (!task) return null;
const formatDeadline = (isoString: string) => {
const date = new Date(isoString);
const dd = String(date.getDate()).padStart(2, '0');
const mm = String(date.getMonth() + 1).padStart(2, '0');
const yy = String(date.getFullYear()).slice(-2);
const hh = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
return `${dd}/${mm}/${yy} pada jam ${hh}:${min}`;
};
return (
<div>
{/* Header Utama */}
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-[#1a1a1a]">{targetedTask.title}</h1>
<h1 className="text-3xl font-bold text-[#1a1a1a]">{task.title}</h1>
<button
className="flex items-center gap-2 border border-gray-400 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 transition-all"
onClick={() => setOpenModal(true)}
@@ -42,7 +50,7 @@ export default function MentorProjectsDetailPage() {
<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">
{targetedTask.description}
{task.description}
</p>
</div>
<div className="w-full md:w-120 flex flex-col justify-start">
@@ -52,8 +60,7 @@ export default function MentorProjectsDetailPage() {
<Calendar size={20} />
</div>
<div>
<div className="text-base font-bold text-gray-900">{targetedTask.deadline.label}</div>
<div className="text-xs text-gray-400">at {targetedTask.deadline.time}</div>
<div className="text-base font-bold text-gray-900">{formatDeadline(task.deadline_at)}</div>
</div>
</div>
</div>
@@ -66,13 +73,9 @@ export default function MentorProjectsDetailPage() {
<div className="flex items-center gap-3">
<h2 className="text-xl font-bold text-gray-900">Intern Submissions</h2>
<span className="bg-gray-100 text-gray-500 font-semibold text-xs px-2.5 py-0.5 rounded-md border border-gray-200">
{targetedTask.submissions?.length || 0} total
{task.submissions?.length || 0} total
</span>
</div>
<div className="flex items-center gap-4 text-gray-500">
<button className="hover:text-gray-800"><SlidersHorizontal size={18} /></button>
<button className="hover:text-gray-800"><Search size={18} /></button>
</div>
</div>
{/* Isi Tabel */}
@@ -87,37 +90,37 @@ export default function MentorProjectsDetailPage() {
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{targetedTask.submissions?.map((sub, idx) => {
// Cari relasi profil lengkap intern lewat email
const internProfile = targetedProject.interns.find(i => i.email === sub.internEmail);
if (!internProfile) return null;
{task.submissions.map((sub, idx) => {
return (
<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" />
<span className="font-semibold text-sm text-gray-800">{internProfile.name}</span>
{sub.profile_picture
? <img src={sub.profile_picture} className="w-9 h-9 rounded-full object-cover" />
: <div className="w-9 h-9 rounded-full bg-red-800 flex-shrink-0" />
}
<span className="font-semibold text-sm text-gray-800">{sub.full_name}</span>
</td>
{/* Kolom Tanggal Kumpul */}
<td className="px-6 py-4 text-sm text-gray-500">
{sub.submittedAt || "-"}
{sub.submitted_at
? new Date(sub.submitted_at).toLocaleDateString("id-ID")
: "-"}
</td>
{/* Kolom Badge Status */}
<td className="px-6 py-4">
{sub.status === "Done" && (
{sub.display_status === "done" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-green-50 text-green-700 border border-green-200">
<span className="w-1.5 h-1.5 bg-green-500 rounded-full"></span> Done
</span>
)}
{sub.status === "Pending" && (
{sub.display_status === "pending" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-amber-50 text-amber-700 border border-amber-200">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full"></span> Pending
</span>
)}
{sub.status === "Overdue" && (
{sub.display_status === "overdue" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-red-700 text-white border">
<span className="w-1.5 h-1.5 bg-white rounded-full"></span> Overdue
</span>
@@ -130,8 +133,8 @@ export default function MentorProjectsDetailPage() {
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" } }
`/mentor/projects/${slug}/${taskSlug}/${toSlug(sub.full_name)}`,
{ state: { status: sub.display_status === "done" ? "submitted" : sub.display_status === "overdue" ? "overdue" : "pending" } }
)
}
>
@@ -145,8 +148,6 @@ export default function MentorProjectsDetailPage() {
</table>
</div>
</div>
{/* EditSubmissionModal sekarang berada di dalam induk div utama */}
{openModal && (
<EditSubmissionModal
isOpen={openModal}
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Plus } from 'lucide-react';
import ManageInternsModal from './components/AddInternsDialog';
import type { ProjectDetail, ProjectMember } from '../../../types/project.types';
import { useProjectInterns } from '../../../hooks/useProjects';
export default function InternsTab({ project }: { project: ProjectDetail }) {
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -54,7 +55,7 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
>
<div className="flex items-center gap-3">
<span className=" text-sm text-[#333]">
{member.full_name}
{member.email}
</span>
</div>
<span className=" text-[13px] text-[#555] font-semibold">
@@ -1,144 +1,90 @@
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 ───────────────────────────────────────────────────────────────
import { Download, Clock, AlertCircle } from "lucide-react";
import { useTaskSubmissions } from "../../../hooks/useTasks";
import type { DisplayStatus } from "../../../types/task.types";
import { StatusTable } from "./components/StatusSubmissionTable";
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 { slug, taskSlug, nameIntern } = useParams<{
slug: string;
taskSlug: string;
nameIntern: string;
}>();
const location = useLocation();
const status: DisplayStatus = 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>
);
}
const { task, loading, error } = useTaskSubmissions(taskSlug!, slug);
if (loading)
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>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-32 bg-box-secondary rounded-2xl animate-pulse" />
))}
</div>
);
if (error) return <p className="text-red-500 text-sm">{error}</p>;
if (!task) return null;
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
const internSubmission = task.submissions.find(
(s) => toSlug(s.full_name) === nameIntern
);
if (!internSubmission)
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">Submission 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="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" />
{internSubmission.full_name} — {internSubmission.email}
</p>
</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>
{/* Submission Detail */}
<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>
{internSubmission.submission?.file_path && (
<a
href={internSubmission.submission.file_path}
target="_blank"
rel="noopener noreferrer"
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
</a>
)}
</div>
<StatusTable
status={status}
submission={internSubmission.submission}
/>
</div>
</div>
);
}
@@ -1,19 +1,8 @@
import { useRef, useEffect, useState } from "react";
import { X, Calendar } from "lucide-react";
import {
LuCode,
LuAlbum,
LuCloud,
LuSmartphone,
LuSettings,
LuUsers,
LuClipboardList,
LuPenTool,
LuLightbulb,
LuShield,
LuMail,
LuSendHorizontal
} from "react-icons/lu";
import { X } from "lucide-react";
import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../..//../lib/ProjectIcons";
import { LuSendHorizontal, LuMail } from "react-icons/lu";
import { useCreateProject } from "../../../../hooks/useProjects";
interface CreateProjectModalProps {
isOpen: boolean;
@@ -24,7 +13,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
const dialogRef = useRef<HTMLDialogElement>(null);
// State Form Management
const [selectedIcon, setSelectedIcon] = useState("code");
const [selectedIcon, setSelectedIcon] = useState(PROJECT_ICON_CODES[0]);
const [projectName, setProjectName] = useState("");
const [projectDescription, setProjectDescription] = useState("");
const [emailInput, setEmailInput] = useState("");
@@ -34,20 +23,12 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
]);
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const { create, loading, error } = useCreateProject();
// Daftar Icon Pilihan di baris atas
const projectIcons = [
{ id: "code", icon: LuCode },
{ id: "chart", icon: LuAlbum },
{ id: "cloud", icon: LuCloud },
{ id: "mobile", icon: LuSmartphone },
{ id: "settings", icon: LuSettings },
{ id: "users", icon: LuUsers },
{ id: "task", icon: LuClipboardList },
{ id: "design", icon: LuPenTool },
{ id: "idea", icon: LuLightbulb },
{ id: "secure", icon: LuShield },
];
const projectIcons = PROJECT_ICON_CODES.map((id) => ({
id,
icon: PROJECT_ICON_MAP[id],
}));
useEffect(() => {
const dialog = dialogRef.current;
@@ -88,19 +69,19 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
};
// Handler Submit Utama
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const payload = {
icon: selectedIcon,
name: projectName,
const result = await create({
project_icon: selectedIcon,
project_name: projectName,
description: projectDescription,
emails: invitedEmails,
startDate,
endDate
};
console.log("Launching Project:", payload);
alert("Project Launched Successfully!");
onClose();
member_emails: invitedEmails,
start_date: startDate,
end_date: endDate,
});
if (result) {
onClose();
}
};
return (
@@ -125,8 +106,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
</div>
{/* Body Form Modal */}
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-3 md:space-y-4 overflow-y-auto flex-1">
<form onSubmit={handleSubmit} className="p-4 md:p-6 space-y-3 md:space-y-4 overflow-y-auto flex-1">
{/* Section: Select Project Icon */}
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 tracking-wide">
@@ -141,9 +121,9 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
key={item.id}
type="button"
onClick={() => setSelectedIcon(item.id)}
className={`w-8 h-8 flex items-center justify-center rounded-lg border text-lg transition-all ${isSelected
? "border-[#B30000] bg-red-50/50 text-[#B30000] ring-2 ring-[#B30000]/20 font-bold"
: "border-gray-200 bg-gray-50 text-gray-500 hover:bg-gray-100"
className={`w-10 h-10 flex items-center justify-center rounded-lg border text-lg transition-all ${isSelected
? "border-red bg-red-foreground text-red ring-2 ring-red/20 font-bold"
: "border-card-outline bg-card text-font hover:bg-gray-100"
}`}
>
<IconComponent className={isSelected ? "stroke-[2.5]" : "stroke-[2]"} />
@@ -174,7 +154,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
Project Description
</label>
<textarea
rows={4}
rows={3}
placeholder="Outline the project goals, deliverables, and technical requirements..."
value={projectDescription}
onChange={(e) => setProjectDescription(e.target.value)}
@@ -249,7 +229,6 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
className="w-full pl-4 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none"
/>
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
<Calendar className="w-4 h-4" />
</span>
</div>
</div>
@@ -268,12 +247,15 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
className="w-full pl-4 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none"
/>
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
<Calendar className="w-4 h-4" />
</span>
</div>
</div>
</div>
{error && (
<p className="text-xs text-red-600 text-center font-medium">{error}</p>
)}
{/* Action Row Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-6">
<button
@@ -286,9 +268,10 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
<button
type="submit"
className="w-full sm:w-[240px] py-2.5 bg-[#B30000] hover:bg-[#990000] text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide"
disabled={loading}
className="w-full sm:w-[240px] py-2.5 bg-[#B30000] hover:bg-[#990000] disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide"
>
Launch Project
{loading ? "Launching..." : "Launch Project"}
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
</button>
</div>
@@ -41,8 +41,6 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
});
if (result) onSuccess();
};
// JSX sama persis, hanya ganti handleSubmit dan tambah disabled pada button submit:
return (
<dialog ref={dialogRef} className="custom-dialog p-[1.5rem]">
<div className="dialog-header">
@@ -54,28 +52,43 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
<X className="w-5 h-5 stroke-[2.5]" />
</button>
</div>
<form onSubmit={handleSubmit} className="px-6 space-y-4 overflow-y-auto flex-1">
<form onSubmit={handleSubmit} className="px-6 space-y-2 overflow-y-auto flex-1">
<div>
<label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label>
<input type="text" required placeholder="e.g., Design High-Fidelity Prototypes" value={taskTitle}
<input
type="text"
required
placeholder="e.g., Design High-Fidelity Prototypes"
value={taskTitle}
onChange={(e) => setTaskTitle(e.target.value)}
className="w-full px-3 py-4 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Description</label>
<textarea rows={4} placeholder="Outline the requirements and objectives..." value={description}
<textarea
rows={4}
placeholder="Outline the requirements and objectives..."
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Deadline Date</label>
<input type="date" required value={deadlineDate} onChange={(e) => setDeadlineDate(e.target.value)}
<input
type="date"
required
value={deadlineDate}
onChange={(e) => setDeadlineDate(e.target.value)}
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time</label>
<input type="time" required value={specificTime} onChange={(e) => setSpecificTime(e.target.value)}
<input
type="time"
required
value={specificTime}
onChange={(e) => setSpecificTime(e.target.value)}
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
</div>
</div>
@@ -84,9 +97,8 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
<div className="grid grid-cols-2 gap-3">
{(["file_upload", "url_link"] as SubmissionType[]).map((type) => (
<div key={type} onClick={() => setSubmissionType(type)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
submissionType === type ? "border-[#B30000] bg-[#B30000] text-white font-semibold" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
}`}>
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${submissionType === type ? "border-[#B30000] bg-[#B30000] text-white font-semibold" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
}`}>
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === type ? "border-white bg-white" : "border-gray-300"}`}>
{submissionType === type && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
</div>
@@ -1,4 +1,3 @@
import type { Task } from "../../../../lib/mockProjects";
import { useNavigate, useParams } from "react-router-dom";
import type { ProjectTask } from "../../../../types/task.types";
@@ -8,14 +7,12 @@ const formatDate = (date: string) =>
const formatTime = (dateStr: string) =>
new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
export default function MentorTaskCard({ task }: {task: ProjectTask}) {
const navigate = useNavigate();
const { slug } = useParams<{ slug: string }>();
const handleCardClick = () => {
navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`)
navigate(`/mentor/projects/${slug}/${task.slug}?type=${task.submission_type}`)
};
return (
@@ -0,0 +1,102 @@
// components/SubmissionStatusPanel.tsx
import { FileText, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react";
import type { TaskSubmissionData, DisplayStatus } from "../../../../types/task.types";
// ─── StatusBadge ────────────────────────────────────────────────────────────
export function StatusBadge({ status }: { status: DisplayStatus }) {
if (status === "submitted" || status === "done")
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" />
{status === "done" ? "Done" : "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>
);
}
// ─── StatusTable ────────────────────────────────────────────────────────────
interface StatusTableProps {
status: DisplayStatus;
submission: TaskSubmissionData | null;
}
export function StatusTable({ status, submission }: StatusTableProps) {
const hasSubmission = status === "submitted" || status === "done";
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: "Submitted at",
value: submission?.submitted_at
? <span className="text-sm text-gray-700">{new Date(submission.submitted_at).toLocaleString("id-ID")}</span>
: <span className="text-sm text-gray-400">-</span>,
},
{
label: "File submission",
value: hasSubmission && submission?.file_path
? (
<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>{submission.file_path.split("/").pop()}</span>
</div>
)
: hasSubmission && submission?.url_link
? (
<a
href={submission.url_link}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 underline break-all"
>
{submission.url_link}
</a>
)
: <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>
);
}