feat: Refactor mentor components and implement project management features

- Removed unused imports and interfaces in HomeProjectList component.
- Added ArrowDownNarrowWide icon to ProjectTimeline component for visual enhancement.
- Updated layout imports to reflect new component structure.
- Created BaseProjectsPage for mentor to manage projects with navigation.
- Introduced MentorCertificatePage as a placeholder for future certificate functionality.
- Deleted obsolete HomeMentorPage and replaced it with HomePage for mentor dashboard.
- Added InternsPage and updated MentorInternPage for better intern management.
- Implemented ProjectForumTab and ProjectsInternTab for project details and intern listings.
- Developed TaskTab for task management within projects.
- Created SidebarMentor for navigation in mentor dashboard.
- Enhanced login functionality with user role-based navigation.
- Updated mockData to include task submission counts and certificate data structure.
- Added new assets for certificate representation.
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-05 14:33:03 +07:00
parent 8146545779
commit e59e63a21e
38 changed files with 1408 additions and 331 deletions
+25 -14
View File
@@ -29,15 +29,19 @@ import EditFaq from "./Pages/EditFAQ";
import EditFeedback from "./Pages/EditFeedback";
import EditLowongan from "./Pages/EditLowongan";
import HomeInternPage from "./Pages/internify/intern/HomeInternPage";
import {InternLayout, MentorLayout} from "./Pages/internify/layout";
import { InternLayout, MentorLayout } from "./Pages/internify/layout";
import ProjectsPage from "./Pages/internify/intern/ProjectsPage";
import SertificatePage from "./Pages/internify/intern/SertificatePage";
import ProjectDetailPage from "./Pages/internify/intern/ProjectDetailPage";
import HomeMentorPage from "./Pages/internify/mentor/HomeMentorPage";
import MentorProjectsPage from "./Pages/internify/mentor/MentorProjectsPage";
import MentorCertificatePage from "./Pages/internify/mentor/MentorCertificatePage";
import MentorInternPage from "./Pages/internify/mentor/MentorInternPage";
import MentorDetailProject from "./Pages/internify/mentor/MentorDetailProject";
import HomeMentorPage from "./Pages/internify/mentor/HomePage";
import MentorProjectsPage from "./Pages/internify/mentor/BaseProjectsPage";
import MentorCertificatePage from "./Pages/internify/mentor/CertificatePage";
import MentorInternPage from "./Pages/internify/mentor/InternsPage";
import MentorDetailProject from "./Pages/internify/mentor/ProjectsPage";
import InternifyLogin from "./Pages/internify/page"
import FAQPage from "./Pages/internify/FAQpage";
import NotificationList from "./Pages/internify/NotificationList";
import SettingsContent from "./Pages/internify/SettingsPage";
function App() {
return (
@@ -62,7 +66,7 @@ function App() {
<Route path="/add-product-humic" element={<AddProductHumic />} />
<Route path="/edit-product-humic/:id" element={<EditProductHumic />} />
<Route path="/kontent-aktif/edit/:id" element={<EditKontenAdmin />} />
<Route path="/internships-details-admin/:id"element={<InternshipDetailsAdmin />}/>
<Route path="/internships-details-admin/:id" element={<InternshipDetailsAdmin />} />
<Route path="/partnership-admin" element={<PartnershipsList />} />
<Route path="/add-partnership" element={<AddPartnershipAdmin />} />
<Route path="/edit-partnership/:id" element={<EditPartnerships />} />
@@ -77,20 +81,27 @@ function App() {
</Route>
<Route path="/login-internify" element={<InternifyLogin />} />
{/* // Endpoints for intern-specific pages */}
<Route path="/intern" element={<InternLayout />}>
<Route index element={<HomeInternPage />} />
<Route path="projects" element={<ProjectsPage />} />
<Route path="certificates" element={<SertificatePage />} />
<Route path="projects/:id" element={<ProjectDetailPage/>}/>
<Route path="projects/:id" element={<ProjectDetailPage />} />
<Route path="faq" element={<FAQPage />} />
<Route path="notifications" element={<NotificationList />} />
<Route path="settings" element={<SettingsContent />} />
</Route>
<Route path="mentor-intern" element={<MentorLayout/>}>
<Route index element={<HomeMentorPage/>}/>
<Route path="projects" element={<MentorProjectsPage/>} />
<Route path="certificates" element={<MentorCertificatePage/>} />
<Route path="intern" element={<MentorInternPage/>} />
<Route path="projects/:id" element={<MentorDetailProject />}/>
<Route path="/mentor" element={<MentorLayout />}>
<Route index element={<HomeMentorPage />} />
<Route path="projects" element={<MentorProjectsPage />} />
<Route path="certificates" element={<MentorCertificatePage />} />
<Route path="intern" element={<MentorInternPage />} />
<Route path="projects/:id" element={<MentorDetailProject />} />
<Route path="faq" element={<FAQPage />} />
<Route path="notifications" element={<NotificationList />} />
<Route path="settings" element={<SettingsContent />} />
</Route>
</Routes>
</Router>
@@ -1,5 +1,6 @@
import { Briefcase, Globe, Users } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { FaInstagram, FaLinkedin, FaRegEnvelope } from "react-icons/fa";
const footerLinks = {
Internship: [
@@ -16,9 +17,9 @@ const footerLinks = {
};
const socials = [
{ label: "Website", icon: Globe, href: "https://placeholder.com" },
{ label: "Team", icon: Users, href: "https://placeholder.com" },
{ label: "Work", icon: Briefcase, href: "https://placeholder.com" },
{ label: "Instagram", icon: FaInstagram, href: "https://placeholder.com" },
{ label: "LinkedIn", icon: FaLinkedin, href: "https://placeholder.com" },
{ label: "Email", icon: FaRegEnvelope, href: "https://placeholder.com" },
];
@@ -26,13 +27,12 @@ export function FooterIntern() {
const navigate = useNavigate();
return (
<footer className="bg-white border-t border-gray-100 px-14 py-8 flex gap-12 flex-wrap">
<footer className="bg-white border-t border-gray-300 px-14 py-8 flex gap-12 flex-wrap">
{/* Brand info */}
<div className="flex flex-col gap-2 flex-shrink-0 w-52">
<p className="text-sm font-bold text-gray-900">Internify Logo</p>
<address className="text-[12.5px] text-gray-400 leading-relaxed not-italic">
Gedung Kultubai Selatan, Blok F<br />
Jl. Telekomunikasi, Terusan Buah Batu Bandung<br />
<div className="flex flex-col gap-2 flex-shrink-0 w-70">
<address className="text-xs text-gray-800 leading-relaxed not-italic">
Gedung Kultubai Selatan, Blok F
Jl. Telekomunikasi, Terusan Buah Batu Bandung
Jawa Barat, Indonesia. 40257
</address>
<div className="flex gap-2 mt-1">
@@ -43,7 +43,7 @@ export function FooterIntern() {
target="_blank"
rel="noopener noreferrer"
aria-label={label}
className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center text-gray-400 hover:bg-red-50 hover:text-red-600 transition-colors"
className="w-10 h-10 border rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-red-50 hover:text-red-600 transition-colors"
>
<Icon className="w-4 h-4" />
</a>
@@ -56,12 +56,12 @@ export function FooterIntern() {
{Object.entries(footerLinks).map(([title, links]) => (
<div key={title} className="flex flex-col gap-2.5 min-w-[110px]">
<p className="text-[13px] font-bold text-gray-900">{title}</p>
<ul className="flex flex-col gap-2">
<ul className="flex flex-col gap-0.5">
{links.map(({ label, path }) => (
<li key={label}>
<button
onClick={() => navigate(path)}
className="text-[12.5px] text-gray-400 hover:text-red-600 transition-colors text-left bg-transparent border-none p-0 cursor-pointer"
className="text-[12.5px] text-gray-600 hover:text-red-800 transition-colors text-left bg-transparent border-none p-0 cursor-pointer"
>
{label}
</button>
+10
View File
@@ -109,6 +109,16 @@ const Navbar = () => {
>
Internship
</button>
<button
onClick={() => {
navigate("/login-internify");
setOpenMenu(false);
}}
className="block w-full text-left hover:text-blue-600 transition"
>
Login
</button>
</div>
)}
</nav>
+91 -227
View File
@@ -1,234 +1,98 @@
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { useNavigate } from 'react-router-dom';
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, User, ChevronRight } from 'lucide-react';
import { useState } from 'react';
const SidebarAdmin = () => {
const navItems = [
{ 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 location = useLocation();
const [active, setActive] = useState("/admin");
const handleNav = (path: string) => {
setActive(path);
navigate(path);
};
return (
<div className="bg-[#EDF2F7] w-[300px] min-h-[87vh] flex flex-col justify-between p-4">
<div className="container-sidebar">
<h2 className="font-semibold text-[#2C5282] text-[20px]">Menu</h2>
<div className="list-of-sidebar flex flex-col p-7 gap-4">
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/dashboard"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA] "
<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">
{/* Brand */}
<div className="flex items-center gap-2.5 px-2 pb-4">
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0">
</div>
<div>
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
<p className="text-[11px] text-gray-400">LMS Portal</p>
</div>
</div>
<div className="h-px bg-gray-100 my-1" />
{/* Main nav */}
<nav className="flex flex-col gap-0.5">
{navItems.map(({ label, icon: Icon, path }) => {
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"
}`}
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
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>
<h2 className="text-[#C3423F] text-[16px] font-semibold">Keluar</h2>
</div>
</div>
<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>
export default SidebarAdmin;
<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 SidebarIntern
+69
View File
@@ -0,0 +1,69 @@
import { useNavigate } from "react-router-dom";
import { ChevronRight } from "lucide-react";
const mockFAQData = [
{
id: 1,
question: "How do I generate internship certificates?",
answer: "Once an intern has completed all required project milestones and their final evaluation is approved by a manager, go to the 'Manage Certificates' tab. Select the intern's name, click 'Generate Certificate', and the system will automatically populate the official Internify template with their specific details."
},
{
id: 2,
question: "What are the technical requirements for interns?",
answer: "Internify is a web-based LMS. Interns simply need a modern web browser (Chrome, Firefox, or Safari) and a stable internet connection. Some engineering modules may require local development environments, which are detailed in the specific project onboarding documentation."
},
{
id: 3,
question: "How often is the project dashboard updated?",
answer: "The dashboard updates in real-time as interns submit tasks or participate in the forum. Weekly progress reports are automatically generated every Friday at 5:00 PM EST and can be exported as PDF files from the Dashboard view."
}
];
export default function FAQPage() {
const navigate = useNavigate();
return (
<div className="w-full max-w-5xl px-6 py-6 font-sans">
{/* 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 */}
<div className="mb-10">
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">FAQ</h1>
<p className="text-sm text-gray-500 mt-1">
Quick answers to common questions about the Internify LMS platform.
</p>
</div>
{/* FAQ Lists */}
<div className="space-y-8 max-w-4xl">
{mockFAQData.map((faq) => (
<div key={faq.id} className="relative pl-6 group">
{/* 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 */}
<h2 className="text-[17px] font-bold text-[#8A0000] tracking-tight leading-snug mb-2">
{faq.question}
</h2>
{/* Item Answer */}
<p className="text-sm text-gray-500 font-medium leading-relaxed tracking-normal">
{faq.answer}
</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>
);
}
+169
View File
@@ -0,0 +1,169 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRight, Check, Download, Info, Award } from "lucide-react";
import { LuFileSpreadsheet } from "react-icons/lu"; // Alternatif icon task dari react-icons
const initialNotifications = [
{
id: 1,
type: "new-task",
title: "New Task Assigned",
time: "Just now",
isNew: true,
group: "TODAY",
description: "Mentor Sarah Johnson has pushed a new task: User Persona Analysis"
},
{
id: 2,
type: "achievement",
title: "Certificate Ready",
time: "2 hours ago",
isNew: false,
group: "TODAY",
description: "Your certificate for UI Engineering Phase 1 is now available for download!"
},
{
id: 3,
type: "system",
title: "System Maintenance",
time: "2 days ago",
isNew: false,
group: "YESTERDAY",
description: "Internify will undergo scheduled maintenance this Sunday from 02:00 AM to 04:00 AM GMT+7."
}
];
export default function NotificationList() {
const navigate = useNavigate();
const [notifications, setNotifications] = useState(initialNotifications);
const handleMarkAllRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, isNew: false })));
alert("All notifications marked as read!");
};
return (
<div className="w-full max-w-5xl px-6 py-6 font-sans">
{/* 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 */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">Notifications</h1>
<p className="text-sm text-gray-500 mt-1">
Keep track of your interns' progress and system updates.
</p>
</div>
<button
onClick={handleMarkAllRead}
className="flex items-center gap-1.5 self-start sm:self-center px-4 py-2 border border-gray-200 rounded-lg text-xs font-semibold text-gray-700 bg-white hover:bg-gray-50 transition-colors shadow-sm"
>
<Check className="w-4 h-4 text-green-600 stroke-[3]" />
Mark all as read
</button>
</div>
{/* List Group Section */}
<div className="space-y-6">
{/* Render Group TODAY */}
<div className="space-y-3">
{notifications.filter(n => n.group === "TODAY").map((item) => (
<div
key={item.id}
className={`relative flex items-start gap-4 p-5 bg-white border border-gray-100 rounded-xl shadow-[0_2px_10px_rgba(0,0,0,0.01)] ${
item.type === 'new-task' ? 'border-l-4 border-l-[#B30000]' : ''
}`}
>
{/* Icon Container */}
<div className={`p-2.5 rounded-xl ${
item.type === 'new-task' ? 'bg-red-50 text-red-600' : 'bg-gray-100 text-gray-600'
}`}>
{item.type === 'new-task' && <LuFileSpreadsheet className="w-5 h-5" />}
{item.type === 'achievement' && <Check className="w-5 h-5 stroke-[2.5]" />}
</div>
{/* Content text */}
<div className="flex-grow space-y-1">
<div className="flex flex-wrap items-center gap-2">
{item.isNew && (
<span className="bg-[#B30000] text-white font-extrabold text-[9px] px-2 py-0.5 rounded uppercase tracking-wider">
NEW
</span>
)}
<h3 className="text-sm font-bold text-gray-900">{item.title}</h3>
</div>
{/* Custom description coloring logic */}
<p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
{item.type === 'new-task' ? (
<>Mentor <span className="font-semibold text-gray-900">Sarah Johnson</span> has pushed a new task: <span className="italic font-semibold text-gray-800">User Persona Analysis</span></>
) : item.type === 'achievement' ? (
<>Your certificate for <span className="font-semibold text-gray-900">UI Engineering Phase 1</span> is now available for download!</>
) : item.description}
</p>
{/* Conditional Actions */}
{item.type === 'new-task' && (
<div className="flex items-center gap-2 pt-3">
<button className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors">
View Task
</button>
<button className="px-4 py-1.5 border border-gray-200 text-gray-500 hover:text-gray-700 hover:bg-gray-50 text-xs font-semibold rounded-lg transition-colors">
Later
</button>
</div>
)}
{item.type === 'achievement' && (
<button className="flex items-center gap-1 text-xs font-bold text-[#B30000] hover:underline pt-2">
<Download className="w-3.5 h-3.5" />
Download Certificate
</button>
)}
</div>
{/* Time */}
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap self-start md:absolute md:top-5 md:right-5">
{item.time}
</span>
</div>
))}
</div>
{/* Header Section YESTERDAY */}
<div className="space-y-3 pt-2">
<h2 className="text-[10px] font-bold tracking-widest text-gray-400 uppercase">
YESTERDAY
</h2>
{notifications.filter(n => n.group === "YESTERDAY").map((item) => (
<div
key={item.id}
className="relative flex items-start gap-4 p-5 bg-gray-50 border border-gray-100 rounded-xl"
>
<div className="p-2.5 bg-gray-200 text-gray-500 rounded-xl">
<Info className="w-5 h-5" />
</div>
<div className="flex-grow space-y-1">
<h3 className="text-sm font-bold text-gray-700">{item.title}</h3>
<p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
{item.description}
</p>
</div>
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap md:absolute md:top-5 md:right-5">
{item.time}
</span>
</div>
))}
</div>
</div>
</div>
);
}
+140
View File
@@ -0,0 +1,140 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRight } from "lucide-react"; // dari lucide-react
// Mock Data Profile awal
const mockProfileData = {
fullName: "JonathanKristina",
email: "JonathanKristina@gmail.com",
bio: "UIUX holic"
};
export default function SettingsContent() {
const navigate = useNavigate();
const [formData, setFormData] = useState(mockProfileData);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSaveChanges = (e: React.FormEvent) => {
e.preventDefault();
// Simulasi penyimpanan data sukses
alert("Changes saved successfully!");
console.log("Saved Data:", formData);
};
// Mendapatkan inisial huruf pertama untuk avatar profile gambar
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
return (
<div className="w-full max-w-5xl px-6 py-6 font-sans">
{/* 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 */}
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">Settings</h1>
<p className="text-sm text-gray-500 mt-1">
Manage and edit your profile and account information.
</p>
</div>
{/* 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">
{/* Card Header */}
<div className="p-6 border-b border-gray-100">
<h2 className="text-base font-bold text-gray-900">Profile Information</h2>
<p className="text-xs text-gray-400 mt-0.5">
Update your photo and personal details.
</p>
</div>
{/* Card Body & Form */}
<form onSubmit={handleSaveChanges} className="p-6 space-y-6">
{/* Profile Photo Display Row */}
<div className="flex items-center gap-4 py-2">
<div className="w-16 h-16 bg-gray-100 border border-gray-200 rounded-xl flex items-center justify-center shadow-sm">
<span className="text-2xl font-black text-gray-900 tracking-tighter">
{initialLetter}
</span>
</div>
<div>
<h3 className="text-xs font-bold text-gray-900">Profile Photo</h3>
<p className="text-sm text-gray-600 mt-0.5">{formData.fullName}</p>
</div>
</div>
<hr className="border-t border-dashed border-gray-200" />
{/* Grid Inputs (Full Name & Email Address) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Field: Full Name */}
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
Full Name
</label>
<input
type="text"
name="fullName"
value={formData.fullName}
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"
/>
</div>
{/* Field: Email Address */}
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
Email Address
</label>
<input
type="email"
name="email"
value={formData.email}
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"
/>
</div>
</div>
{/* Field: Professional Bio */}
<div className="space-y-1">
<label className="text-[10px] font-bold tracking-wider text-gray-400 uppercase block">
Professional Bio
</label>
<textarea
name="bio"
rows={4}
value={formData.bio}
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"
/>
<p className="text-[11px] text-gray-400 font-normal pt-0.5">
Brief description for your profile. URLs and @mentions are allowed.
</p>
</div>
{/* Form Action Footer Row */}
<div className="flex justify-end pt-4">
<button
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"
>
Save Changes
</button>
</div>
</form>
</div>
</div>
);
}
@@ -1,7 +1,7 @@
import HomeHeader from "./components/HomeHeader";
import HomeProjectList from "./components/HomeProjectList";
import HomeCalendar from "./components/HomeCalendar";
import TaskTimeline from "./components/TaskTImeline";
import TaskTimeline from "./components/TaskTimeline";
// PR: tambahin breadcrumb
export default function HomeInternPage() {
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { mockProjects } from '../../../lib/mockData';
import ForumTab from './ProjectForum';
import ParticipantsTab from './ProjectParticipants';
import ForumTab from './ForumTab';
import ParticipantsTab from './ParticipantTab';
import { ChevronLeft } from 'lucide-react';
// ─── ProjectDetailPage ─────────────────────────────────────────────────────────
+22 -7
View File
@@ -1,18 +1,33 @@
import { certificateData } from "../../../lib/mockData";
import CertificateAvailable from "./components/CertificateAvailabe";
import CertificateNotAvailable from "./components/CertificateNotAvailable";
const SertificatePage = () => {
const data = certificateData; // ganti isReceived di object atas
return (
<div className="px-15 py-10">
<div className="px-15 py-10 bg-white min-h-screen">
{/* Header */}
<div className="mb-6 flex flex-col gap-1">
<h1 className="text-4xl font-bold mb-2">Certificates</h1>
<p className="font-light text-gray-500">View and download internship certificates</p>
</div>
<div>
<div>
{/* Content */}
<div>
{data.isReceived ? (
<CertificateAvailable data={data} />
) : (
<CertificateNotAvailable
progress={data.projectProgress}
remainingTasks={data.remainingTasks}
/>
)}
</div>
</div>
);
};
</div>
</div>
)
}
export default SertificatePage
@@ -0,0 +1,142 @@
import { useState } from "react";
import { Download, History , Link, Award, Printer } from "lucide-react";
import { type CertificateData, type CertificateHistory } from "../../../../lib/mockData";
import certificateImg from "../../../../assets/certificate.png";
export default function CertificateAvailable({ data }: { data: CertificateData }) {
const { certificate, history } = data;
const [copied, setCopied] = useState(false);
const handleCopyLink = () => {
navigator.clipboard.writeText(window.location.href);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="flex flex-col lg:flex-row gap-6 items-start">
{/* LEFT: Certificate Preview + Actions */}
<div className="w-full lg:w-4/6 flex flex-col gap-4">
{/* Certificate Card */}
<div className="flex rounded-xl p-10 mb-3 border border-gray-300 shadow-md items-center justify-center">
<img
src={certificateImg}
alt="Certificate"
className="rounded-xl w-[550px] h-[350px]"
/>
</div>
{/* Program Summary */}
<div className="grid grid-cols-2 gap-5">
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
<div className="flex items-center gap-2 mb-4">
<h3 className="font-semibold text-gray-800 text-sm">Ringkasan Program</h3>
</div>
<div className="space-y-3">
{[
{ label: "Nama Program", value: certificate.programName },
{ label: "Durasi", value: certificate.duration },
{
label: "Nilai Akhir",
value: (
<span className="text-red-600 font-bold">
{certificate.finalScore}/100 ({certificate.grade})
</span>
),
},
{
label: "Status",
value: (
<span className="bg-green-100 text-green-700 text-xs font-bold px-2 py-0.5 rounded-full">
{certificate.status}
</span>
),
},
].map((row) => (
<div
key={row.label}
className="flex justify-between items-center text-sm border-b border-gray-50 pb-2 last:border-0"
>
<span className="text-gray-500">{row.label}</span>
<span className="text-gray-800 font-medium">{row.value}</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
<div className="flex flex-col gap-3">
{/* Unduh Sertifikat */}
<button className="flex items-center justify-center gap-2 w-full bg-red-700 hover:bg-red-800 active:scale-95 transition-all text-white font-semibold py-3.5 rounded-xl text-sm">
<Download className="w-4 h-4" />
Unduh Sertifikat (PDF)
</button>
{/* Bagikan ke LinkedIn */}
<button className="flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 active:scale-95 transition-all text-white font-semibold py-3.5 rounded-xl text-sm">
Bagikan ke LinkedIn
</button>
{/* Cetak & Salin Link */}
<div className="grid grid-cols-2 gap-3">
<button className="flex flex-col items-center justify-center gap-1.5 border border-gray-200 hover:bg-gray-50 active:scale-95 transition-all text-red-700 font-medium py-3.5 rounded-xl text-sm">
<Printer className="w-5 h-5" />
Cetak
</button>
<button
onClick={handleCopyLink}
className="flex flex-col items-center justify-center gap-1.5 border border-gray-200 hover:bg-gray-50 active:scale-95 transition-all text-red-700 font-medium py-3.5 rounded-xl text-sm"
>
<Link className="w-5 h-5" />
{copied ? "Copied!" : "Salin Link"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* RIGHT: Certificate History */}
<div className="w-full lg:w-2/6 flex-shrink-0">
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
<div className="flex items-center gap-2 mb-4">
<History className="w-5 h-5 text-red-700" />
<h3 className="font-semibold text-gray-800 text-lg">Certificate History</h3>
</div>
<div className="space-y-3">
{history.map((item: CertificateHistory) => (
<div
key={item.id}
className="flex flex-col gap-3 p-3 bg-white border border-gray-200 shadow-sm rounded-xl hover:bg-gray-50 transition-colors cursor-pointer"
>
<div className="w-full flex justify-center">
<img
src={certificateImg}
alt="Certificate"
className="rounded-xl h-[200px]"
/>
</div>
<div className="w-full">
<p className="text-xs">{item.month}</p>
<p className="text-sm font-semibold text-gray-800 leading-tight">{item.title}</p>
<p className="text-sm text-gray-500">
Grade: {item.grade}
</p>
</div>
</div>
))}
</div>
<div className="w-full my-3 flex justify-end">
<button className="text-sm text-red-500 font-semibold hover:underline">
See more
</button>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,43 @@
import { Lock, Clock } from "lucide-react";
export default function CertificateNotAvailable({ progress, remainingTasks }: { progress: number; remainingTasks: number }) {
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">
{/* Icon */}
<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-20 h-20 flex items-center rounded-lg justify-center bg-gray-300">
<Lock className="w-10 h-10 text-red-700" />
</div>
<div className="absolute -bottom-2 -right-2 w-6 h-6 bg-amber-500 rounded- flex items-center justify-center shadow-md">
<Clock className="w-4 h-4 text-yellow-800" />
</div>
</div>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">Sertifikat Belum Tersedia</h2>
<p className="text-gray-500 text-sm leading-relaxed mb-6">
Anda harus menyelesaikan semua tugas proyek sebelum dapat mengunduh sertifikat kelulusan.
Silakan periksa kembali progres tugas Anda di menu Proyek.
</p>
{/* Progress Bar */}
<div className="w-full">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
<span className="text-sm font-bold text-red-700">{progress}%</span>
</div>
<div className="w-full h-2.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-red-700 rounded-full transition-all duration-700"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-2 text-left">
Selesaikan {remainingTasks} tugas lagi untuk membuka akses.
</p>
</div>
</div>
);
}
@@ -83,7 +83,7 @@ const HomeCalendar = () => {
}
return (
<div className=" px-13 py-5">
<div className=" px-13 py-5 border border-gray-200 rounded-2xl">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-semibold text-gray-800">
@@ -1,15 +1,6 @@
import { Code2, ChessKing, Beef } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { mockProjects } from '../../../../lib/mockData'
interface Project {
name: string
role: string
logo: React.ElementType
iconBg: string
iconColor: string
}
const HomeProjectList = () => {
const navigate = useNavigate();
return (
@@ -1,4 +1,5 @@
import type { Task } from "../../../../lib/mockData";
import { ArrowDownNarrowWide } from "lucide-react";
const formatDate = (date: Date) =>
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
@@ -9,10 +10,11 @@ export default function ProjectTimeline({ tasks }: { tasks: Task[] }) {
return (
<div>
<div className="flex items-center gap-1.5 mb-4">
<div className="flex items-center gap-1.5 mb-4 justify-between">
<span className="font-semibold text-sm text-[#1a1a1a]">
Timeline
</span>
<ArrowDownNarrowWide className="w-5 h-5 text-gray-500"/>
</div>
<ul className="relative pl-4">
+2 -2
View File
@@ -1,8 +1,8 @@
import { Outlet } from "react-router-dom";
import Header from './Header';
import Header from './mentor/components/Header';
import SidebarIntern from './intern/components/SidebarIntern';
import SidebarAdmin from '../../Layout/SidebarAdmin';
import FooterIntern from './intern/components/FooterIntern';
import FooterIntern from '../../Layout/Footer-Internify';
export const InternLayout = () => {
return (
@@ -0,0 +1,51 @@
import { mockProjects } from '../../../lib/mockData'
import MentorProjectCard from './components/MentorProjectCard'
import { Plus } from 'lucide-react'
import { useNavigate } from 'react-router-dom' // Import useNavigate
const MentorProjectsPage = () => {
const navigate = useNavigate()
const handleJoinProject = () => {
// Navigasi ke halaman tujuan, silakan ganti '/path-tujuan' sesuai rute kamu
navigate('/path-tujuan')
}
return (
<div className="px-14 py-15 bg-gray-50 min-h-screen">
{/* Breadcrumb simulasi sesuai gambar */}
<div className="text-xs text-gray-500 mb-2">
<span className="text-red-600 font-medium">Home</span> &gt; <span>List Projects</span>
</div>
<div className="flex flex-col gap-1 mb-6">
<h1 className="text-4xl font-bold text-gray-900">List Projects</h1>
<p className="text-lg text-gray-500">Create, manage, and monitor ongoing projects and assignments.</p>
</div>
<div className="flex flex-wrap gap-4 items-stretch">
{/* Render List Project */}
{mockProjects.map(project => (
<MentorProjectCard key={project.id} {...project} />
))}
{/* Tombol "Join New Project" dengan Border Putus-putus */}
<button
onClick={handleJoinProject}
className="w-[180px] min-h-[230px] border-2 border-dashed border-gray-400 rounded-xl flex flex-col items-center justify-center gap-3 p-4 bg-white hover:bg-gray-100 hover:border-red-600 transition-colors group"
>
{/* Lingkaran Plus */}
<div className="w-12 h-12 rounded-full border border-red-600 flex items-center justify-center text-red-600 group-hover:bg-red-50 transition-colors">
<Plus size={24} />
</div>
{/* Teks Deskripsi */}
<span className="text-sm font-semibold text-red-600 text-center leading-tight">
Join New Project
</span>
</button>
</div>
</div>
)
}
export default MentorProjectsPage
@@ -1,11 +0,0 @@
import React from 'react'
const HomeMentorPage = () => {
return (
<div>
</div>
)
}
export default HomeMentorPage
+24
View File
@@ -0,0 +1,24 @@
import HomeHeader from '../intern/components/HomeHeader'
import HomeCalendar from '../intern/components/HomeCalendar'
import HomeMentorProject from './components/HomeMentorProject'
const HomeMentorPage = () => {
return (
<div className="flex-1 px-14 py-10 bg-gray-50">
<HomeHeader />
<div className="flex flex-col gap-5">
<div className="">
<div className="bg-white border border-gray-200 rounded-xl p-6">
<HomeMentorProject />
</div>
</div>
<div className="bg-white rounded-xl">
<HomeCalendar />
</div>
</div>
</div>
)
}
export default HomeMentorPage
@@ -0,0 +1,5 @@
export default function MentorInternPage() {
return(
<div></div>
)
}
@@ -1,11 +0,0 @@
import React from 'react'
const MentorDetailProject = () => {
return (
<div>
</div>
)
}
export default MentorDetailProject
@@ -1,11 +0,0 @@
import React from 'react'
const MentorInternPage = () => {
return (
<div>
</div>
)
}
export default MentorInternPage
@@ -1,11 +0,0 @@
import React from 'react'
const MentorProjectsPage = () => {
return (
<div>
</div>
)
}
export default MentorProjectsPage
@@ -0,0 +1,26 @@
import type { Project } from '../../../lib/mockData';
import ProjectTimeline from '../intern/components/ProjectTimeline';
import HeroProject from '../intern/components/Hero-project';
import MentorTaskCard from './components/MentorTaskCard';
export default function MentorForumTab({ project }: { project: Project }) {
return (
<div>
{/* Hero banner */}
<HeroProject title={project.name} description={project.description} />
{/* Timeline + Task list */}
<div className="flex gap-7 items-start">
<div className="w-1/5 bg-white rounded-xl p-5 shadow-md shrink-0">
<ProjectTimeline tasks={project.tasks} />
</div>
<div className="w-4/5">
{project.tasks.map((task, idx) => (
<MentorTaskCard key={idx} task={task} />
))}
</div>
</div>
</div>
);
}
@@ -0,0 +1,45 @@
import type { Project, Intern } from '../../../lib/mockData';
export default function InternsTab({ project }: { project: Project }) {
return (
<div>
{/* Mentor */}
<div className="mb-8">
<h3 className=" font-bold text-base text-[#1a1a1a] mb-3.5">
Mentor
</h3>
<div className="border-b border-[#eee] pb-4">
<div className="flex items-center gap-3">
<span className=" text-sm text-[#333]">
{project.mentor}
</span>
</div>
</div>
</div>
{/* Members */}
<div>
<h3 className="font-bold text-base text-[#1a1a1a] mb-3.5">
Members
</h3>
{project.interns.map((intern: Intern, idx: number) => (
<div
key={idx}
className={`flex items-center justify-between py-3 ${
idx < project.interns.length - 1 ? 'border-b border-[#f0f0f0]' : ''
}`}
>
<div className="flex items-center gap-3">
<span className=" text-sm text-[#333]">
{intern.email}
</span>
</div>
<span className=" text-[13px] text-[#555] font-semibold">
{intern.role}
</span>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,83 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { mockProjects } from '../../../lib/mockData';
import { ChevronLeft } from 'lucide-react';
import MentorForumTab from './ProjectForumTab';
import InternsTab from './ProjectsInternTab';
import TaskTab from './TaskTab';
// ─── ProjectDetailPage ─────────────────────────────────────────────────────────
export default function ProjectDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<'forum' | 'participants' | 'Task'>('forum');
const handleBack = () => {
if (window.history.length > 1) {
navigate(-1);
} else {
navigate('/intern/projects');
}
};
const project = mockProjects.find((p) => p.id === Number(id));
if (!project) {
return (
<div className="p-10 text-[#888]">
Project tidak ditemukan.
</div>
);
}
const tabClass = (tab: string) =>
`pr-0 py-2.5 mr-7 text-sm bg-transparent border-0 cursor-pointer transition-colors duration-150 border-b-2 ${activeTab === tab
? 'font-bold text-[#C0392B] border-[#C0392B]'
: 'font-medium text-[#666] border-transparent'
}`;
return (
<div className="bg-gray-50 min-h-screen">
<div className="px-24 py-10">
{/* Back Button */}
<button
onClick={handleBack}
className="flex items-center gap-2 mb-6 text-sm font-medium text-[#666] hover:text-[#C0392B] transition-colors duration-150 group"
>
<span className='flex flex-row gap-2'>
<ChevronLeft width={20} height={20} className="transition-transform duration-150 group-hover:-translate-x-1" />
Kembali ke Daftar Project
</span>
</button>
{/* Tabs */}
<div className="flex mb-4 border-b-1 border-gray-300">
<button className={tabClass('forum')} onClick={() => setActiveTab('forum')}>
Forum
</button>
<button className={tabClass('participants')} onClick={() => setActiveTab('participants')}>
Participants
</button>
<button className={tabClass('Task')} onClick={() => setActiveTab('Task')}>
Tasks
</button>
</div>
{/* Tab content */}
{(() => {
switch (activeTab) {
case 'forum':
return <MentorForumTab project={project} />;
case 'participants':
return <InternsTab project={project} />;
case 'Task':
return <TaskTab project={project} />; // Tambahkan komponen TaskTab di sini
default:
return null;
}
})()}
</div>
</div>
);
}
+79
View File
@@ -0,0 +1,79 @@
import type { Project, Task } from '../../../lib/mockData';
import { ClipboardList, Pencil, Trash2 } from 'lucide-react';
export default function TaskTab({ project }: { project: Project }) {
const handleAddTask = () => {
console.log('Trigger modal / halaman tambah tugas');
};
const handleEditTask = (taskTitle: string) => {
console.log('Edit task:', taskTitle);
};
const handleDeleteTask = (taskTitle: string) => {
console.log('Delete task:', taskTitle);
};
return (
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100 min-h-[400px]">
{/* Header Section */}
<div className="flex justify-between items-center mb-6">
<h2 className="text-3xl font-bold text-slate-800 tracking-tight">Tasks</h2>
<button
onClick={handleAddTask}
className="bg-[#C0392B] hover:bg-[#A93226] text-white px-6 py-2.5 rounded-full font-bold text-sm transition-colors shadow-sm"
>
Add Task
</button>
</div>
<hr className="border-gray-200/80 mb-4" />
{/* Task List Section */}
<div className="flex flex-col">
{project.tasks && project.tasks.length > 0 ? (
project.tasks.map((task: Task, idx: number) => (
<div
key={idx}
className={`flex items-center justify-between py-4 group ${
idx < project.tasks.length - 1 ? 'border-b border-gray-100' : ''
}`}
>
{/* Task Title & Icon */}
<div className="flex items-center gap-3 min-w-0">
<ClipboardList size={22} className="text-slate-600 shrink-0" />
<span className="text-base text-slate-700 font-medium truncate">
{task.title}
</span>
</div>
{/* Action Buttons (Edit & Delete) */}
<div className="flex items-center gap-4 text-slate-600 shrink-0">
<button
onClick={() => handleEditTask(task.title)}
className="hover:text-blue-600 transition-colors p-1"
title="Edit Task"
>
<Pencil size={20} />
</button>
<button
onClick={() => handleDeleteTask(task.title)}
className="hover:text-red-600 transition-colors p-1"
title="Delete Task"
>
<Trash2 size={20} />
</button>
</div>
</div>
))
) : (
<div className="text-center py-10 text-gray-400 text-sm">
Belum ada tugas yang dibuat untuk proyek ini.
</div>
)}
</div>
</div>
);
}
@@ -1,5 +1,5 @@
import { Bell } from "lucide-react"
import humiclogo from "../../assets/logo.png";
import humiclogo from "../../../../assets/logo.png";
const Header = () => {
return (
@@ -0,0 +1,39 @@
import { useNavigate } from 'react-router-dom'
import { mockProjects } from '../../../../lib/mockData'
import { Plus } from 'lucide-react';
const HomeMentorProject = () => {
const navigate = useNavigate();
return (
<div className='flex flex-col'>
<div className='flex flex-row justify-between mb-3 px-5 items-center'>
<h2 className="text-lg font-semibold text-gray-800 mb-4">Projects</h2>
<button className='bg-gray-300 px-2 py-1 rounded-xl font-bold hover:bg-gray-400'>
<span className='text-sm flex flex-row items-center gap-3'>
<Plus className='w-3 h-3'/>
Add
</span>
</button>
</div>
<div className="flex flex-col gap-3">
{mockProjects.map((project, index) => {
return (
<div
key={index}
onClick={() => navigate(`/intern/projects/${project.id}`)}
className="flex items-center gap-4 border border-gray-200 rounded-lg px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors duration-150"
>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-gray-300`}>
</div>
<div>
<p className="text-sm font-semibold text-gray-800">{project.name}</p>
</div>
</div>
)
})}
</div>
</div>
)
}
export default HomeMentorProject
@@ -0,0 +1,63 @@
import { Trash2, UsersRound } from 'lucide-react'
import type { Intern } from '../../../../lib/mockData'
import { useNavigate } from 'react-router-dom'
interface ProjectCardProps {
id: number | string
name: string
interns: Intern[]
thumbnailUrl?: string
}
export default function MentorProjectCard({
id,
name,
interns,
thumbnailUrl
}: ProjectCardProps) {
const navigate = useNavigate();
const handleCardClick = () => {
navigate(`/admin/projects/${id}`)
};
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
console.log('Delete project dengan id:', id)
}
return (
<div
onClick={handleCardClick}
className="flex flex-col bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[180px] min-h-[270px]hover:bg-gray-50 transition-colors">
{/* Thumbnail */}
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden shrink-0">
{thumbnailUrl && (
<img src={thumbnailUrl} alt={name} className="w-full h-full object-cover" />
)}
</div>
{/* Content*/}
<div className="p-3 flex flex-col gap-2 flex-1 justify-between">
<div className="flex flex-col gap-1">
<p className="text-sm font-semibold text-gray-800 leading-tight line-clamp-2">
{name}
</p>
</div>
{/* Interns Count */}
<div className="flex items-center gap-2 mt-auto">
<UsersRound className='text-gray-400 w-4 h-4' />
<span className="text-xs text-gray-500 truncate">
{interns.length}/{interns.length}
</span>
</div>
</div>
{/* Footer */}
<div className="border-t border-gray-300 text-gray-400 flex justify-end p-2 bg-gray-50/50">
<button onClick={handleDeleteClick} className="hover:text-red-600 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
)
}
@@ -0,0 +1,34 @@
import type { Task } from "../../../../lib/mockData";
const formatDate = (date: Date) =>
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
export default function MentorTaskCard({ task }: { task: Task }) {
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="flex-1 min-w-0">
<div className=" text-[11px] text-[#888] mb-1">
Deadline : {formatDate(task.deadline.date)} at {task.deadline.time}
</div>
<div className=" font-bold text-[15px] text-[#1a1a1a] mb-1">
{task.title}
</div>
<div className=" text-[13px] text-[#666]">
{task.description}
</div>
</div>
<div className="shrink-0">
<span
className={`inline-block font-bold text-xs px-2.5 py-[3px] rounded-md tracking-[0.5px] border ${
task.isSubmitted
? "bg-[#27AE60] text-white border-[#27AE60]"
: "bg-white text-[#1a1a1a] border-gray-400"
}`}
>
{task.submittedCount}/{task.totalInterns}
</span>
</div>
</div>
);
}
+170
View File
@@ -0,0 +1,170 @@
import { useState } from "react";
import logo from "../../assets/logo.png";
import { useNavigate } from "react-router-dom";
import { Mail, Lock, Eye, EyeOff } from 'lucide-react';
// Data user dummy untuk keperluan simulasi navigasi frontend
const DataUsers = [
{ "Name": "John Doe", "Email": "john@email.com", "Password": "password123", "role": "intern" },
{ "Name": "Jane Smith", "Email": "jane@email.com", "Password": "password123", "role": "mentor" },
];
export default function InternifyLogin() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState("");
const handleSignIn = (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!email || !password) {
setError("Email dan password wajib diisi.");
return;
}
const user = DataUsers.find(
(u) => u.Email.toLowerCase() === email.toLowerCase() && u.Password === password
);
if (user) {
// 3. Gunakan navigate() sesuai dengan role masing-masing
if (user.role === "admin") {
navigate("/admin");
} else if (user.role === "intern") {
navigate("/intern");
} else if (user.role === "mentor") {
navigate("/mentor");
} else {
setError("Role user tidak dikenali.");
}
} else {
setError("Email atau password salah.");
}
};
return (
<div className="min-h-screen bg-[#F9F9F9] flex flex-col justify-between items-center py-8 px-4 font-sans selection:bg-red-200">
<div className="hidden md:block"></div>
{/* Main Container */}
<div className="w-full max-w-[440px] flex flex-col items-center">
{/* Logo Section */}
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-2 font-bold text-2xl tracking-tight text-black">
<img src={logo} alt="Logo" className="w-32 h-20" />
</div>
<p className="text-[10px] tracking-widest text-yellow-950 font-semibold mt-1 uppercase">
HUMIC LMS PORTAL
</p>
</div>
{/* Card Login */}
<div className="max-w-md bg-white rounded-2xl border border-gray-100 p-8 md:p-12">
<h2 className="text-xl font-bold text-gray-900 mb-1">Welcome back</h2>
<p className="text-sm text-gray-500 mb-6">Access your dashboard with your credentials.</p>
<form onSubmit={handleSignIn} className="space-y-4">
{/* Input Email */}
<div className="space-y-1.5">
<label className="text-xs font-medium text-gray-700">Email address</label>
<div className="relative">
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
<Mail className="w-4 h-4 ml-1" />
</span>
<input
type="email"
placeholder="name@company.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors"
/>
</div>
</div>
{/* Input Password */}
<div className="space-y-1.5">
<label className="text-xs font-medium text-gray-700">Password</label>
<div className="relative">
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
<Lock className="w-4 h-4 ml-1" />
</span>
<input
type={showPassword ? "text" : "password"}
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
>
{showPassword ? (
<Eye className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
</button>
</div>
</div>
{/* Error Message */}
{error && (
<p className="text-xs text-red-600 bg-red-50 p-2 rounded-md font-medium">
{error}
</p>
)}
{/* Button Sign In */}
<button
type="submit"
className="w-full bg-[#B30000] hover:bg-[#990000] text-white font-medium text-sm py-2.5 rounded-lg transition-colors shadow-sm mt-2"
>
Sign In
</button>
</form>
{/* Forgot Password */}
<div className="text-center mt-6">
<a href="#" className="text-xs font-semibold text-red-600 hover:underline">
Forgot Password?
</a>
</div>
{/* Divider & Support */}
<div className="mt-1">
<div className="relative flex py-2 items-center">
<div className="flex-grow border-t border-gray-100"></div>
<span className="flex-shrink mx-4 text-[10px] font-bold tracking-wider text-gray-400 uppercase">
SUPPORT
</span>
<div className="flex-grow border-t border-gray-100"></div>
</div>
<button className="w-full mt-2 flex items-center justify-center gap-2 border border-gray-200 rounded-full py-1.5 text-xs text-gray-600 hover:bg-gray-50 transition-colors font-medium">
<svg className="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Frequently Asked Questions
</button>
</div>
</div>
</div>
{/* Footer Section */}
<div className="text-center text-[11px] text-gray-400 space-y-1">
<p>© 2024 Internify. CoE Humic Engineering.</p>
<div className="space-x-2">
<a href="#" className="hover:underline">Privacy Policy</a>
<span>•</span>
<a href="#" className="hover:underline">Terms of Service</a>
</div>
</div>
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+57 -11
View File
@@ -24,6 +24,8 @@ export interface Task {
description: string;
deadline: Deadline;
isSubmitted: boolean;
submittedCount: number;
totalInterns: number;
}
export interface Intern {
@@ -57,24 +59,32 @@ export const mockProjects: Project[] = [
title: "Laporan tugas",
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" },
submittedCount: 6,
totalInterns: 6,
isSubmitted: true,
},
{
title: "Laporan tugas 1",
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" },
submittedCount: 2,
totalInterns: 6,
isSubmitted: false,
},
{
title: "Laporan tugas 2",
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" },
submittedCount: 3,
totalInterns: 6,
isSubmitted: false,
},
{
title: "Laporan tugas 3",
description: "Berisi wireframe halaman dashboard aplikasi internify",
deadline: { date: new Date(2026, 5, 5), label: "Laporan tugas 3 – 5 Jun", time: "23:59 PM" },
submittedCount: 0,
totalInterns: 6,
isSubmitted: false,
},
],
@@ -101,24 +111,32 @@ export const mockProjects: Project[] = [
title: "Analisis Kebutuhan Sistem",
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" },
submittedCount: 6,
totalInterns: 6,
isSubmitted: true,
},
{
title: "Desain Database Schema",
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" },
submittedCount: 6,
totalInterns: 6,
isSubmitted: true,
},
{
title: "Implementasi Auth Module",
description: "Pengembangan sistem autentikasi dan otorisasi multi-role",
deadline: { date: new Date(2026, 4, 18), label: "Auth Module – 18 Mei", time: "23:59 PM" },
submittedCount: 3,
totalInterns: 6,
isSubmitted: true,
},
{
title: "Modul Posting Lowongan",
description: "Fitur buat, edit, dan publish lowongan pekerjaan",
deadline: { date: new Date(2026, 4, 25), label: "Posting Lowongan – 25 Mei", time: "23:59 PM" },
submittedCount: 0,
totalInterns: 6,
isSubmitted: false,
},
],
@@ -146,25 +164,33 @@ export const mockProjects: Project[] = [
title: "Setup Pipeline Data",
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" },
submittedCount: 6,
totalInterns: 6,
isSubmitted: true,
},
{
title: "Desain Komponen Chart",
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" },
submittedCount: 6,
totalInterns: 6,
isSubmitted: true,
},
{
title: "Dashboard Overview Page",
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" },
isSubmitted: true,
submittedCount: 0,
totalInterns: 6,
isSubmitted: false,
},
{
title: "Laporan Penjualan",
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" },
isSubmitted: true,
submittedCount: 0,
totalInterns: 6,
isSubmitted: false,
},
],
interns: [
@@ -182,8 +208,34 @@ export const mockProjects: Project[] = [
},
];
export const certificateData = {
isReceived: true, // ubah ke false untuk tampilan "belum tersedia"
export interface Certificate {
id: string;
issuedDate: string;
recipientName: string;
programName: string;
duration: string;
finalScore: number;
grade: string;
status: string;
}
export interface CertificateHistory {
id: number;
month: string;
title: string;
grade: string;
}
export interface CertificateData {
isReceived: boolean;
projectProgress: number;
remainingTasks: number;
certificate: Certificate;
history: CertificateHistory[];
}
export const certificateData: CertificateData = {
isReceived: false, // ubah ke false untuk tampilan "belum tersedia"
projectProgress: 65,
remainingTasks: 3,
certificate: {
@@ -208,12 +260,6 @@ export const certificateData = {
month: "Maret 2023",
title: "UI/UX Design Essentials",
grade: "A",
},
{
id: 3,
month: "Januari 2023",
title: "Data Analytics Bootcamp",
grade: "B+",
},
}
],
};