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:
Kemas M. Aryadary Rasyad
2026-06-22 21:01:25 +07:00
parent ff68115fca
commit dc64fadfdd
21 changed files with 785 additions and 190 deletions
+41 -40
View File
@@ -28,23 +28,24 @@ import Dashboard from "./Pages/Dashboard";
import EditFaq from "./Pages/EditFAQ"; import EditFaq from "./Pages/EditFAQ";
import EditFeedback from "./Pages/EditFeedback"; import EditFeedback from "./Pages/EditFeedback";
import EditLowongan from "./Pages/EditLowongan"; import EditLowongan from "./Pages/EditLowongan";
import HomeInternPage from "./Pages/internify/intern/HomeInternPage"; import HomeInternPage from "./Pages/Internify/intern/HomeInternPage";
import { InternLayout, MentorLayout } from "./Pages/internify/layout"; import { InternLayout, MentorLayout } from "./Pages/Internify/layout";
import ProjectsPage from "./Pages/internify/intern/ProjectsPage"; import ProjectsPage from "./Pages/Internify/intern/ProjectsPage";
import SertificatePage from "./Pages/internify/intern/SertificatePage"; import SertificatePage from "./Pages/Internify/intern/SertificatePage";
import ProjectDetailPage from "./Pages/internify/intern/ProjectDetailPage"; import ProjectDetailPage from "./Pages/Internify/intern/ProjectDetailPage";
import HomeMentorPage from "./Pages/internify/mentor/HomePage"; import HomeMentorPage from "./Pages/Internify/mentor/HomePage";
import MentorProjectsPage from "./Pages/internify/mentor/BaseProjectsPage"; import MentorProjectsPage from "./Pages/Internify/mentor/BaseProjectsPage";
import MentorCertificatePage from "./Pages/internify/mentor/CertificatePage"; import MentorCertificatePage from "./Pages/Internify/mentor/CertificatePage";
import MentorInternPage from "./Pages/internify/mentor/InternsPage"; import MentorInternPage from "./Pages/Internify/mentor/InternsPage";
import MentorDetailProject from "./Pages/internify/mentor/ProjectsPage"; import MentorDetailProject from "./Pages/Internify/mentor/ProjectsPage";
import InternifyLogin from "./Pages/internify/page" import InternifyLogin from "./Pages/Internify/LoginInternify"
import FAQPage from "./Pages/internify/FAQpage"; import FAQPage from "./Pages/Internify/FAQpage";
import NotificationList from "./Pages/internify/NotificationList"; import NotificationList from "./Pages/Internify/NotificationList";
import SettingsContent from "./Pages/internify/SettingsPage"; import SettingsContent from "./Pages/Internify/SettingsPage";
import TaskSubmission from "./Pages/internify/intern/TaskSubmission"; import TaskSubmission from "./Pages/Internify/intern/TaskSubmission";
import SertificateList from "./Pages/internify/intern/SertificateList"; import SertificateList from "./Pages/Internify/intern/SertificateList";
import MentorProjectsDetailPage from "./Pages/internify/mentor/MentorProjectDetailPage"; import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage";
import ProtectedRoutes from "./Pages/utils/ProtectedRoute";
function App() { function App() {
return ( return (
@@ -60,7 +61,6 @@ function App() {
<Route path="/details-product/:id" element={<DetailsProduct />} /> <Route path="/details-product/:id" element={<DetailsProduct />} />
<Route path="/login-admin" element={<LoginAdmin />} /> <Route path="/login-admin" element={<LoginAdmin />} />
{/* Endpoint khusus admin login langsung di inject ajah */}
<Route element={<PrivateRoute />}> <Route element={<PrivateRoute />}>
<Route path="/add-product-admin" element={<AddProduct />} /> <Route path="/add-product-admin" element={<AddProduct />} />
<Route path="/internships-list" element={<InternshipList />} /> <Route path="/internships-list" element={<InternshipList />} />
@@ -85,29 +85,30 @@ function App() {
</Route> </Route>
<Route path="/login-internify" element={<InternifyLogin />} /> <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 path="/intern" element={<InternLayout />}>
<Route index element={<HomeMentorPage />} /> <Route index element={<HomeInternPage />} />
<Route path="projects" element={<MentorProjectsPage />} /> <Route path="projects" element={<ProjectsPage />} />
<Route path="certificates" element={<MentorCertificatePage />} /> <Route path="certificates" element={<SertificatePage />} />
<Route path="intern" element={<MentorInternPage />} /> <Route path="certificates/all" element={<SertificateList />} />
<Route path="projects/:slug" element={<MentorDetailProject />} /> <Route path="projects/:slug" element={<ProjectDetailPage />} />
<Route path="projects/:slug/:taskSlug" element={<MentorProjectsDetailPage />} /> <Route path="projects/:slug/:TaskSlug" element={<TaskSubmission />} />
<Route path="faq" element={<FAQPage />} /> <Route path="faq" element={<FAQPage />} />
<Route path="notifications" element={<NotificationList />} /> <Route path="notifications" element={<NotificationList />} />
<Route path="settings" element={<SettingsContent />} /> <Route path="settings" element={<SettingsContent />} />
</Route> </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> </Routes>
</Router> </Router>
); );
+2 -2
View File
@@ -3,14 +3,14 @@ import { mockProjects } from '../../../lib/mockData'
export default function ProjectsPage() { export default function ProjectsPage() {
return ( return (
<div className=""> <div>
<div className="flex flex-col gap-1 mb-6"> <div className="flex flex-col gap-1 mb-6">
<span className="page-title">List Projects</span> <span className="page-title">List Projects</span>
<h1 className="page-title-desc">View assigned projects and track their progress.</h1> <h1 className="page-title-desc">View assigned projects and track their progress.</h1>
</div> </div>
{/* Grid card */} {/* 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 => ( {mockProjects.map(project => (
<ProjectCard key={project.id} {...project} /> <ProjectCard key={project.id} {...project} />
))} ))}
@@ -25,7 +25,7 @@ const ProjectCard = ({
const navigate = useNavigate() const navigate = useNavigate()
return ( 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 */} {/* Thumbnail */}
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden"> <div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden">
{thumbnailUrl && ( {thumbnailUrl && (
@@ -36,7 +36,7 @@ const ProjectCard = ({
{/* Content */} {/* Content */}
<div className="min-h-[180px] max-h-[210px] flex-1 flex flex-col justify-between py-3"> <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"> <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 */} {/* Mentor */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -31,8 +31,8 @@ export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
<aside <aside
className={` 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 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 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-[calc(100vh-50px)] w-[270px] z-40 pt-5 fixed top-0 left-0 h-screen w-[270px] z-40 pt-5
${isOpen ? "translate-x-0" : "-translate-x-full"} ${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 */} {/* Tombol Close silang (X): Hanya muncul di HP saat sidebar terbuka */}
<button <button
onClick={closeSidebar} 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" /> <X className="w-5 h-5" />
</button> </button>
+17 -5
View File
@@ -1,20 +1,20 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { Search, SlidersHorizontal, Plus, Trash2 } from "lucide-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 // State Filter & Search
const [searchEmail, setSearchEmail] = useState(""); const [searchEmail, setSearchEmail] = useState("");
const [selectedProject, setSelectedProject] = useState("All Projects"); const [selectedProject, setSelectedProject] = useState("All Projects");
const [isModalOpen, setIsModalOpen] = useState(false);
// Handler Aksi Kursor/Tombol // Handler Aksi Kursor/Tombol
const handleAddAssignment = (id: number) => { const handleAddAssignment = (id: number) => {
alert(`Assign/Add action clicked for intern ID: ${id}`); alert(`Assign/Add action clicked for intern ID: ${id}`);
}; };
const handleDeleteIntern = (id: number) => { const handleDeleteIntern = (id: number) => {
alert(`Deleted intern ID: ${id}`); alert(`Deleted intern ID: ${id}`);
}; };
// Logic Filtering Data menggunakan useMemo // Logic Filtering Data menggunakan useMemo
@@ -100,8 +100,15 @@ export default function InternsManagement() {
</div> </div>
</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 */} {/* 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 */} {/* 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="bg-[#B30000] text-white px-6 py-3.5 flex items-center text-xs font-bold uppercase tracking-wider">
@@ -189,8 +196,13 @@ export default function InternsManagement() {
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{isModalOpen &&
<AddInternsModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
}
</div> </div>
); );
} }
@@ -1,18 +1,15 @@
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useState } from "react";
import { mockProjects } from "../../../lib/mockProjects"; import { mockProjects } from "../../../lib/mockProjects";
import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react"; import { Calendar, Edit, SlidersHorizontal, Search } from "lucide-react";
import EditSubmissionModal from "./components/EditSubmissionTask";
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); 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() { export default function MentorProjectsDetailPage() {
const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>();
const targetedProject = mockProjects.find((p) => toSlug(p.name) === slug); const targetedProject = mockProjects.find((p) => toSlug(p.name) === slug);
if (!targetedProject) { if (!targetedProject) {
return <div className="p-10 text-[#888]">Project tidak ditemukan.</div>; return <div className="p-10 text-[#888]">Project tidak ditemukan.</div>;
} }
@@ -22,13 +19,17 @@ export default function MentorProjectsDetailPage() {
return <div className="p-10 text-[#888]">Tugas tidak ditemukan.</div>; return <div className="p-10 text-[#888]">Tugas tidak ditemukan.</div>;
} }
const [openModal, setOpenModal] = useState(false);
return ( return (
<div> <div>
{/* Header Utama */} {/* Header Utama */}
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-[#1a1a1a]">{targetedTask.title}</h1> <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 size={16} />
Edit Task Edit Task
</button> </button>
@@ -93,9 +94,7 @@ export default function MentorProjectsDetailPage() {
<tr key={idx} className="hover:bg-gray-50/50 transition-colors"> <tr key={idx} className="hover:bg-gray-50/50 transition-colors">
{/* Kolom Nama & Profil */} {/* Kolom Nama & Profil */}
<td className="px-6 py-4 flex items-center gap-3"> <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"> <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"/>
{getInitials(internProfile.name)}
</div>
<span className="font-semibold text-sm text-gray-800">{internProfile.name}</span> <span className="font-semibold text-sm text-gray-800">{internProfile.name}</span>
</td> </td>
@@ -136,6 +135,14 @@ export default function MentorProjectsDetailPage() {
</table> </table>
</div> </div>
</div> </div>
{/* EditSubmissionModal sekarang berada di dalam induk div utama */}
{openModal && (
<EditSubmissionModal
isOpen={openModal}
onClose={() => setOpenModal(false)}
/>
)}
</div> </div>
); );
} }
+9 -1
View File
@@ -2,10 +2,12 @@ import type { Project, Task } from '../../../lib/mockData';
import { ClipboardList, Pencil, Trash2 } from 'lucide-react'; import { ClipboardList, Pencil, Trash2 } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import CreateTaskModal from './components/CreateTaskDialog'; import CreateTaskModal from './components/CreateTaskDialog';
import EditTaskModal from './components/EditTaskDialog';
export default function TaskTab({ project }: { project: Project }) { export default function TaskTab({ project }: { project: Project }) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const handleEditTask = (taskTitle: string) => { const handleEditTask = (taskTitle: string) => {
console.log('Edit task:', taskTitle); console.log('Edit task:', taskTitle);
@@ -51,7 +53,7 @@ export default function TaskTab({ project }: { project: Project }) {
{/* Action Buttons (Edit & Delete) */} {/* Action Buttons (Edit & Delete) */}
<div className="flex items-center gap-4 text-slate-600 shrink-0"> <div className="flex items-center gap-4 text-slate-600 shrink-0">
<button <button
onClick={() => handleEditTask(task.title)} onClick={() => setEditModalOpen(true)}
className="hover:text-blue-600 transition-colors p-1" className="hover:text-blue-600 transition-colors p-1"
title="Edit Task" title="Edit Task"
> >
@@ -79,6 +81,12 @@ export default function TaskTab({ project }: { project: Project }) {
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
/> />
} }
{editModalOpen &&
<EditTaskModal
isOpen={editModalOpen}
onClose={() => setEditModalOpen(false)}
/>
}
</div> </div>
); );
} }
@@ -10,7 +10,6 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
const dialogRef = useRef<HTMLDialogElement>(null); const dialogRef = useRef<HTMLDialogElement>(null);
const [searchQuery, setSearchQuery] = useState("Jonathankristin@gmail.com"); const [searchQuery, setSearchQuery] = useState("Jonathankristin@gmail.com");
// Data Intern Terundang (Simulasi list atas yang ada ikon sampah)
const [invitedList, setInvitedList] = useState([ const [invitedList, setInvitedList] = useState([
{ id: 1, email: "Jonathankristina@gmail.com", initials: "JK" } { id: 1, email: "Jonathankristina@gmail.com", initials: "JK" }
]); ]);
@@ -49,60 +48,62 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
ref={dialogRef} ref={dialogRef}
className="custom-dialog" className="custom-dialog"
> >
{/* Search Input Area */} <div className="px-3 py-5">
<div className="relative flex items-center mb-4"> {/* Search Input Area */}
<input <div className="relative flex items-center mb-4">
type="text" <input
value={searchQuery} type="text"
onChange={(e) => setSearchQuery(e.target.value)} value={searchQuery}
placeholder="Search by email..." onChange={(e) => setSearchQuery(e.target.value)}
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" 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> <Search className="absolute right-3.5 w-4 h-4 text-gray-400" />
</div>
{/* List Container (Scrollable jika data banyak) */} {/* List Container (Scrollable jika data banyak) */}
<div className="overflow-y-auto space-y-3 pr-1 flex-1"> <div className="overflow-y-auto space-y-3 pr-1 flex-1">
{/* List 1: Intern Yang Sudah Tergabung / Ditambahkan */} {/* List 1: Intern Yang Sudah Tergabung / Ditambahkan */}
{invitedList.map((intern) => ( {invitedList.map((intern) => (
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors"> <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="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"> <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} {intern.initials}
</div>
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
</div> </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> </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 */} {/* List 2: Hasil Pencarian / Belum Ditambahkan */}
{searchResult.map((intern) => ( {searchResult.map((intern) => (
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors"> <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="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"> <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} {intern.initials}
</div>
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
</div> </div>
<span className="text-sm font-medium text-gray-600">{intern.email}</span> <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>
<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> </div>
</dialog> </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 */} {/* Header Modal */}
<div className="dialog-header"> <div className="dialog-header">
<div> <div className="px-3">
<h2 className="text-xl font-bold text-gray-900 tracking-tight">Create New Project</h2> <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"> <p className="text-xs text-gray-400 font-medium mt-0.5">
Launch a new internship assignment or trackable milestone. Launch a new internship assignment or trackable milestone.
@@ -125,7 +125,7 @@ export default function CreateProjectModal({ isOpen, onClose }: CreateProjectMod
</div> </div>
{/* Body Form Modal */} {/* 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 */} {/* Section: Select Project Icon */}
<div className="space-y-2"> <div className="space-y-2">
@@ -1,5 +1,5 @@
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import { X, Calendar, Clock, FileText, Link } from "lucide-react"; import { X, FileText, Link } from "lucide-react";
interface CreateTaskModalProps { interface CreateTaskModalProps {
isOpen: boolean; isOpen: boolean;
@@ -51,12 +51,12 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
return ( return (
<dialog <dialog
ref={dialogRef} ref={dialogRef}
className="custom-dialog" className="custom-dialog p-[1.5rem]"
> >
{/* Header Modal */} {/* Header Modal */}
<div className="dialog-header"> <div className="dialog-header">
<div> <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"> <p className="text-xs text-gray-400 font-medium mt-0.5">
Fill in the details for the next intern assignment. Fill in the details for the next intern assignment.
</p> </p>
@@ -67,10 +67,10 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
</div> </div>
{/* Body Form Modal (Scrollable jika layar pendek) */} {/* 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 */} {/* Task Title */}
<div className="space-y-1.5"> <div className="">
<label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label> <label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label>
<input <input
type="text" type="text"
@@ -78,7 +78,7 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
placeholder="e.g., Design High-Fidelity Prototypes" placeholder="e.g., Design High-Fidelity Prototypes"
value={taskTitle} value={taskTitle}
onChange={(e) => setTaskTitle(e.target.value)} 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> </div>
@@ -104,9 +104,8 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
required required
value={deadlineDate} value={deadlineDate}
onChange={(e) => setDeadlineDate(e.target.value)} 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>
</div> </div>
@@ -118,9 +117,8 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
required required
value={specificTime} value={specificTime}
onChange={(e) => setSpecificTime(e.target.value)} 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> </div>
</div> </div>
@@ -172,13 +170,13 @@ export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProp
<button <button
type="button" type="button"
onClick={onClose} 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 Cancel
</button> </button>
<button <button
type="submit" 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 Save Task
</button> </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>
);
}
-18
View File
@@ -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} />;
}
-18
View File
@@ -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} />;
}
+58
View File
@@ -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;
+20
View File
@@ -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;
View File
+1
View File
@@ -0,0 +1 @@
+11 -10
View File
@@ -20,7 +20,7 @@
--color-card: #f3f3f3; --color-card: #f3f3f3;
--color-card-hover: #d5d5d5; --color-card-hover: #d5d5d5;
--color-primary: #6d180d; --color-primary: #ad2b19;
--color-primary-foreground: #ffffff; --color-primary-foreground: #ffffff;
--color-border: #a8a9aa; --color-border: #a8a9aa;
@@ -32,6 +32,8 @@
} }
.container { .container {
flex: 1;
min-width: 0; /* penting! mencegah overflow */
padding-inline: calc(var(--spacing) * 14); padding-inline: calc(var(--spacing) * 14);
padding-block: calc(var(--spacing) * 12); padding-block: calc(var(--spacing) * 12);
background-color: var(--color-gray-50); background-color: var(--color-gray-50);
@@ -74,13 +76,15 @@ dialog.custom-dialog {
margin: auto; margin: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: calc(100% - 2rem); width: calc(100% - 2rem);
max-width: 540px; max-width: 540px;
max-height: calc(100vh - 4rem);
max-height: 90vh;
border-radius: 1rem; /* 16px */ border-radius: 1rem; /* 16px */
border: 1px solid #f3f4f6; /* border-gray-100 */ border: 1px solid #f3f4f6; /* border-gray-100 */
background-color: #ffffff; background-color: #ffffff;
padding: 1.5rem; /* 24px */
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); /* shadow-2xl */ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); /* shadow-2xl */
/* Animasi (Masuk) */ /* Animasi (Masuk) */
@@ -109,13 +113,10 @@ dialog.custom-dialog::backdrop {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
padding-left: 1rem;
padding-left: 1.5rem; /* 24px */ padding-right: 1.5rem;
padding-right: 1.5rem; /* 24px */ padding-top: 1rem;
padding-top: 1rem; /* 16px */ border-bottom: 2px solid #f3f4f6; /* border-gray-100 */
padding-bottom: 1rem; /* 16px */
border-bottom: 1px solid #f3f4f6; /* border-gray-100 */
} }
/* Breakpoint Medium (md:) - Aktif saat lebar layar >= 768px */ /* Breakpoint Medium (md:) - Aktif saat lebar layar >= 768px */