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