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
@@ -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>
);
}