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