feat: Implement task editing functionality and improve UI components
- Added EditTaskModal component for editing tasks. - Integrated EditTaskModal into TaskTab for task editing. - Refactored AddInternsDialog for better UI and functionality. - Created AddInternsToProjects component for adding interns to projects. - Enhanced CreateProjectDialog and CreateTaskDialog with improved styling. - Introduced EditSubmissionTask component for editing submission tasks. - Added ProtectedRoutes for route protection based on user roles. - Implemented API utility for handling requests with authorization. - Removed obsolete InternRoute and MentorRoute files. - Updated global CSS styles for better responsiveness and aesthetics.
This commit is contained in:
+41
-40
@@ -28,23 +28,24 @@ import Dashboard from "./Pages/Dashboard";
|
||||
import EditFaq from "./Pages/EditFAQ";
|
||||
import EditFeedback from "./Pages/EditFeedback";
|
||||
import EditLowongan from "./Pages/EditLowongan";
|
||||
import HomeInternPage from "./Pages/internify/intern/HomeInternPage";
|
||||
import { InternLayout, MentorLayout } from "./Pages/internify/layout";
|
||||
import ProjectsPage from "./Pages/internify/intern/ProjectsPage";
|
||||
import SertificatePage from "./Pages/internify/intern/SertificatePage";
|
||||
import ProjectDetailPage from "./Pages/internify/intern/ProjectDetailPage";
|
||||
import HomeMentorPage from "./Pages/internify/mentor/HomePage";
|
||||
import MentorProjectsPage from "./Pages/internify/mentor/BaseProjectsPage";
|
||||
import MentorCertificatePage from "./Pages/internify/mentor/CertificatePage";
|
||||
import MentorInternPage from "./Pages/internify/mentor/InternsPage";
|
||||
import MentorDetailProject from "./Pages/internify/mentor/ProjectsPage";
|
||||
import InternifyLogin from "./Pages/internify/page"
|
||||
import FAQPage from "./Pages/internify/FAQpage";
|
||||
import NotificationList from "./Pages/internify/NotificationList";
|
||||
import SettingsContent from "./Pages/internify/SettingsPage";
|
||||
import TaskSubmission from "./Pages/internify/intern/TaskSubmission";
|
||||
import SertificateList from "./Pages/internify/intern/SertificateList";
|
||||
import MentorProjectsDetailPage from "./Pages/internify/mentor/MentorProjectDetailPage";
|
||||
import HomeInternPage from "./Pages/Internify/intern/HomeInternPage";
|
||||
import { InternLayout, MentorLayout } from "./Pages/Internify/layout";
|
||||
import ProjectsPage from "./Pages/Internify/intern/ProjectsPage";
|
||||
import SertificatePage from "./Pages/Internify/intern/SertificatePage";
|
||||
import ProjectDetailPage from "./Pages/Internify/intern/ProjectDetailPage";
|
||||
import HomeMentorPage from "./Pages/Internify/mentor/HomePage";
|
||||
import MentorProjectsPage from "./Pages/Internify/mentor/BaseProjectsPage";
|
||||
import MentorCertificatePage from "./Pages/Internify/mentor/CertificatePage";
|
||||
import MentorInternPage from "./Pages/Internify/mentor/InternsPage";
|
||||
import MentorDetailProject from "./Pages/Internify/mentor/ProjectsPage";
|
||||
import InternifyLogin from "./Pages/Internify/LoginInternify"
|
||||
import FAQPage from "./Pages/Internify/FAQpage";
|
||||
import NotificationList from "./Pages/Internify/NotificationList";
|
||||
import SettingsContent from "./Pages/Internify/SettingsPage";
|
||||
import TaskSubmission from "./Pages/Internify/intern/TaskSubmission";
|
||||
import SertificateList from "./Pages/Internify/intern/SertificateList";
|
||||
import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage";
|
||||
import ProtectedRoutes from "./Pages/utils/ProtectedRoute";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -60,7 +61,6 @@ function App() {
|
||||
<Route path="/details-product/:id" element={<DetailsProduct />} />
|
||||
<Route path="/login-admin" element={<LoginAdmin />} />
|
||||
|
||||
{/* Endpoint khusus admin login langsung di inject ajah */}
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route path="/add-product-admin" element={<AddProduct />} />
|
||||
<Route path="/internships-list" element={<InternshipList />} />
|
||||
@@ -85,29 +85,30 @@ function App() {
|
||||
</Route>
|
||||
|
||||
<Route path="/login-internify" element={<InternifyLogin />} />
|
||||
<Route path="/intern" element={<InternLayout />}>
|
||||
<Route index element={<HomeInternPage />} />
|
||||
<Route path="projects" element={<ProjectsPage />} />
|
||||
<Route path="certificates" element={<SertificatePage />} />
|
||||
<Route path="certificates/all" element={<SertificateList />} />
|
||||
<Route path="projects/:slug" element={<ProjectDetailPage />} />
|
||||
<Route path="projects/:slug/:TaskSlug" element={<TaskSubmission />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/mentor" element={<MentorLayout />}>
|
||||
<Route index element={<HomeMentorPage />} />
|
||||
<Route path="projects" element={<MentorProjectsPage />} />
|
||||
<Route path="certificates" element={<MentorCertificatePage />} />
|
||||
<Route path="intern" element={<MentorInternPage />} />
|
||||
<Route path="projects/:slug" element={<MentorDetailProject />} />
|
||||
<Route path="projects/:slug/:taskSlug" element={<MentorProjectsDetailPage />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
<Route path="/intern" element={<InternLayout />}>
|
||||
<Route index element={<HomeInternPage />} />
|
||||
<Route path="projects" element={<ProjectsPage />} />
|
||||
<Route path="certificates" element={<SertificatePage />} />
|
||||
<Route path="certificates/all" element={<SertificateList />} />
|
||||
<Route path="projects/:slug" element={<ProjectDetailPage />} />
|
||||
<Route path="projects/:slug/:TaskSlug" element={<TaskSubmission />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/mentor" element={<MentorLayout />}>
|
||||
<Route index element={<HomeMentorPage />} />
|
||||
<Route path="projects" element={<MentorProjectsPage />} />
|
||||
<Route path="certificates" element={<MentorCertificatePage />} />
|
||||
<Route path="intern" element={<MentorInternPage />} />
|
||||
<Route path="projects/:slug" element={<MentorDetailProject />} />
|
||||
<Route path="projects/:slug/:taskSlug" element={<MentorProjectsDetailPage />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,14 @@ import { mockProjects } from '../../../lib/mockData'
|
||||
|
||||
export default function ProjectsPage() {
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<div className="flex flex-col gap-1 mb-6">
|
||||
<span className="page-title">List Projects</span>
|
||||
<h1 className="page-title-desc">View assigned projects and track their progress.</h1>
|
||||
</div>
|
||||
|
||||
{/* Grid card */}
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="grid grid-cols-1 gap-6 md:gap-4 md:grid-cols-3 lg:grid-cols-4">
|
||||
{mockProjects.map(project => (
|
||||
<ProjectCard key={project.id} {...project} />
|
||||
))}
|
||||
|
||||
@@ -25,7 +25,7 @@ const ProjectCard = ({
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[230px] flex flex-col">
|
||||
<div className="bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-full flex flex-col">
|
||||
{/* Thumbnail */}
|
||||
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden">
|
||||
{thumbnailUrl && (
|
||||
@@ -36,7 +36,7 @@ const ProjectCard = ({
|
||||
{/* Content */}
|
||||
<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>
|
||||
<p className="text-xs lg:text-sm font-semibold text-gray-800 leading-tight">{name}</p>
|
||||
|
||||
{/* Mentor */}
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -31,8 +31,8 @@ export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
|
||||
<aside
|
||||
className={`
|
||||
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
|
||||
md:sticky md:top-[50px] md:h-[calc(100vh-73px)] md:w-[250px] md:translate-x-0 md:z-8
|
||||
fixed top-0 left-0 h-[calc(100vh-50px)] w-[270px] z-40 pt-5
|
||||
lg:sticky lg:top-[50px] lg:h-[calc(100vh-73px)] lg:w-[250px] lg:translate-x-0 lg:z-8
|
||||
fixed top-0 left-0 h-screen w-[270px] z-40 pt-5
|
||||
${isOpen ? "translate-x-0" : "-translate-x-full"}
|
||||
`}
|
||||
>
|
||||
@@ -49,7 +49,7 @@ export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
|
||||
{/* 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"
|
||||
className="p-1 text-gray-500 hover:bg-gray-100 rounded-lg lg:hidden"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { Search, SlidersHorizontal, Plus, Trash2 } from "lucide-react";
|
||||
import { mockInternsData } from "../../../lib/mockData";
|
||||
import { mockInternsData } from "../../../lib/mockData";
|
||||
import AddInternsModal from "./components/AddInternsToProjects";
|
||||
|
||||
|
||||
export default function InternsManagement() {
|
||||
export default function InternsManagement() {
|
||||
// State Filter & Search
|
||||
const [searchEmail, setSearchEmail] = useState("");
|
||||
const [selectedProject, setSelectedProject] = useState("All Projects");
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
// 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}`);
|
||||
alert(`Deleted intern ID: ${id}`);
|
||||
};
|
||||
|
||||
// Logic Filtering Data menggunakan useMemo
|
||||
@@ -42,7 +42,7 @@ export default function InternsManagement() {
|
||||
<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">
|
||||
@@ -100,9 +100,16 @@ export default function InternsManagement() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button className="inline-flex items-center gap-2 p-3 bg-red-700 text-white rounded-xl shadow-sm hover:bg-red-800 transition-colors focus:outline-none focus:ring-2 focus:ring-red-300" onClick={() => setIsModalOpen(true)}>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add
|
||||
</button>
|
||||
</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">
|
||||
|
||||
<div className="w-full mt-3 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>
|
||||
@@ -115,16 +122,16 @@ export default function InternsManagement() {
|
||||
{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}
|
||||
<img
|
||||
src={intern.avatar}
|
||||
alt={intern.name}
|
||||
className="w-10 h-10 rounded-full object-cover border border-gray-100 shadow-sm"
|
||||
/>
|
||||
) : (
|
||||
@@ -150,13 +157,13 @@ export default function InternsManagement() {
|
||||
|
||||
{/* Column 3: Action Buttons Block Right aligned */}
|
||||
<div className="w-1/6 flex items-center justify-end gap-3.5 pr-2">
|
||||
<button
|
||||
<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
|
||||
<button
|
||||
onClick={() => handleDeleteIntern(intern.id)}
|
||||
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
@@ -179,7 +186,7 @@ export default function InternsManagement() {
|
||||
<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
|
||||
@@ -189,8 +196,13 @@ export default function InternsManagement() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{isModalOpen &&
|
||||
<AddInternsModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,35 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { mockProjects } from "../../../lib/mockProjects";
|
||||
import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react";
|
||||
import EditSubmissionModal from "./components/EditSubmissionTask";
|
||||
|
||||
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
// Helper untuk mendapatkan inisial nama avatar
|
||||
const getInitials = (name: string) => {
|
||||
return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
export default function MentorProjectsDetailPage() {
|
||||
const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>();
|
||||
|
||||
const targetedProject = mockProjects.find((p) => toSlug(p.name) === slug);
|
||||
|
||||
if (!targetedProject) {
|
||||
return <div className="p-10 text-[#888]">Project tidak ditemukan.</div>;
|
||||
}
|
||||
|
||||
|
||||
const targetedTask = targetedProject.tasks.find((t) => toSlug(t.title) === taskSlug);
|
||||
if (!targetedTask) {
|
||||
return <div className="p-10 text-[#888]">Tugas tidak ditemukan.</div>;
|
||||
}
|
||||
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
{/* Header Utama */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold text-[#1a1a1a]">{targetedTask.title}</h1>
|
||||
<button className="flex items-center gap-2 border border-gray-400 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 transition-all">
|
||||
<button
|
||||
className="flex items-center gap-2 border border-gray-400 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 transition-all"
|
||||
onClick={() => setOpenModal(true)}
|
||||
>
|
||||
<Edit size={16} />
|
||||
Edit Task
|
||||
</button>
|
||||
@@ -93,12 +94,10 @@ export default function MentorProjectsDetailPage() {
|
||||
<tr key={idx} className="hover:bg-gray-50/50 transition-colors">
|
||||
{/* Kolom Nama & Profil */}
|
||||
<td className="px-6 py-4 flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-gray-200 text-gray-500 font-semibold text-xs flex items-center justify-center border border-gray-300">
|
||||
{getInitials(internProfile.name)}
|
||||
</div>
|
||||
<div className="w-9 h-9 rounded-full bg-red-800 text-gray-500 font-semibold text-xs flex items-center justify-center border border-gray-300"/>
|
||||
<span className="font-semibold text-sm text-gray-800">{internProfile.name}</span>
|
||||
</td>
|
||||
|
||||
|
||||
{/* Kolom Tanggal Kumpul */}
|
||||
<td className="px-6 py-4 text-sm text-gray-500">
|
||||
{sub.submittedAt || "-"}
|
||||
@@ -136,6 +135,14 @@ export default function MentorProjectsDetailPage() {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* EditSubmissionModal sekarang berada di dalam induk div utama */}
|
||||
{openModal && (
|
||||
<EditSubmissionModal
|
||||
isOpen={openModal}
|
||||
onClose={() => setOpenModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,12 @@ import type { Project, Task } from '../../../lib/mockData';
|
||||
import { ClipboardList, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CreateTaskModal from './components/CreateTaskDialog';
|
||||
import EditTaskModal from './components/EditTaskDialog';
|
||||
|
||||
export default function TaskTab({ project }: { project: Project }) {
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
|
||||
const handleEditTask = (taskTitle: string) => {
|
||||
console.log('Edit task:', taskTitle);
|
||||
@@ -51,7 +53,7 @@ export default function TaskTab({ project }: { project: Project }) {
|
||||
{/* Action Buttons (Edit & Delete) */}
|
||||
<div className="flex items-center gap-4 text-slate-600 shrink-0">
|
||||
<button
|
||||
onClick={() => handleEditTask(task.title)}
|
||||
onClick={() => setEditModalOpen(true)}
|
||||
className="hover:text-blue-600 transition-colors p-1"
|
||||
title="Edit Task"
|
||||
>
|
||||
@@ -79,6 +81,12 @@ export default function TaskTab({ project }: { project: Project }) {
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
}
|
||||
{editModalOpen &&
|
||||
<EditTaskModal
|
||||
isOpen={editModalOpen}
|
||||
onClose={() => setEditModalOpen(false)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,7 @@ interface ManageInternsModalProps {
|
||||
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" }
|
||||
]);
|
||||
@@ -49,60 +48,62 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
|
||||
ref={dialogRef}
|
||||
className="custom-dialog"
|
||||
>
|
||||
{/* 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>
|
||||
<div className="px-3 py-5">
|
||||
{/* 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 Container (Scrollable jika data banyak) */}
|
||||
<div className="overflow-y-auto space-y-3 pr-1 flex-1">
|
||||
|
||||
{/* 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}
|
||||
{/* 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>
|
||||
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
|
||||
<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>
|
||||
<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>
|
||||
))}
|
||||
|
||||
))}
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { LuUserPlus, LuMail, LuBriefcase, LuFolderOpen } from "react-icons/lu";
|
||||
|
||||
interface AddInternsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function AddInternsModal({ isOpen, onClose }: AddInternsModalProps) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
// Form States
|
||||
const [email, setEmail] = useState("");
|
||||
const [position, setPosition] = useState("");
|
||||
const [projectName, setProjectName] = useState("");
|
||||
|
||||
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 = { email, position, projectName };
|
||||
console.log("Adding Intern:", payload);
|
||||
alert("Intern Added Successfully!");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog ref={dialogRef} className="custom-dialog">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 px-6 py-5 bg-primary rounded-t-2xl flex-shrink-0">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-bold text-white tracking-tight leading-tight">
|
||||
Add Project Member
|
||||
</h2>
|
||||
<p className="text-xs font-medium mt-0.5 text-white/70">
|
||||
Assign a new intern to an active engineering squad.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg p-1.5 text-white/60 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form Body */}
|
||||
<form onSubmit={handleSubmit} className="bg-gray-100 px-6 py-6 space-y-4 overflow-y-auto flex-1">
|
||||
|
||||
{/* Email Address */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold tracking-wide text-foreground">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-border">
|
||||
<LuMail className="w-4 h-4 stroke-[2]" />
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="intern.name@gmail.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-white text-sm font-medium border border-border rounded-radius text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold tracking-wide text-foreground">
|
||||
Position
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-border">
|
||||
<LuBriefcase className="w-4 h-4 stroke-[2]" />
|
||||
</span>
|
||||
<select
|
||||
required
|
||||
value={position}
|
||||
onChange={(e) => setPosition(e.target.value)}
|
||||
className={`w-full pl-10 pr-9 py-2.5 bg-white text-sm font-medium appearance-none cursor-pointer border border-border rounded-radius focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all text-foreground`}
|
||||
>
|
||||
<option value="" disabled>Select position</option>
|
||||
<option value="frontend">Frontend Developer</option>
|
||||
<option value="backend">Backend Developer</option>
|
||||
<option value="fullstack">Fullstack Developer</option>
|
||||
<option value="designer">UI/UX Designer</option>
|
||||
<option value="mobile">Mobile Developer</option>
|
||||
<option value="devops">DevOps Engineer</option>
|
||||
<option value="qa">QA Engineer</option>
|
||||
</select>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-border">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Name */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold tracking-wide text-foreground">
|
||||
Project Name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-border">
|
||||
<LuFolderOpen className="w-4 h-4 stroke-[2]" />
|
||||
</span>
|
||||
<select
|
||||
required
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
className={`w-full pl-10 pr-9 py-2.5 bg-white text-sm font-medium appearance-none cursor-pointer border border-border rounded-radius focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all text-foreground`}
|
||||
>
|
||||
<option value="" disabled>Select project assignment</option>
|
||||
<option value="internify">Internify Project</option>
|
||||
<option value="rekrutmen">Rekrutmen Digital Platform</option>
|
||||
<option value="ecommerce">E-Commerce Analytics Dashboard</option>
|
||||
</select>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-border">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/30" />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 py-2.5 text-sm font-semibold bg-white hover:bg-gray-50 border border-border rounded-radius text-foreground transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 py-2.5 text-sm font-bold text-white bg-primary hover:bg-danger rounded-radius shadow-md flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<LuUserPlus className="w-4 h-4 stroke-[2.5]" />
|
||||
Add Member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
|
||||
>
|
||||
{/* Header Modal */}
|
||||
<div className="dialog-header">
|
||||
<div>
|
||||
<div className="px-3">
|
||||
<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.
|
||||
@@ -125,7 +125,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
|
||||
</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">
|
||||
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-3 md:space-y-4 overflow-y-auto flex-1">
|
||||
|
||||
{/* Section: Select Project Icon */}
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { X, Calendar, Clock, FileText, Link } from "lucide-react";
|
||||
import { X, FileText, Link } from "lucide-react";
|
||||
|
||||
interface CreateTaskModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -51,12 +51,12 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className="custom-dialog"
|
||||
className="custom-dialog p-[1.5rem]"
|
||||
>
|
||||
{/* Header Modal */}
|
||||
<div className="dialog-header">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-[#B30000] tracking-tight">Create New Task</h2>
|
||||
<h2 className="text-[26px] 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>
|
||||
@@ -67,10 +67,10 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
|
||||
</div>
|
||||
|
||||
{/* Body Form Modal (Scrollable jika layar pendek) */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto flex-1">
|
||||
<form onSubmit={handleSubmit} className="px-6 space-y-4 overflow-y-auto flex-1">
|
||||
|
||||
{/* Task Title */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -78,7 +78,7 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
|
||||
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]"
|
||||
className="w-full px-3 py-4 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>
|
||||
|
||||
@@ -104,9 +104,8 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
|
||||
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]"
|
||||
className="w-full pl-3 pr-9 py-3 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>
|
||||
|
||||
@@ -118,9 +117,8 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
|
||||
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]"
|
||||
className="w-full pl-3 pr-9 py-3 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>
|
||||
@@ -172,13 +170,13 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
|
||||
<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"
|
||||
className="p-3 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"
|
||||
className="p-3 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md"
|
||||
>
|
||||
Save Task
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { LuSave, LuCalendar } from "react-icons/lu";
|
||||
|
||||
interface EditTaskModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function EditSubmissionModal({ isOpen, onClose }: EditTaskModalProps) {
|
||||
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="custom-dialog p-[1.5rem]">
|
||||
|
||||
{/* Header */}
|
||||
<div className="dialog-header">
|
||||
<h2 className="text-xl font-bold text-gray-900 tracking-tight">Task</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg text-gray-400 hover:bg-gray-100 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form Body */}
|
||||
<form onSubmit={handleSubmit} className="px-6 pb-6 space-y-4 overflow-y-auto flex-1">
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g., Laporan tugas 2"
|
||||
value={taskTitle}
|
||||
onChange={(e) => setTaskTitle(e.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-800 placeholder-gray-400 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">Deskripsi</label>
|
||||
<textarea
|
||||
rows={5}
|
||||
placeholder="Outline the requirements and objectives..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-800 placeholder-gray-400 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Deadline & Submission Type Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
{/* Deadline Date */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Tenggat Waktu</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.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] appearance-none"
|
||||
/>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
||||
<LuCalendar className="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submission Type Dropdown */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Tipe Pengumpulan</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={submissionType}
|
||||
onChange={(e) => setSubmissionType(e.target.value as "file" | "url")}
|
||||
className="w-full pl-3 pr-9 py-2.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="file">File</option>
|
||||
<option value="url">URL Link</option>
|
||||
</select>
|
||||
{/* Custom chevron icon */}
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-gray-100" />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-5 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"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-5 py-2.5 bg-[#B30000] hover:bg-[#990000] text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center gap-2"
|
||||
>
|
||||
<LuSave className="w-4 h-4 stroke-[2.5]" />
|
||||
Simpan Tugas
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { X, FileText, Link } from "lucide-react";
|
||||
|
||||
interface EditTaskModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function EditTaskModal({ isOpen, onClose }: EditTaskModalProps) {
|
||||
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="custom-dialog p-[1.5rem]"
|
||||
>
|
||||
{/* Header Modal */}
|
||||
<div className="dialog-header">
|
||||
<div>
|
||||
<h2 className="text-[26px] font-bold text-[#B30000] tracking-tight">Edut 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="px-6 space-y-4 overflow-y-auto flex-1">
|
||||
|
||||
{/* Task Title */}
|
||||
<div className="">
|
||||
<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-4 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-3 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>
|
||||
</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-3 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>
|
||||
</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="p-3 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="p-3 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md"
|
||||
>
|
||||
Save Task
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// App.tsx / router.tsx
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import HomeInternPage from "../internify/intern/HomeInternPage";
|
||||
import {InternLayout} from "../internify/mentor/layout";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <InternLayout />,
|
||||
children: [
|
||||
{ path: "home", element: <HomeInternPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// App.tsx / router.tsx
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import HomeMentorPage from "../internify/mentor/HomePage";
|
||||
import {MentorLayout} from "../internify/mentor/layout";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <MentorLayout />,
|
||||
children: [
|
||||
{ path: "home", element: <HomeMentorPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Outlet, Navigate } from "react-router-dom";
|
||||
|
||||
interface PrivateRouteProps {
|
||||
allowedRoles?: ("admin" | "intern" | "mentor")[];
|
||||
}
|
||||
|
||||
// Helper function untuk decode payload JWT tanpa library external
|
||||
const decodeJWT = (token: string) => {
|
||||
try {
|
||||
const base64Url = token.split(".")[1]; // Ambil bagian payload (segmen kedua)
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
window
|
||||
.atob(base64)
|
||||
.split("")
|
||||
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
);
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ProtectedRoutes = ({ allowedRoles }: PrivateRouteProps) => {
|
||||
// 1. Ambil token dari cookie
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
// Jika tidak punya token, tendang ke login admin awal
|
||||
if (!token) {
|
||||
return <Navigate to="/login-admin" replace />;
|
||||
}
|
||||
|
||||
// 2. Decode token untuk mendapatkan role asli dari Backend
|
||||
const decoded = decodeJWT(token);
|
||||
const userRole = decoded?.role as "admin" | "intern" | "mentor" | undefined;
|
||||
|
||||
// Jika token corrupt atau tidak bisa di-decode, hanguskan dan tendang ke login
|
||||
if (!userRole) {
|
||||
return <Navigate to="/login-admin" replace />;
|
||||
}
|
||||
|
||||
// 3. Periksa hak akses role terhadap halaman yang dituju
|
||||
if (allowedRoles && !allowedRoles.includes(userRole)) {
|
||||
// Jika role tidak diizinkan, kembalikan ke base dashboard masing-masing role
|
||||
if (userRole === "admin") return <Navigate to="/dashboard" replace />;
|
||||
if (userRole === "mentor") return <Navigate to="/mentor" replace />;
|
||||
if (userRole === "intern") return <Navigate to="/intern" replace />;
|
||||
}
|
||||
|
||||
// Jika lolos seleksi, render komponen di dalamnya
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default ProtectedRoutes;
|
||||
@@ -0,0 +1,20 @@
|
||||
import axios from "axios";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
});
|
||||
|
||||
// Otomatis tempel Token Bearer di setiap request ke BE
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
export default api;
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+11
-10
@@ -20,7 +20,7 @@
|
||||
--color-card: #f3f3f3;
|
||||
--color-card-hover: #d5d5d5;
|
||||
|
||||
--color-primary: #6d180d;
|
||||
--color-primary: #ad2b19;
|
||||
--color-primary-foreground: #ffffff;
|
||||
|
||||
--color-border: #a8a9aa;
|
||||
@@ -32,6 +32,8 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
min-width: 0; /* penting! mencegah overflow */
|
||||
padding-inline: calc(var(--spacing) * 14);
|
||||
padding-block: calc(var(--spacing) * 12);
|
||||
background-color: var(--color-gray-50);
|
||||
@@ -74,13 +76,15 @@ dialog.custom-dialog {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: calc(100% - 2rem);
|
||||
max-width: 540px;
|
||||
max-height: calc(100vh - 4rem);
|
||||
|
||||
max-height: 90vh;
|
||||
|
||||
border-radius: 1rem; /* 16px */
|
||||
border: 1px solid #f3f4f6; /* border-gray-100 */
|
||||
background-color: #ffffff;
|
||||
padding: 1.5rem; /* 24px */
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); /* shadow-2xl */
|
||||
|
||||
/* Animasi (Masuk) */
|
||||
@@ -109,13 +113,10 @@ dialog.custom-dialog::backdrop {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
padding-left: 1.5rem; /* 24px */
|
||||
padding-right: 1.5rem; /* 24px */
|
||||
padding-top: 1rem; /* 16px */
|
||||
padding-bottom: 1rem; /* 16px */
|
||||
|
||||
border-bottom: 1px solid #f3f4f6; /* border-gray-100 */
|
||||
padding-left: 1rem;
|
||||
padding-right: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-bottom: 2px solid #f3f4f6; /* border-gray-100 */
|
||||
}
|
||||
|
||||
/* Breakpoint Medium (md:) - Aktif saat lebar layar >= 768px */
|
||||
|
||||
Reference in New Issue
Block a user