feat: add react-hot-toast for notifications, enhance project detail page, and update task handling

- Added `react-hot-toast` dependency for improved notification handling.
- Introduced `MentorProjectsDetailPage` for detailed project and task views.
- Updated routing to include task-specific paths.
- Enhanced breadcrumb navigation to reflect new project structure.
- Refactored task handling in `MentorTaskCard` and `ProjectTimeline` components.
- Updated mock data to include detailed task submissions and deadlines.
- Improved UI elements for better user experience in project management.
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-16 17:54:00 +07:00
parent f9bb21a961
commit ff68115fca
19 changed files with 317 additions and 182 deletions
+1
View File
@@ -34,6 +34,7 @@ const breadcrumbRoutes = [
{ path: '/mentor', breadcrumb: 'Home' },
{ 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/intern', breadcrumb: 'List Intern' },
{ path: '/mentor/faq', breadcrumb: 'FAQ' },
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Project } from '../../../lib/mockData';
import type { Project } from '../../../lib/mockProjects';
import ProjectTimeline from './components/ProjectTimeline';
import TaskCard from './components/ProjectTaskCard';
import HeroProject from './components/Hero-project';
@@ -27,7 +27,7 @@ export default function ProjectDetailPage() {
}`;
return (
<div className="">
<div>
<div>
{/* Tabs */}
<div className="flex mb-4">
@@ -1,16 +1,7 @@
import { useState, useEffect } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { DEADLINES } from '../../../../lib/mockDeadline'
interface Deadline {
date: number
month: number
year: number
label: string
}
const DEADLINES: Deadline[] = [
{ date: 30, month: 6, year: 2026, label: 'Tenggat Proyek Internify Internify - 4 May' },
]
const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
const MONTH_NAMES = [
@@ -19,7 +10,7 @@ const MONTH_NAMES = [
]
function getDaysInMonth(year: number, month: number) {
return new Date(year, month+1, 0).getDate()
return new Date(year, month + 1, 0).getDate()
}
function getFirstDayOfMonth(year: number, month: number) {
@@ -27,7 +18,7 @@ function getFirstDayOfMonth(year: number, month: number) {
}
const HomeCalendar = () => {
const [today, setToday] = useState(new Date())
const [today, setToday] = useState(new Date())
useEffect(() => {
const interval = setInterval(() => {
@@ -67,11 +58,18 @@ const [today, setToday] = useState(new Date())
currentYear === today.getFullYear()
const hasDeadline = (day: number) =>
DEADLINES.some(d => d.date === day && d.month - 1 === currentMonth && d.year === currentYear)
DEADLINES.some(d =>
d.date.getDate() === day &&
d.date.getMonth() === currentMonth &&
d.date.getFullYear() === currentYear
)
const upcomingDeadlines = DEADLINES.filter(
d => d.month === currentMonth && d.year === currentYear
)
const upcomingDeadlines = DEADLINES.filter(d => {
return (
d.date.getMonth() === currentMonth &&
d.date.getFullYear() === currentYear
);
}).sort((a, b) => a.date.getTime() - b.date.getTime());
// Previous month trailing days
const prevMonthDays = getDaysInMonth(currentYear, currentMonth - 1 < 0 ? 11 : currentMonth - 1)
@@ -135,16 +133,16 @@ const [today, setToday] = useState(new Date())
{/* Current month days */}
{currentDays.map(day => (
<div key={day} className="flex justify-center items-center py-1 px-4">
<div key={day} className="flex justify-center items-center py-0.5">
{isToday(day) ? (
<span className="w-8 h-7 flex items-center justify-center border-b-2 border-red-800 text-xs font-semibold">
{day}
</span>
) : (
<span
className={`w-full h-7 flex items-center justify-center text-xs rounded
className={`w-full h-7 flex items-center justify-center text-xs
${isBold(day) ? 'font-bold text-gray-800' : 'text-gray-700'}
${hasDeadline(day) ? 'bg-red-700 text-white' : ''}
${hasDeadline(day) ? 'bg-red-800 text-white' : ''}
transition-colors`}
>
{day}
@@ -1,4 +1,4 @@
import type { Task } from "../../../../lib/mockData";
import type { Task } from "../../../../lib/mockProjects";
import { ArrowDownNarrowWide } from "lucide-react";
const formatDate = (date: Date) =>
@@ -11,7 +11,7 @@ export default function ProjectTimeline({ tasks }: { tasks: Task[] }) {
return (
<div>
<div className="flex items-center gap-1.5 mb-4 justify-between">
<span className="font-semibold text-sm text-[#1a1a1a]">
<span className="font-semibold text-md text-[#1a1a1a]">
Timeline
</span>
<ArrowDownNarrowWide className="w-5 h-5 text-gray-500"/>
@@ -29,7 +29,7 @@ export default function ProjectTimeline({ tasks }: { tasks: Task[] }) {
<li key={idx} className="relative mb-6 list-none">
{/* Dot */}
<div
className={`absolute -left-4 top-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${active ? 'bg-[#C0392B]' : 'bg-[#ccc]'
className={`absolute -left-5 top-0.5 w-3 h-3 rounded-full border-2 border-white ${active ? 'bg-[#C0392B]' : 'bg-[#ccc]'
} ${isToday ? 'shadow-md' : ''}`}
/>
<div
@@ -1,5 +1,5 @@
import { useState, useRef } from "react";
import { FileText, UploadCloud, Trash2, X, FileUp } from "lucide-react";
import { FileText, UploadCloud, X, FileUp } from "lucide-react";
import { LuSendHorizontal } from "react-icons/lu";
interface TaskFormFileProps {
@@ -36,7 +36,7 @@ export default function MentorProjectsPage(){
<div className="flex flex-wrap gap-4 items-stretch">
{mockProjects.map(project => (
<MentorProjectCard key={project.id} {...project} />
<MentorProjectCard key={project.id} {...project} tasks={project.tasks} />
))}
</div>
@@ -0,0 +1,141 @@
import { useParams } from "react-router-dom";
import { mockProjects } from "../../../lib/mockProjects";
import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react";
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
// Helper untuk mendapatkan inisial nama avatar
const getInitials = (name: string) => {
return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
};
export default function MentorProjectsDetailPage() {
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>;
}
return (
<div>
{/* Header Utama */}
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-[#1a1a1a]">{targetedTask.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">
<Edit size={16} />
Edit Task
</button>
</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="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}
</p>
</div>
<div className="w-full md:w-120 flex flex-col justify-start">
<h2 className="text-xs font-sans text-gray-400 tracking-wider uppercase mb-2">Deadline</h2>
<div className="flex items-start gap-3">
<div className="text-red-600 mt-0.5">
<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>
</div>
</div>
</div>
{/* Panel Tabel Intern Submissions */}
<div className="border border-gray-300 rounded-xl bg-white overflow-hidden shadow-sm">
{/* Header Tabel Kontrol */}
<div className="p-5 flex justify-between items-center border-b border-gray-200">
<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
</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 */}
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-gray-50 border-b border-gray-200 text-xs font-bold text-gray-400 tracking-wider uppercase">
<th className="px-6 py-4">Intern Name</th>
<th className="px-6 py-4">Submitted Date</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4 text-right">Actions</th>
</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;
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-gray-200 text-gray-500 font-semibold text-xs flex items-center justify-center border border-gray-300">
{getInitials(internProfile.name)}
</div>
<span className="font-semibold text-sm text-gray-800">{internProfile.name}</span>
</td>
{/* Kolom Tanggal Kumpul */}
<td className="px-6 py-4 text-sm text-gray-500">
{sub.submittedAt || "-"}
</td>
{/* Kolom Badge Status */}
<td className="px-6 py-4">
{sub.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" && (
<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" && (
<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>
)}
</td>
{/* 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">
View Submission <span className="text-xs">&rarr;</span>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
}
@@ -1,4 +1,4 @@
import type { Project } from '../../../lib/mockData';
import type { Project } from '../../../lib/mockProjects';
import ProjectTimeline from '../intern/components/ProjectTimeline';
import HeroProject from '../intern/components/Hero-project';
import MentorTaskCard from './components/MentorTaskCard';
@@ -17,7 +17,10 @@ export default function MentorForumTab({ project }: { project: Project }) {
<div className="w-4/5">
{project.tasks.map((task, idx) => (
<MentorTaskCard key={idx} task={task} />
<MentorTaskCard
key={idx}
task={task}
projectName={project.name} />
))}
</div>
</div>
@@ -45,7 +45,7 @@ export default function InternsTab({ project }: { project: Project }) {
>
<div className="flex items-center gap-3">
<span className=" text-sm text-[#333]">
{intern.email}
{intern.name}
</span>
</div>
<span className=" text-[13px] text-[#555] font-semibold">
+2 -3
View File
@@ -1,11 +1,10 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { mockProjects } from '../../../lib/mockProjects';
import MentorForumTab from './ProjectForumTab';
import InternsTab from './ProjectsInternTab';
import TaskTab from './TaskTab';
// ─── ProjectDetailPage ─────────────────────────────────────────────────────────
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
export default function ProjectDetailPage() {
@@ -53,7 +52,7 @@ export default function ProjectDetailPage() {
case 'participants':
return <InternsTab project={project} />;
case 'Task':
return <TaskTab project={project} />; // Tambahkan komponen TaskTab di sini
return <TaskTab project={project} />;
default:
return null;
}
@@ -1,17 +1,28 @@
import { useNavigate } from 'react-router-dom'
import { mockProjects } from '../../../../lib/mockData'
import { useState } from 'react';
import { Plus } from 'lucide-react';
import CreateProjectModal from './CreateProjectDialog';
const HomeMentorProject = () => {
const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
const openDialog = () => {
setIsModalOpen(true);
};
const closeDialog = () => {
setIsModalOpen(false);
};
return (
<div className='flex flex-col'>
<div className='flex flex-row justify-between mb-3 px-5 items-center'>
<h2 className="text-lg font-semibold text-gray-800 mb-4">Projects</h2>
<button className='bg-gray-300 px-2 py-1 rounded-xl font-bold hover:bg-gray-400'>
<span className='text-sm flex flex-row items-center gap-3'>
<Plus className='w-3 h-3'/>
Add
<button
className='bg-gray-300 p-1.5 rounded-lg font-bold hover:bg-gray-400'
onClick={openDialog} >
<span className='text-sm flex flex-row justify-between items-center gap-3.5'>
<Plus className='w-4 h-4' />
Add
</span>
</button>
</div>
@@ -20,7 +31,7 @@ const HomeMentorProject = () => {
return (
<div
key={index}
onClick={() => navigate(`/intern/projects/${project.id}`)}
onClick={() => navigate(`/intern/projects/${project.name}`)}
className="flex items-center gap-4 border border-gray-200 rounded-lg px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors duration-150"
>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-gray-300`}>
@@ -32,6 +43,12 @@ const HomeMentorProject = () => {
)
})}
</div>
{isModalOpen && (
<CreateProjectModal
isOpen={isModalOpen}
onClose={closeDialog}
/>
)}
</div>
)
}
@@ -1,11 +1,12 @@
import { Trash2, UsersRound } from 'lucide-react'
import type { Intern } from '../../../../lib/mockProjects'
import type { Intern, Task } from '../../../../lib/mockProjects'
import { useNavigate } from 'react-router-dom'
interface ProjectCardProps {
id: number | string
name: string
interns: Intern[]
tasks: Task[]
thumbnailUrl?: string
}
@@ -1,34 +1,46 @@
import type { Task } from "../../../../lib/mockData";
import type { Task } from "../../../../lib/mockProjects";
import { useNavigate } from "react-router-dom";
const formatDate = (date: Date) =>
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
export default function MentorTaskCard({ task }: { task: Task }) {
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
interface MentorTaskCardProps {
task: Task;
projectName: string;
}
export default function MentorTaskCard({ task, projectName }: MentorTaskCardProps) {
const navigate = useNavigate();
const handleCardClick = () => {
// Pastikan jika projectName belum ter-render, proses navigasi tidak bikin crash
if (!projectName || !task?.title) return;
const projectSlug = toSlug(projectName);
const taskSlug = toSlug(task.title);
// Rute mutlak menuju target kamu: /mentor/projects/:slug/:taskSlug
navigate(`/mentor/projects/${projectSlug}/${taskSlug}`);
};
return (
<div className="border border-[#eee] rounded-[10px] px-5 py-4 mb-3 bg-white shadow-[0_1px_4px_rgba(0,0,0,0.05)] flex items-start justify-between gap-3">
<div
className="border border-[#eee] rounded-[10px] px-5 py-4 mb-3 bg-white shadow-lg flex items-start justify-between gap-3 cursor-pointer hover:border-gray-300 transition-colors"
onClick={handleCardClick}
>
<div className="flex-1 min-w-0">
<div className=" text-[11px] text-[#888] mb-1">
Deadline : {formatDate(task.deadline.date)} at {task.deadline.time}
<div className="text-[11px] text-[#888] mb-1">
Deadline : {task.deadline ? `${formatDate(task.deadline.date)} at ${task.deadline.time}` : '-'}
</div>
<div className=" font-bold text-[15px] text-[#1a1a1a] mb-1">
<div className="font-bold text-[15px] text-[#1a1a1a] mb-1">
{task.title}
</div>
<div className=" text-[13px] text-[#666]">
<div className="text-[13px] text-[#666]">
{task.description}
</div>
</div>
<div className="shrink-0">
<span
className={`inline-block font-bold text-xs px-2.5 py-[3px] rounded-md tracking-[0.5px] border ${
task.isSubmitted
? "bg-[#27AE60] text-white border-[#27AE60]"
: "bg-white text-[#1a1a1a] border-gray-400"
}`}
>
{task.submittedCount}/{task.totalInterns}
</span>
</div>
</div>
);
}