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:
@@ -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,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">→</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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user