feat: add modals for managing interns, creating projects, and tasks
- Implemented AddInternsDialog for inviting and managing interns. - Created CreateProjectDialog for launching new projects with detailed forms. - Added CreateTaskDialog for task creation with deadline and submission type options. - Enhanced Header component with navigation functionality. - Updated MentorProjectCard to navigate correctly to mentor projects. - Refactored SidebarMentor for improved navigation and mobile responsiveness. - Adjusted Internify login logic to streamline user role navigation. - Added CSS styles for consistent layout and design. - Introduced mock data for interns to facilitate development and testing.
This commit is contained in:
@@ -42,6 +42,7 @@ import InternifyLogin from "./Pages/internify/page"
|
|||||||
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";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -88,6 +89,7 @@ function App() {
|
|||||||
<Route path="projects" element={<ProjectsPage />} />
|
<Route path="projects" element={<ProjectsPage />} />
|
||||||
<Route path="certificates" element={<SertificatePage />} />
|
<Route path="certificates" element={<SertificatePage />} />
|
||||||
<Route path="projects/:id" element={<ProjectDetailPage />} />
|
<Route path="projects/:id" element={<ProjectDetailPage />} />
|
||||||
|
<Route path="tasks/:id/submit" 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 />} />
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ const Navbar = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/login-internify");
|
navigate("/login-internify");
|
||||||
setOpenMenu(false);
|
|
||||||
}}
|
}}
|
||||||
className="block w-full text-left hover:text-blue-600 transition"
|
className="block w-full text-left hover:text-blue-600 transition"
|
||||||
>
|
>
|
||||||
|
|||||||
+225
-89
@@ -1,98 +1,234 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, User, ChevronRight } from 'lucide-react';
|
import { useLocation } from "react-router-dom";
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const navItems = [
|
const SidebarAdmin = () => {
|
||||||
{ label: "Home", icon: Home, path: "/admin" },
|
|
||||||
{ label: "List Projects", icon: FolderKanban, path: "/admin/projects" },
|
|
||||||
{ label: "Interns", icon: User, path: "/admin/intern" },
|
|
||||||
{ label: "Certificates", icon: Award, path: "/admin/certificates" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const prefItems = [
|
|
||||||
{ label: "Settings", icon: Settings, path: "/admin/settings" },
|
|
||||||
{ label: "FAQ", icon: HelpCircle, path: "/admin/faq" },
|
|
||||||
{ label: "Logout", icon: LogOut, path: "/admin/logout", danger: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
function SidebarIntern() {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [active, setActive] = useState("/admin");
|
const location = useLocation();
|
||||||
|
|
||||||
const handleNav = (path: string) => {
|
|
||||||
setActive(path);
|
|
||||||
navigate(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-[279px] bg-white border-r border-gray-100 flex flex-col py-5 px-3 gap-1 sticky top-[73px] h-[calc(100vh-73px)] overflow-y-auto flex-shrink-0">
|
<div className="bg-[#EDF2F7] w-[300px] min-h-[87vh] flex flex-col justify-between p-4">
|
||||||
{/* Brand */}
|
<div className="container-sidebar">
|
||||||
<div className="flex items-center gap-2.5 px-2 pb-4">
|
<h2 className="font-semibold text-[#2C5282] text-[20px]">Menu</h2>
|
||||||
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0">
|
<div className="list-of-sidebar flex flex-col p-7 gap-4">
|
||||||
</div>
|
<div
|
||||||
<div>
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
|
location.pathname === "/dashboard"
|
||||||
<p className="text-[11px] text-gray-400">LMS Portal</p>
|
? "bg-[#D3DFEA] font-bold"
|
||||||
|
: "hover:bg-[#D3DFEA] "
|
||||||
|
}`}
|
||||||
|
onClick={() => navigate("/dashboard")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
className="size-8 text-[#2C5282]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 3 L5 8 A4 4 0 0 0 3 11 L3 15 A4 4 0 0 0 7 19 L17 19 A4 4 0 0 0 21 15 L21 11 A4 4 0 0 0 19 8 L12 3 Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 13 v4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2 className="text-[#2C5282]">Dashboard</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
|
location.pathname === "/product-list"
|
||||||
|
? "bg-[#D3DFEA] font-bold"
|
||||||
|
: "hover:bg-[#D3DFEA] "
|
||||||
|
}`}
|
||||||
|
onClick={() => navigate("/product-list")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-8 text-[#2C5282]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h2 className="text-[#2C5282]">Project</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
|
location.pathname === "/lowongan-list"
|
||||||
|
? "bg-[#D3DFEA] font-bold"
|
||||||
|
: "hover:bg-[#D3DFEA]"
|
||||||
|
}`}
|
||||||
|
onClick={() => navigate("/lowongan-list")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-8 text-[#2C5282]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 10.5v6m3-3H9m4.06-7.19-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2 className="text-[#2C5282]">Lowongan</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
|
location.pathname === "/partnership-admin"
|
||||||
|
? "bg-[#D3DFEA] font-bold"
|
||||||
|
: "hover:bg-[#D3DFEA]"
|
||||||
|
}`}
|
||||||
|
onClick={() => navigate("/partnership-admin")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-8 text-[#2C5282]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2 className="text-[#2C5282]">Partnership</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
|
location.pathname === "/internships-list"
|
||||||
|
? "bg-[#D3DFEA] font-bold"
|
||||||
|
: "hover:bg-[#D3DFEA]"
|
||||||
|
}`}
|
||||||
|
onClick={() => navigate("/internships-list")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-8 text-[#2C5282]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2 className="text-[#2C5282]">Pelamar</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
|
location.pathname === "/feedbacks-list"
|
||||||
|
? "bg-[#D3DFEA] font-bold"
|
||||||
|
: "hover:bg-[#D3DFEA]"
|
||||||
|
}`}
|
||||||
|
onClick={() => navigate("/feedback-list")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-8 text-[#2C5282]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M4 4h16a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-5l-3 3-3-3H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||||
|
/>
|
||||||
|
<circle cx="8" cy="10" r="1.2" fill="currentColor" />
|
||||||
|
<circle cx="12" cy="10" r="1.2" fill="currentColor" />
|
||||||
|
<circle cx="16" cy="10" r="1.2" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2 className="text-[#2C5282]">Feedback</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
|
location.pathname === "/faq-list"
|
||||||
|
? "bg-[#D3DFEA] font-bold"
|
||||||
|
: "hover:bg-[#D3DFEA]"
|
||||||
|
}`}
|
||||||
|
onClick={() => navigate("/faq-list")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-8 text-[#2C5282]"
|
||||||
|
>
|
||||||
|
{/* Balon chat */}
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M4 4h16a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-5l-3 3-3-3H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||||
|
/>
|
||||||
|
{/* Tanda tanya */}
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9.75 9.5a2.25 2.25 0 1 1 4.5 0c0 1.25-.75 1.75-1.5 1.85s-1 1-1 1.75"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="16" r="0.6" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h2 className="text-[#2C5282]">FAQ</h2>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-px bg-gray-100 my-1" />
|
<div
|
||||||
|
className="logout flex flex-row items-center cursor-pointer gap-4 p-3 rounded-2xl hover:bg-[#F8D2D2] hover:font-bold mx-7"
|
||||||
|
onClick={() => {
|
||||||
|
document.cookie =
|
||||||
|
"token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;";
|
||||||
|
navigate("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-8 text-[#C3423F]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
{/* Main nav */}
|
<h2 className="text-[#C3423F] text-[16px] font-semibold">Keluar</h2>
|
||||||
<nav className="flex flex-col gap-0.5">
|
</div>
|
||||||
{navItems.map(({ label, icon: Icon, path }) => {
|
</div>
|
||||||
const isActive = active === path;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={path}
|
|
||||||
onClick={() => handleNav(path)}
|
|
||||||
className={`flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-[13.5px] font-medium text-left transition-colors
|
|
||||||
${isActive
|
|
||||||
? "bg-red-50 text-red-600 font-semibold border-l-4 border-red-600"
|
|
||||||
: "text-gray-500 hover:bg-red-50 hover:text-red-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
|
||||||
<span>{label}</span>
|
|
||||||
{isActive && (
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="h-px bg-gray-100 my-1" />
|
|
||||||
|
|
||||||
{/* Preferences */}
|
|
||||||
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
|
|
||||||
PREFERENCES
|
|
||||||
</p>
|
|
||||||
<nav className="flex flex-col gap-0.5">
|
|
||||||
{prefItems.map(({ label, icon: Icon, path, danger }) => {
|
|
||||||
const isActive = active === path;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={path}
|
|
||||||
onClick={() => handleNav(path)}
|
|
||||||
className={`flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-[13.5px] font-medium text-left transition-colors
|
|
||||||
${isActive
|
|
||||||
? "bg-red-50 text-red-600 font-semibold"
|
|
||||||
: danger
|
|
||||||
? "text-gray-400 hover:bg-red-50 hover:text-red-600"
|
|
||||||
: "text-gray-500 hover:bg-red-50 hover:text-red-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
|
||||||
<span>{label}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default SidebarAdmin;
|
||||||
export default SidebarIntern
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { ChevronRight } from "lucide-react";
|
|
||||||
|
|
||||||
const mockFAQData = [
|
const mockFAQData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -20,47 +17,30 @@ const mockFAQData = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function FAQPage() {
|
export default function FAQPage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl px-6 py-6 font-sans">
|
<div className="container">
|
||||||
{/* Breadcrumb */}
|
|
||||||
<div className="flex items-center text-xs text-gray-400 font-medium mb-1">
|
|
||||||
<span className="cursor-pointer hover:underline" onClick={() => navigate("/")}>Home</span>
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 mx-1" />
|
|
||||||
<span className="text-[#B30000] font-semibold">FAQ</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Header Title */}
|
{/* Header Title */}
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">FAQ</h1>
|
<h1 className="page-title">FAQ</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="page-title-desc">
|
||||||
Quick answers to common questions about the Internify LMS platform.
|
Quick answers to common questions about the Internify LMS platform.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FAQ Lists */}
|
{/* FAQ Lists */}
|
||||||
<div className="space-y-8 max-w-4xl">
|
<div className="space-y-8 max-w-auto">
|
||||||
{mockFAQData.map((faq) => (
|
{mockFAQData.map((faq) => (
|
||||||
<div key={faq.id} className="relative pl-6 group">
|
<div key={faq.id} className="relative shadow-md group py-5 px-10 rounded-md bg-white">
|
||||||
|
|
||||||
{/* Indikator Garis/Kotak Kecil Merah di Sisi Kiri sesuai gambar */}
|
|
||||||
<div className="absolute left-0 top-1.5 w-1 h-3.5 bg-[#B30000] rounded-sm transition-transform duration-200 group-hover:scale-y-125"></div>
|
|
||||||
|
|
||||||
{/* Item Question */}
|
{/* Item Question */}
|
||||||
<h2 className="text-[17px] font-bold text-[#8A0000] tracking-tight leading-snug mb-2">
|
<h2 className="text-[17px] font-bold text-[#8A0000] tracking-tight leading-snug mb-2 border-l-4 border-[#8A0000] pl-3">
|
||||||
{faq.question}
|
{faq.question}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Item Answer */}
|
{/* Item Answer */}
|
||||||
<p className="text-sm text-gray-500 font-medium leading-relaxed tracking-normal">
|
<p className="text-[14px] pl-8 text-[#8A0000] font-light leading-relaxed tracking-normal">
|
||||||
{faq.answer}
|
{faq.answer}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Divider Line tipis di bawah setiap item, kecuali item terakhir */}
|
|
||||||
{faq.id !== mockFAQData.length && (
|
|
||||||
<div className="border-b border-gray-100 pt-8"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { Check, Download, Info } from "lucide-react";
|
||||||
import { ChevronRight, Check, Download, Info, Award } from "lucide-react";
|
|
||||||
import { LuFileSpreadsheet } from "react-icons/lu"; // Alternatif icon task dari react-icons
|
import { LuFileSpreadsheet } from "react-icons/lu"; // Alternatif icon task dari react-icons
|
||||||
|
|
||||||
const initialNotifications = [
|
const initialNotifications = [
|
||||||
@@ -34,7 +33,6 @@ const initialNotifications = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function NotificationList() {
|
export default function NotificationList() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [notifications, setNotifications] = useState(initialNotifications);
|
const [notifications, setNotifications] = useState(initialNotifications);
|
||||||
|
|
||||||
const handleMarkAllRead = () => {
|
const handleMarkAllRead = () => {
|
||||||
@@ -43,19 +41,12 @@ export default function NotificationList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl px-6 py-6 font-sans">
|
<div className="container">
|
||||||
{/* Breadcrumb */}
|
|
||||||
<div className="flex items-center text-xs text-gray-400 font-medium mb-1">
|
|
||||||
<span className="cursor-pointer hover:underline" onClick={() => navigate("/")}>Home</span>
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 mx-1" />
|
|
||||||
<span className="text-[#B30000] font-semibold">Notification</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">Notifications</h1>
|
<h1 className="page-title">Notifications</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="page-title-desc">
|
||||||
Keep track of your interns' progress and system updates.
|
Keep track of your interns' progress and system updates.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { ChevronRight } from "lucide-react"; // dari lucide-react
|
|
||||||
|
|
||||||
// Mock Data Profile awal
|
// Mock Data Profile awal
|
||||||
const mockProfileData = {
|
const mockProfileData = {
|
||||||
@@ -10,7 +8,6 @@ const mockProfileData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingsContent() {
|
export default function SettingsContent() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [formData, setFormData] = useState(mockProfileData);
|
const [formData, setFormData] = useState(mockProfileData);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
@@ -29,24 +26,18 @@ export default function SettingsContent() {
|
|||||||
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
|
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-5xl px-6 py-6 font-sans">
|
<div className="container">
|
||||||
{/* Breadcrumb */}
|
|
||||||
<div className="flex items-center text-xs text-gray-400 font-medium mb-1">
|
|
||||||
<span className="cursor-pointer hover:underline" onClick={() => navigate("/")}>Home</span>
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 mx-1" />
|
|
||||||
<span className="text-[#B30000] font-semibold">Settings</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Header Title */}
|
{/* Header Title */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">Settings</h1>
|
<h1 className="page-title">Settings</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="page-title-desc">
|
||||||
Manage and edit your profile and account information.
|
Manage and edit your profile and account information.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* White Card Container */}
|
{/* White Card Container */}
|
||||||
<div className="w-full bg-white rounded-xl border border-gray-100 shadow-[0_2px_12px_rgba(0,0,0,0.01)] overflow-hidden">
|
<div className="w-full bg-white rounded-xl border border-gray-100 shadow-lg overflow-hidden">
|
||||||
|
|
||||||
{/* Card Header */}
|
{/* Card Header */}
|
||||||
<div className="p-6 border-b border-gray-100">
|
<div className="p-6 border-b border-gray-100">
|
||||||
@@ -79,7 +70,7 @@ export default function SettingsContent() {
|
|||||||
|
|
||||||
{/* Field: Full Name */}
|
{/* Field: Full Name */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
|
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
|
||||||
Full Name
|
Full Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -93,7 +84,7 @@ export default function SettingsContent() {
|
|||||||
|
|
||||||
{/* Field: Email Address */}
|
{/* Field: Email Address */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
|
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
|
||||||
Email Address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -108,7 +99,7 @@ export default function SettingsContent() {
|
|||||||
|
|
||||||
{/* Field: Professional Bio */}
|
{/* Field: Professional Bio */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
|
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
|
||||||
Professional Bio
|
Professional Bio
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -118,13 +109,13 @@ export default function SettingsContent() {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 transition-colors bg-white font-medium resize-none"
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 transition-colors bg-white font-medium resize-none"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-gray-400 font-normal pt-0.5">
|
<p className="text-[11px] text-gray-700 font-normal pt-0.5">
|
||||||
Brief description for your profile. URLs and @mentions are allowed.
|
Brief description for your profile. URLs and @mentions are allowed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Action Footer Row */}
|
{/* Form Action Footer Row */}
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-6 py-2 bg-[#B30000] hover:bg-[#990000] text-white font-bold text-xs rounded-lg shadow-sm transition-colors uppercase tracking-wider"
|
className="px-6 py-2 bg-[#B30000] hover:bg-[#990000] text-white font-bold text-xs rounded-lg shadow-sm transition-colors uppercase tracking-wider"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import TaskTimeline from "./components/TaskTimeline";
|
|||||||
// PR: tambahin breadcrumb
|
// PR: tambahin breadcrumb
|
||||||
export default function HomeInternPage() {
|
export default function HomeInternPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 px-14 py-10 bg-gray-50">
|
<div className="container flex-1">
|
||||||
<HomeHeader />
|
<HomeHeader />
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="grid grid-cols-2 gap-5">
|
<div className="grid grid-cols-2 gap-5">
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export default function ProjectDetailPage() {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 min-h-screen">
|
<div className="container">
|
||||||
<div className="px-24 py-10">
|
<div>
|
||||||
|
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { mockProjects } from '../../../lib/mockData'
|
|||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="px-14 py-15 bg-gray-50 min-h-screen">
|
<div className="container">
|
||||||
<div className="flex flex-col gap-1 mb-6">
|
<div className="flex flex-col gap-1 mb-6">
|
||||||
<span className="text-4xl font-bold">List Projects</span>
|
<span className="page-title">List Projects</span>
|
||||||
<h1 className="text-lg text-gray-500">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 */}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ const SertificatePage = () => {
|
|||||||
const data = certificateData; // ganti isReceived di object atas
|
const data = certificateData; // ganti isReceived di object atas
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-15 py-10 bg-white min-h-screen">
|
<div className="container">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex flex-col gap-1">
|
<div className="mb-12 flex flex-col gap-1">
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold mb-2">Certificates</h1>
|
<h1 className="page-title">Certificates</h1>
|
||||||
<p className="font-light text-gray-500">View and download internship certificates</p>
|
<p className="page-title-desc">View and download internship certificates</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { ChevronRight, Calendar, FileText, Info } from "lucide-react";
|
||||||
|
import TaskFormFile from "./components/TaskForm-file";
|
||||||
|
import TaskFormLink from "./components/TaskForm-link";
|
||||||
|
|
||||||
|
export default function TaskSubmission() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// State untuk menentukan jenis input ("file" atau "link")
|
||||||
|
const [submissionType, setSubmissionType] = useState<"file" | "link">("file");
|
||||||
|
|
||||||
|
const handleFileSubmit = (file: File) => {
|
||||||
|
console.log(`Submitting File for Task ${id}:`, file.name);
|
||||||
|
alert(`File ${file.name} berhasil dikirim!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLinkSubmit = (link: string) => {
|
||||||
|
console.log(`Submitting Link for Task ${id}:`, link);
|
||||||
|
alert(`Link berhasil dikirim!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container space-y-4">
|
||||||
|
{/* Header Card */}
|
||||||
|
<div className="w-full bg-white rounded-2xl border border-gray-100 p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)]">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Laporan Tugas 2</h1>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-gray-400 font-semibold mt-1.5">
|
||||||
|
<Calendar className="w-3.5 h-3.5 stroke-[2.5]" />
|
||||||
|
<span>Due Oct 25, 2024 at 23:59</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Switcher Demo Sederhana (Bisa kamu hapus jika tipe datang dari database/routing) */}
|
||||||
|
<div className="flex gap-1 bg-gray-100 p-1 rounded-lg text-xs font-bold">
|
||||||
|
<button
|
||||||
|
onClick={() => setSubmissionType("file")}
|
||||||
|
className={`px-3 py-1 rounded-md transition-colors ${submissionType === "file" ? "bg-white text-gray-900 shadow-sm" : "text-gray-500"}`}
|
||||||
|
>
|
||||||
|
Mode File
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSubmissionType("link")}
|
||||||
|
className={`px-3 py-1 rounded-md transition-colors ${submissionType === "link" ? "bg-white text-gray-900 shadow-sm" : "text-gray-500"}`}
|
||||||
|
>
|
||||||
|
Mode Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task Description Card */}
|
||||||
|
<div className="w-full bg-white rounded-2xl border border-gray-100 p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)] space-y-3">
|
||||||
|
<h2 className="text-base font-bold text-gray-800 tracking-tight">Task Description</h2>
|
||||||
|
<p className="text-sm text-gray-500 font-medium leading-relaxed">
|
||||||
|
Pada tugas pertama ini, Anda diminta untuk melakukan analisis mendalam terhadap kebutuhan pengguna
|
||||||
|
untuk sistem "Smart Campus Utility". Analisis ini akan menjadi fondasi bagi perancangan antarmuka pada
|
||||||
|
fase berikutnya.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Area Card */}
|
||||||
|
<div className="w-full bg-white rounded-2xl border border-gray-100 p-6 shadow-[0_4px_20px_rgba(0,0,0,0.01)] space-y-5">
|
||||||
|
<div className="flex items-center gap-2 border-b border-gray-50 pb-2">
|
||||||
|
<FileText className="w-4 h-4 text-[#B30000] stroke-[2.5]" />
|
||||||
|
<h2 className="text-base font-bold text-gray-800 tracking-tight">Submit Task</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Render Komponen secara Kondisional */}
|
||||||
|
{submissionType === "file" ? (
|
||||||
|
<TaskFormFile taskId={id} onSubmitSuccess={handleFileSubmit} />
|
||||||
|
) : (
|
||||||
|
<TaskFormLink taskId={id} onSubmitSuccess={handleLinkSubmit} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<div className="w-full bg-gray-50 border border-gray-100 rounded-lg p-3 flex items-start gap-2.5">
|
||||||
|
<Info className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-[11px] text-gray-400 font-medium leading-relaxed">
|
||||||
|
Once submitted, your work will be locked for mentor review. You can request a re-submission if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { Lock, Clock } from "lucide-react";
|
|||||||
|
|
||||||
export default function CertificateNotAvailable({ progress, remainingTasks }: { progress: number; remainingTasks: number }) {
|
export default function CertificateNotAvailable({ progress, remainingTasks }: { progress: number; remainingTasks: number }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white w-full rounded-2xl border border-gray-100 shadow-sm p-20 flex flex-col items-center text-center max-w-3/5 mx-auto z-10">
|
<div className="bg-white w-full rounded-2xl border border-gray-100 shadow-sm py-16 p-23 flex flex-col items-center text-center max-w-3/5 mx-auto z-10">
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<div className="relative mb-6">
|
<div className="relative mb-6">
|
||||||
<div className=" w-33 h-33 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
<div className=" w-33 h-33 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ const HomeCalendar = () => {
|
|||||||
className={`w-full h-7 flex items-center justify-center text-xs rounded
|
className={`w-full h-7 flex items-center justify-center text-xs rounded
|
||||||
${isBold(day) ? 'font-bold text-gray-800' : 'text-gray-700'}
|
${isBold(day) ? 'font-bold text-gray-800' : 'text-gray-700'}
|
||||||
${hasDeadline(day) ? 'bg-red-700 text-white' : ''}
|
${hasDeadline(day) ? 'bg-red-700 text-white' : ''}
|
||||||
hover:bg-gray-100 cursor-pointer transition-colors`}
|
transition-colors`}
|
||||||
>
|
>
|
||||||
{day}
|
{day}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const HomeHeader = () => {
|
const HomeHeader = () => {
|
||||||
const name = "Karina";
|
const name = "Karina";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-start px-0 mb-5">
|
<div className="flex items-start px-0 mb-5">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Hi, {name}!</h1>
|
<h1 className="page-title">Hi, {name}!</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { Task } from "../../../../lib/mockData";
|
import type { Task } from "../../../../lib/mockData";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const formatDate = (date: Date) =>
|
const formatDate = (date: Date) =>
|
||||||
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
|
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||||
|
|
||||||
export default function TaskCard({ task }: { task: Task }) {
|
export default function TaskCard({ task }: { task: Task }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<div className="border border-[#eee] rounded-[10px] px-5 py-4 mb-3 bg-white shadow-[0_1px_4px_rgba(0,0,0,0.05)] flex items-start justify-between gap-3">
|
<div className="border border-gray-200 rounded-[10px] px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className=" text-[11px] text-[#888] mb-1">
|
<div className=" text-[11px] text-[#888] mb-1">
|
||||||
Deadline : {formatDate(task.deadline.date)} at {task.deadline.time}
|
Deadline : {formatDate(task.deadline.date)} at {task.deadline.time}
|
||||||
@@ -24,8 +26,8 @@ export default function TaskCard({ task }: { task: Task }) {
|
|||||||
DONE
|
DONE
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<button className=" bg-white text-[#1a1a1a] border-[1.5px] border-[#d0d0d0] font-semibold text-xs px-3.5 py-[5px] rounded-md cursor-pointer whitespace-nowrap transition-colors duration-150 hover:border-[#C0392B] hover:text-[#C0392B]">
|
<button className=" bg-white text-[#1a1a1a] border-[1.5px] border-[#d0d0d0] font-semibold text-xs px-3.5 py-[5px] rounded-md cursor-pointer whitespace-nowrap transition-colors duration-150 hover:border-[#C0392B] hover:text-[#C0392B]" onClick={() => navigate(`/intern/tasks/${task.id}/submit`)}>
|
||||||
Mark as Done
|
Add Submission
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,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-[260px] flex flex-col">
|
<div className="bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[230px] 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 && (
|
||||||
@@ -33,41 +33,44 @@ const ProjectCard = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-3 flex flex-col gap-2">
|
<div className="min-h-[180px] max-h-[210px] flex-1 flex flex-col justify-between py-3">
|
||||||
<p className="text-sm font-semibold text-gray-800 leading-tight">{name}</p>
|
<div className="px-3 flex flex-col gap-2">
|
||||||
|
<p className="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">
|
||||||
<div className="w-6 h-6 rounded-full bg-gray-200 overflow-hidden flex-shrink-0">
|
<div className="w-6 h-6 rounded-full bg-gray-200 overflow-hidden flex-shrink-0">
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<img src={avatarUrl} alt={mentor} className="w-full h-full object-cover" />
|
<img src={avatarUrl} alt={mentor} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full bg-gray-300" />
|
<div className="w-full h-full bg-gray-300" />
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500 truncate">{mentor}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500 truncate">{mentor}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-red-600 rounded-full transition-all duration-300"
|
className="h-full bg-red-600 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-gray-400 text-right">
|
||||||
|
{tasksDone}/{tasksTotal}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-gray-400 text-right">
|
|
||||||
{tasksDone}/{tasksTotal}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<button
|
<div className="flex items-end justify-end px-3 pb-1">
|
||||||
onClick={() => navigate(`/intern/projects/${id}`)}
|
<button
|
||||||
className="w-full bg-red-600 hover:bg-red-700 text-white text-xs font-medium py-1.5 rounded-lg transition-colors"
|
onClick={() => navigate(`/intern/projects/${id}`)}
|
||||||
>
|
className="bg-red-600 hover:bg-red-700 text-white text-xs font-medium py-1.5 px-3 rounded-xl transition-colors"
|
||||||
View Project
|
>
|
||||||
</button>
|
View Project
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
|
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight, X } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: "Home", icon: Home, path: "/intern" },
|
{ label: "Home", icon: Home, path: "/intern" },
|
||||||
@@ -11,36 +10,62 @@ const navItems = [
|
|||||||
const prefItems = [
|
const prefItems = [
|
||||||
{ label: "Settings", icon: Settings, path: "/intern/settings" },
|
{ label: "Settings", icon: Settings, path: "/intern/settings" },
|
||||||
{ label: "FAQ", icon: HelpCircle, path: "/intern/faq" },
|
{ label: "FAQ", icon: HelpCircle, path: "/intern/faq" },
|
||||||
{ label: "Logout", icon: LogOut, path: "/intern/logout", danger: true },
|
{ label: "Logout", icon: LogOut, path: "/login-internify", danger: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
function SidebarIntern() {
|
interface SidebarProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
closeSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [active, setActive] = useState("/intern");
|
const location = useLocation();
|
||||||
|
|
||||||
const handleNav = (path: string) => {
|
const handleNav = (path: string) => {
|
||||||
setActive(path);
|
|
||||||
navigate(path);
|
navigate(path);
|
||||||
|
closeSidebar(); // Otomatis tutup sidebar setelah klik menu di HP
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-[279px] bg-white border-r border-gray-100 flex flex-col py-5 px-3 gap-1 sticky top-[73px] h-[calc(100vh-73px)] overflow-y-auto flex-shrink-0">
|
<aside
|
||||||
{/* Brand */}
|
className={`
|
||||||
<div className="flex items-center gap-2.5 px-2 pb-4">
|
/* Struktur Dasar Layout */
|
||||||
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0">
|
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
|
||||||
</div>
|
|
||||||
<div>
|
/* Kondisi Layar Desktop (>= md) */
|
||||||
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
|
md:sticky md:top-[73px] md:h-[calc(100vh-73px)] md:w-[250px] md:translate-x-0 md:z-8
|
||||||
<p className="text-[11px] text-gray-400">LMS Portal</p>
|
|
||||||
|
/* Kondisi Layar Mobile (< md) dan Logika Aktif-nya */
|
||||||
|
fixed top-0 left-0 h-screen w-[270px] z-40 pt-5
|
||||||
|
${isOpen ? "translate-x-0" : "-translate-x-full"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Brand & Tombol Close untuk Mobile */}
|
||||||
|
<div className="flex items-center justify-between px-2 pb-4">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
|
||||||
|
<p className="text-[11px] text-gray-400">LMS Portal</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tombol Close silang (X): Hanya muncul di HP saat sidebar terbuka */}
|
||||||
|
<button
|
||||||
|
onClick={closeSidebar}
|
||||||
|
className="p-1 text-gray-500 hover:bg-gray-100 rounded-md md:hidden"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-px bg-gray-100 my-1" />
|
<div className="h-px bg-gray-300 my-1" />
|
||||||
|
|
||||||
{/* Main nav */}
|
{/* Main nav */}
|
||||||
<nav className="flex flex-col gap-0.5">
|
<nav className="flex flex-col gap-0.5">
|
||||||
{navItems.map(({ label, icon: Icon, path }) => {
|
{navItems.map(({ label, icon: Icon, path }) => {
|
||||||
const isActive = active === path;
|
const isActive = location.pathname === path;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={path}
|
key={path}
|
||||||
@@ -53,15 +78,13 @@ function SidebarIntern() {
|
|||||||
>
|
>
|
||||||
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isActive && (
|
{isActive && <ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />}
|
||||||
<ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="h-px bg-gray-100 my-1" />
|
<div className="h-px bg-gray-200 my-1" />
|
||||||
|
|
||||||
{/* Preferences */}
|
{/* Preferences */}
|
||||||
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
|
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
|
||||||
@@ -69,7 +92,7 @@ function SidebarIntern() {
|
|||||||
</p>
|
</p>
|
||||||
<nav className="flex flex-col gap-0.5">
|
<nav className="flex flex-col gap-0.5">
|
||||||
{prefItems.map(({ label, icon: Icon, path, danger }) => {
|
{prefItems.map(({ label, icon: Icon, path, danger }) => {
|
||||||
const isActive = active === path;
|
const isActive = location.pathname === path;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={path}
|
key={path}
|
||||||
@@ -88,10 +111,6 @@ function SidebarIntern() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default SidebarIntern
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { useState, useRef } from "react";
|
||||||
|
import { FileText, UploadCloud } from "lucide-react";
|
||||||
|
import { LuSendHorizontal } from "react-icons/lu";
|
||||||
|
|
||||||
|
interface TaskFormFileProps {
|
||||||
|
taskId: string | undefined;
|
||||||
|
onSubmitSuccess: (file: File) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TaskFormFile({ taskId, onSubmitSuccess }: TaskFormFileProps) {
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [isDragActive, setIsDragActive] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
validateAndSetFile(e.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrag = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.type === "dragenter" || e.type === "dragover") {
|
||||||
|
setIsDragActive(true);
|
||||||
|
} else if (e.type === "dragleave") {
|
||||||
|
setIsDragActive(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragActive(false);
|
||||||
|
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||||
|
validateAndSetFile(e.dataTransfer.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAndSetFile = (selectedFile: File) => {
|
||||||
|
const allowedExtensions = ["pdf", "docx", "zip"];
|
||||||
|
const fileExtension = selectedFile.name.split(".").pop()?.toLowerCase();
|
||||||
|
|
||||||
|
if (fileExtension && allowedExtensions.includes(fileExtension)) {
|
||||||
|
if (selectedFile.size <= 10 * 1024 * 1024) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
} else {
|
||||||
|
alert("Ukuran file melebihi batas maksimal 10MB.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Format file tidak didukung. Harap gunakan PDF, DOCX, atau ZIP.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!file) {
|
||||||
|
alert("Silakan pilih atau jatuhkan berkas tugas Anda terlebih dahulu.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmitSuccess(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept=".pdf,.docx,.zip"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onDragEnter={handleDrag}
|
||||||
|
onDragOver={handleDrag}
|
||||||
|
onDragLeave={handleDrag}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className={`w-full min-h-[180px] border-2 border-dashed rounded-xl flex flex-col items-center justify-center p-6 text-center cursor-pointer transition-all ${
|
||||||
|
isDragActive
|
||||||
|
? "border-[#B30000] bg-red-50/40"
|
||||||
|
: "border-gray-200 bg-gray-50/50 hover:bg-gray-50 hover:border-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{file ? (
|
||||||
|
<div className="space-y-2 pointer-events-none animate-in fade-in duration-200">
|
||||||
|
<div className="w-12 h-12 bg-green-50 text-green-600 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<FileText className="w-6 h-6 stroke-[2]" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-bold text-gray-800 max-w-md truncate">{file.name}</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
{(file.size / (1024 * 1024)).toFixed(2)} MB • Klik untuk mengganti berkas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 pointer-events-none">
|
||||||
|
<UploadCloud className="w-10 h-10 text-gray-400 mx-auto stroke-[1.5]" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-gray-700">
|
||||||
|
Click to upload <span className="font-medium text-gray-500">or drag & drop</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-gray-400 font-semibold mt-1">
|
||||||
|
PDF, DOCX, or ZIP (Max. 10MB)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-[#B30000] hover:bg-[#990000] text-white font-bold text-base py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer"
|
||||||
|
>
|
||||||
|
Submit Task
|
||||||
|
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Link } from "lucide-react";
|
||||||
|
import { LuSendHorizontal } from "react-icons/lu";
|
||||||
|
|
||||||
|
interface TaskFormLinkProps {
|
||||||
|
taskId: string | undefined;
|
||||||
|
onSubmitSuccess: (link: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TaskFormLink({ taskId, onSubmitSuccess }: TaskFormLinkProps) {
|
||||||
|
const [submissionLink, setSubmissionLink] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!submissionLink.trim()) {
|
||||||
|
alert("Silakan masukkan link tugas Anda terlebih dahulu.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmitSuccess(submissionLink);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[11px] font-bold text-gray-700 tracking-wide block">
|
||||||
|
Submission Link (PDF)
|
||||||
|
</label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<span className="absolute left-3.5 text-gray-400">
|
||||||
|
<Link className="w-4 h-4 stroke-[2]" />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
required
|
||||||
|
placeholder="https://example.com/your-pdf-link"
|
||||||
|
value={submissionLink}
|
||||||
|
onChange={(e) => setSubmissionLink(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 bg-gray-50/50 border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-700 focus:outline-none focus:bg-white focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-gray-400 font-medium pt-0.5">
|
||||||
|
Ensure the link is accessible to the mentor (e.g., Google Drive, Dropbox).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-[#B30000] hover:bg-[#990000] text-white font-bold text-base py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer"
|
||||||
|
>
|
||||||
|
Submit Task
|
||||||
|
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,16 +1,26 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { useState } from "react";
|
||||||
import Header from './mentor/components/Header';
|
import Header from './mentor/components/Header';
|
||||||
import SidebarIntern from './intern/components/SidebarIntern';
|
import SidebarIntern from './intern/components/SidebarIntern';
|
||||||
import SidebarAdmin from '../../Layout/SidebarAdmin';
|
import SidebarMentor from './mentor/components/SidebarMentor';
|
||||||
import FooterIntern from '../../Layout/Footer-Internify';
|
import FooterIntern from '../../Layout/Footer-Internify';
|
||||||
|
|
||||||
export const InternLayout = () => {
|
export const InternLayout = () => {
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
|
||||||
|
const closeSidebar = () => setIsSidebarOpen(false);
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col">
|
<div className="min-h-screen bg-gray-50 flex flex-col relative">
|
||||||
<Header />
|
<Header toggleSidebar={toggleSidebar}/>
|
||||||
<div className="flex flex-1">
|
<div className="flex flex-1 relative">
|
||||||
<SidebarIntern />
|
<SidebarIntern isOpen={isSidebarOpen} closeSidebar={closeSidebar} />
|
||||||
<main className="flex-1 flex flex-col min-w-0">
|
{isSidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/20 z-20 md:hidden min-h-screen"
|
||||||
|
onClick={closeSidebar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<main className="flex-1 flex flex-col">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,11 +30,20 @@ export const InternLayout = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MentorLayout = () => {
|
export const MentorLayout = () => {
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
|
||||||
|
const closeSidebar = () => setIsSidebarOpen(false);
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col">
|
<div className="min-h-screen bg-gray-50 flex flex-col">
|
||||||
<Header />
|
<Header toggleSidebar={toggleSidebar}/>
|
||||||
<div className="flex flex-1">
|
<div className="flex flex-1">
|
||||||
<SidebarAdmin />
|
<SidebarMentor isOpen={isSidebarOpen} closeSidebar={closeSidebar} />
|
||||||
|
{isSidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/20 z-20 md:hidden min-h-screen"
|
||||||
|
onClick={closeSidebar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<main className="flex-1 flex flex-col min-w-0">
|
<main className="flex-1 flex flex-col min-w-0">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
import { mockProjects } from '../../../lib/mockData'
|
import { mockProjects } from '../../../lib/mockData';
|
||||||
import MentorProjectCard from './components/MentorProjectCard'
|
import MentorProjectCard from './components/MentorProjectCard';
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom' // Import useNavigate
|
import { useState } from 'react';
|
||||||
|
import CreateProjectModal from './components/CreateProjectDialog';
|
||||||
|
|
||||||
const MentorProjectsPage = () => {
|
export default function MentorProjectsPage(){
|
||||||
const navigate = useNavigate()
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleJoinProject = () => {
|
const openDialog = () => {
|
||||||
// Navigasi ke halaman tujuan, silakan ganti '/path-tujuan' sesuai rute kamu
|
setIsModalOpen(true);
|
||||||
navigate('/path-tujuan')
|
};
|
||||||
}
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-14 py-15 bg-gray-50 min-h-screen">
|
<div className="container">
|
||||||
{/* Breadcrumb simulasi sesuai gambar */}
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div className="text-xs text-gray-500 mb-2">
|
<div className="flex flex-col">
|
||||||
<span className="text-red-600 font-medium">Home</span> > <span>List Projects</span>
|
<h1 className="page-title">List Projects</h1>
|
||||||
</div>
|
<p className="page-title-desc">Create, manage, and monitor ongoing projects and assignments.</p>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-1 mb-6">
|
<div>
|
||||||
<h1 className="text-4xl font-bold text-gray-900">List Projects</h1>
|
{/* Tombol pemicu buka modal */}
|
||||||
<p className="text-lg text-gray-500">Create, manage, and monitor ongoing projects and assignments.</p>
|
<button
|
||||||
|
onClick={openDialog}
|
||||||
|
className="flex items-center gap-2 px-4 py-1 bg-[#C0392B] text-white rounded-lg hover:bg-[#A93226] transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-stretch">
|
<div className="flex flex-wrap gap-4 items-stretch">
|
||||||
{/* Render List Project */}
|
|
||||||
{mockProjects.map(project => (
|
{mockProjects.map(project => (
|
||||||
<MentorProjectCard key={project.id} {...project} />
|
<MentorProjectCard key={project.id} {...project} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Tombol "Join New Project" dengan Border Putus-putus */}
|
|
||||||
<button
|
|
||||||
onClick={handleJoinProject}
|
|
||||||
className="w-[180px] min-h-[230px] border-2 border-dashed border-gray-400 rounded-xl flex flex-col items-center justify-center gap-3 p-4 bg-white hover:bg-gray-100 hover:border-red-600 transition-colors group"
|
|
||||||
>
|
|
||||||
{/* Lingkaran Plus */}
|
|
||||||
<div className="w-12 h-12 rounded-full border border-red-600 flex items-center justify-center text-red-600 group-hover:bg-red-50 transition-colors">
|
|
||||||
<Plus size={24} />
|
|
||||||
</div>
|
|
||||||
{/* Teks Deskripsi */}
|
|
||||||
<span className="text-sm font-semibold text-red-600 text-center leading-tight">
|
|
||||||
Join New Project
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MentorProjectsPage
|
{isModalOpen && (
|
||||||
|
<CreateProjectModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={closeDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,246 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
PlusCircle,
|
||||||
|
UploadCloud,
|
||||||
|
Eye,
|
||||||
|
Download,
|
||||||
|
Trash2,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
DownloadCloud
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { LuHistory, LuSendHorizontal } from 'react-icons/lu';
|
||||||
|
|
||||||
|
// Mock Data untuk Tabel Riwayat Sertifikat
|
||||||
|
const initialHistoryData = [
|
||||||
|
{ id: 1, name: "Jonathan Kristina", email: "JonathanKristina@gmail.com", project: "Internify Project", date: "24 Oct 2024", initials: "JK" },
|
||||||
|
{ id: 2, name: "Ahmad Faisal", email: "ahmad.faisal@gmail.com", project: "Web Dev Fundamentals", date: "15 Sep 2024", initials: "AF" },
|
||||||
|
{ id: 3, name: "Siti Rahma", email: "siti.rahma@gmail.com", project: "UI/UX Essentials", date: "10 Aug 2024", initials: "SR" },
|
||||||
|
{ id: 4, name: "David Beckham", email: "david.b@gmail.com", project: "Engineering Intern 101", date: "05 Aug 2024", initials: "DB" },
|
||||||
|
];
|
||||||
|
|
||||||
const MentorCertificatePage = () => {
|
const MentorCertificatePage = () => {
|
||||||
|
// State Form Management
|
||||||
|
const [selectedIntern, setSelectedIntern] = useState("JonathanKristina@gmail.com");
|
||||||
|
const [selectedProject, setSelectedProject] = useState("Internify Project (Web Development)");
|
||||||
|
const [issueDate, setIssueDate] = useState("2024-10-24");
|
||||||
|
const [expiryDate, setExpiryDate] = useState("");
|
||||||
|
|
||||||
|
// State Data Tabel
|
||||||
|
const [historyData, setHistoryData] = useState(initialHistoryData);
|
||||||
|
|
||||||
|
const handleIssueCertificate = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = {
|
||||||
|
intern: selectedIntern,
|
||||||
|
project: selectedProject,
|
||||||
|
issueDate,
|
||||||
|
expiryDate
|
||||||
|
};
|
||||||
|
console.log("Issuing Certificate:", payload);
|
||||||
|
alert("Certificate Issued Successfully!");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteHistory = (id: number) => {
|
||||||
|
if(confirm("Are you sure you want to delete this certificate log?")) {
|
||||||
|
setHistoryData(historyData.filter(item => item.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="px-6 md:px-14 py-8 md:py-12 bg-gray-50 min-h-screen">
|
||||||
|
{/* Breadcrumb & Header Title */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<p className="text-xs font-semibold text-[#B30000] mb-1 flex items-center gap-1 cursor-pointer">
|
||||||
|
Certificates <span className="text-gray-400 font-normal">></span>
|
||||||
|
</p>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 tracking-tight">Certificates</h1>
|
||||||
|
<p className="text-sm md:text-base text-gray-500 mt-1">
|
||||||
|
Administrate, issue, and track professional certifications for all interns.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Grid Content */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||||
|
|
||||||
|
{/* Kiri: Form Issue New Certificate (4 Kolom) */}
|
||||||
|
<div className="lg:col-span-5 xl:col-span-4 bg-white border border-gray-100 rounded-2xl p-6 md:p-8 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<PlusCircle className="w-6 h-6 text-[#B30000]" />
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 tracking-tight">Issue New Certificate</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleIssueCertificate} className="space-y-5">
|
||||||
|
{/* Select Intern */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Select Intern</label>
|
||||||
|
<select
|
||||||
|
value={selectedIntern}
|
||||||
|
onChange={(e) => setSelectedIntern(e.target.value)}
|
||||||
|
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||||
|
>
|
||||||
|
<option value="JonathanKristina@gmail.com">JonathanKristina@gmail.com</option>
|
||||||
|
<option value="ahmad.faisal@gmail.com">ahmad.faisal@gmail.com</option>
|
||||||
|
<option value="siti.rahma@gmail.com">siti.rahma@gmail.com</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Select Project / Course */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Select Project / Course</label>
|
||||||
|
<select
|
||||||
|
value={selectedProject}
|
||||||
|
onChange={(e) => setSelectedProject(e.target.value)}
|
||||||
|
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||||
|
>
|
||||||
|
<option value="Internify Project (Web Development)">Internify Project (Web Development)</option>
|
||||||
|
<option value="Web Dev Fundamentals">Web Dev Fundamentals</option>
|
||||||
|
<option value="UI/UX Essentials">UI/UX Essentials</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Certificate Image Upload Area */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Certificate Image</label>
|
||||||
|
<div className="border-2 border-dashed border-red-200/60 bg-red-50/10 rounded-xl p-6 text-center flex flex-col items-center justify-center cursor-pointer hover:bg-red-50/20 transition-colors group">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mb-2 group-hover:scale-105 transition-transform">
|
||||||
|
<UploadCloud className="w-5 h-5 text-[#B30000]" />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-bold text-gray-800">Upload Certificate Image</p>
|
||||||
|
<p className="text-[10px] text-gray-400 mt-0.5">PNG, JPG up to 5MB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Issue Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
value={issueDate}
|
||||||
|
onChange={(e) => setIssueDate(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-500 tracking-wide">Expiry (Optional)</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={expiryDate}
|
||||||
|
onChange={(e) => setExpiryDate(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-3 mt-4 bg-[#B30000] hover:bg-[#990000] text-white text-sm font-bold rounded-xl transition-colors shadow-sm flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<LuSendHorizontal className="w-4 h-4" />
|
||||||
|
Issue Certificate
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kanan: Issued Certificates History (8 Kolom) */}
|
||||||
|
<div className="lg:col-span-7 lg:col-span-8 bg-white border border-gray-100 rounded-2xl shadow-sm overflow-hidden flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
{/* Header Table */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-5 border-b border-gray-50">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<LuHistory className="w-5 h-5 text-red-600 stroke-[2.5]" />
|
||||||
|
<h2 className="text-lg font-bold text-gray-900 tracking-tight">Issued Certificates History</h2>
|
||||||
|
</div>
|
||||||
|
<button className="text-xs font-bold text-[#B30000] hover:text-[#990000] flex items-center gap-1.5 bg-red-50/50 px-3 py-1.5 rounded-lg border border-red-100 transition-colors">
|
||||||
|
<span>Export CSV</span>
|
||||||
|
<DownloadCloud className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Responsive Table Wrapper */}
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50/70 border-b border-gray-100">
|
||||||
|
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Intern Name</th>
|
||||||
|
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Project</th>
|
||||||
|
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase">Issue Date</th>
|
||||||
|
<th className="px-6 py-3.5 text-[10.5px] font-bold tracking-wider text-gray-400 uppercase text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-50">
|
||||||
|
{historyData.map((row) => (
|
||||||
|
<tr key={row.id} className="hover:bg-gray-50/40 transition-colors">
|
||||||
|
{/* Kolom Nama Intern + Avatar Ringkas */}
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gray-100 border border-gray-200/60 flex items-center justify-center text-xs font-bold text-gray-500 flex-shrink-0">
|
||||||
|
{row.initials}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-gray-900 leading-tight">{row.name}</p>
|
||||||
|
<p className="text-[11px] text-gray-400 mt-0.5">{row.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{/* Kolom Nama Proyek */}
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<p className="text-sm text-gray-600 font-medium">{row.project}</p>
|
||||||
|
</td>
|
||||||
|
{/* Kolom Tanggal Terbit */}
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<p className="text-sm text-gray-500 font-medium">{row.date}</p>
|
||||||
|
</td>
|
||||||
|
{/* Kolom Deretan Aksi */}
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<button className="p-1.5 text-gray-400 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors" title="View Certificate">
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors" title="Download">
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteHistory(row.id)}
|
||||||
|
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
||||||
|
title="Delete Log"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bagian Footer Tabel & Paginasi */}
|
||||||
|
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50/30 flex flex-col sm:flex-row gap-3 items-center justify-between text-xs font-medium text-gray-500">
|
||||||
|
<p>Showing 1 to {historyData.length} of 1,284 entries</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50 disabled:opacity-50">
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-[#B30000] text-white font-bold shadow-sm">1</button>
|
||||||
|
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-white border border-gray-200 text-gray-600 hover:bg-gray-50">2</button>
|
||||||
|
<button className="w-7 h-7 flex items-center justify-center rounded-md bg-white border border-gray-200 text-gray-600 hover:bg-gray-50">3</button>
|
||||||
|
<button className="p-1 border border-gray-200 rounded-md bg-white text-gray-400 hover:bg-gray-50">
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MentorCertificatePage
|
export default MentorCertificatePage;
|
||||||
@@ -4,7 +4,7 @@ import HomeMentorProject from './components/HomeMentorProject'
|
|||||||
|
|
||||||
const HomeMentorPage = () => {
|
const HomeMentorPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 px-14 py-10 bg-gray-50">
|
<div className="container flex-1 bg-gray-50">
|
||||||
<HomeHeader />
|
<HomeHeader />
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="">
|
<div className="">
|
||||||
|
|||||||
@@ -1,5 +1,196 @@
|
|||||||
export default function MentorInternPage() {
|
import { useState, useMemo } from "react";
|
||||||
return(
|
import { Search, SlidersHorizontal, Plus, Trash2 } from "lucide-react";
|
||||||
<div></div>
|
import { mockInternsData } from "../../../lib/mockData";
|
||||||
)
|
|
||||||
|
|
||||||
|
export default function InternsManagement() {
|
||||||
|
// State Filter & Search
|
||||||
|
const [searchEmail, setSearchEmail] = useState("");
|
||||||
|
const [selectedProject, setSelectedProject] = useState("All Projects");
|
||||||
|
|
||||||
|
// Handler Aksi Kursor/Tombol
|
||||||
|
const handleAddAssignment = (id: number) => {
|
||||||
|
alert(`Assign/Add action clicked for intern ID: ${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteIntern = (id: number) => {
|
||||||
|
alert(`Deleted intern ID: ${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logic Filtering Data menggunakan useMemo
|
||||||
|
const filteredInterns = useMemo(() => {
|
||||||
|
return mockInternsData.filter((intern) => {
|
||||||
|
const matchesEmail = intern.email.toLowerCase().includes(searchEmail.toLowerCase());
|
||||||
|
const matchesProject =
|
||||||
|
selectedProject === "All Projects" ||
|
||||||
|
intern.projectName.toLowerCase() === selectedProject.toLowerCase();
|
||||||
|
return matchesEmail && matchesProject;
|
||||||
|
});
|
||||||
|
}, [searchEmail, selectedProject]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-16 py-12 font-sans">
|
||||||
|
{/* Header Title */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="page-title">Interns Management</h1>
|
||||||
|
<p className="page-title-desc">
|
||||||
|
Manage role-based access and project assignments for active interns.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Row & Stats Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 items-center mb-6">
|
||||||
|
{/* Left Control Inputs */}
|
||||||
|
<div className="lg:col-span-2 flex flex-col sm:flex-row items-center gap-3">
|
||||||
|
|
||||||
|
{/* Search Box Input */}
|
||||||
|
<div className="relative w-full sm:max-w-xs">
|
||||||
|
<span className="absolute inset-y-0 left-3 flex items-center text-gray-400">
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter by email"
|
||||||
|
value={searchEmail}
|
||||||
|
onChange={(e) => setSearchEmail(e.target.value)}
|
||||||
|
className="w-full pl-9 pr-4 py-2 bg-gray-100 border border-transparent rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:bg-white focus:border-gray-200 font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Select Project Dropdown */}
|
||||||
|
<div className="relative w-full sm:w-48">
|
||||||
|
<select
|
||||||
|
value={selectedProject}
|
||||||
|
onChange={(e) => setSelectedProject(e.target.value)}
|
||||||
|
className="w-full pl-3 pr-8 py-2 bg-gray-100 border border-transparent rounded-lg text-sm text-gray-700 font-medium appearance-none focus:outline-none focus:bg-white focus:border-gray-200"
|
||||||
|
>
|
||||||
|
<option value="All Projects">All Projects</option>
|
||||||
|
<option value="Internify Project">Internify Project</option>
|
||||||
|
<option value="MBG Project">MBG Project</option>
|
||||||
|
<option value="Unassigned">Unassigned</option>
|
||||||
|
</select>
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500">
|
||||||
|
<svg className="fill-current h-4 w-4" viewBox="0 0 20 20">
|
||||||
|
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icon Sliders / Filter Button */}
|
||||||
|
<button className="p-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors hidden sm:block">
|
||||||
|
<SlidersHorizontal className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Info Box Stats */}
|
||||||
|
<div className="bg-white border border-gray-100 rounded-xl px-6 py-2.5 flex items-center justify-around text-center shadow-[0_2px_10px_rgba(0,0,0,0.01)] w-full lg:max-w-sm ml-auto">
|
||||||
|
<div>
|
||||||
|
<span className="block text-xl font-bold text-red-600 leading-none">124</span>
|
||||||
|
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase mt-1 block">
|
||||||
|
TOTAL INTERNS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-8 w-px bg-gray-100 mx-2"></div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-xl font-bold text-green-600 leading-none">92</span>
|
||||||
|
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase mt-1 block">
|
||||||
|
ACTIVE ASSIGNMENTS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Table Container Block */}
|
||||||
|
<div className="w-full bg-white rounded-xl border border-gray-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] overflow-hidden">
|
||||||
|
|
||||||
|
{/* Red Table Header Block */}
|
||||||
|
<div className="bg-[#B30000] text-white px-6 py-3.5 flex items-center text-xs font-bold uppercase tracking-wider">
|
||||||
|
<div className="w-1/2">Intern Details</div>
|
||||||
|
<div className="w-1/3">Current Project</div>
|
||||||
|
<div className="w-1/6 text-right pr-4">Actions</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Body Iteration Rows */}
|
||||||
|
<div className="divide-y divide-gray-100">
|
||||||
|
{filteredInterns.length > 0 ? (
|
||||||
|
filteredInterns.map((intern, index) => {
|
||||||
|
const isUnassigned = intern.projectName === "Unassigned";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`${intern.id}-${index}`} className="px-6 py-4 flex items-center hover:bg-gray-50/60 transition-colors">
|
||||||
|
|
||||||
|
{/* Column 1: Intern Details (Avatar + Name & Email) */}
|
||||||
|
<div className="w-1/2 flex items-center gap-3">
|
||||||
|
{intern.avatar ? (
|
||||||
|
<img
|
||||||
|
src={intern.avatar}
|
||||||
|
alt={intern.name}
|
||||||
|
className="w-10 h-10 rounded-full object-cover border border-gray-100 shadow-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-teal-400 to-emerald-500 flex items-center justify-center border border-gray-100 shadow-sm text-white text-xs font-bold">
|
||||||
|
{intern.name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-gray-900 leading-tight">{intern.name}</h4>
|
||||||
|
<p className="text-xs text-gray-400 font-medium mt-0.5">{intern.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 2: Current Project Title & Role Accent */}
|
||||||
|
<div className="w-1/3">
|
||||||
|
<h5 className={`text-sm font-semibold leading-tight ${isUnassigned ? 'text-gray-500 italic' : 'text-gray-900'}`}>
|
||||||
|
{intern.projectName}
|
||||||
|
</h5>
|
||||||
|
<p className={`text-xs font-bold mt-0.5 ${isUnassigned ? 'text-red-500' : 'text-red-600'}`}>
|
||||||
|
{intern.role}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column 3: Action Buttons Block Right aligned */}
|
||||||
|
<div className="w-1/6 flex items-center justify-end gap-3.5 pr-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddAssignment(intern.id)}
|
||||||
|
className="p-1.5 bg-[#B30000] hover:bg-[#990000] text-white rounded-lg transition-colors shadow-sm"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 stroke-[3]" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteIntern(intern.id)}
|
||||||
|
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 stroke-[2]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-sm text-gray-400 font-medium">
|
||||||
|
No interns found matching the filter criteria.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Red Table Pagination Bar Footer */}
|
||||||
|
<div className="bg-[#B30000] text-white px-6 py-3.5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs font-medium">
|
||||||
|
<div>
|
||||||
|
Showing <span className="font-bold">{filteredInterns.length}</span> of 124 interns
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 self-end sm:self-auto">
|
||||||
|
<button className="px-3 py-1.5 bg-black/20 hover:bg-black/30 rounded-md font-semibold opacity-50 cursor-not-allowed transition-colors">
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button className="px-4 py-1.5 bg-white text-gray-900 hover:bg-gray-100 rounded-md font-bold shadow-sm transition-colors">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
import { X, Search, Plus, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface ManageInternsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ManageInternsModal({ isOpen, onClose }: ManageInternsModalProps) {
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("Jonathankristin@gmail.com");
|
||||||
|
|
||||||
|
// Data Intern Terundang (Simulasi list atas yang ada ikon sampah)
|
||||||
|
const [invitedList, setInvitedList] = useState([
|
||||||
|
{ id: 1, email: "Jonathankristina@gmail.com", initials: "JK" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Data Rekomendasi/Hasil Pencarian (Simulasi list bawah yang ada ikon plus)
|
||||||
|
const [searchResult, setSearchResult] = useState([
|
||||||
|
{ id: 2, email: "Jonathankristin@gmail.com", initials: "JK" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
dialog.showModal();
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
dialog.close();
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.addEventListener("cancel", handleCancel);
|
||||||
|
return () => {
|
||||||
|
dialog.removeEventListener("cancel", handleCancel);
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className="fixed inset-0 m-auto flex flex-col w-[calc(100%-2rem)] max-w-[540px] max-h-[calc(100vh-4rem)] rounded-2xl border border-gray-100 bg-white p-6 shadow-2xl backdrop:bg-black/40 backdrop:backdrop-blur-sm animate-in fade-in zoom-in-95 duration-200"
|
||||||
|
>
|
||||||
|
{/* Search Input Area */}
|
||||||
|
<div className="relative flex items-center mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search by email..."
|
||||||
|
className="w-full pl-4 pr-10 py-3 bg-white border border-gray-200 rounded-xl text-sm placeholder-gray-400 text-gray-700 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium shadow-sm"
|
||||||
|
/>
|
||||||
|
<Search className="absolute right-3.5 w-4 h-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List Container (Scrollable jika data banyak) */}
|
||||||
|
<div className="overflow-y-auto space-y-3 pr-1 flex-1">
|
||||||
|
|
||||||
|
{/* List 1: Intern Yang Sudah Tergabung / Ditambahkan */}
|
||||||
|
{invitedList.map((intern) => (
|
||||||
|
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-full bg-gray-200 border border-gray-300 flex items-center justify-center text-xs font-bold text-gray-500">
|
||||||
|
{intern.initials}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setInvitedList(invitedList.filter(i => i.id !== intern.id))}
|
||||||
|
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* List 2: Hasil Pencarian / Belum Ditambahkan */}
|
||||||
|
{searchResult.map((intern) => (
|
||||||
|
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-full bg-gray-200 border border-gray-300 flex items-center justify-center text-xs font-bold text-gray-500">
|
||||||
|
{intern.initials}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setInvitedList([...invitedList, intern]);
|
||||||
|
setSearchResult(searchResult.filter(i => i.id !== intern.id));
|
||||||
|
}}
|
||||||
|
className="p-2 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
import { X, Calendar } from "lucide-react";
|
||||||
|
import {
|
||||||
|
LuCode,
|
||||||
|
LuAlbum,
|
||||||
|
LuCloud,
|
||||||
|
LuSmartphone,
|
||||||
|
LuSettings,
|
||||||
|
LuUsers,
|
||||||
|
LuClipboardList,
|
||||||
|
LuPenTool,
|
||||||
|
LuLightbulb,
|
||||||
|
LuShield,
|
||||||
|
LuMail,
|
||||||
|
LuSendHorizontal
|
||||||
|
} from "react-icons/lu";
|
||||||
|
|
||||||
|
interface CreateProjectModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateProjectModal({ isOpen, onClose }: CreateProjectModalProps) {
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
|
// State Form Management
|
||||||
|
const [selectedIcon, setSelectedIcon] = useState("code");
|
||||||
|
const [projectName, setProjectName] = useState("");
|
||||||
|
const [projectDescription, setProjectDescription] = useState("");
|
||||||
|
const [emailInput, setEmailInput] = useState("");
|
||||||
|
const [invitedEmails, setInvitedEmails] = useState<string[]>([
|
||||||
|
"alex.dev@gmail.com",
|
||||||
|
"sarah.engineer@gmail.com"
|
||||||
|
]);
|
||||||
|
const [startDate, setStartDate] = useState("");
|
||||||
|
const [endDate, setEndDate] = useState("");
|
||||||
|
|
||||||
|
// Daftar Icon Pilihan di baris atas
|
||||||
|
const projectIcons = [
|
||||||
|
{ id: "code", icon: LuCode },
|
||||||
|
{ id: "chart", icon: LuAlbum },
|
||||||
|
{ id: "cloud", icon: LuCloud },
|
||||||
|
{ id: "mobile", icon: LuSmartphone },
|
||||||
|
{ id: "settings", icon: LuSettings },
|
||||||
|
{ id: "users", icon: LuUsers },
|
||||||
|
{ id: "task", icon: LuClipboardList },
|
||||||
|
{ id: "design", icon: LuPenTool },
|
||||||
|
{ id: "idea", icon: LuLightbulb },
|
||||||
|
{ id: "secure", icon: LuShield },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
dialog.showModal();
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
dialog.close();
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.addEventListener("cancel", handleCancel);
|
||||||
|
return () => {
|
||||||
|
dialog.removeEventListener("cancel", handleCancel);
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
// Fungsi menambah email undangan baru
|
||||||
|
const handleAddEmail = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (emailInput.trim() && !invitedEmails.includes(emailInput.trim())) {
|
||||||
|
setInvitedEmails([...invitedEmails, emailInput.trim()]);
|
||||||
|
setEmailInput("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fungsi menghapus email undangan
|
||||||
|
const handleRemoveEmail = (emailToRemove: string) => {
|
||||||
|
setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler Submit Utama
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = {
|
||||||
|
icon: selectedIcon,
|
||||||
|
name: projectName,
|
||||||
|
description: projectDescription,
|
||||||
|
emails: invitedEmails,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
};
|
||||||
|
console.log("Launching Project:", payload);
|
||||||
|
alert("Project Launched Successfully!");
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className="fixed inset-0 m-auto flex flex-col w-[calc(100%-2rem)] max-w-[620px] max-h-[calc(100vh-4rem)] rounded-2xl border border-gray-100 bg-white p-0 shadow-2xl backdrop:bg-black/40 backdrop:backdrop-blur-sm animate-in fade-in zoom-in-95 duration-200"
|
||||||
|
>
|
||||||
|
{/* Header Modal */}
|
||||||
|
<div className="flex items-start justify-between px-6 md:px-8 py-4 md:py-5 border-b border-gray-100">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 tracking-tight">Create New Project</h2>
|
||||||
|
<p className="text-xs text-gray-400 font-medium mt-0.5">
|
||||||
|
Launch a new internship assignment or trackable milestone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors mt-1"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5 stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body Form Modal */}
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-4 md:space-y-5 overflow-y-auto flex-1">
|
||||||
|
|
||||||
|
{/* Section: Select Project Icon */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
|
Select Project Icon
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{projectIcons.map((item) => {
|
||||||
|
const IconComponent = item.icon;
|
||||||
|
const isSelected = selectedIcon === item.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedIcon(item.id)}
|
||||||
|
className={`w-10 h-10 flex items-center justify-center rounded-lg border text-lg transition-all ${isSelected
|
||||||
|
? "border-[#B30000] bg-red-50/50 text-[#B30000] ring-2 ring-[#B30000]/20 font-bold"
|
||||||
|
: "border-gray-200 bg-gray-50 text-gray-500 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<IconComponent className={isSelected ? "stroke-[2.5]" : "stroke-[2]"} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section: Project Name */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
|
Project Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="e.g., Q3 Infrastructure Upgrade"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(e) => setProjectName(e.target.value)}
|
||||||
|
className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section: Project Description */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
|
Project Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder="Outline the project goals, deliverables, and technical requirements..."
|
||||||
|
value={projectDescription}
|
||||||
|
onChange={(e) => setProjectDescription(e.target.value)}
|
||||||
|
className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium resize-none leading-relaxed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section: Invite via Gmail */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
|
Invite via Gmail
|
||||||
|
</label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<span className="absolute left-3.5 text-gray-400">
|
||||||
|
<LuMail className="w-4 h-4 stroke-[2.5]" />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="intern.name@gmail.com"
|
||||||
|
value={emailInput}
|
||||||
|
onChange={(e) => setEmailInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddEmail(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full pl-10 pr-20 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddEmail}
|
||||||
|
className="absolute right-2 px-3 py-1 bg-gray-100 hover:bg-gray-200 text-gray-800 text-[11px] font-bold rounded-md transition-colors tracking-wider uppercase"
|
||||||
|
>
|
||||||
|
ADD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badges List Email */}
|
||||||
|
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||||
|
{invitedEmails.map((email) => (
|
||||||
|
<div
|
||||||
|
key={email}
|
||||||
|
className="flex items-center gap-1 bg-gray-100 text-gray-600 px-2.5 py-1 rounded-full text-xs font-medium border border-gray-200/60"
|
||||||
|
>
|
||||||
|
<span>{email}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveEmail(email)}
|
||||||
|
className="text-gray-400 hover:text-gray-600 rounded-full p-0.5"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 stroke-[3]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section: Dates Grid (Start Date & End Date) */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{/* Start Date */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
|
Start Date
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
className="w-full pl-4 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none"
|
||||||
|
/>
|
||||||
|
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* End Date */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||||
|
End Date
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
className="w-full pl-4 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none"
|
||||||
|
/>
|
||||||
|
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Row Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-full sm:w-[150px] py-2.5 border border-gray-200 rounded-xl text-sm font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full sm:w-[240px] py-2.5 bg-[#B30000] hover:bg-[#990000] text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide"
|
||||||
|
>
|
||||||
|
Launch Project
|
||||||
|
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
import { X, Calendar, Clock, FileText, Link } from "lucide-react";
|
||||||
|
|
||||||
|
interface CreateTaskModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProps) {
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
|
// Form States
|
||||||
|
const [taskTitle, setTaskTitle] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [deadlineDate, setDeadlineDate] = useState("");
|
||||||
|
const [specificTime, setSpecificTime] = useState("");
|
||||||
|
const [submissionType, setSubmissionType] = useState<"file" | "url">("file");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
dialog.showModal();
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
dialog.close();
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.addEventListener("cancel", handleCancel);
|
||||||
|
return () => {
|
||||||
|
dialog.removeEventListener("cancel", handleCancel);
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = { taskTitle, description, deadlineDate, specificTime, submissionType };
|
||||||
|
console.log("Saving Task:", payload);
|
||||||
|
alert("Task Saved Successfully!");
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className="fixed inset-0 m-auto flex flex-col w-[calc(100%-2rem)] max-w-[480px] max-h-[calc(100vh-4rem)] rounded-2xl border border-gray-100 bg-white p-0 shadow-2xl backdrop:bg-black/40 backdrop:backdrop-blur-sm animate-in fade-in zoom-in-95 duration-200"
|
||||||
|
>
|
||||||
|
{/* Header Modal */}
|
||||||
|
<div className="flex items-start justify-between px-6 py-5 border-b border-gray-50">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-[#B30000] tracking-tight">Create New Task</h2>
|
||||||
|
<p className="text-xs text-gray-400 font-medium mt-0.5">
|
||||||
|
Fill in the details for the next intern assignment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors">
|
||||||
|
<X className="w-5 h-5 stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body Form Modal (Scrollable jika layar pendek) */}
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto flex-1">
|
||||||
|
|
||||||
|
{/* Task Title */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="e.g., Design High-Fidelity Prototypes"
|
||||||
|
value={taskTitle}
|
||||||
|
onChange={(e) => setTaskTitle(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Description</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder="Outline the requirements and objectives..."
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deadline Grid (Date & Time) */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Deadline Date</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
value={deadlineDate}
|
||||||
|
onChange={(e) => setDeadlineDate(e.target.value)}
|
||||||
|
className="w-full pl-3 pr-9 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||||
|
/>
|
||||||
|
<Calendar className="absolute right-3 top-2.5 w-4 h-4 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time (Jam)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
required
|
||||||
|
value={specificTime}
|
||||||
|
onChange={(e) => setSpecificTime(e.target.value)}
|
||||||
|
className="w-full pl-3 pr-9 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||||
|
/>
|
||||||
|
<Clock className="absolute right-3 top-2.5 w-4 h-4 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submission Type Card Radio */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-gray-700 tracking-wide">Submission Type</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
|
||||||
|
{/* Opsi 1: File Upload */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSubmissionType("file")}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
|
||||||
|
submissionType === "file"
|
||||||
|
? "border-[#B30000] bg-[#B30000] text-white font-semibold"
|
||||||
|
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === "file" ? "border-white bg-white" : "border-gray-300"}`}>
|
||||||
|
{submissionType === "file" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||||
|
</div>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
<span className="text-xs">File Upload</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Opsi 2: URL Link */}
|
||||||
|
<div
|
||||||
|
onClick={() => setSubmissionType("url")}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
|
||||||
|
submissionType === "url"
|
||||||
|
? "border-[#B30000] bg-[#B30000] text-white font-semibold"
|
||||||
|
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === "url" ? "border-white bg-white" : "border-gray-300"}`}>
|
||||||
|
{submissionType === "url" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||||
|
</div>
|
||||||
|
<Link className="w-4 h-4" />
|
||||||
|
<span className="text-xs">URL Link</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-px bg-gray-100 my-2" />
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex items-center justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-2 border border-gray-200 rounded-xl text-xs font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-5 py-2 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md"
|
||||||
|
>
|
||||||
|
Save Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import { Bell } from "lucide-react"
|
import { Bell, Menu } from "lucide-react"
|
||||||
import humiclogo from "../../../../assets/logo.png";
|
import humiclogo from "../../../../assets/logo.png";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const Header = () => {
|
export default function Header({ toggleSidebar }: { toggleSidebar: () => void }) {
|
||||||
|
const nav = useNavigate();
|
||||||
return (
|
return (
|
||||||
<header className='sticky top-0 flex w-full justify-between items-center py-4 px-12 bg-white shadow-md z-10'>
|
<header className='sticky top-0 flex w-full justify-between items-center py-4 px-12 bg-white border-b border-gray-300 z-10'>
|
||||||
<div className=''>
|
<div className='flex items-center gap-3'>
|
||||||
<img
|
{/* Tombol Burger Menu: Hanya muncul di layar < md */}
|
||||||
src={humiclogo}
|
<button
|
||||||
alt="Humic Logo"
|
onClick={toggleSidebar}
|
||||||
className="w-[130px] cursor-pointer"
|
className="p-1 text-gray-600 hover:bg-gray-100 rounded-md md:hidden"
|
||||||
/>
|
>
|
||||||
|
<Menu className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={()=>nav('/intern')}>
|
||||||
|
<img src={humiclogo} alt="Humic Logo" className="w-[130px] cursor-pointer" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4 items-center">
|
<div className="flex flex-row gap-6 items-center">
|
||||||
<Bell className="w-6 h-6 hover:text-red-800"/>
|
<button onClick={()=>nav("notifications")}>
|
||||||
<div className="bg-blue-500 rounded-full w-8 h-8">
|
<Bell className="w-6 h-6 hover:text-red-800"/>
|
||||||
|
</button>
|
||||||
|
<div className="bg-blue-500 rounded-xl w-9 h-9">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Header
|
|
||||||
@@ -17,7 +17,7 @@ export default function MentorProjectCard({
|
|||||||
}: ProjectCardProps) {
|
}: ProjectCardProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleCardClick = () => {
|
const handleCardClick = () => {
|
||||||
navigate(`/admin/projects/${id}`)
|
navigate(`/mentor/projects/${id}`)
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
|||||||
@@ -1,69 +1,91 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
|
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight, X } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
// belum kutambahin
|
// belum kutambahin
|
||||||
{ label: "Home", icon: Home, path: "" },
|
{ label: "Home", icon: Home, path: "/mentor" },
|
||||||
{ label: "List Projects", icon: FolderKanban, path: "" },
|
{ label: "List Projects", icon: FolderKanban, path: "/mentor/projects" },
|
||||||
{ label: "Interns", icon: FolderKanban, path: "" },
|
{ label: "Interns", icon: FolderKanban, path: "/mentor/intern" },
|
||||||
{ label: "Certificates", icon: Award, path: "" },
|
{ label: "Certificates", icon: Award, path: "/mentor/certificates" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const prefItems = [
|
const prefItems = [
|
||||||
{ label: "Settings", icon: Settings, path: "/home-intern/settings" },
|
{ label: "Settings", icon: Settings, path: "/mentor/settings" },
|
||||||
{ label: "FAQ", icon: HelpCircle, path: "/home-intern/faq" },
|
{ label: "FAQ", icon: HelpCircle, path: "/mentor/faq" },
|
||||||
{ label: "Logout", icon: LogOut, path: "/home-intern/logout", danger: true },
|
{ label: "Logout", icon: LogOut, path: "/login-internify", danger: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
function SidebarIntern() {
|
interface SidebarProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
closeSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SidebarMentor({ isOpen, closeSidebar }: SidebarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [active, setActive] = useState("/home-intern");
|
const location = useLocation();
|
||||||
|
|
||||||
const handleNav = (path: string) => {
|
const handleNav = (path: string) => {
|
||||||
setActive(path);
|
|
||||||
navigate(path);
|
navigate(path);
|
||||||
|
closeSidebar();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-[240px] bg-white border-r border-gray-100 flex flex-col py-5 px-3 gap-1 sticky top-[73px] h-[calc(100vh-73px)] overflow-y-auto flex-shrink-0">
|
<aside
|
||||||
{/* Brand */}
|
className={`
|
||||||
<div className="flex items-center gap-2.5 px-2 pb-4">
|
/* Struktur Dasar Layout */
|
||||||
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0">
|
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
|
||||||
</div>
|
|
||||||
<div>
|
/* Kondisi Layar Desktop (>= md) */
|
||||||
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
|
md:sticky md:top-[73px] md:h-[calc(100vh-73px)] md:w-[250px] md:translate-x-0 md:z-8
|
||||||
<p className="text-[11px] text-gray-400">LMS Portal</p>
|
|
||||||
|
/* Kondisi Layar Mobile (< md) dan Logika Aktif-nya */
|
||||||
|
fixed top-0 left-0 h-screen w-[270px] z-40 pt-5
|
||||||
|
${isOpen ? "translate-x-0" : "-translate-x-full"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Brand & Tombol Close untuk Mobile */}
|
||||||
|
<div className="flex items-center justify-between px-2 pb-4">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
|
||||||
|
<p className="text-[11px] text-gray-400">LMS Portal</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={closeSidebar}
|
||||||
|
className="p-1 text-gray-500 hover:bg-gray-100 rounded-md md:hidden"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-px bg-gray-100 my-1" />
|
<div className="h-px bg-gray-300 my-1" />
|
||||||
|
|
||||||
{/* Main nav */}
|
{/* Main nav */}
|
||||||
<nav className="flex flex-col gap-0.5">
|
<nav className="flex flex-col gap-0.5">
|
||||||
{navItems.map(({ label, icon: Icon, path }) => {
|
{navItems.map(({ label, icon: Icon, path }) => {
|
||||||
const isActive = active === path;
|
const isActive = location.pathname === path;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={path}
|
key={path}
|
||||||
onClick={() => handleNav(path)}
|
onClick={() => handleNav(path)}
|
||||||
className={`flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-[13.5px] font-medium text-left transition-colors
|
className={`flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-[13.5px] font-medium text-left transition-colors
|
||||||
${isActive
|
${isActive
|
||||||
? "bg-red-50 text-red-600 font-semibold border-l-4 border-red-600"
|
? "bg-red-50 text-red-600 font-semibold border-l-4 border-red-600"
|
||||||
: "text-gray-500 hover:bg-red-50 hover:text-red-600"
|
: "text-gray-500 hover:bg-red-50 hover:text-red-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isActive && (
|
{isActive && <ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />}
|
||||||
<ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="h-px bg-gray-100 my-1" />
|
<div className="h-px bg-gray-200 my-1" />
|
||||||
|
|
||||||
{/* Preferences */}
|
{/* Preferences */}
|
||||||
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
|
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
|
||||||
@@ -71,7 +93,7 @@ function SidebarIntern() {
|
|||||||
</p>
|
</p>
|
||||||
<nav className="flex flex-col gap-0.5">
|
<nav className="flex flex-col gap-0.5">
|
||||||
{prefItems.map(({ label, icon: Icon, path, danger }) => {
|
{prefItems.map(({ label, icon: Icon, path, danger }) => {
|
||||||
const isActive = active === path;
|
const isActive = location.pathname === path;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={path}
|
key={path}
|
||||||
@@ -94,6 +116,3 @@ function SidebarIntern() {
|
|||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default SidebarIntern
|
|
||||||
|
|||||||
@@ -31,10 +31,7 @@ export default function InternifyLogin() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
// 3. Gunakan navigate() sesuai dengan role masing-masing
|
if (user.role === "intern") {
|
||||||
if (user.role === "admin") {
|
|
||||||
navigate("/admin");
|
|
||||||
} else if (user.role === "intern") {
|
|
||||||
navigate("/intern");
|
navigate("/intern");
|
||||||
} else if (user.role === "mentor") {
|
} else if (user.role === "mentor") {
|
||||||
navigate("/mentor");
|
navigate("/mentor");
|
||||||
|
|||||||
@@ -11,3 +11,37 @@
|
|||||||
font-family: JakartaSans, sans-serif;
|
font-family: JakartaSans, sans-serif;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding-inline: calc(var(--spacing) * 14);
|
||||||
|
padding-block: calc(var(--spacing) * 12);
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: var(--text-3xl) /* 1.875rem = 30px */;
|
||||||
|
line-height: var(--tw-leading, var(--text-3xl--line-height) /* calc(2.25 / 1.875) ≈ 1.2 */);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
--tw-tracking: var(--tracking-tight) /* -0.025em */;
|
||||||
|
letter-spacing: var(--tracking-tight) /* -0.025em */;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title-desc {
|
||||||
|
font-size: var(--text-sm) /* 0.875rem = 14px */;
|
||||||
|
color: var(--color-gray-500);
|
||||||
|
margin-top: calc(var(--spacing) * 1)
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ export const DEADLINES: Deadline[] = [
|
|||||||
export const deadlineDates: Date[] = DEADLINES.map((d) => d.date);
|
export const deadlineDates: Date[] = DEADLINES.map((d) => d.date);
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
deadline: Deadline;
|
deadline: Deadline;
|
||||||
@@ -56,6 +57,7 @@ export const mockProjects: Project[] = [
|
|||||||
"Internify merupakan website Human Capital (HUMC) yang dirancang sebagai platform pendaftaran dan pengelolaan program magang secara digital. Sistem ini dibuat untuk membantu proses rekrutmen peserta magang mulai dari pendaftaran, seleksi, hingga monitoring aktivitas peserta selama program berlangsung.\n\nFokus utama dari pengembangan Internify adalah pembuatan Learning Management System (LMS) yang digunakan untuk mendukung kegiatan pembelajaran dan pengelolaan peserta magang. LMS pada Internify menyediakan fitur seperti akses materi pembelajaran, pengumpulan tugas, monitoring progress peserta, absensi, sertifikat, hingga komunikasi antara mentor dan peserta magang.",
|
"Internify merupakan website Human Capital (HUMC) yang dirancang sebagai platform pendaftaran dan pengelolaan program magang secara digital. Sistem ini dibuat untuk membantu proses rekrutmen peserta magang mulai dari pendaftaran, seleksi, hingga monitoring aktivitas peserta selama program berlangsung.\n\nFokus utama dari pengembangan Internify adalah pembuatan Learning Management System (LMS) yang digunakan untuk mendukung kegiatan pembelajaran dan pengelolaan peserta magang. LMS pada Internify menyediakan fitur seperti akses materi pembelajaran, pengumpulan tugas, monitoring progress peserta, absensi, sertifikat, hingga komunikasi antara mentor dan peserta magang.",
|
||||||
tasks: [
|
tasks: [
|
||||||
{
|
{
|
||||||
|
id: 1,
|
||||||
title: "Laporan tugas",
|
title: "Laporan tugas",
|
||||||
description: "Berisi design ulux, dan user flow dari aplikasi internify",
|
description: "Berisi design ulux, dan user flow dari aplikasi internify",
|
||||||
deadline: { date: new Date(2026, 4, 1), label: "Laporan tugas – 1 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 1), label: "Laporan tugas – 1 Mei", time: "23:59 PM" },
|
||||||
@@ -64,6 +66,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: true,
|
isSubmitted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 2,
|
||||||
title: "Laporan tugas 1",
|
title: "Laporan tugas 1",
|
||||||
description: "Berisi design ulux, dan user flow dari aplikasi internify",
|
description: "Berisi design ulux, dan user flow dari aplikasi internify",
|
||||||
deadline: { date: new Date(2026, 4, 14), label: "Laporan tugas 1 – 14 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 14), label: "Laporan tugas 1 – 14 Mei", time: "23:59 PM" },
|
||||||
@@ -72,6 +75,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: false,
|
isSubmitted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 3,
|
||||||
title: "Laporan tugas 2",
|
title: "Laporan tugas 2",
|
||||||
description: "Berisi design ulux, dan user flow dari aplikasi internify",
|
description: "Berisi design ulux, dan user flow dari aplikasi internify",
|
||||||
deadline: { date: new Date(2026, 4, 28), label: "Laporan tugas 2 – 28 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 28), label: "Laporan tugas 2 – 28 Mei", time: "23:59 PM" },
|
||||||
@@ -80,6 +84,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: false,
|
isSubmitted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 4,
|
||||||
title: "Laporan tugas 3",
|
title: "Laporan tugas 3",
|
||||||
description: "Berisi wireframe halaman dashboard aplikasi internify",
|
description: "Berisi wireframe halaman dashboard aplikasi internify",
|
||||||
deadline: { date: new Date(2026, 5, 5), label: "Laporan tugas 3 – 5 Jun", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 5, 5), label: "Laporan tugas 3 – 5 Jun", time: "23:59 PM" },
|
||||||
@@ -108,6 +113,7 @@ export const mockProjects: Project[] = [
|
|||||||
"Platform digital yang memudahkan proses rekrutmen karyawan baru secara end-to-end. Mulai dari posting lowongan, screening CV otomatis, penjadwalan wawancara, hingga onboarding karyawan baru.\n\nSistem ini mengintegrasikan berbagai modul HR seperti manajemen data kandidat, penilaian kompetensi berbasis AI, dan dashboard analytics untuk tim HR dalam mengambil keputusan rekrutmen yang lebih efektif dan efisien.",
|
"Platform digital yang memudahkan proses rekrutmen karyawan baru secara end-to-end. Mulai dari posting lowongan, screening CV otomatis, penjadwalan wawancara, hingga onboarding karyawan baru.\n\nSistem ini mengintegrasikan berbagai modul HR seperti manajemen data kandidat, penilaian kompetensi berbasis AI, dan dashboard analytics untuk tim HR dalam mengambil keputusan rekrutmen yang lebih efektif dan efisien.",
|
||||||
tasks: [
|
tasks: [
|
||||||
{
|
{
|
||||||
|
id: 1,
|
||||||
title: "Analisis Kebutuhan Sistem",
|
title: "Analisis Kebutuhan Sistem",
|
||||||
description: "Dokumentasi kebutuhan fungsional dan non-fungsional platform rekrutmen",
|
description: "Dokumentasi kebutuhan fungsional dan non-fungsional platform rekrutmen",
|
||||||
deadline: { date: new Date(2026, 4, 5), label: "Analisis Kebutuhan – 5 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 5), label: "Analisis Kebutuhan – 5 Mei", time: "23:59 PM" },
|
||||||
@@ -116,6 +122,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: true,
|
isSubmitted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 2,
|
||||||
title: "Desain Database Schema",
|
title: "Desain Database Schema",
|
||||||
description: "Rancangan struktur database untuk kandidat, lowongan, dan proses seleksi",
|
description: "Rancangan struktur database untuk kandidat, lowongan, dan proses seleksi",
|
||||||
deadline: { date: new Date(2026, 4, 10), label: "Desain Database – 10 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 10), label: "Desain Database – 10 Mei", time: "23:59 PM" },
|
||||||
@@ -124,6 +131,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: true,
|
isSubmitted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 3,
|
||||||
title: "Implementasi Auth Module",
|
title: "Implementasi Auth Module",
|
||||||
description: "Pengembangan sistem autentikasi dan otorisasi multi-role",
|
description: "Pengembangan sistem autentikasi dan otorisasi multi-role",
|
||||||
deadline: { date: new Date(2026, 4, 18), label: "Auth Module – 18 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 18), label: "Auth Module – 18 Mei", time: "23:59 PM" },
|
||||||
@@ -132,6 +140,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: true,
|
isSubmitted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 4,
|
||||||
title: "Modul Posting Lowongan",
|
title: "Modul Posting Lowongan",
|
||||||
description: "Fitur buat, edit, dan publish lowongan pekerjaan",
|
description: "Fitur buat, edit, dan publish lowongan pekerjaan",
|
||||||
deadline: { date: new Date(2026, 4, 25), label: "Posting Lowongan – 25 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 25), label: "Posting Lowongan – 25 Mei", time: "23:59 PM" },
|
||||||
@@ -161,6 +170,7 @@ export const mockProjects: Project[] = [
|
|||||||
"Dashboard analitik komprehensif untuk platform e-commerce yang menyajikan insight real-time tentang performa penjualan, perilaku pelanggan, dan efektivitas kampanye marketing.\n\nProyek ini mencakup pengembangan pipeline data dari berbagai sumber transaksi, visualisasi interaktif menggunakan chart library modern, serta fitur export laporan dalam berbagai format untuk kebutuhan manajemen bisnis.",
|
"Dashboard analitik komprehensif untuk platform e-commerce yang menyajikan insight real-time tentang performa penjualan, perilaku pelanggan, dan efektivitas kampanye marketing.\n\nProyek ini mencakup pengembangan pipeline data dari berbagai sumber transaksi, visualisasi interaktif menggunakan chart library modern, serta fitur export laporan dalam berbagai format untuk kebutuhan manajemen bisnis.",
|
||||||
tasks: [
|
tasks: [
|
||||||
{
|
{
|
||||||
|
id: 1,
|
||||||
title: "Setup Pipeline Data",
|
title: "Setup Pipeline Data",
|
||||||
description: "Konfigurasi ETL pipeline dari database transaksi ke data warehouse",
|
description: "Konfigurasi ETL pipeline dari database transaksi ke data warehouse",
|
||||||
deadline: { date: new Date(2026, 4, 3), label: "Setup Pipeline – 3 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 3), label: "Setup Pipeline – 3 Mei", time: "23:59 PM" },
|
||||||
@@ -169,6 +179,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: true,
|
isSubmitted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 2,
|
||||||
title: "Desain Komponen Chart",
|
title: "Desain Komponen Chart",
|
||||||
description: "Pembuatan reusable chart component untuk berbagai tipe visualisasi data",
|
description: "Pembuatan reusable chart component untuk berbagai tipe visualisasi data",
|
||||||
deadline: { date: new Date(2026, 4, 12), label: "Komponen Chart – 12 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 12), label: "Komponen Chart – 12 Mei", time: "23:59 PM" },
|
||||||
@@ -177,6 +188,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: true,
|
isSubmitted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 3,
|
||||||
title: "Dashboard Overview Page",
|
title: "Dashboard Overview Page",
|
||||||
description: "Halaman utama dengan ringkasan KPI dan metric utama bisnis",
|
description: "Halaman utama dengan ringkasan KPI dan metric utama bisnis",
|
||||||
deadline: { date: new Date(2026, 4, 20), label: "Overview Page – 20 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 20), label: "Overview Page – 20 Mei", time: "23:59 PM" },
|
||||||
@@ -185,6 +197,7 @@ export const mockProjects: Project[] = [
|
|||||||
isSubmitted: false,
|
isSubmitted: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 4,
|
||||||
title: "Laporan Penjualan",
|
title: "Laporan Penjualan",
|
||||||
description: "Modul analitik penjualan dengan filter periode dan kategori produk",
|
description: "Modul analitik penjualan dengan filter periode dan kategori produk",
|
||||||
deadline: { date: new Date(2026, 4, 28), label: "Laporan Penjualan – 28 Mei", time: "23:59 PM" },
|
deadline: { date: new Date(2026, 4, 28), label: "Laporan Penjualan – 28 Mei", time: "23:59 PM" },
|
||||||
@@ -263,3 +276,55 @@ export const certificateData: CertificateData = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock Data Interns
|
||||||
|
export const mockInternsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Alex Rivera",
|
||||||
|
email: "alex.rivera@internify.edu",
|
||||||
|
projectName: "Internify Project",
|
||||||
|
role: "UI/UX",
|
||||||
|
avatar: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=100&q=80" // Contoh avatar placeholder
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Sarah Jenkins",
|
||||||
|
email: "s.jenkins@internify.edu",
|
||||||
|
projectName: "Internify Project",
|
||||||
|
role: "Frontend",
|
||||||
|
avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=100&q=80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Michael Chen",
|
||||||
|
email: "m.chen@internify.edu",
|
||||||
|
projectName: "MBG Project",
|
||||||
|
role: "Backend",
|
||||||
|
avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=100&q=80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Elena Rodriguez",
|
||||||
|
email: "e.rod@internify.edu",
|
||||||
|
projectName: "Unassigned",
|
||||||
|
role: "-",
|
||||||
|
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=100&q=80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Alex Rivera",
|
||||||
|
email: "alex.rivera@internify.edu",
|
||||||
|
projectName: "Internify Project",
|
||||||
|
role: "UI/UX",
|
||||||
|
avatar: "" // Simulasi tanpa gambar avatar (menggunakan placeholder inisial/gradient)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: "Sarah Jenkins",
|
||||||
|
email: "s.jenkins@internify.edu",
|
||||||
|
projectName: "Internify Project",
|
||||||
|
role: "Backend",
|
||||||
|
avatar: ""
|
||||||
|
}
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user