feat: add modals for managing interns, creating projects, and tasks

- Implemented AddInternsDialog for inviting and managing interns.
- Created CreateProjectDialog for launching new projects with detailed forms.
- Added CreateTaskDialog for task creation with deadline and submission type options.
- Enhanced Header component with navigation functionality.
- Updated MentorProjectCard to navigate correctly to mentor projects.
- Refactored SidebarMentor for improved navigation and mobile responsiveness.
- Adjusted Internify login logic to streamline user role navigation.
- Added CSS styles for consistent layout and design.
- Introduced mock data for interns to facilitate development and testing.
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-06 23:47:22 +07:00
parent e59e63a21e
commit 46cd26f4d2
33 changed files with 1886 additions and 332 deletions
+7 -27
View File
@@ -1,6 +1,3 @@
import { useNavigate } from "react-router-dom";
import { ChevronRight } from "lucide-react";
const mockFAQData = [
{
id: 1,
@@ -20,47 +17,30 @@ const mockFAQData = [
];
export default function FAQPage() {
const navigate = useNavigate();
return (
<div className="w-full max-w-5xl px-6 py-6 font-sans">
{/* Breadcrumb */}
<div className="flex items-center text-xs text-gray-400 font-medium mb-1">
<span className="cursor-pointer hover:underline" onClick={() => navigate("/")}>Home</span>
<ChevronRight className="w-3.5 h-3.5 mx-1" />
<span className="text-[#B30000] font-semibold">FAQ</span>
</div>
<div className="container">
{/* Header Title */}
<div className="mb-10">
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">FAQ</h1>
<p className="text-sm text-gray-500 mt-1">
<h1 className="page-title">FAQ</h1>
<p className="page-title-desc">
Quick answers to common questions about the Internify LMS platform.
</p>
</div>
{/* FAQ Lists */}
<div className="space-y-8 max-w-4xl">
<div className="space-y-8 max-w-auto">
{mockFAQData.map((faq) => (
<div key={faq.id} className="relative pl-6 group">
{/* Indikator Garis/Kotak Kecil Merah di Sisi Kiri sesuai gambar */}
<div className="absolute left-0 top-1.5 w-1 h-3.5 bg-[#B30000] rounded-sm transition-transform duration-200 group-hover:scale-y-125"></div>
<div key={faq.id} className="relative shadow-md group py-5 px-10 rounded-md bg-white">
{/* Item Question */}
<h2 className="text-[17px] font-bold text-[#8A0000] tracking-tight leading-snug mb-2">
<h2 className="text-[17px] font-bold text-[#8A0000] tracking-tight leading-snug mb-2 border-l-4 border-[#8A0000] pl-3">
{faq.question}
</h2>
{/* Item Answer */}
<p className="text-sm text-gray-500 font-medium leading-relaxed tracking-normal">
<p className="text-[14px] pl-8 text-[#8A0000] font-light leading-relaxed tracking-normal">
{faq.answer}
</p>
{/* Divider Line tipis di bawah setiap item, kecuali item terakhir */}
{faq.id !== mockFAQData.length && (
<div className="border-b border-gray-100 pt-8"></div>
)}
</div>
))}
</div>
+4 -13
View File
@@ -1,6 +1,5 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRight, Check, Download, Info, Award } from "lucide-react";
import { Check, Download, Info } from "lucide-react";
import { LuFileSpreadsheet } from "react-icons/lu"; // Alternatif icon task dari react-icons
const initialNotifications = [
@@ -34,7 +33,6 @@ const initialNotifications = [
];
export default function NotificationList() {
const navigate = useNavigate();
const [notifications, setNotifications] = useState(initialNotifications);
const handleMarkAllRead = () => {
@@ -43,19 +41,12 @@ export default function NotificationList() {
};
return (
<div className="w-full max-w-5xl px-6 py-6 font-sans">
{/* Breadcrumb */}
<div className="flex items-center text-xs text-gray-400 font-medium mb-1">
<span className="cursor-pointer hover:underline" onClick={() => navigate("/")}>Home</span>
<ChevronRight className="w-3.5 h-3.5 mx-1" />
<span className="text-[#B30000] font-semibold">Notification</span>
</div>
<div className="container">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">Notifications</h1>
<p className="text-sm text-gray-500 mt-1">
<h1 className="page-title">Notifications</h1>
<p className="page-title-desc">
Keep track of your interns' progress and system updates.
</p>
</div>
+9 -18
View File
@@ -1,6 +1,4 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRight } from "lucide-react"; // dari lucide-react
// Mock Data Profile awal
const mockProfileData = {
@@ -10,7 +8,6 @@ const mockProfileData = {
};
export default function SettingsContent() {
const navigate = useNavigate();
const [formData, setFormData] = useState(mockProfileData);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
@@ -29,24 +26,18 @@ export default function SettingsContent() {
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
return (
<div className="w-full max-w-5xl px-6 py-6 font-sans">
{/* Breadcrumb */}
<div className="flex items-center text-xs text-gray-400 font-medium mb-1">
<span className="cursor-pointer hover:underline" onClick={() => navigate("/")}>Home</span>
<ChevronRight className="w-3.5 h-3.5 mx-1" />
<span className="text-[#B30000] font-semibold">Settings</span>
</div>
<div className="container">
{/* Header Title */}
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">Settings</h1>
<p className="text-sm text-gray-500 mt-1">
<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-gray-100 shadow-[0_2px_12px_rgba(0,0,0,0.01)] overflow-hidden">
<div className="w-full bg-white rounded-xl border border-gray-100 shadow-lg overflow-hidden">
{/* Card Header */}
<div className="p-6 border-b border-gray-100">
@@ -79,7 +70,7 @@ export default function SettingsContent() {
{/* Field: Full Name */}
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
Full Name
</label>
<input
@@ -93,7 +84,7 @@ export default function SettingsContent() {
{/* Field: Email Address */}
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
Email Address
</label>
<input
@@ -108,7 +99,7 @@ export default function SettingsContent() {
{/* Field: Professional Bio */}
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
Professional Bio
</label>
<textarea
@@ -118,13 +109,13 @@ export default function SettingsContent() {
onChange={handleChange}
className="w-full px-3 py-2 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 transition-colors bg-white font-medium resize-none"
/>
<p className="text-[11px] text-gray-400 font-normal pt-0.5">
<p className="text-[11px] text-gray-700 font-normal pt-0.5">
Brief description for your profile. URLs and @mentions are allowed.
</p>
</div>
{/* Form Action Footer Row */}
<div className="flex justify-end pt-4">
<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"
@@ -6,7 +6,7 @@ import TaskTimeline from "./components/TaskTimeline";
// PR: tambahin breadcrumb
export default function HomeInternPage() {
return (
<div className="flex-1 px-14 py-10 bg-gray-50">
<div className="container flex-1">
<HomeHeader />
<div className="flex flex-col gap-5">
<div className="grid grid-cols-2 gap-5">
@@ -36,8 +36,8 @@ export default function ProjectDetailPage() {
}`;
return (
<div className="bg-gray-50 min-h-screen">
<div className="px-24 py-10">
<div className="container">
<div>
{/* Back Button */}
<button
+3 -3
View File
@@ -3,10 +3,10 @@ import { mockProjects } from '../../../lib/mockData'
export default function ProjectsPage() {
return (
<div className="px-14 py-15 bg-gray-50 min-h-screen">
<div className="container">
<div className="flex flex-col gap-1 mb-6">
<span className="text-4xl font-bold">List Projects</span>
<h1 className="text-lg text-gray-500">View assigned projects and track their progress.</h1>
<span className="page-title">List Projects</span>
<h1 className="page-title-desc">View assigned projects and track their progress.</h1>
</div>
{/* Grid card */}
@@ -6,12 +6,12 @@ const SertificatePage = () => {
const data = certificateData; // ganti isReceived di object atas
return (
<div className="px-15 py-10 bg-white min-h-screen">
<div className="container">
{/* Header */}
<div className="mb-6 flex flex-col gap-1">
<div className="mb-12 flex flex-col gap-1">
<h1 className="text-4xl font-bold mb-2">Certificates</h1>
<p className="font-light text-gray-500">View and download internship certificates</p>
<h1 className="page-title">Certificates</h1>
<p className="page-title-desc">View and download internship certificates</p>
</div>
{/* Content */}
@@ -0,0 +1,89 @@
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ChevronRight, Calendar, FileText, Info } from "lucide-react";
import TaskFormFile from "./components/TaskForm-file";
import TaskFormLink from "./components/TaskForm-link";
export default function TaskSubmission() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// State untuk menentukan jenis input ("file" atau "link")
const [submissionType, setSubmissionType] = useState<"file" | "link">("file");
const handleFileSubmit = (file: File) => {
console.log(`Submitting File for Task ${id}:`, file.name);
alert(`File ${file.name} berhasil dikirim!`);
};
const handleLinkSubmit = (link: string) => {
console.log(`Submitting Link for Task ${id}:`, link);
alert(`Link berhasil dikirim!`);
};
return (
<div className="container 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="flex justify-between items-start">
<div>
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Laporan Tugas 2</h1>
<div className="flex items-center gap-1.5 text-xs text-gray-400 font-semibold mt-1.5">
<Calendar className="w-3.5 h-3.5 stroke-[2.5]" />
<span>Due Oct 25, 2024 at 23:59</span>
</div>
</div>
{/* Switcher Demo Sederhana (Bisa kamu hapus jika tipe datang dari database/routing) */}
<div className="flex gap-1 bg-gray-100 p-1 rounded-lg text-xs font-bold">
<button
onClick={() => setSubmissionType("file")}
className={`px-3 py-1 rounded-md transition-colors ${submissionType === "file" ? "bg-white text-gray-900 shadow-sm" : "text-gray-500"}`}
>
Mode File
</button>
<button
onClick={() => setSubmissionType("link")}
className={`px-3 py-1 rounded-md transition-colors ${submissionType === "link" ? "bg-white text-gray-900 shadow-sm" : "text-gray-500"}`}
>
Mode Link
</button>
</div>
</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">
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.
</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">
<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>
</div>
{/* Render Komponen secara Kondisional */}
{submissionType === "file" ? (
<TaskFormFile taskId={id} onSubmitSuccess={handleFileSubmit} />
) : (
<TaskFormLink taskId={id} onSubmitSuccess={handleLinkSubmit} />
)}
{/* Info Box */}
<div className="w-full bg-gray-50 border border-gray-100 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.
</p>
</div>
</div>
</div>
);
}
@@ -2,7 +2,7 @@ import { Lock, Clock } from "lucide-react";
export default function CertificateNotAvailable({ progress, remainingTasks }: { progress: number; remainingTasks: number }) {
return (
<div className="bg-white w-full rounded-2xl border border-gray-100 shadow-sm p-20 flex flex-col items-center text-center max-w-3/5 mx-auto z-10">
<div className="bg-white w-full rounded-2xl border border-gray-100 shadow-sm py-16 p-23 flex flex-col items-center text-center max-w-3/5 mx-auto z-10">
{/* Icon */}
<div className="relative mb-6">
<div className=" w-33 h-33 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
@@ -137,7 +137,7 @@ const HomeCalendar = () => {
className={`w-full h-7 flex items-center justify-center text-xs rounded
${isBold(day) ? 'font-bold text-gray-800' : 'text-gray-700'}
${hasDeadline(day) ? 'bg-red-700 text-white' : ''}
hover:bg-gray-100 cursor-pointer transition-colors`}
transition-colors`}
>
{day}
</span>
@@ -1,9 +1,9 @@
const HomeHeader = () => {
const name = "Karina";
const name = "Karina";
return (
<div>
<div className="flex items-start px-0 mb-5">
<h1 className="text-2xl font-bold text-gray-900">Hi, {name}!</h1>
<h1 className="page-title">Hi, {name}!</h1>
</div>
</div>
)
@@ -1,11 +1,13 @@
import type { Task } from "../../../../lib/mockData";
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 TaskCard({ task }: { task: Task }) {
const navigate = useNavigate();
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-gray-200 rounded-[10px] px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className=" text-[11px] text-[#888] mb-1">
Deadline : {formatDate(task.deadline.date)} at {task.deadline.time}
@@ -24,8 +26,8 @@ export default function TaskCard({ task }: { task: Task }) {
DONE
</span>
) : (
<button className=" bg-white text-[#1a1a1a] border-[1.5px] border-[#d0d0d0] font-semibold text-xs px-3.5 py-[5px] rounded-md cursor-pointer whitespace-nowrap transition-colors duration-150 hover:border-[#C0392B] hover:text-[#C0392B]">
Mark as Done
<button className=" bg-white text-[#1a1a1a] border-[1.5px] border-[#d0d0d0] font-semibold text-xs px-3.5 py-[5px] rounded-md cursor-pointer whitespace-nowrap transition-colors duration-150 hover:border-[#C0392B] hover:text-[#C0392B]" onClick={() => navigate(`/intern/tasks/${task.id}/submit`)}>
Add Submission
</button>
)}
</div>
@@ -24,7 +24,7 @@ const ProjectCard = ({
const navigate = useNavigate()
return (
<div className="bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[260px] flex flex-col">
<div className="bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[230px] flex flex-col">
{/* Thumbnail */}
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden">
{thumbnailUrl && (
@@ -33,41 +33,44 @@ const ProjectCard = ({
</div>
{/* Content */}
<div className="p-3 flex flex-col gap-2">
<p className="text-sm font-semibold text-gray-800 leading-tight">{name}</p>
<div className="min-h-[180px] max-h-[210px] flex-1 flex flex-col justify-between py-3">
<div className="px-3 flex flex-col gap-2">
<p className="text-sm font-semibold text-gray-800 leading-tight">{name}</p>
{/* Mentor */}
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-gray-200 overflow-hidden flex-shrink-0">
{avatarUrl ? (
<img src={avatarUrl} alt={mentor} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gray-300" />
)}
{/* Mentor */}
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-gray-200 overflow-hidden flex-shrink-0">
{avatarUrl ? (
<img src={avatarUrl} alt={mentor} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gray-300" />
)}
</div>
<span className="text-xs text-gray-500 truncate">{mentor}</span>
</div>
<span className="text-xs text-gray-500 truncate">{mentor}</span>
</div>
{/* Progress bar */}
<div className="flex flex-col gap-1">
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-red-600 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
{/* Progress bar */}
<div className="flex flex-col gap-1">
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-red-600 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-[10px] text-gray-400 text-right">
{tasksDone}/{tasksTotal}
</span>
</div>
<span className="text-[10px] text-gray-400 text-right">
{tasksDone}/{tasksTotal}
</span>
</div>
{/* Button */}
<button
onClick={() => navigate(`/intern/projects/${id}`)}
className="w-full bg-red-600 hover:bg-red-700 text-white text-xs font-medium py-1.5 rounded-lg transition-colors"
>
View Project
</button>
<div className="flex items-end justify-end px-3 pb-1">
<button
onClick={() => navigate(`/intern/projects/${id}`)}
className="bg-red-600 hover:bg-red-700 text-white text-xs font-medium py-1.5 px-3 rounded-xl transition-colors"
>
View Project
</button>
</div>
</div>
</div>
)
@@ -1,6 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight, X } from 'lucide-react';
const navItems = [
{ label: "Home", icon: Home, path: "/intern" },
@@ -11,36 +10,62 @@ const navItems = [
const prefItems = [
{ label: "Settings", icon: Settings, path: "/intern/settings" },
{ label: "FAQ", icon: HelpCircle, path: "/intern/faq" },
{ label: "Logout", icon: LogOut, path: "/intern/logout", danger: true },
{ label: "Logout", icon: LogOut, path: "/login-internify", danger: true },
];
function SidebarIntern() {
interface SidebarProps {
isOpen: boolean;
closeSidebar: () => void;
}
export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
const navigate = useNavigate();
const [active, setActive] = useState("/intern");
const location = useLocation();
const handleNav = (path: string) => {
setActive(path);
navigate(path);
closeSidebar(); // Otomatis tutup sidebar setelah klik menu di HP
};
return (
<aside className="w-[279px] bg-white border-r border-gray-100 flex flex-col py-5 px-3 gap-1 sticky top-[73px] h-[calc(100vh-73px)] overflow-y-auto flex-shrink-0">
{/* Brand */}
<div className="flex items-center gap-2.5 px-2 pb-4">
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0">
</div>
<div>
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
<p className="text-[11px] text-gray-400">LMS Portal</p>
<aside
className={`
/* Struktur Dasar Layout */
bg-white border-r border-gray-300 flex flex-col py-5 px-3 gap-1 overflow-y-auto flex-shrink-0 transition-transform duration-300 ease-in-out
/* Kondisi Layar Desktop (>= md) */
md:sticky md:top-[73px] md:h-[calc(100vh-73px)] md:w-[250px] md:translate-x-0 md:z-8
/* Kondisi Layar Mobile (< md) dan Logika Aktif-nya */
fixed top-0 left-0 h-screen w-[270px] z-40 pt-5
${isOpen ? "translate-x-0" : "-translate-x-full"}
`}
>
{/* Brand & Tombol Close untuk Mobile */}
<div className="flex items-center justify-between px-2 pb-4">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0"></div>
<div>
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
<p className="text-[11px] text-gray-400">LMS Portal</p>
</div>
</div>
{/* Tombol Close silang (X): Hanya muncul di HP saat sidebar terbuka */}
<button
onClick={closeSidebar}
className="p-1 text-gray-500 hover:bg-gray-100 rounded-md md:hidden"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="h-px bg-gray-100 my-1" />
<div className="h-px bg-gray-300 my-1" />
{/* Main nav */}
<nav className="flex flex-col gap-0.5">
{navItems.map(({ label, icon: Icon, path }) => {
const isActive = active === path;
const isActive = location.pathname === path;
return (
<button
key={path}
@@ -53,15 +78,13 @@ function SidebarIntern() {
>
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
<span>{label}</span>
{isActive && (
<ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />
)}
{isActive && <ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />}
</button>
);
})}
</nav>
<div className="h-px bg-gray-100 my-1" />
<div className="h-px bg-gray-200 my-1" />
{/* Preferences */}
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
@@ -69,7 +92,7 @@ function SidebarIntern() {
</p>
<nav className="flex flex-col gap-0.5">
{prefItems.map(({ label, icon: Icon, path, danger }) => {
const isActive = active === path;
const isActive = location.pathname === path;
return (
<button
key={path}
@@ -88,10 +111,6 @@ function SidebarIntern() {
);
})}
</nav>
</aside>
);
}
export default SidebarIntern
}
@@ -0,0 +1,121 @@
import { useState, useRef } from "react";
import { FileText, UploadCloud } from "lucide-react";
import { LuSendHorizontal } from "react-icons/lu";
interface TaskFormFileProps {
taskId: string | undefined;
onSubmitSuccess: (file: File) => void;
}
export default function TaskFormFile({ taskId, onSubmitSuccess }: TaskFormFileProps) {
const [file, setFile] = useState<File | null>(null);
const [isDragActive, setIsDragActive] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
validateAndSetFile(e.target.files[0]);
}
};
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setIsDragActive(true);
} else if (e.type === "dragleave") {
setIsDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
validateAndSetFile(e.dataTransfer.files[0]);
}
};
const validateAndSetFile = (selectedFile: File) => {
const allowedExtensions = ["pdf", "docx", "zip"];
const fileExtension = selectedFile.name.split(".").pop()?.toLowerCase();
if (fileExtension && allowedExtensions.includes(fileExtension)) {
if (selectedFile.size <= 10 * 1024 * 1024) {
setFile(selectedFile);
} else {
alert("Ukuran file melebihi batas maksimal 10MB.");
}
} else {
alert("Format file tidak didukung. Harap gunakan PDF, DOCX, atau ZIP.");
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
alert("Silakan pilih atau jatuhkan berkas tugas Anda terlebih dahulu.");
return;
}
onSubmitSuccess(file);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".pdf,.docx,.zip"
className="hidden"
/>
<div
onDragEnter={handleDrag}
onDragOver={handleDrag}
onDragLeave={handleDrag}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`w-full min-h-[180px] border-2 border-dashed rounded-xl flex flex-col items-center justify-center p-6 text-center cursor-pointer transition-all ${
isDragActive
? "border-[#B30000] bg-red-50/40"
: "border-gray-200 bg-gray-50/50 hover:bg-gray-50 hover:border-gray-300"
}`}
>
{file ? (
<div className="space-y-2 pointer-events-none animate-in fade-in duration-200">
<div className="w-12 h-12 bg-green-50 text-green-600 rounded-full flex items-center justify-center mx-auto">
<FileText className="w-6 h-6 stroke-[2]" />
</div>
<p className="text-sm font-bold text-gray-800 max-w-md truncate">{file.name}</p>
<p className="text-xs text-gray-400">
{(file.size / (1024 * 1024)).toFixed(2)} MB • Klik untuk mengganti berkas
</p>
</div>
) : (
<div className="space-y-2 pointer-events-none">
<UploadCloud className="w-10 h-10 text-gray-400 mx-auto stroke-[1.5]" />
<div>
<p className="text-sm font-bold text-gray-700">
Click to upload <span className="font-medium text-gray-500">or drag & drop</span>
</p>
<p className="text-[11px] text-gray-400 font-semibold mt-1">
PDF, DOCX, or ZIP (Max. 10MB)
</p>
</div>
</div>
)}
</div>
<button
type="submit"
className="w-full bg-[#B30000] hover:bg-[#990000] text-white font-bold text-base py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer"
>
Submit Task
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
</button>
</form>
);
}
@@ -0,0 +1,55 @@
import { useState } from "react";
import { Link } from "lucide-react";
import { LuSendHorizontal } from "react-icons/lu";
interface TaskFormLinkProps {
taskId: string | undefined;
onSubmitSuccess: (link: string) => void;
}
export default function TaskFormLink({ taskId, onSubmitSuccess }: TaskFormLinkProps) {
const [submissionLink, setSubmissionLink] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!submissionLink.trim()) {
alert("Silakan masukkan link tugas Anda terlebih dahulu.");
return;
}
onSubmitSuccess(submissionLink);
};
return (
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-1.5">
<label className="text-[11px] font-bold text-gray-700 tracking-wide block">
Submission Link (PDF)
</label>
<div className="relative flex items-center">
<span className="absolute left-3.5 text-gray-400">
<Link className="w-4 h-4 stroke-[2]" />
</span>
<input
type="url"
required
placeholder="https://example.com/your-pdf-link"
value={submissionLink}
onChange={(e) => setSubmissionLink(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-gray-50/50 border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-700 focus:outline-none focus:bg-white focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
/>
</div>
<p className="text-[11px] text-gray-400 font-medium pt-0.5">
Ensure the link is accessible to the mentor (e.g., Google Drive, Dropbox).
</p>
</div>
<button
type="submit"
className="w-full bg-[#B30000] hover:bg-[#990000] text-white font-bold text-base py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer"
>
Submit Task
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
</button>
</form>
);
}
+27 -8
View File
@@ -1,16 +1,26 @@
import { Outlet } from "react-router-dom";
import { useState } from "react";
import Header from './mentor/components/Header';
import SidebarIntern from './intern/components/SidebarIntern';
import SidebarAdmin from '../../Layout/SidebarAdmin';
import SidebarMentor from './mentor/components/SidebarMentor';
import FooterIntern from '../../Layout/Footer-Internify';
export const InternLayout = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
const closeSidebar = () => setIsSidebarOpen(false);
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
<Header />
<div className="flex flex-1">
<SidebarIntern />
<main className="flex-1 flex flex-col min-w-0">
<div className="min-h-screen bg-gray-50 flex flex-col relative">
<Header toggleSidebar={toggleSidebar}/>
<div className="flex flex-1 relative">
<SidebarIntern isOpen={isSidebarOpen} closeSidebar={closeSidebar} />
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black/20 z-20 md:hidden min-h-screen"
onClick={closeSidebar}
/>
)}
<main className="flex-1 flex flex-col">
<Outlet />
</main>
</div>
@@ -20,11 +30,20 @@ export const InternLayout = () => {
};
export const MentorLayout = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
const closeSidebar = () => setIsSidebarOpen(false);
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
<Header />
<Header toggleSidebar={toggleSidebar}/>
<div className="flex flex-1">
<SidebarAdmin />
<SidebarMentor isOpen={isSidebarOpen} closeSidebar={closeSidebar} />
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black/20 z-20 md:hidden min-h-screen"
onClick={closeSidebar}
/>
)}
<main className="flex-1 flex flex-col min-w-0">
<Outlet />
</main>
+39 -39
View File
@@ -1,51 +1,51 @@
import { mockProjects } from '../../../lib/mockData'
import MentorProjectCard from './components/MentorProjectCard'
import { Plus } from 'lucide-react'
import { useNavigate } from 'react-router-dom' // Import useNavigate
import { mockProjects } from '../../../lib/mockData';
import MentorProjectCard from './components/MentorProjectCard';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import CreateProjectModal from './components/CreateProjectDialog';
const MentorProjectsPage = () => {
const navigate = useNavigate()
export default function MentorProjectsPage(){
const [isModalOpen, setIsModalOpen] = useState(false);
const handleJoinProject = () => {
// Navigasi ke halaman tujuan, silakan ganti '/path-tujuan' sesuai rute kamu
navigate('/path-tujuan')
}
const openDialog = () => {
setIsModalOpen(true);
};
const closeDialog = () => {
setIsModalOpen(false);
};
return (
<div className="px-14 py-15 bg-gray-50 min-h-screen">
{/* Breadcrumb simulasi sesuai gambar */}
<div className="text-xs text-gray-500 mb-2">
<span className="text-red-600 font-medium">Home</span> &gt; <span>List Projects</span>
</div>
<div className="flex flex-col gap-1 mb-6">
<h1 className="text-4xl font-bold text-gray-900">List Projects</h1>
<p className="text-lg text-gray-500">Create, manage, and monitor ongoing projects and assignments.</p>
<div className="container">
<div className="flex justify-between items-center mb-6">
<div className="flex flex-col">
<h1 className="page-title">List Projects</h1>
<p className="page-title-desc">Create, manage, and monitor ongoing projects and assignments.</p>
</div>
<div>
{/* Tombol pemicu buka modal */}
<button
onClick={openDialog}
className="flex items-center gap-2 px-4 py-1 bg-[#C0392B] text-white rounded-lg hover:bg-[#A93226] transition-colors duration-150"
>
<Plus className="w-4 h-4" />
<span>Add</span>
</button>
</div>
</div>
<div className="flex flex-wrap gap-4 items-stretch">
{/* Render List Project */}
{mockProjects.map(project => (
<MentorProjectCard key={project.id} {...project} />
))}
{/* Tombol "Join New Project" dengan Border Putus-putus */}
<button
onClick={handleJoinProject}
className="w-[180px] min-h-[230px] border-2 border-dashed border-gray-400 rounded-xl flex flex-col items-center justify-center gap-3 p-4 bg-white hover:bg-gray-100 hover:border-red-600 transition-colors group"
>
{/* Lingkaran Plus */}
<div className="w-12 h-12 rounded-full border border-red-600 flex items-center justify-center text-red-600 group-hover:bg-red-50 transition-colors">
<Plus size={24} />
</div>
{/* Teks Deskripsi */}
<span className="text-sm font-semibold text-red-600 text-center leading-tight">
Join New Project
</span>
</button>
</div>
</div>
)
}
export default MentorProjectsPage
{isModalOpen && (
<CreateProjectModal
isOpen={isModalOpen}
onClose={closeDialog}
/>
)}
</div>
);
};
+240 -5
View File
@@ -1,11 +1,246 @@
import React from 'react'
import React, { useState } from 'react';
import {
PlusCircle,
UploadCloud,
Eye,
Download,
Trash2,
ChevronLeft,
ChevronRight,
DownloadCloud
} from 'lucide-react';
import { LuHistory, LuSendHorizontal } from 'react-icons/lu';
// Mock Data untuk Tabel Riwayat Sertifikat
const initialHistoryData = [
{ id: 1, name: "Jonathan Kristina", email: "JonathanKristina@gmail.com", project: "Internify Project", date: "24 Oct 2024", initials: "JK" },
{ id: 2, name: "Ahmad Faisal", email: "ahmad.faisal@gmail.com", project: "Web Dev Fundamentals", date: "15 Sep 2024", initials: "AF" },
{ id: 3, name: "Siti Rahma", email: "siti.rahma@gmail.com", project: "UI/UX Essentials", date: "10 Aug 2024", initials: "SR" },
{ id: 4, name: "David Beckham", email: "david.b@gmail.com", project: "Engineering Intern 101", date: "05 Aug 2024", initials: "DB" },
];
const MentorCertificatePage = () => {
// State Form Management
const [selectedIntern, setSelectedIntern] = useState("JonathanKristina@gmail.com");
const [selectedProject, setSelectedProject] = useState("Internify Project (Web Development)");
const [issueDate, setIssueDate] = useState("2024-10-24");
const [expiryDate, setExpiryDate] = useState("");
// State Data Tabel
const [historyData, setHistoryData] = useState(initialHistoryData);
const handleIssueCertificate = (e: React.FormEvent) => {
e.preventDefault();
const payload = {
intern: selectedIntern,
project: selectedProject,
issueDate,
expiryDate
};
console.log("Issuing Certificate:", payload);
alert("Certificate Issued Successfully!");
};
const handleDeleteHistory = (id: number) => {
if(confirm("Are you sure you want to delete this certificate log?")) {
setHistoryData(historyData.filter(item => item.id !== id));
}
};
return (
<div>
<div className="px-6 md:px-14 py-8 md:py-12 bg-gray-50 min-h-screen">
{/* Breadcrumb & Header Title */}
<div className="mb-8">
<p className="text-xs font-semibold text-[#B30000] mb-1 flex items-center gap-1 cursor-pointer">
Certificates <span className="text-gray-400 font-normal">&gt;</span>
</p>
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 tracking-tight">Certificates</h1>
<p className="text-sm md:text-base text-gray-500 mt-1">
Administrate, issue, and track professional certifications for all interns.
</p>
</div>
{/* Main Grid Content */}
<div className="grid grid-cols-1 lg:grid-cols-12 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="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>
</div>
<form onSubmit={handleIssueCertificate} className="space-y-5">
{/* Select Intern */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Select Intern</label>
<select
value={selectedIntern}
onChange={(e) => setSelectedIntern(e.target.value)}
className="w-full px-3 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"
>
<option value="JonathanKristina@gmail.com">JonathanKristina@gmail.com</option>
<option value="ahmad.faisal@gmail.com">ahmad.faisal@gmail.com</option>
<option value="siti.rahma@gmail.com">siti.rahma@gmail.com</option>
</select>
</div>
{/* Select Project / Course */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Select Project / Course</label>
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
className="w-full px-3 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"
>
<option value="Internify Project (Web Development)">Internify Project (Web Development)</option>
<option value="Web Dev Fundamentals">Web Dev Fundamentals</option>
<option value="UI/UX Essentials">UI/UX Essentials</option>
</select>
</div>
{/* Certificate Image Upload Area */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Certificate Image</label>
<div className="border-2 border-dashed border-red-200/60 bg-red-50/10 rounded-xl p-6 text-center flex flex-col items-center justify-center cursor-pointer hover:bg-red-50/20 transition-colors group">
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mb-2 group-hover:scale-105 transition-transform">
<UploadCloud className="w-5 h-5 text-[#B30000]" />
</div>
<p className="text-xs font-bold text-gray-800">Upload Certificate Image</p>
<p className="text-[10px] text-gray-400 mt-0.5">PNG, JPG up to 5MB</p>
</div>
</div>
{/* Date Grid */}
<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">Issue Date</label>
<input
type="date"
required
value={issueDate}
onChange={(e) => setIssueDate(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-red-600 focus:ring-1 focus:ring-red-600"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-500 tracking-wide">Expiry (Optional)</label>
<input
type="date"
value={expiryDate}
onChange={(e) => setExpiryDate(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-red-600 focus:ring-1 focus:ring-red-600"
/>
</div>
</div>
{/* Submit Button */}
<button
type="submit"
className="w-full py-3 mt-4 bg-[#B30000] hover:bg-[#990000] text-white text-sm font-bold rounded-xl transition-colors shadow-sm flex items-center justify-center gap-2"
>
<LuSendHorizontal className="w-4 h-4" />
Issue Certificate
</button>
</form>
</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>
{/* Header Table */}
<div className="flex items-center justify-between px-6 py-5 border-b border-gray-50">
<div className="flex items-center gap-2.5">
<LuHistory className="w-5 h-5 text-red-600 stroke-[2.5]" />
<h2 className="text-lg font-bold text-gray-900 tracking-tight">Issued Certificates History</h2>
</div>
<button className="text-xs font-bold text-[#B30000] hover:text-[#990000] flex items-center gap-1.5 bg-red-50/50 px-3 py-1.5 rounded-lg border border-red-100 transition-colors">
<span>Export CSV</span>
<DownloadCloud className="w-3.5 h-3.5" />
</button>
</div>
{/* Responsive Table Wrapper */}
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-gray-50/70 border-b border-gray-100">
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Intern Name</th>
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Project</th>
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Issue Date</th>
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase text-center">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{historyData.map((row) => (
<tr key={row.id} className="hover:bg-gray-50/40 transition-colors">
{/* Kolom Nama Intern + Avatar Ringkas */}
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gray-100 border border-gray-200/60 flex items-center justify-center text-xs font-bold text-gray-500 flex-shrink-0">
{row.initials}
</div>
<div>
<p className="text-sm font-bold text-gray-900 leading-tight">{row.name}</p>
<p className="text-[11px] text-gray-400 mt-0.5">{row.email}</p>
</div>
</div>
</td>
{/* Kolom Nama Proyek */}
<td className="px-6 py-4">
<p className="text-sm text-gray-600 font-medium">{row.project}</p>
</td>
{/* Kolom Tanggal Terbit */}
<td className="px-6 py-4">
<p className="text-sm text-gray-500 font-medium">{row.date}</p>
</td>
{/* Kolom Deretan Aksi */}
<td className="px-6 py-4">
<div className="flex items-center justify-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 Certificate">
<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
onClick={() => handleDeleteHistory(row.id)}
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
title="Delete Log"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Bagian Footer Tabel & Paginasi */}
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50/30 flex flex-col sm:flex-row gap-3 items-center justify-between text-xs font-medium text-gray-500">
<p>Showing 1 to {historyData.length} of 1,284 entries</p>
<div className="flex items-center gap-1">
<button className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50 disabled:opacity-50">
<ChevronLeft className="w-4 h-4" />
</button>
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-[#B30000] text-white font-bold shadow-sm">1</button>
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-white border border-gray-200 text-gray-600 hover:bg-gray-50">2</button>
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-white border border-gray-200 text-gray-600 hover:bg-gray-50">3</button>
<button className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50">
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
)
);
}
export default MentorCertificatePage
export default MentorCertificatePage;
+1 -1
View File
@@ -4,7 +4,7 @@ import HomeMentorProject from './components/HomeMentorProject'
const HomeMentorPage = () => {
return (
<div className="flex-1 px-14 py-10 bg-gray-50">
<div className="container flex-1 bg-gray-50">
<HomeHeader />
<div className="flex flex-col gap-5">
<div className="">
+195 -4
View File
@@ -1,5 +1,196 @@
export default function MentorInternPage() {
return(
<div></div>
)
import { useState, useMemo } from "react";
import { Search, SlidersHorizontal, Plus, Trash2 } from "lucide-react";
import { mockInternsData } from "../../../lib/mockData";
export default function InternsManagement() {
// State Filter & Search
const [searchEmail, setSearchEmail] = useState("");
const [selectedProject, setSelectedProject] = useState("All Projects");
// Handler Aksi Kursor/Tombol
const handleAddAssignment = (id: number) => {
alert(`Assign/Add action clicked for intern ID: ${id}`);
};
const handleDeleteIntern = (id: number) => {
alert(`Deleted intern ID: ${id}`);
};
// Logic Filtering Data menggunakan useMemo
const filteredInterns = useMemo(() => {
return mockInternsData.filter((intern) => {
const matchesEmail = intern.email.toLowerCase().includes(searchEmail.toLowerCase());
const matchesProject =
selectedProject === "All Projects" ||
intern.projectName.toLowerCase() === selectedProject.toLowerCase();
return matchesEmail && matchesProject;
});
}, [searchEmail, selectedProject]);
return (
<div className="w-full px-16 py-12 font-sans">
{/* Header Title */}
<div className="mb-6">
<h1 className="page-title">Interns Management</h1>
<p className="page-title-desc">
Manage role-based access and project assignments for active interns.
</p>
</div>
{/* 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">
{/* 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">
<Search className="w-4 h-4" />
</span>
<input
type="text"
placeholder="Filter by email"
value={searchEmail}
onChange={(e) => setSearchEmail(e.target.value)}
className="w-full pl-9 pr-4 py-2 bg-gray-100 border border-transparent rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:bg-white focus:border-gray-200 font-medium"
/>
</div>
{/* Select Project Dropdown */}
<div className="relative w-full sm:w-48">
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
className="w-full pl-3 pr-8 py-2 bg-gray-100 border border-transparent rounded-lg text-sm text-gray-700 font-medium appearance-none focus:outline-none focus:bg-white focus:border-gray-200"
>
<option value="All Projects">All Projects</option>
<option value="Internify Project">Internify Project</option>
<option value="MBG Project">MBG Project</option>
<option value="Unassigned">Unassigned</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500">
<svg className="fill-current h-4 w-4" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
</svg>
</div>
</div>
{/* Icon Sliders / Filter Button */}
<button className="p-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors hidden sm:block">
<SlidersHorizontal className="w-4 h-4" />
</button>
</div>
{/* Right Info Box Stats */}
<div className="bg-white border border-gray-100 rounded-xl px-6 py-2.5 flex items-center justify-around text-center shadow-[0_2px_10px_rgba(0,0,0,0.01)] w-full lg:max-w-sm ml-auto">
<div>
<span className="block text-xl font-bold text-red-600 leading-none">124</span>
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase mt-1 block">
TOTAL INTERNS
</span>
</div>
<div className="h-8 w-px bg-gray-100 mx-2"></div>
<div>
<span className="block text-xl font-bold text-green-600 leading-none">92</span>
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase mt-1 block">
ACTIVE ASSIGNMENTS
</span>
</div>
</div>
</div>
{/* Main Table Container Block */}
<div className="w-full bg-white rounded-xl border border-gray-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] overflow-hidden">
{/* Red Table Header Block */}
<div className="bg-[#B30000] text-white px-6 py-3.5 flex items-center text-xs font-bold uppercase tracking-wider">
<div className="w-1/2">Intern Details</div>
<div className="w-1/3">Current Project</div>
<div className="w-1/6 text-right pr-4">Actions</div>
</div>
{/* Table Body Iteration Rows */}
<div className="divide-y divide-gray-100">
{filteredInterns.length > 0 ? (
filteredInterns.map((intern, index) => {
const isUnassigned = intern.projectName === "Unassigned";
return (
<div key={`${intern.id}-${index}`} className="px-6 py-4 flex items-center hover:bg-gray-50/60 transition-colors">
{/* Column 1: Intern Details (Avatar + Name & Email) */}
<div className="w-1/2 flex items-center gap-3">
{intern.avatar ? (
<img
src={intern.avatar}
alt={intern.name}
className="w-10 h-10 rounded-full object-cover border border-gray-100 shadow-sm"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-teal-400 to-emerald-500 flex items-center justify-center border border-gray-100 shadow-sm text-white text-xs font-bold">
{intern.name.charAt(0)}
</div>
)}
<div>
<h4 className="text-sm font-bold text-gray-900 leading-tight">{intern.name}</h4>
<p className="text-xs text-gray-400 font-medium mt-0.5">{intern.email}</p>
</div>
</div>
{/* Column 2: Current Project Title & Role Accent */}
<div className="w-1/3">
<h5 className={`text-sm font-semibold leading-tight ${isUnassigned ? 'text-gray-500 italic' : 'text-gray-900'}`}>
{intern.projectName}
</h5>
<p className={`text-xs font-bold mt-0.5 ${isUnassigned ? 'text-red-500' : 'text-red-600'}`}>
{intern.role}
</p>
</div>
{/* Column 3: Action Buttons Block Right aligned */}
<div className="w-1/6 flex items-center justify-end gap-3.5 pr-2">
<button
onClick={() => handleAddAssignment(intern.id)}
className="p-1.5 bg-[#B30000] hover:bg-[#990000] text-white rounded-lg transition-colors shadow-sm"
>
<Plus className="w-4 h-4 stroke-[3]" />
</button>
<button
onClick={() => handleDeleteIntern(intern.id)}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4 stroke-[2]" />
</button>
</div>
</div>
);
})
) : (
<div className="text-center py-12 text-sm text-gray-400 font-medium">
No interns found matching the filter criteria.
</div>
)}
</div>
{/* Red Table Pagination Bar Footer */}
<div className="bg-[#B30000] text-white px-6 py-3.5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs font-medium">
<div>
Showing <span className="font-bold">{filteredInterns.length}</span> of 124 interns
</div>
<div className="flex items-center gap-2 self-end sm:self-auto">
<button className="px-3 py-1.5 bg-black/20 hover:bg-black/30 rounded-md font-semibold opacity-50 cursor-not-allowed transition-colors">
Previous
</button>
<button className="px-4 py-1.5 bg-white text-gray-900 hover:bg-gray-100 rounded-md font-bold shadow-sm transition-colors">
Next
</button>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,109 @@
import { useRef, useEffect, useState } from "react";
import { X, Search, Plus, Trash2 } from "lucide-react";
interface ManageInternsModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function ManageInternsModal({ isOpen, onClose }: ManageInternsModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
const [searchQuery, setSearchQuery] = useState("Jonathankristin@gmail.com");
// Data Intern Terundang (Simulasi list atas yang ada ikon sampah)
const [invitedList, setInvitedList] = useState([
{ id: 1, email: "Jonathankristina@gmail.com", initials: "JK" }
]);
// Data Rekomendasi/Hasil Pencarian (Simulasi list bawah yang ada ikon plus)
const [searchResult, setSearchResult] = useState([
{ id: 2, email: "Jonathankristin@gmail.com", initials: "JK" }
]);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
dialog.showModal();
document.body.style.overflow = "hidden";
} else {
dialog.close();
document.body.style.overflow = "unset";
}
const handleCancel = (e: Event) => {
e.preventDefault();
onClose();
};
dialog.addEventListener("cancel", handleCancel);
return () => {
dialog.removeEventListener("cancel", handleCancel);
document.body.style.overflow = "unset";
};
}, [isOpen, onClose]);
return (
<dialog
ref={dialogRef}
className="fixed inset-0 m-auto flex flex-col w-[calc(100%-2rem)] max-w-[540px] max-h-[calc(100vh-4rem)] rounded-2xl border border-gray-100 bg-white p-6 shadow-2xl backdrop:bg-black/40 backdrop:backdrop-blur-sm animate-in fade-in zoom-in-95 duration-200"
>
{/* Search Input Area */}
<div className="relative flex items-center mb-4">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by email..."
className="w-full pl-4 pr-10 py-3 bg-white border border-gray-200 rounded-xl text-sm placeholder-gray-400 text-gray-700 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium shadow-sm"
/>
<Search className="absolute right-3.5 w-4 h-4 text-gray-400" />
</div>
{/* List Container (Scrollable jika data banyak) */}
<div className="overflow-y-auto space-y-3 pr-1 flex-1">
{/* List 1: Intern Yang Sudah Tergabung / Ditambahkan */}
{invitedList.map((intern) => (
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gray-200 border border-gray-300 flex items-center justify-center text-xs font-bold text-gray-500">
{intern.initials}
</div>
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
</div>
<button
onClick={() => setInvitedList(invitedList.filter(i => i.id !== intern.id))}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
{/* List 2: Hasil Pencarian / Belum Ditambahkan */}
{searchResult.map((intern) => (
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gray-200 border border-gray-300 flex items-center justify-center text-xs font-bold text-gray-500">
{intern.initials}
</div>
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
</div>
<button
onClick={() => {
setInvitedList([...invitedList, intern]);
setSearchResult(searchResult.filter(i => i.id !== intern.id));
}}
className="p-2 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors"
>
<Plus className="w-5 h-5" />
</button>
</div>
))}
</div>
</dialog>
);
}
@@ -0,0 +1,299 @@
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";
interface CreateProjectModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function CreateProjectModal({ isOpen, onClose }: CreateProjectModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
// State Form Management
const [selectedIcon, setSelectedIcon] = useState("code");
const [projectName, setProjectName] = useState("");
const [projectDescription, setProjectDescription] = useState("");
const [emailInput, setEmailInput] = useState("");
const [invitedEmails, setInvitedEmails] = useState<string[]>([
"alex.dev@gmail.com",
"sarah.engineer@gmail.com"
]);
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
// 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 },
];
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
dialog.showModal();
document.body.style.overflow = "hidden";
} else {
dialog.close();
document.body.style.overflow = "unset";
}
const handleCancel = (e: Event) => {
e.preventDefault();
onClose();
};
dialog.addEventListener("cancel", handleCancel);
return () => {
dialog.removeEventListener("cancel", handleCancel);
document.body.style.overflow = "unset";
};
}, [isOpen, onClose]);
// Fungsi menambah email undangan baru
const handleAddEmail = (e: React.FormEvent) => {
e.preventDefault();
if (emailInput.trim() && !invitedEmails.includes(emailInput.trim())) {
setInvitedEmails([...invitedEmails, emailInput.trim()]);
setEmailInput("");
}
};
// Fungsi menghapus email undangan
const handleRemoveEmail = (emailToRemove: string) => {
setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove));
};
// Handler Submit Utama
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const payload = {
icon: selectedIcon,
name: projectName,
description: projectDescription,
emails: invitedEmails,
startDate,
endDate
};
console.log("Launching Project:", payload);
alert("Project Launched Successfully!");
onClose();
};
return (
<dialog
ref={dialogRef}
className="fixed inset-0 m-auto flex flex-col w-[calc(100%-2rem)] max-w-[620px] max-h-[calc(100vh-4rem)] rounded-2xl border border-gray-100 bg-white p-0 shadow-2xl backdrop:bg-black/40 backdrop:backdrop-blur-sm animate-in fade-in zoom-in-95 duration-200"
>
{/* Header Modal */}
<div className="flex items-start justify-between px-6 md:px-8 py-4 md:py-5 border-b border-gray-100">
<div>
<h2 className="text-xl font-bold text-gray-900 tracking-tight">Create New Project</h2>
<p className="text-xs text-gray-400 font-medium mt-0.5">
Launch a new internship assignment or trackable milestone.
</p>
</div>
<button
onClick={onClose}
className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors mt-1"
>
<X className="w-5 h-5 stroke-[2.5]" />
</button>
</div>
{/* Body Form Modal */}
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-4 md:space-y-5 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">
Select Project Icon
</label>
<div className="flex flex-wrap gap-2">
{projectIcons.map((item) => {
const IconComponent = item.icon;
const isSelected = selectedIcon === item.id;
return (
<button
key={item.id}
type="button"
onClick={() => setSelectedIcon(item.id)}
className={`w-10 h-10 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"
}`}
>
<IconComponent className={isSelected ? "stroke-[2.5]" : "stroke-[2]"} />
</button>
);
})}
</div>
</div>
{/* Section: Project Name */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">
Project Name
</label>
<input
type="text"
required
placeholder="e.g., Q3 Infrastructure Upgrade"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
/>
</div>
{/* Section: Project Description */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">
Project Description
</label>
<textarea
rows={4}
placeholder="Outline the project goals, deliverables, and technical requirements..."
value={projectDescription}
onChange={(e) => setProjectDescription(e.target.value)}
className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium resize-none leading-relaxed"
/>
</div>
{/* Section: Invite via Gmail */}
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 tracking-wide">
Invite via Gmail
</label>
<div className="relative flex items-center">
<span className="absolute left-3.5 text-gray-400">
<LuMail className="w-4 h-4 stroke-[2.5]" />
</span>
<input
type="email"
placeholder="intern.name@gmail.com"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddEmail(e);
}
}}
className="w-full pl-10 pr-20 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
/>
<button
type="button"
onClick={handleAddEmail}
className="absolute right-2 px-3 py-1 bg-gray-100 hover:bg-gray-200 text-gray-800 text-[11px] font-bold rounded-md transition-colors tracking-wider uppercase"
>
ADD
</button>
</div>
{/* Badges List Email */}
<div className="flex flex-wrap gap-1.5 pt-1">
{invitedEmails.map((email) => (
<div
key={email}
className="flex items-center gap-1 bg-gray-100 text-gray-600 px-2.5 py-1 rounded-full text-xs font-medium border border-gray-200/60"
>
<span>{email}</span>
<button
type="button"
onClick={() => handleRemoveEmail(email)}
className="text-gray-400 hover:text-gray-600 rounded-full p-0.5"
>
<X className="w-3 h-3 stroke-[3]" />
</button>
</div>
))}
</div>
</div>
{/* Section: Dates Grid (Start Date & End Date) */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Start Date */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">
Start Date
</label>
<div className="relative">
<input
type="date"
required
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
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>
{/* End Date */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">
End Date
</label>
<div className="relative">
<input
type="date"
required
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
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>
{/* Action Row Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-6">
<button
type="button"
onClick={onClose}
className="w-full sm:w-[150px] py-2.5 border border-gray-200 rounded-xl text-sm font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<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"
>
Launch Project
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
</button>
</div>
</form>
</dialog>
);
}
@@ -0,0 +1,190 @@
import { useRef, useEffect, useState } from "react";
import { X, Calendar, Clock, FileText, Link } from "lucide-react";
interface CreateTaskModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
// Form States
const [taskTitle, setTaskTitle] = useState("");
const [description, setDescription] = useState("");
const [deadlineDate, setDeadlineDate] = useState("");
const [specificTime, setSpecificTime] = useState("");
const [submissionType, setSubmissionType] = useState<"file" | "url">("file");
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
dialog.showModal();
document.body.style.overflow = "hidden";
} else {
dialog.close();
document.body.style.overflow = "unset";
}
const handleCancel = (e: Event) => {
e.preventDefault();
onClose();
};
dialog.addEventListener("cancel", handleCancel);
return () => {
dialog.removeEventListener("cancel", handleCancel);
document.body.style.overflow = "unset";
};
}, [isOpen, onClose]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const payload = { taskTitle, description, deadlineDate, specificTime, submissionType };
console.log("Saving Task:", payload);
alert("Task Saved Successfully!");
onClose();
};
return (
<dialog
ref={dialogRef}
className="fixed inset-0 m-auto flex flex-col w-[calc(100%-2rem)] max-w-[480px] max-h-[calc(100vh-4rem)] rounded-2xl border border-gray-100 bg-white p-0 shadow-2xl backdrop:bg-black/40 backdrop:backdrop-blur-sm animate-in fade-in zoom-in-95 duration-200"
>
{/* Header Modal */}
<div className="flex items-start justify-between px-6 py-5 border-b border-gray-50">
<div>
<h2 className="text-xl font-bold text-[#B30000] tracking-tight">Create New Task</h2>
<p className="text-xs text-gray-400 font-medium mt-0.5">
Fill in the details for the next intern assignment.
</p>
</div>
<button onClick={onClose} className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors">
<X className="w-5 h-5 stroke-[2.5]" />
</button>
</div>
{/* Body Form Modal (Scrollable jika layar pendek) */}
<form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto flex-1">
{/* Task Title */}
<div className="space-y-1.5">
<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}
onChange={(e) => setTaskTitle(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]"
/>
</div>
{/* Description */}
<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}
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>
{/* Deadline Grid (Date & Time) */}
<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>
<div className="relative">
<input
type="date"
required
value={deadlineDate}
onChange={(e) => setDeadlineDate(e.target.value)}
className="w-full pl-3 pr-9 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]"
/>
<Calendar className="absolute right-3 top-2.5 w-4 h-4 text-gray-400 pointer-events-none" />
</div>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time (Jam)</label>
<div className="relative">
<input
type="time"
required
value={specificTime}
onChange={(e) => setSpecificTime(e.target.value)}
className="w-full pl-3 pr-9 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]"
/>
<Clock className="absolute right-3 top-2.5 w-4 h-4 text-gray-400 pointer-events-none" />
</div>
</div>
</div>
{/* Submission Type Card Radio */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Submission Type</label>
<div className="grid grid-cols-2 gap-3">
{/* Opsi 1: File Upload */}
<div
onClick={() => setSubmissionType("file")}
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
submissionType === "file"
? "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 === "file" ? "border-white bg-white" : "border-gray-300"}`}>
{submissionType === "file" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
</div>
<FileText className="w-4 h-4" />
<span className="text-xs">File Upload</span>
</div>
{/* Opsi 2: URL Link */}
<div
onClick={() => setSubmissionType("url")}
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
submissionType === "url"
? "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 === "url" ? "border-white bg-white" : "border-gray-300"}`}>
{submissionType === "url" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
</div>
<Link className="w-4 h-4" />
<span className="text-xs">URL Link</span>
</div>
</div>
</div>
<div className="h-px bg-gray-100 my-2" />
{/* Action Buttons */}
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="px-5 py-2 border border-gray-200 rounded-xl text-xs font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md"
>
Save Task
</button>
</div>
</form>
</dialog>
);
}
@@ -1,25 +1,33 @@
import { Bell } from "lucide-react"
import { Bell, Menu } from "lucide-react"
import humiclogo from "../../../../assets/logo.png";
import { useNavigate } from "react-router-dom";
const Header = () => {
export default function Header({ toggleSidebar }: { toggleSidebar: () => void }) {
const nav = useNavigate();
return (
<header className='sticky top-0 flex w-full justify-between items-center py-4 px-12 bg-white shadow-md z-10'>
<div className=''>
<img
src={humiclogo}
alt="Humic Logo"
className="w-[130px] cursor-pointer"
/>
<header className='sticky top-0 flex w-full justify-between items-center py-4 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"
>
<Menu className="w-6 h-6" />
</button>
<button onClick={()=>nav('/intern')}>
<img src={humiclogo} alt="Humic Logo" className="w-[130px] cursor-pointer" />
</button>
</div>
<div className="flex flex-row gap-4 items-center">
<Bell className="w-6 h-6 hover:text-red-800"/>
<div className="bg-blue-500 rounded-full w-8 h-8">
<div className="flex flex-row gap-6 items-center">
<button onClick={()=>nav("notifications")}>
<Bell className="w-6 h-6 hover:text-red-800"/>
</button>
<div className="bg-blue-500 rounded-xl w-9 h-9">
</div>
</div>
</header>
)
}
export default Header
}
@@ -17,7 +17,7 @@ export default function MentorProjectCard({
}: ProjectCardProps) {
const navigate = useNavigate();
const handleCardClick = () => {
navigate(`/admin/projects/${id}`)
navigate(`/mentor/projects/${id}`)
};
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -1,69 +1,91 @@
import { useNavigate } from 'react-router-dom';
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight, X } from 'lucide-react';
const navItems = [
const navItems = [
// belum kutambahin
{ label: "Home", icon: Home, path: "" },
{ label: "List Projects", icon: FolderKanban, path: "" },
{ label: "Interns", icon: FolderKanban, path: "" },
{ label: "Certificates", icon: Award, path: "" },
{ label: "Home", icon: Home, path: "/mentor" },
{ label: "List Projects", icon: FolderKanban, path: "/mentor/projects" },
{ label: "Interns", icon: FolderKanban, path: "/mentor/intern" },
{ label: "Certificates", icon: Award, path: "/mentor/certificates" },
];
const prefItems = [
{ label: "Settings", icon: Settings, path: "/home-intern/settings" },
{ label: "FAQ", icon: HelpCircle, path: "/home-intern/faq" },
{ label: "Logout", icon: LogOut, path: "/home-intern/logout", danger: true },
{ label: "Settings", icon: Settings, path: "/mentor/settings" },
{ label: "FAQ", icon: HelpCircle, path: "/mentor/faq" },
{ label: "Logout", icon: LogOut, path: "/login-internify", danger: true },
];
function SidebarIntern() {
interface SidebarProps {
isOpen: boolean;
closeSidebar: () => void;
}
export default function SidebarMentor({ isOpen, closeSidebar }: SidebarProps) {
const navigate = useNavigate();
const [active, setActive] = useState("/home-intern");
const location = useLocation();
const handleNav = (path: string) => {
setActive(path);
navigate(path);
closeSidebar();
};
return (
<aside className="w-[240px] bg-white border-r border-gray-100 flex flex-col py-5 px-3 gap-1 sticky top-[73px] h-[calc(100vh-73px)] overflow-y-auto flex-shrink-0">
{/* Brand */}
<div className="flex items-center gap-2.5 px-2 pb-4">
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0">
</div>
<div>
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
<p className="text-[11px] text-gray-400">LMS Portal</p>
<aside
className={`
/* Struktur Dasar Layout */
bg-white border-r border-gray-300 flex flex-col py-5 px-3 gap-1 overflow-y-auto flex-shrink-0 transition-transform duration-300 ease-in-out
/* Kondisi Layar Desktop (>= md) */
md:sticky md:top-[73px] md:h-[calc(100vh-73px)] md:w-[250px] md:translate-x-0 md:z-8
/* Kondisi Layar Mobile (< md) dan Logika Aktif-nya */
fixed top-0 left-0 h-screen w-[270px] z-40 pt-5
${isOpen ? "translate-x-0" : "-translate-x-full"}
`}
>
{/* Brand & Tombol Close untuk Mobile */}
<div className="flex items-center justify-between px-2 pb-4">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0"></div>
<div>
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
<p className="text-[11px] text-gray-400">LMS Portal</p>
</div>
</div>
<button
onClick={closeSidebar}
className="p-1 text-gray-500 hover:bg-gray-100 rounded-md md:hidden"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="h-px bg-gray-100 my-1" />
<div className="h-px bg-gray-300 my-1" />
{/* Main nav */}
<nav className="flex flex-col gap-0.5">
{navItems.map(({ label, icon: Icon, path }) => {
const isActive = active === path;
const isActive = location.pathname === path;
return (
<button
key={path}
onClick={() => handleNav(path)}
className={`flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-[13.5px] font-medium text-left transition-colors
${isActive
${isActive
? "bg-red-50 text-red-600 font-semibold border-l-4 border-red-600"
: "text-gray-500 hover:bg-red-50 hover:text-red-600"
}`}
>
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
<span>{label}</span>
{isActive && (
<ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />
)}
{isActive && <ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />}
</button>
);
})}
</nav>
<div className="h-px bg-gray-100 my-1" />
<div className="h-px bg-gray-200 my-1" />
{/* Preferences */}
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
@@ -71,7 +93,7 @@ function SidebarIntern() {
</p>
<nav className="flex flex-col gap-0.5">
{prefItems.map(({ label, icon: Icon, path, danger }) => {
const isActive = active === path;
const isActive = location.pathname === path;
return (
<button
key={path}
@@ -94,6 +116,3 @@ function SidebarIntern() {
</aside>
);
}
export default SidebarIntern
+1 -4
View File
@@ -31,10 +31,7 @@ export default function InternifyLogin() {
);
if (user) {
// 3. Gunakan navigate() sesuai dengan role masing-masing
if (user.role === "admin") {
navigate("/admin");
} else if (user.role === "intern") {
if (user.role === "intern") {
navigate("/intern");
} else if (user.role === "mentor") {
navigate("/mentor");