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