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:
Kemas M. Aryadary Rasyad
2026-06-06 23:47:22 +07:00
parent e59e63a21e
commit 46cd26f4d2
33 changed files with 1886 additions and 332 deletions
@@ -6,7 +6,7 @@ import TaskTimeline from "./components/TaskTimeline";
// PR: tambahin breadcrumb
export default function HomeInternPage() {
return (
<div className="flex-1 px-14 py-10 bg-gray-50">
<div className="container flex-1">
<HomeHeader />
<div className="flex flex-col gap-5">
<div className="grid grid-cols-2 gap-5">
@@ -36,8 +36,8 @@ export default function ProjectDetailPage() {
}`;
return (
<div className="bg-gray-50 min-h-screen">
<div className="px-24 py-10">
<div className="container">
<div>
{/* Back Button */}
<button
+3 -3
View File
@@ -3,10 +3,10 @@ import { mockProjects } from '../../../lib/mockData'
export default function ProjectsPage() {
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">
<span className="text-4xl font-bold">List Projects</span>
<h1 className="text-lg text-gray-500">View assigned projects and track their progress.</h1>
<span className="page-title">List Projects</span>
<h1 className="page-title-desc">View assigned projects and track their progress.</h1>
</div>
{/* Grid card */}
@@ -6,12 +6,12 @@ const SertificatePage = () => {
const data = certificateData; // ganti isReceived di object atas
return (
<div className="px-15 py-10 bg-white min-h-screen">
<div className="container">
{/* 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>
<p className="font-light text-gray-500">View and download internship certificates</p>
<h1 className="page-title">Certificates</h1>
<p className="page-title-desc">View and download internship certificates</p>
</div>
{/* 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 }) {
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 */}
<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">
@@ -137,7 +137,7 @@ const HomeCalendar = () => {
className={`w-full h-7 flex items-center justify-center text-xs rounded
${isBold(day) ? 'font-bold text-gray-800' : 'text-gray-700'}
${hasDeadline(day) ? 'bg-red-700 text-white' : ''}
hover:bg-gray-100 cursor-pointer transition-colors`}
transition-colors`}
>
{day}
</span>
@@ -1,9 +1,9 @@
const HomeHeader = () => {
const name = "Karina";
const name = "Karina";
return (
<div>
<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>
)
@@ -1,11 +1,13 @@
import type { Task } from "../../../../lib/mockData";
import { useNavigate } from "react-router-dom";
const formatDate = (date: Date) =>
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
export default function TaskCard({ task }: { task: Task }) {
const navigate = useNavigate();
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=" text-[11px] text-[#888] mb-1">
Deadline : {formatDate(task.deadline.date)} at {task.deadline.time}
@@ -24,8 +26,8 @@ export default function TaskCard({ task }: { task: Task }) {
DONE
</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]">
Mark as Done
<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`)}>
Add Submission
</button>
)}
</div>
@@ -24,7 +24,7 @@ const ProjectCard = ({
const navigate = useNavigate()
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 */}
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden">
{thumbnailUrl && (
@@ -33,41 +33,44 @@ const ProjectCard = ({
</div>
{/* Content */}
<div className="p-3 flex flex-col gap-2">
<p className="text-sm font-semibold text-gray-800 leading-tight">{name}</p>
<div className="min-h-[180px] max-h-[210px] flex-1 flex flex-col justify-between py-3">
<div className="px-3 flex flex-col gap-2">
<p className="text-sm font-semibold text-gray-800 leading-tight">{name}</p>
{/* Mentor */}
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-gray-200 overflow-hidden flex-shrink-0">
{avatarUrl ? (
<img src={avatarUrl} alt={mentor} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gray-300" />
)}
{/* Mentor */}
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-gray-200 overflow-hidden flex-shrink-0">
{avatarUrl ? (
<img src={avatarUrl} alt={mentor} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gray-300" />
)}
</div>
<span className="text-xs text-gray-500 truncate">{mentor}</span>
</div>
<span className="text-xs text-gray-500 truncate">{mentor}</span>
</div>
{/* Progress bar */}
<div className="flex flex-col gap-1">
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-red-600 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
{/* Progress bar */}
<div className="flex flex-col gap-1">
<div className="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-red-600 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-[10px] text-gray-400 text-right">
{tasksDone}/{tasksTotal}
</span>
</div>
<span className="text-[10px] text-gray-400 text-right">
{tasksDone}/{tasksTotal}
</span>
</div>
{/* Button */}
<button
onClick={() => navigate(`/intern/projects/${id}`)}
className="w-full bg-red-600 hover:bg-red-700 text-white text-xs font-medium py-1.5 rounded-lg transition-colors"
>
View Project
</button>
<div className="flex items-end justify-end px-3 pb-1">
<button
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>
</div>
</div>
</div>
)
@@ -1,6 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight, X } from 'lucide-react';
const navItems = [
{ label: "Home", icon: Home, path: "/intern" },
@@ -11,36 +10,62 @@ const navItems = [
const prefItems = [
{ label: "Settings", icon: Settings, path: "/intern/settings" },
{ 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 [active, setActive] = useState("/intern");
const location = useLocation();
const handleNav = (path: string) => {
setActive(path);
navigate(path);
closeSidebar(); // Otomatis tutup sidebar setelah klik menu di HP
};
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">
{/* 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>
<aside
className={`
/* Struktur Dasar Layout */
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
/* Kondisi Layar Desktop (>= md) */
md:sticky md:top-[73px] md:h-[calc(100vh-73px)] md:w-[250px] md:translate-x-0 md:z-8
/* 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>
{/* 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 className="h-px bg-gray-100 my-1" />
<div className="h-px bg-gray-300 my-1" />
{/* Main nav */}
<nav className="flex flex-col gap-0.5">
{navItems.map(({ label, icon: Icon, path }) => {
const isActive = active === path;
const isActive = location.pathname === path;
return (
<button
key={path}
@@ -53,15 +78,13 @@ function SidebarIntern() {
>
<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" />
)}
{isActive && <ChevronRight className="w-3.5 h-3.5 ml-auto opacity-60" />}
</button>
);
})}
</nav>
<div className="h-px bg-gray-100 my-1" />
<div className="h-px bg-gray-200 my-1" />
{/* Preferences */}
<p className="text-[10px] font-semibold tracking-widest text-gray-400 px-3 py-1.5">
@@ -69,7 +92,7 @@ function SidebarIntern() {
</p>
<nav className="flex flex-col gap-0.5">
{prefItems.map(({ label, icon: Icon, path, danger }) => {
const isActive = active === path;
const isActive = location.pathname === path;
return (
<button
key={path}
@@ -88,10 +111,6 @@ function SidebarIntern() {
);
})}
</nav>
</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>
);
}