feat: implement mentor dashboard features including project task management, submission tracking, and modal-based task creation.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user