feat: enhance task creation and editing with success/error toasts
- Added success and error toast notifications for task creation in CreateTaskDialog. - Implemented success and error toast notifications for task updates in EditTaskDialog. - Integrated custom toast notifications in MentorProjectCard for project archiving. - Removed unused mock data files related to projects, interns, deadlines, and certificates. - Updated useProjectTasks hook to accept projectId as a number instead of a string. - Improved error handling in useSubmission hook methods to throw errors instead of returning null.
This commit is contained in:
+1
-1
@@ -135,7 +135,7 @@ function App() {
|
||||
<Route path="projects/:slug/:taskSlug/:nameIntern" element={<ViewInternSubmission />} />
|
||||
<Route path="faq" element={<FAQPage />} />
|
||||
<Route path="notifications" element={<NotificationList />} />
|
||||
<Route path="settings" element={<MentorSettingsPage />} />
|
||||
<Route path="settings" element={<SettingsContent />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
+2
-9
@@ -70,17 +70,13 @@ const Faq = () => {
|
||||
fetchFaq();
|
||||
}, []);
|
||||
|
||||
// Auto-scroll ke section FAQ kalau ada #faq
|
||||
useEffect(() => {
|
||||
if (location.hash === "#faq") {
|
||||
// wrapper ada di Landing: <div id="faq"><Faq/></div>
|
||||
// ini memastikan scroll halus
|
||||
const el = document.getElementById("faq");
|
||||
el?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}, [location.hash]);
|
||||
|
||||
// Auto-open FAQ tertentu kalau ?faq=ID ada dan data sudah ke-load
|
||||
useEffect(() => {
|
||||
if (!faqIdFromUrl) return;
|
||||
if (faqData.length === 0) return;
|
||||
@@ -88,8 +84,6 @@ const Faq = () => {
|
||||
const idx = faqData.findIndex((f) => f.id === faqIdFromUrl);
|
||||
if (idx !== -1) {
|
||||
setOpenIndex(idx);
|
||||
|
||||
// Optional: setelah open, pastikan tetap terlihat
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById("faq");
|
||||
el?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
@@ -99,7 +93,6 @@ const Faq = () => {
|
||||
|
||||
return (
|
||||
<div className="hero-5-section flex flex-col lg:flex-row items-start lg:items-center justify-between mt-25 px-6 md:px-16 lg:px-[150px] pb-[60px] gap-10">
|
||||
{/* UI tetap sama persis */}
|
||||
<div className="content-of-5 w-full lg:w-[610px]">
|
||||
<h2 className="font-bold text-2xl md:text-3xl">
|
||||
Frequently Asked Questions
|
||||
@@ -117,7 +110,7 @@ const Faq = () => {
|
||||
|
||||
<div className="faq-content-list space-y-6 w-full lg:w-[700px]">
|
||||
{!loading && !error && faqData.length === 0 && (
|
||||
<div className="bg-[#FAF0EF] shadow-md rounded-sm p-4">
|
||||
<div className="bg-card shadow-md rounded-sm p-4">
|
||||
<p className="text-sm text-gray-700">Belum ada FAQ.</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -125,7 +118,7 @@ const Faq = () => {
|
||||
{faqData.map((faq, index) => (
|
||||
<div
|
||||
key={faq.id ?? index}
|
||||
className="bg-[#FAF0EF] shadow-md rounded-sm"
|
||||
className="bg-cardshadow-md rounded-sm"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleItem(index)}
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import useBreadcrumbs from "use-react-router-breadcrumbs";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { mockProjects } from "../../lib/mockData";
|
||||
import { useMyProjects, useProjects } from "../../hooks/useProjects";
|
||||
import { useProjectTasks } from "../../hooks/useTasks";
|
||||
|
||||
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
// Dynamic breadcrumb: ubah slug jadi nama project asli
|
||||
const ProjectNameBreadcrumb = ({ match }: { match: { params: { slug?: string } } }) => {
|
||||
const slug = match.params.slug;
|
||||
const project = mockProjects.find((p) => toSlug(p.name) === slug);
|
||||
return <>{project ? project.name : slug}</>;
|
||||
type BreadcrumbMatch = {
|
||||
match: { params: { slug?: string; taskSlug?: string } };
|
||||
};
|
||||
|
||||
const TaskNameBreadcrumb = ({ match }: { match: { params: { slug?: string; taskSlug?: string } } }) => {
|
||||
const { slug, taskSlug } = match.params;
|
||||
const project = mockProjects.find((p) => toSlug(p.name) === slug);
|
||||
const task = project?.tasks.find((t) => toSlug(t.title) === taskSlug);
|
||||
return <>{task ? task.title : taskSlug}</>;
|
||||
const unslugify = (slug = '') =>
|
||||
slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
const ProjectBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const { projects } = useProjects();
|
||||
const slug = match.params.slug;
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
return <>{project?.project_name ?? unslugify(slug)}</>;
|
||||
};
|
||||
|
||||
const TaskBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const {slug, taskSlug} = match.params;
|
||||
|
||||
const { projects } = useMyProjects();
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
|
||||
const { tasks } = useProjectTasks(project?.id ?? 0);
|
||||
const task = tasks?.find((t) => t.slug === taskSlug);
|
||||
return <>{task?.title ?? unslugify(taskSlug)}</>;
|
||||
};
|
||||
|
||||
const breadcrumbRoutes = [
|
||||
// --- AREA INTERN ---
|
||||
{ path: '/intern', breadcrumb: 'Home' },
|
||||
{ path: '/intern/projects', breadcrumb: 'Projects' },
|
||||
{ path: '/intern/projects/:slug', breadcrumb: ProjectNameBreadcrumb },
|
||||
{ path: '/intern/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
||||
{ path: '/intern/certificates', breadcrumb: 'Certificates' },
|
||||
{ path: '/intern/projects/:slug/:taskSlug', breadcrumb: TaskNameBreadcrumb },
|
||||
{ path: '/intern/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
||||
{ path: '/intern/faq', breadcrumb: 'FAQ' },
|
||||
{ path: '/intern/notifications', breadcrumb: 'Notifications' },
|
||||
{ path: '/intern/settings', breadcrumb: 'Settings' },
|
||||
@@ -33,8 +43,8 @@ const breadcrumbRoutes = [
|
||||
// --- AREA MENTOR / ADMIN ---
|
||||
{ path: '/mentor', breadcrumb: 'Home' },
|
||||
{ path: '/mentor/projects', breadcrumb: 'List Projects' },
|
||||
{ path: '/mentor/projects/:slug', breadcrumb: ProjectNameBreadcrumb },
|
||||
{ path: '/mentor/projects/:slug/:taskSlug', breadcrumb: TaskNameBreadcrumb },
|
||||
{ path: '/mentor/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
||||
{ path: '/mentor/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
||||
{ path: '/mentor/certificates', breadcrumb: 'Certificates' },
|
||||
{ path: '/mentor/intern', breadcrumb: 'List Intern' },
|
||||
{ path: '/mentor/faq', breadcrumb: 'FAQ' },
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function FAQPage() {
|
||||
setLoading(true);
|
||||
api.get("/faq-api/get")
|
||||
.then((res) => {
|
||||
setFaq(res.data.data); // ambil array dari field "data"
|
||||
setFaq(res.data.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal fetch FAQ:", err);
|
||||
@@ -41,14 +41,14 @@ export default function FAQPage() {
|
||||
) : (
|
||||
<div className="space-y-8 max-w-auto">
|
||||
{faq.map((item) => (
|
||||
<div key={item.id} className="relative shadow-md group py-5 px-10 rounded-md bg-white">
|
||||
<div key={item.id} className="relative shadow-md group py-5 px-10 rounded-md bg-white border-l-6 border-red hover:shadow-xl">
|
||||
|
||||
{/* Item Question */}
|
||||
<h2 className="text-[17px] font-bold text-[#8A0000] tracking-tight leading-snug mb-2 border-l-4 border-[#8A0000] pl-3">
|
||||
<h2 className="text-[17px] font-bold text-red tracking-tight leading-snug mb-2">
|
||||
{item.pertanyaan}
|
||||
</h2>
|
||||
|
||||
<p className="text-[14px] pl-8 text-[#8A0000] font-light leading-relaxed tracking-normal">
|
||||
<p className="text-[14px] pl-8 text-redfont-light leading-relaxed tracking-normal">
|
||||
{item.jawaban}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useCurrentUser, useUpdateProfile } from "../../hooks/useUser";
|
||||
import { customToast } from "../utils/showToast";
|
||||
import { Pen } from "lucide-react";
|
||||
|
||||
export default function SettingsContent() {
|
||||
const { user, loading } = useCurrentUser();
|
||||
export default function MentorSettingsPage() {
|
||||
const { user, loading: userLoading } = useCurrentUser();
|
||||
const { save, loading: saving, error: errorMsg, successMsg } = useUpdateProfile();
|
||||
const isIntern = user?.role === "intern";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: "",
|
||||
@@ -12,13 +14,27 @@ export default function SettingsContent() {
|
||||
bio: "",
|
||||
});
|
||||
|
||||
const [photoFile, setPhotoFile] = useState<File | null>(null);
|
||||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setFormData({
|
||||
fullName: user.full_name || "",
|
||||
fullName:
|
||||
user.full_name ||
|
||||
`${user.nama_depan ?? ""} ${user.nama_belakang ?? ""}`.trim() ||
|
||||
"",
|
||||
email: user.email || "",
|
||||
bio: user.professional_bio || "",
|
||||
});
|
||||
if (user.profile_picture) {
|
||||
setPhotoPreview(user.profile_picture);
|
||||
} else {
|
||||
setPhotoPreview(null);
|
||||
}
|
||||
setImageError(false);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
@@ -27,10 +43,27 @@ export default function SettingsContent() {
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setPhotoFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => setPhotoPreview(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePhoto = () => {
|
||||
setPhotoFile(null);
|
||||
setPhotoPreview(null);
|
||||
setImageError(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
};
|
||||
|
||||
const handleSaveChanges = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
await customToast.promise(
|
||||
|
||||
await customToast.promise(
|
||||
save({
|
||||
full_name: formData.fullName,
|
||||
email: formData.email,
|
||||
@@ -40,11 +73,11 @@ export default function SettingsContent() {
|
||||
loading: "Menyimpan perubahan...",
|
||||
success: () => ({
|
||||
title: "Perubahan berhasil disimpan!",
|
||||
description: successMsg || "Profil Anda telah diperbarui.",
|
||||
description: successMsg || "",
|
||||
}),
|
||||
error: () => ({
|
||||
title: "Gagal menyimpan perubahan!",
|
||||
description: errorMsg || "Terjadi kesalahan saat menyimpan perubahan.",
|
||||
description: errorMsg || "",
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -52,7 +85,7 @@ export default function SettingsContent() {
|
||||
|
||||
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
|
||||
|
||||
if (loading) {
|
||||
if (userLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-40 text-sm text-gray-400">
|
||||
Loading profile...
|
||||
@@ -61,8 +94,7 @@ export default function SettingsContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
|
||||
<div>
|
||||
{/* Header Title */}
|
||||
<div className="mb-6">
|
||||
<h1 className="page-title">Settings</h1>
|
||||
@@ -72,12 +104,12 @@ export default function SettingsContent() {
|
||||
</div>
|
||||
|
||||
{/* White Card Container */}
|
||||
<div className="w-full bg-white rounded-xl border border-gray-100 shadow-lg overflow-hidden">
|
||||
<div className="w-full bg-white rounded-xl border border-box-border shadow-lg 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">
|
||||
<div className="p-6 border-b border-box-border">
|
||||
<h2 className="text-[20px] font-bold text-foreground">Profile Information</h2>
|
||||
<p className="text-xs text-foreground mt-0.5">
|
||||
Update your photo and personal details.
|
||||
</p>
|
||||
</div>
|
||||
@@ -85,25 +117,80 @@ export default function SettingsContent() {
|
||||
{/* 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>
|
||||
{/* Profile Photo Row */}
|
||||
<div className="flex items-center gap-5 py-2">
|
||||
{/* Avatar dengan badge edit */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-20 h-20 rounded-xl border-2 border-red-100 overflow-hidden bg-gray-100 flex items-center justify-center shadow-sm">
|
||||
{photoPreview && !imageError ? (
|
||||
<img
|
||||
src={photoPreview}
|
||||
alt="Profile"
|
||||
className="w-full h-full object-cover"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-3xl font-black text-gray-900">{initialLetter}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Badge edit icon */}
|
||||
{!isIntern &&
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="absolute -bottom-1.5 -right-1.5 w-6 h-6 bg-[#B30000] rounded-full flex items-center justify-center shadow border-2 border-white hover:bg-[#990000] transition-colors"
|
||||
title="Edit photo"
|
||||
>
|
||||
<Pen className="text-white w-2.5 h-2.5" />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Info & Action Buttons */}
|
||||
{!isIntern &&
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-gray-900 mb-0.5">Profile Photo</h3>
|
||||
<p className="text-xs text-gray-400 mb-3">
|
||||
JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K.
|
||||
</p>
|
||||
{photoFile && (
|
||||
<p className="text-xs text-blue-500 mb-2 font-medium">
|
||||
📎 {photoFile.name}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors"
|
||||
>
|
||||
Upload New
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemovePhoto}
|
||||
className="px-4 py-1.5 bg-white border border-box-border text-gray-600 hover:bg-gray-50 text-xs font-bold rounded-lg transition-colors"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
className="hidden"
|
||||
onChange={handlePhotoUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className="border-t border-dashed border-gray-200" />
|
||||
<hr className="border-t border-dashed border-box-border" />
|
||||
|
||||
{/* Grid Inputs (Full Name & Email Address) */}
|
||||
{/* Grid Inputs */}
|
||||
<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-700 uppercase block">
|
||||
Full Name
|
||||
@@ -113,11 +200,9 @@ export default function SettingsContent() {
|
||||
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"
|
||||
className="w-full px-3 py-2 border border-box-border 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-700 uppercase block">
|
||||
Email Address
|
||||
@@ -127,12 +212,12 @@ export default function SettingsContent() {
|
||||
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"
|
||||
className="w-full px-3 py-2 border border-box-border 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 */}
|
||||
{/* Professional Bio */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold tracking-wider text-gray-700 uppercase block">
|
||||
Professional Bio
|
||||
@@ -142,19 +227,31 @@ export default function SettingsContent() {
|
||||
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"
|
||||
className="w-full px-3 py-2 border border-box-border 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-700 font-normal pt-0.5">
|
||||
<p className="text-[11px] text-gray-500 font-normal pt-0.5">
|
||||
Brief description for your profile. URLs and @mentions are allowed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Action Footer Row */}
|
||||
{/* Feedback Messages
|
||||
{successMsg && (
|
||||
<p className="text-xs text-green-600 bg-green-50 p-2 rounded-md font-medium">
|
||||
{successMsg}
|
||||
</p>
|
||||
)}
|
||||
{errorMsg && (
|
||||
<p className="text-xs text-red-600 bg-red-50 p-2 rounded-md font-medium">
|
||||
{errorMsg}
|
||||
</p>
|
||||
)} */}
|
||||
|
||||
{/* Footer Action */}
|
||||
<div className="flex justify-end pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="px-6 py-2 bg-[#B30000] hover:bg-[#990000] text-white font-bold text-xs rounded-lg shadow-sm transition-colors uppercase tracking-wider"
|
||||
className="px-6 py-2 bg-[#B30000] hover:bg-[#990000] text-white font-bold text-xs rounded-lg shadow-sm transition-colors uppercase tracking-wider disabled:opacity-60"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LuSendHorizontal } from "react-icons/lu";
|
||||
import SubmitStatusTable from "./SubmissionStatusTable";
|
||||
import { useSubmission } from "../../../../hooks/useTasks";
|
||||
import type { TaskSubmissionData } from "../../../../types/task.types";
|
||||
import { customToast } from "../../../utils/showToast";
|
||||
|
||||
interface TaskFormFileProps {
|
||||
taskId: string;
|
||||
@@ -25,10 +26,7 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
||||
selectedFiles.forEach((file) => {
|
||||
const ext = file.name.split(".").pop()?.toLowerCase();
|
||||
if (ext && allowedExtensions.includes(ext)) {
|
||||
if (file.size <= 10 * 1024 * 1024) validFiles.push(file);
|
||||
else alert(`Ukuran file ${file.name} melebihi 10MB.`);
|
||||
} else {
|
||||
alert(`Format ${file.name} tidak didukung. Gunakan PDF.`);
|
||||
if (file.size <= 5 * 1024 * 1024) validFiles.push(file);
|
||||
}
|
||||
});
|
||||
if (validFiles.length > 0) setFiles((prev) => [...prev, ...validFiles]);
|
||||
@@ -52,17 +50,59 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (files.length === 0) { alert("Pilih file terlebih dahulu."); return; }
|
||||
const res = isEditing
|
||||
? await updateFile(submission!.id, files)
|
||||
: await submitFile(files);
|
||||
if (res) { setIsEditing(false); setFiles([]); }
|
||||
if (files.length === 0) {
|
||||
customToast.error(
|
||||
"File belum dipilih!",
|
||||
"Silakan pilih file terlebih dahulu sebelum mengirim."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await customToast.promise(
|
||||
isEditing ? updateFile(submission!.id, files) : submitFile(files),
|
||||
{
|
||||
loading: isEditing ? "Memperbarui file..." : "Mengirim file...",
|
||||
success: () => ({
|
||||
title: isEditing ? "File berhasil diperbarui!" : "File berhasil dikirim!",
|
||||
description: isEditing
|
||||
? "Perubahan submission Anda telah disimpan."
|
||||
: "Submission Anda telah berhasil dikirim.",
|
||||
}),
|
||||
error: (err) => ({
|
||||
title: isEditing ? "Gagal memperbarui file!" : "Gagal mengirim file!",
|
||||
description:
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
"Terjadi kesalahan saat memproses file Anda.",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
setIsEditing(false);
|
||||
setFiles([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm("Yakin ingin menghapus submission ini?")) return;
|
||||
const ok = await remove(submission!.id);
|
||||
if (ok) { setFiles([]); setIsEditing(false); }
|
||||
|
||||
try {
|
||||
const ok = await remove(submission!.id);
|
||||
if (ok) {
|
||||
setFiles([]);
|
||||
setIsEditing(false);
|
||||
customToast.success(
|
||||
"Submission berhasil dihapus!",
|
||||
"Anda dapat mengirim submission baru kapan saja."
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
customToast.error(
|
||||
"Gagal menghapus submission!",
|
||||
err?.response?.data?.message || "Terjadi kesalahan saat menghapus submission."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
@@ -83,8 +123,6 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
||||
);
|
||||
}
|
||||
|
||||
// const currentFileName = submission?.file_path?.split("/").pop();
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
@@ -118,24 +156,23 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
|
||||
</div>
|
||||
) : isEditing && submission?.files && submission.files.length > 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full space-y-2 text-center py-6">
|
||||
<FileText className="w-6 h-6 text-gray-400" />
|
||||
<p className="text-sm font-semibold text-gray-700">File saat ini:</p>
|
||||
<div className="space-y-0.5">
|
||||
{submission.files.map((f) => (
|
||||
<p key={f.id} className="text-sm font-medium text-red-700">
|
||||
<div className="flex items-center gap-2 text-[11px]">
|
||||
<FileText className="w-4 h-4 text-red-700 stroke-[2.5]" />
|
||||
{f.original_name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-400 font-medium pointer-events-none">
|
||||
Klik atau drag & drop untuk mengganti file
|
||||
<p className="text-[14px] text-gray-400 font-medium pointer-events-none">
|
||||
Upload ulang files yang akan diganti ke dalam submissions, atau klik batal
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full space-y-2 pointer-events-none text-center py-6">
|
||||
<UploadCloud className="w-8 h-8 text-gray-400 mx-auto stroke-[1.5]" />
|
||||
<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">PDF, DOCX, or ZIP (Max. 10MB)</p>
|
||||
<p className="text-[11px] text-gray-400 font-semibold">PDF, DOCX, or ZIP (Max.5MB)</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LuSendHorizontal } from "react-icons/lu";
|
||||
import SubmitStatusTable from "./SubmissionStatusTable";
|
||||
import { useSubmission } from "../../../../hooks/useTasks";
|
||||
import type { TaskSubmissionData } from "../../../../types/task.types";
|
||||
import { customToast } from "../../../utils/showToast";
|
||||
|
||||
interface TaskFormLinkProps {
|
||||
taskId: string;
|
||||
@@ -19,18 +20,54 @@ export default function TaskFormLink({ taskId, projectId, deadline, initialSubmi
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!inputLink.trim()) return;
|
||||
const res = isEditing
|
||||
? await updateLink(submission!.id, inputLink)
|
||||
: await submitLink(inputLink);
|
||||
if (res) { setIsEditing(false); }
|
||||
if (!inputLink.trim()) {
|
||||
customToast.error(
|
||||
"Link belum dibuat",
|
||||
"Masukkan link untuk mengirim submission"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await customToast.promise(
|
||||
isEditing ? updateLink(submission!.id, inputLink) : submitLink(inputLink),
|
||||
{
|
||||
loading: isEditing ? "Memperbarui Link..." : "Mengirim Link...",
|
||||
success: () => ({
|
||||
title: isEditing ? "Link diperbaharui" : "Link berhasil dikirim",
|
||||
description: isEditing
|
||||
? "Perubahan submission Anda telah disimpan."
|
||||
: "Submission Anda telah berhasil dikirim.",
|
||||
}),
|
||||
error: (err) => ({
|
||||
title: isEditing ? "Gagal memperbarui link!" : "Gagal mengirim link!",
|
||||
description:
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
"Link yang anda submit gagal dikirim",
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm("Yakin ingin menghapus submission ini?")) return;
|
||||
const ok = await remove(submission!.id);
|
||||
if (ok) setInputLink("");
|
||||
};
|
||||
if (!confirm("Yakin ingin menghapus submission ini?")) return;
|
||||
try {
|
||||
const ok = await remove(submission!.id);
|
||||
if (ok) {
|
||||
setInputLink("");
|
||||
setIsEditing(false);
|
||||
customToast.success(
|
||||
"Submission berhasil dihapus!",
|
||||
"Anda dapat mengirim submission baru kapan saja."
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
customToast.error(
|
||||
"Gagal menghapus submission!",
|
||||
err?.response?.data?.message || "Terjadi kesalahan saat menghapus submission."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (submission && !isEditing) {
|
||||
return (
|
||||
|
||||
@@ -2,9 +2,9 @@ import MentorProjectCard from './components/MentorProjectCard';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CreateProjectModal from './components/CreateProjectDialog';
|
||||
import { customToast } from '../../utils/showToast';
|
||||
import { useProjectsByRole } from '../../../hooks/useListProjects';
|
||||
|
||||
|
||||
export default function MentorProjectsPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { projects, loading, error, refetch } = useProjectsByRole();
|
||||
@@ -38,7 +38,6 @@ export default function MentorProjectsPage() {
|
||||
<p className="page-title-desc">Create, manage, and monitor ongoing projects and assignments.</p>
|
||||
</div>
|
||||
<div>
|
||||
{/* Tombol pemicu buka modal */}
|
||||
<button
|
||||
onClick={openDialog}
|
||||
className="flex items-center gap-2 px-4 py-1 bg-[#C0392B] text-white rounded-lg hover:bg-[#A93226] transition-colors duration-150"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAllInterns, useRemoveMember, useProjects } from "../../../hooks/useP
|
||||
import AddInternsModal from "./components/AddInternsToProjects";
|
||||
import type { InternDetail } from "../../../types/project.types";
|
||||
import type { Project } from "../../../types/project.types";
|
||||
import { customToast } from "../../utils/showToast";
|
||||
|
||||
export default function InternsManagement() {
|
||||
const [searchEmail, setSearchEmail] = useState("");
|
||||
@@ -39,11 +40,16 @@ export default function InternsManagement() {
|
||||
const handleDeleteIntern = async (intern: InternDetail) => {
|
||||
const matchedProject = activeProjects.find((project: Project) => project.project_name === intern.projectName);
|
||||
if (!matchedProject) {
|
||||
console.error("Project not found for intern:", intern);
|
||||
customToast.error('Project not found', `Could not find a matching project for intern ${intern.name}.`);
|
||||
return;
|
||||
}
|
||||
const res = await remove({ id_project: matchedProject.id, id_user: intern.id });
|
||||
if (res !== null) refetch();
|
||||
if (res !== null) {
|
||||
customToast.success('Intern removed', `${intern.name} has been successfully removed from the project.`);
|
||||
refetch();
|
||||
} else {
|
||||
customToast.error('Failed to remove', 'An error occurred while removing the intern. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useCurrentUser, useUpdateProfile } from "../../../hooks/useUser";
|
||||
import { customToast } from "../../utils/showToast";
|
||||
|
||||
export default function MentorSettingsPage() {
|
||||
const { user, loading: userLoading } = useCurrentUser();
|
||||
@@ -59,20 +60,28 @@ export default function MentorSettingsPage() {
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
};
|
||||
|
||||
const handleSaveChanges = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await save({
|
||||
full_name: formData.fullName,
|
||||
email: formData.email,
|
||||
professional_bio: formData.bio,
|
||||
...(photoFile ? { profile_picture: photoFile } : {}),
|
||||
});
|
||||
// Reset file state setelah berhasil save
|
||||
if (!errorMsg) {
|
||||
setPhotoFile(null);
|
||||
setImageError(false);
|
||||
}
|
||||
};
|
||||
const handleSaveChanges = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
await customToast.promise(
|
||||
save({
|
||||
full_name: formData.fullName,
|
||||
email: formData.email,
|
||||
professional_bio: formData.bio,
|
||||
}),
|
||||
{
|
||||
loading: "Menyimpan perubahan...",
|
||||
success: () => ({
|
||||
title: "Perubahan berhasil disimpan!",
|
||||
description: successMsg || "",
|
||||
}),
|
||||
error: () => ({
|
||||
title: "Gagal menyimpan perubahan!",
|
||||
description: errorMsg || "",
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
|
||||
|
||||
@@ -223,7 +232,7 @@ export default function MentorSettingsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feedback Messages */}
|
||||
{/* Feedback Messages
|
||||
{successMsg && (
|
||||
<p className="text-xs text-green-600 bg-green-50 p-2 rounded-md font-medium">
|
||||
{successMsg}
|
||||
@@ -233,7 +242,7 @@ export default function MentorSettingsPage() {
|
||||
<p className="text-xs text-red-600 bg-red-50 p-2 rounded-md font-medium">
|
||||
{errorMsg}
|
||||
</p>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{/* Footer Action */}
|
||||
<div className="flex justify-end pt-2">
|
||||
|
||||
@@ -3,18 +3,20 @@ import { Plus } from 'lucide-react';
|
||||
import ManageInternsModal from './components/AddInternsDialog';
|
||||
import type { ProjectDetail, ProjectMember } from '../../../types/project.types';
|
||||
import { useRemoveMember } from '../../../hooks/useProjects';
|
||||
import { customToast } from '../../utils/showToast';
|
||||
|
||||
export default function InternsTab({ project }: { project: ProjectDetail }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [members, setMembers] = useState<ProjectMember[]>(project.members);
|
||||
const { remove, loading } = useRemoveMember();
|
||||
|
||||
const handleRemove = async (id_user: number) => {
|
||||
if (window.confirm('Are you sure you want to remove this member?')) {
|
||||
const res = await remove({ id_project: project.id, id_user });
|
||||
if (res !== null) {
|
||||
alert('Member removed successfully');
|
||||
window.location.reload();
|
||||
}
|
||||
const res = await remove({ id_project: project.id, id_user });
|
||||
if (res !== null) {
|
||||
setMembers(prev => prev.filter(m => m.id !== id_user));
|
||||
customToast.success('Member removed', 'The intern has been successfully removed from this project.');
|
||||
} else {
|
||||
customToast.error('Failed to remove', 'An error occurred while removing the member. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,32 +53,32 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className='bg-gray-300 border border-gray-500 px-2 py-1 rounded-xl hover:bg-gray-400 transition-colors'
|
||||
className='bg-gray-300 border border-gray-500 px-2 py-1 rounded-md hover:bg-gray-400 transition-colors'
|
||||
>
|
||||
<span className='flex items-center gap-3 text-[14px]'>
|
||||
<span className='flex items-center gap-3 text-[11px]'>
|
||||
<Plus className='w-3 h-3'/>Add
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{project.members.map((member: ProjectMember, idx) => (
|
||||
{members.map((member: ProjectMember, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center justify-between py-3 ${idx < project.members.length - 1 ? 'border-b border-gray-300' : ''
|
||||
className={`flex items-center justify-between py-3 ${idx < members.length - 1 ? 'border-b border-gray-300' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className=" text-sm text-[#333]">
|
||||
<span className=" text-sm text-font">
|
||||
{member.email}
|
||||
</span>
|
||||
</div>
|
||||
<span className=" text-[13px] text-[#555] font-semibold">
|
||||
<span className=" text-[13px] text-font font-semibold">
|
||||
{member.position}
|
||||
</span>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => handleRemove(member.id)}
|
||||
disabled={loading}
|
||||
className='flex items-center gap-1 text-[14px] bg-red-600 text-white p-1 rounded-xl disabled:opacity-50 hover:bg-red-700 transition-colors'
|
||||
className='flex items-center gap-1 text-[11px] bg-red-600 text-white py-1 px-2 rounded-md disabled:opacity-50 hover:bg-red-700 transition-colors'
|
||||
>
|
||||
<Plus className='w-4 h-4'/>Delete
|
||||
</button>
|
||||
@@ -88,7 +90,8 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
projectId={project.id}
|
||||
initialMembers={project.members}
|
||||
initialMembers={members}
|
||||
onMembersChange={(updated) => setMembers(updated)}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { useProjectTasks, useCreateTask, useDeleteTask } from '../../../hooks/useTasks';
|
||||
import { useProjectTasks, useDeleteTask } from '../../../hooks/useTasks';
|
||||
import type { ProjectDetail } from '../../../types/project.types';
|
||||
import type { ProjectTask } from '../../../types/task.types';
|
||||
import { ClipboardList, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CreateTaskModal from './components/CreateTaskDialog';
|
||||
import EditTaskModal from './components/EditTaskDialog';
|
||||
import { customToast } from '../../utils/showToast';
|
||||
|
||||
export default function TaskTab({ project }: { project: ProjectDetail }) {
|
||||
const { tasks, loading, error, refetch } = useProjectTasks(String(project.id));
|
||||
const { tasks, loading, error, refetch } = useProjectTasks(project.id);
|
||||
const { remove } = useDeleteTask();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<ProjectTask | null>(null);
|
||||
|
||||
const handleDeleteTask = async (taskId: number) => {
|
||||
const ok = await remove(taskId, String(project.id));
|
||||
if (ok) refetch();
|
||||
const handleDeleteTask = async (taskId: number,taskTitle: string) => {
|
||||
const del = await remove(taskId, String(project.id));
|
||||
if (del) {
|
||||
try {
|
||||
customToast.success("Berhasil", `Task ${taskTitle} berhasil dihapus`)
|
||||
} catch (error) {
|
||||
customToast.error("Gagal", `Task ${taskTitle} gagal dihapus`)
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return (
|
||||
@@ -61,7 +70,7 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
|
||||
<Pencil size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteTask(task.id)}
|
||||
onClick={() => handleDeleteTask(task.id, task.title)}
|
||||
className="hover:text-red-600 transition-colors p-1"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
|
||||
@@ -5,21 +5,23 @@ import { useAssignableInterns } from "../../../../hooks/useProjects";
|
||||
import { useAssignMember } from '../../../../hooks/useProjects';
|
||||
import type { ProjectMember } from '../../../../types/project.types';
|
||||
import type { AssignableIntern } from '../../../../types/project.types';
|
||||
import { customToast } from "../../../utils/showToast";
|
||||
|
||||
interface ManageInternsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: number;
|
||||
initialMembers: ProjectMember[];
|
||||
onMembersChange?: (updated: ProjectMember[]) => void;
|
||||
}
|
||||
|
||||
export default function ManageInternsModal({ isOpen, onClose, projectId, initialMembers }: ManageInternsModalProps) {
|
||||
export default function ManageInternsModal({ isOpen, onClose, projectId, initialMembers, onMembersChange }: ManageInternsModalProps) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [invitedList, setInvitedList] = useState<ProjectMember[]>(initialMembers);
|
||||
|
||||
const { interns, loading: loadingAssignableInterns } = useAssignableInterns();
|
||||
const { assign, loading: assigning, error } = useAssignMember();
|
||||
const { assign, loading: assigning } = useAssignMember();
|
||||
|
||||
// Sync saat modal dibuka
|
||||
useEffect(() => {
|
||||
@@ -55,22 +57,27 @@ export default function ManageInternsModal({ isOpen, onClose, projectId, initial
|
||||
const handleAssign = async (intern: AssignableIntern) => {
|
||||
const result = await assign({ id_project: projectId, id_user: intern.id });
|
||||
if (result) {
|
||||
setInvitedList(prev => [...prev, {
|
||||
const newMember: ProjectMember = {
|
||||
id: intern.id,
|
||||
id_user: intern.id,
|
||||
// full_name: `${intern.nama_depan} ${intern.nama_belakang}`,
|
||||
full_name: intern.full_name,
|
||||
email: intern.email,
|
||||
avatar: "",
|
||||
kelompok_peminatan: intern.kelompok_peminatan,
|
||||
professional_bio: "",
|
||||
position: intern.position,
|
||||
} as ProjectMember]);
|
||||
} as ProjectMember;
|
||||
const updatedList = [...invitedList, newMember];
|
||||
setInvitedList(updatedList);
|
||||
onMembersChange?.(updatedList);
|
||||
customToast.success('Intern added', `${intern.full_name} has been successfully added to the project.`);
|
||||
} else {
|
||||
customToast.error('Failed to add', 'An error occurred while adding the intern. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const getInitials = (m: AssignableIntern) =>
|
||||
m.full_name.toUpperCase();
|
||||
const getInitials = (m: AssignableIntern) =>
|
||||
m.full_name.split(' ').slice(0, 2).map(n => n[0]).join('').toUpperCase();
|
||||
|
||||
return (
|
||||
<dialog ref={dialogRef} className="custom-dialog">
|
||||
@@ -85,11 +92,7 @@ export default function ManageInternsModal({ isOpen, onClose, projectId, initial
|
||||
/>
|
||||
<Search className="absolute right-3.5 w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
{error &&
|
||||
<div className="p-1 flex justify-center bg-red-foreground border border-red text-sm text-red-500 font-light rounded-md">
|
||||
{error}: Sudah berada di project lain
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<div className="overflow-y-auto space-y-3 pr-1 flex-1 h-[170px]">
|
||||
{/* Sudah jadi member */}
|
||||
@@ -97,7 +100,7 @@ export default function ManageInternsModal({ isOpen, onClose, projectId, initial
|
||||
<div key={member.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-gray-200 border border-gray-300 flex items-center justify-center text-xs font-bold text-gray-500">
|
||||
{member.full_name.slice(0, 2).toUpperCase()}
|
||||
{getInitials(member as unknown as AssignableIntern)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-gray-700">{member.full_name}</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { X } from "lucide-react";
|
||||
import { LuUserPlus, LuUser, LuFolderOpen } from "react-icons/lu";
|
||||
import { useAssignMember, useProjects } from "../../../../hooks/useProjects";
|
||||
import { useAssignableInterns } from "../../../../hooks/useProjects";
|
||||
import { customToast } from "../../../utils/showToast";
|
||||
|
||||
interface AddInternsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -46,8 +47,6 @@ export default function AddInternsModal({ isOpen, onClose, userId }: AddInternsM
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Reset form saat modal dibuka kembali.
|
||||
// Kalau userId diberikan, dropdown langsung ter-select ke intern tsb.
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedUserId(userId !== null ? String(userId) : "");
|
||||
@@ -64,7 +63,10 @@ export default function AddInternsModal({ isOpen, onClose, userId }: AddInternsM
|
||||
id_user: Number(selectedUserId),
|
||||
});
|
||||
if (res !== null) {
|
||||
customToast.success('Intern assigned', 'The intern has been successfully assigned to the project.');
|
||||
onClose();
|
||||
} else {
|
||||
customToast.error('Failed to assign', 'An error occurred while assigning the intern. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import type { Project } from "../../../../types/project.types";
|
||||
|
||||
// ─── StatusBadge ─────────────────────────────────────────────────────────────
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const isCompleted = status === "archived";
|
||||
const isCompleted = status === "active";
|
||||
return (
|
||||
<span
|
||||
className={`text-[10px] font-extrabold tracking-wider px-2.5 py-1 rounded-full border ${
|
||||
isCompleted
|
||||
? "bg-green-50 text-green-600 border-green-200"
|
||||
: "bg-yellow-50 text-yellow-600 border-yellow-200"
|
||||
: "bg-red-50 text-red-600 border-red-200"
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { useRef, useEffect, useState, useCallback } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { PROJECT_ICON_MAP, PROJECT_ICON_CODES } from "../../../../lib/ProjectIcons";
|
||||
import { LuSendHorizontal, LuMail } from "react-icons/lu";
|
||||
import { useCreateProject } from "../../../../hooks/useProjects";
|
||||
import type { CreateProjectPayload } from "../../../../types/project.types";
|
||||
import { useAssignableInterns } from "../../../../hooks/useProjects";
|
||||
import { customToast } from "../../../utils/showToast";
|
||||
|
||||
interface CreateProjectModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -21,6 +23,21 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
||||
const [startDate, setStartDate] = useState("");
|
||||
const [endDate, setEndDate] = useState("");
|
||||
const { create, loading, error } = useCreateProject();
|
||||
const { interns: assignableInterns, loading: loadingInterns } = useAssignableInterns();
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const emailSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Tutup dropdown saat klik di luar area input
|
||||
const handleClickOutside = useCallback((e: MouseEvent) => {
|
||||
if (emailSectionRef.current && !emailSectionRef.current.contains(e.target as Node)) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [handleClickOutside]);
|
||||
|
||||
const projectIcons = PROJECT_ICON_CODES.map((id) => ({
|
||||
id,
|
||||
@@ -51,14 +68,20 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const handleAddEmail = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (emailInput.trim() && !invitedEmails.includes(emailInput.trim())) {
|
||||
setInvitedEmails([...invitedEmails, emailInput.trim()]);
|
||||
const handleAddEmail = (emailToAdd?: string) => {
|
||||
const email = (emailToAdd ?? emailInput).trim();
|
||||
if (email && !invitedEmails.includes(email)) {
|
||||
setInvitedEmails([...invitedEmails, email]);
|
||||
setEmailInput("");
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleAddEmailFromForm = (e: React.FormEvent) => {
|
||||
// e.preventDefault();
|
||||
// handleAddEmail();
|
||||
// };
|
||||
|
||||
const handleRemoveEmail = (emailToRemove: string) => {
|
||||
setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove));
|
||||
};
|
||||
@@ -79,8 +102,12 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
||||
|
||||
const result = await create(payload);
|
||||
if (result) {
|
||||
onSuccess?.(); // panggil refetch di parent (kalau ada)
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
customToast.success(
|
||||
"Project berhasil dibuat!",
|
||||
`${projectName} sudah aktif dan siap dikelola.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,30 +194,71 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">
|
||||
Invite via Gmail
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
<span className="absolute left-3.5 text-gray-400">
|
||||
<LuMail className="w-4 h-4 stroke-[2.5]" />
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="intern.name@gmail.com"
|
||||
value={emailInput}
|
||||
onChange={(e) => setEmailInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddEmail(e);
|
||||
}
|
||||
}}
|
||||
className="w-full pl-10 pr-20 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddEmail}
|
||||
className="absolute right-2 px-3 py-1 bg-gray-100 hover:bg-gray-200 text-gray-800 text-[11px] font-bold rounded-md transition-colors tracking-wider uppercase"
|
||||
>
|
||||
ADD
|
||||
</button>
|
||||
{/* Input email + dropdown sugesti intern */}
|
||||
<div ref={emailSectionRef} className="relative">
|
||||
<div className="relative flex items-center">
|
||||
<span className="absolute left-3.5 text-gray-400">
|
||||
<LuMail className="w-4 h-4 stroke-[2.5]" />
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="intern.name@gmail.com"
|
||||
value={emailInput}
|
||||
onChange={(e) => { setEmailInput(e.target.value); setShowDropdown(true); }}
|
||||
onFocus={() => setShowDropdown(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddEmail();
|
||||
}
|
||||
if (e.key === 'Escape') setShowDropdown(false);
|
||||
}}
|
||||
className="w-full pl-10 pr-20 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddEmail()}
|
||||
className="absolute right-2 px-3 py-1 bg-gray-100 hover:bg-gray-200 text-gray-800 text-[11px] font-bold rounded-md transition-colors tracking-wider uppercase"
|
||||
>
|
||||
ADD
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dropdown daftar intern */}
|
||||
{showDropdown && (
|
||||
<div className="absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-xl shadow-lg max-h-44 overflow-y-auto">
|
||||
{loadingInterns ? (
|
||||
<p className="text-xs text-gray-400 text-center py-3">Memuat data intern...</p>
|
||||
) : (() => {
|
||||
const filtered = assignableInterns.filter(
|
||||
(i) =>
|
||||
!invitedEmails.includes(i.email) &&
|
||||
(i.email.toLowerCase().includes(emailInput.toLowerCase()) ||
|
||||
i.full_name.toLowerCase().includes(emailInput.toLowerCase()))
|
||||
);
|
||||
return filtered.length === 0 ? (
|
||||
<p className="text-xs text-gray-400 text-center py-3">Tidak ada intern ditemukan.</p>
|
||||
) : (
|
||||
filtered.map((intern) => (
|
||||
<button
|
||||
key={intern.id}
|
||||
type="button"
|
||||
onMouseDown={(e) => { e.preventDefault(); handleAddEmail(intern.email); }}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-7 h-7 rounded-full bg-red-100 text-red-700 flex items-center justify-center text-[10px] font-bold shrink-0">
|
||||
{intern.full_name.split(' ').slice(0, 2).map((n) => n[0]).join('').toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs font-semibold text-gray-800 truncate">{intern.full_name}</span>
|
||||
<span className="text-[11px] text-gray-400 truncate">{intern.email}</span>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badges List Email */}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRef, useEffect, useState } from "react";
|
||||
import { X, FileText, Link } from "lucide-react";
|
||||
import { useCreateTask } from '../../../../hooks/useTasks';
|
||||
import type { SubmissionType } from '../../../../types/task.types';
|
||||
import { customToast } from '../../../utils/showToast';
|
||||
|
||||
interface CreateTaskModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -39,7 +40,12 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
|
||||
specific_time: specificTime,
|
||||
submission_type: submissionType,
|
||||
});
|
||||
if (result) onSuccess();
|
||||
if (result) {
|
||||
customToast.success('Task created', `"${taskTitle}" has been successfully added to the project.`);
|
||||
onSuccess();
|
||||
} else {
|
||||
customToast.error('Failed to create task', 'An error occurred while saving the task. Please try again.');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<dialog ref={dialogRef} className="custom-dialog p-[1.5rem]">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRef, useEffect, useState } from "react";
|
||||
import { X, FileText, Link } from "lucide-react";
|
||||
import { useUpdateTask } from '../../../../hooks/useTasks';
|
||||
import type { ProjectTask, SubmissionType } from '../../../../types/task.types';
|
||||
import { customToast } from '../../../utils/showToast';
|
||||
|
||||
interface EditTaskModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -48,7 +49,12 @@ export default function EditTaskModal({ isOpen, onClose, task, onSuccess }: Edit
|
||||
specific_time: specificTime,
|
||||
submission_type: submissionType,
|
||||
}, String(task.id_project));
|
||||
if (result) onSuccess();
|
||||
if (result) {
|
||||
customToast.success('Task updated', `"${taskTitle}" has been successfully updated.`);
|
||||
onSuccess();
|
||||
} else {
|
||||
customToast.error('Failed to update task', 'An error occurred while saving changes. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as LucideIcons from 'lucide-react';
|
||||
import type { LucideProps } from 'lucide-react';
|
||||
import type { Project } from '../../../../types/project.types';
|
||||
import { useArchiveProject } from '../../../../hooks/useProjects';
|
||||
import { customToast } from '../../../utils/showToast';
|
||||
|
||||
const getDynamicIcon = (iconName: string) => {
|
||||
const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1);
|
||||
@@ -11,14 +12,16 @@ const getDynamicIcon = (iconName: string) => {
|
||||
return Icon ?? LucideIcons.FolderOpen;
|
||||
};
|
||||
|
||||
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
|
||||
interface MentorProjectCardProps extends Project {
|
||||
onArchived?: () => void;
|
||||
}
|
||||
|
||||
export default function MentorProjectCard(project: Project) {
|
||||
export default function MentorProjectCard({ onArchived, ...project} : MentorProjectCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const { archive, loading } = useArchiveProject();
|
||||
|
||||
const handleCardClick = () => {
|
||||
navigate(`/mentor/projects/${toSlug(project.slug)}`)
|
||||
navigate(`/mentor/projects/${project.slug}`)
|
||||
};
|
||||
const Icon = getDynamicIcon(project.project_icon);
|
||||
|
||||
@@ -31,12 +34,19 @@ export default function MentorProjectCard(project: Project) {
|
||||
const result = await archive(project.id);
|
||||
|
||||
if (result) {
|
||||
window.alert("Project berhasil diarsipkan.");
|
||||
window.location.reload();
|
||||
customToast.success(
|
||||
"Berhasil!",
|
||||
"Projek Berhasil Diarsipkan (archived)"
|
||||
)
|
||||
onArchived?.();
|
||||
} else {
|
||||
window.alert("Gagal mengarsipkan project.");
|
||||
customToast.error(
|
||||
"Gagal!",
|
||||
"Projek gagal diarsipkan (archived)"
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleCardClick}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// utils/toast.tsx
|
||||
import toast, { type Toast } from 'react-hot-toast';
|
||||
import { FiXCircle, FiX } from 'react-icons/fi';
|
||||
import { FiX } from 'react-icons/fi';
|
||||
import { FaCheck } from "react-icons/fa";
|
||||
import { IoAlertOutline } from "react-icons/io5";
|
||||
|
||||
|
||||
+11
-10
@@ -27,7 +27,7 @@ import { useCurrentUser } from "./useUser";
|
||||
import { useMyTasks } from "./useProjects";
|
||||
|
||||
// GET /task-api/projects/{id_project}/tasks
|
||||
export const useProjectTasks = (projectId: string) => {
|
||||
export const useProjectTasks = (projectId: number) => {
|
||||
const [tasks, setTasks] = useState<ProjectTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -180,8 +180,9 @@ export const useSubmission = (
|
||||
const res = await submitTaskFile(taskId, files, projectId);
|
||||
if (res) setSubmission(res);
|
||||
return res;
|
||||
} catch {
|
||||
} catch (err){
|
||||
setError("Gagal submit file.");
|
||||
throw error;
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -195,9 +196,9 @@ export const useSubmission = (
|
||||
const res = await submitTaskLink(taskId, url, projectId);
|
||||
if (res) setSubmission(res);
|
||||
return res;
|
||||
} catch {
|
||||
} catch (err){
|
||||
setError("Gagal submit link.");
|
||||
return null;
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -210,9 +211,9 @@ export const useSubmission = (
|
||||
const res = await updateSubmissionFile(submissionId, files);
|
||||
if (res) setSubmission(res);
|
||||
return res;
|
||||
} catch {
|
||||
} catch (err){
|
||||
setError("Gagal update submission.");
|
||||
return null;
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -225,9 +226,9 @@ export const useSubmission = (
|
||||
const res = await updateSubmissionLink(submissionId, url);
|
||||
if (res) setSubmission(res);
|
||||
return res;
|
||||
} catch {
|
||||
} catch (err){
|
||||
setError("Gagal update submission.");
|
||||
return null;
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -240,9 +241,9 @@ export const useSubmission = (
|
||||
await deleteSubmission(submissionId);
|
||||
setSubmission(null);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
setError("Gagal hapus submission.");
|
||||
return false;
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
export interface Deadline {
|
||||
date: Date;
|
||||
label: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export const DEADLINES: Deadline[] = [
|
||||
{
|
||||
date: new Date(2025, 4, 4), // May 4, 2025
|
||||
label: "Laporan Progress 1 Internify – 4 Juni",
|
||||
time: "23:59 PM",
|
||||
},
|
||||
{
|
||||
date: new Date(2026, 5, 5),
|
||||
label: "Laporan Progress 2 Internify - 5 May",
|
||||
time: "23:59 PM",
|
||||
},
|
||||
];
|
||||
|
||||
export const deadlineDates: Date[] = DEADLINES.map((d) => d.date);
|
||||
|
||||
export interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
deadline: Deadline;
|
||||
isSubmitted: boolean;
|
||||
submittedCount: number;
|
||||
totalInterns: number;
|
||||
}
|
||||
|
||||
export interface Intern {
|
||||
name: string;
|
||||
role: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
tasks: Task[];
|
||||
interns: Intern[];
|
||||
mentor: string;
|
||||
avatarUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
progress: number;
|
||||
tasksDone: number;
|
||||
tasksTotal: number;
|
||||
}
|
||||
|
||||
export const mockProjects: Project[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Internify Project",
|
||||
description:
|
||||
"Internify merupakan website Human Capital (HUMC) yang dirancang sebagai platform pendaftaran dan pengelolaan program magang secara digital. Sistem ini dibuat untuk membantu proses rekrutmen peserta magang mulai dari pendaftaran, seleksi, hingga monitoring aktivitas peserta selama program berlangsung.\n\nFokus utama dari pengembangan Internify adalah pembuatan Learning Management System (LMS) yang digunakan untuk mendukung kegiatan pembelajaran dan pengelolaan peserta magang. LMS pada Internify menyediakan fitur seperti akses materi pembelajaran, pengumpulan tugas, monitoring progress peserta, absensi, sertifikat, hingga komunikasi antara mentor dan peserta magang.",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
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,
|
||||
},
|
||||
],
|
||||
interns: [
|
||||
{ name: "Jonathan Kristina", role: "Backend", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Anisa Rahmawati", role: "Backend", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Budi Santoso", role: "Frontend", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Cindy Permata", role: "Frontend", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Dewa Aditya", role: "UI/UX", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Eka Pratiwi", role: "UI/UX", email: "jonathankristina@gmail.com" },
|
||||
],
|
||||
mentor: "jonathankristina",
|
||||
progress: 12,
|
||||
tasksDone: 1,
|
||||
tasksTotal: 8,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Rekrutmen Digital Platform",
|
||||
description:
|
||||
"Platform digital yang memudahkan proses rekrutmen karyawan baru secara end-to-end. Mulai dari posting lowongan, screening CV otomatis, penjadwalan wawancara, hingga onboarding karyawan baru.\n\nSistem ini mengintegrasikan berbagai modul HR seperti manajemen data kandidat, penilaian kompetensi berbasis AI, dan dashboard analytics untuk tim HR dalam mengambil keputusan rekrutmen yang lebih efektif dan efisien.",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
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,
|
||||
},
|
||||
],
|
||||
interns: [
|
||||
{ name: "Fajar Nugroho", role: "Backend", email: "fajarnugroho@gmail.com" },
|
||||
{ name: "Gita Melinda", role: "Backend", email: "gitamelinda@gmail.com" },
|
||||
{ name: "Hendra Wijaya", role: "Frontend", email: "hendrawijaya@gmail.com" },
|
||||
{ name: "Indira Sari", role: "Frontend", email: "indirasari@gmail.com" },
|
||||
{ name: "Joko Purnomo", role: "UI/UX", email: "jokopurnomo@gmail.com" },
|
||||
{ name: "Kartika Dewi", role: "QA", email: "kartikadewi@gmail.com" },
|
||||
{ name: "Lukman Hakim", role: "QA", email: "lukmanhakim@gmail.com" },
|
||||
],
|
||||
mentor: "seniormHR@company.com",
|
||||
progress: 37,
|
||||
tasksDone: 3,
|
||||
tasksTotal: 8,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "E-Commerce Analytics Dashboard",
|
||||
description:
|
||||
"Dashboard analitik komprehensif untuk platform e-commerce yang menyajikan insight real-time tentang performa penjualan, perilaku pelanggan, dan efektivitas kampanye marketing.\n\nProyek ini mencakup pengembangan pipeline data dari berbagai sumber transaksi, visualisasi interaktif menggunakan chart library modern, serta fitur export laporan dalam berbagai format untuk kebutuhan manajemen bisnis.",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
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" },
|
||||
submittedCount: 0,
|
||||
totalInterns: 6,
|
||||
isSubmitted: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
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" },
|
||||
submittedCount: 0,
|
||||
totalInterns: 6,
|
||||
isSubmitted: false,
|
||||
},
|
||||
],
|
||||
interns: [
|
||||
{ name: "Maya Anggraeni", role: "Backend", email: "mayaanggraeni@gmail.com" },
|
||||
{ name: "Nando Pratama", role: "Backend", email: "nandopratama@gmail.com" },
|
||||
{ name: "Olivia Susanto", role: "Frontend", email: "oliviasusanto@gmail.com" },
|
||||
{ name: "Pandu Kusuma", role: "Frontend", email: "pandukukusuma@gmail.com" },
|
||||
{ name: "Qori Ramadhan", role: "Data Analyst", email: "qoriramadhan@gmail.com" },
|
||||
{ name: "Rini Handayani", role: "UI/UX", email: "rinihandayani@gmail.com" },
|
||||
],
|
||||
mentor: "leadanalyst@company.com",
|
||||
progress: 50,
|
||||
tasksDone: 4,
|
||||
tasksTotal: 8,
|
||||
},
|
||||
];
|
||||
|
||||
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: {
|
||||
id: "INF-2024-88392",
|
||||
issuedDate: "24 Oct 2024",
|
||||
recipientName: "JonathanKristina",
|
||||
programName: "Internify Project",
|
||||
duration: "8 Minggu",
|
||||
finalScore: 94,
|
||||
grade: "A",
|
||||
status: "VERIFIED",
|
||||
},
|
||||
history: [
|
||||
{
|
||||
id: 1,
|
||||
month: "Juni 2023",
|
||||
title: "Web Development Fundamentals",
|
||||
grade: "A+",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
month: "Maret 2023",
|
||||
title: "UI/UX Design Essentials",
|
||||
grade: "A",
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
// Mock Data Interns
|
||||
export const mockInternsData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Alex Rivera",
|
||||
email: "alex.rivera@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "UI/UX",
|
||||
avatar: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=100&q=80" // Contoh avatar placeholder
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Sarah Jenkins",
|
||||
email: "s.jenkins@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "Frontend",
|
||||
avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=100&q=80"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Michael Chen",
|
||||
email: "m.chen@internify.edu",
|
||||
projectName: "MBG Project",
|
||||
role: "Backend",
|
||||
avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=100&q=80"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Elena Rodriguez",
|
||||
email: "e.rod@internify.edu",
|
||||
projectName: "Unassigned",
|
||||
role: "-",
|
||||
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=100&q=80"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Alex Rivera",
|
||||
email: "alex.rivera@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "UI/UX",
|
||||
avatar: "" // Simulasi tanpa gambar avatar (menggunakan placeholder inisial/gradient)
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Sarah Jenkins",
|
||||
email: "s.jenkins@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "Backend",
|
||||
avatar: ""
|
||||
}
|
||||
];
|
||||
@@ -1,18 +0,0 @@
|
||||
export interface Deadline {
|
||||
date: Date;
|
||||
label: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export const DEADLINES: Deadline[] = [
|
||||
{
|
||||
date: new Date(2026, 5, 29),
|
||||
label: "Laporan Progress 1 Internify",
|
||||
time: "23:59 PM",
|
||||
},
|
||||
{
|
||||
date: new Date(2026, 5, 30),
|
||||
label: "Laporan Progress 2 Internify",
|
||||
time: "23:59 PM",
|
||||
},
|
||||
];
|
||||
@@ -1,59 +0,0 @@
|
||||
export interface InternData {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
projectName: string;
|
||||
role: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export const mockInternsData: InternData[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Alex Rivera",
|
||||
email: "alex.rivera@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "UI/UX",
|
||||
avatar: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=100&q=80",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Sarah Jenkins",
|
||||
email: "s.jenkins@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "Frontend",
|
||||
avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=100&q=80",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Michael Chen",
|
||||
email: "m.chen@internify.edu",
|
||||
projectName: "MBG Project",
|
||||
role: "Backend",
|
||||
avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=100&q=80",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Elena Rodriguez",
|
||||
email: "e.rod@internify.edu",
|
||||
projectName: "Unassigned",
|
||||
role: "-",
|
||||
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=100&q=80",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Alex Rivera",
|
||||
email: "alex.rivera@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "UI/UX",
|
||||
avatar: "",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Sarah Jenkins",
|
||||
email: "s.jenkins@internify.edu",
|
||||
projectName: "Internify Project",
|
||||
role: "Backend",
|
||||
avatar: "",
|
||||
},
|
||||
];
|
||||
@@ -1,228 +0,0 @@
|
||||
export interface Submission {
|
||||
internEmail: string;
|
||||
submittedAt: string;
|
||||
status: "Done" | "Pending" | "Overdue";
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "file" | "link";
|
||||
deadline: { date: Date; label: string; time: string };
|
||||
isSubmitted: boolean;
|
||||
submittedCount: number;
|
||||
totalInterns: number;
|
||||
submissions: Submission[];
|
||||
}
|
||||
|
||||
export interface Intern {
|
||||
name: string;
|
||||
role: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
tasks: Task[];
|
||||
interns: Intern[];
|
||||
mentor: string;
|
||||
progress: number;
|
||||
tasksDone: number;
|
||||
tasksTotal: number;
|
||||
}
|
||||
|
||||
// ─── Project 1 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const project1Interns: Intern[] = [
|
||||
{ name: "Jonathan Kristina", role: "Backend", email: "jonathankristina@gmail.com" },
|
||||
{ name: "Anisa Rahmawati", role: "Backend", email: "anisarahmawati@gmail.com" },
|
||||
{ name: "Budi Santoso", role: "Frontend", email: "budisantoso@gmail.com" },
|
||||
{ name: "Cindy Permata", role: "Frontend", email: "cindypermata@gmail.com" },
|
||||
{ name: "Dewa Aditya", role: "UI/UX", email: "dewaaditya@gmail.com" },
|
||||
{ name: "Eka Pratiwi", role: "UI/UX", email: "ekapratiwi@gmail.com" },
|
||||
];
|
||||
|
||||
// ─── Project 2 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const project2Interns: Intern[] = [
|
||||
{ name: "Fajar Nugroho", role: "Backend", email: "fajarnugroho@gmail.com" },
|
||||
{ name: "Gita Lestari", role: "Backend", email: "gitalestari@gmail.com" },
|
||||
{ name: "Hendra Wijaya", role: "Frontend", email: "hendrawijaya@gmail.com" },
|
||||
{ name: "Indah Permata", role: "Frontend", email: "indahpermata@gmail.com" },
|
||||
{ name: "Joko Susilo", role: "UI/UX", email: "jokosusilo@gmail.com" },
|
||||
{ name: "Kartika Dewi", role: "UI/UX", email: "kartikadewi@gmail.com" },
|
||||
];
|
||||
|
||||
// ─── Project 3 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const project3Interns: Intern[] = [
|
||||
{ name: "Maya Anggraeni", role: "Backend", email: "mayaanggraeni@gmail.com" },
|
||||
{ name: "Nanda Pratama", role: "Backend", email: "nandapratama@gmail.com" },
|
||||
{ name: "Oscar Firmansyah",role: "Frontend", email: "oscarfirmansyah@gmail.com"},
|
||||
{ name: "Putri Handayani", role: "Frontend", email: "putrihandayani@gmail.com" },
|
||||
{ name: "Rizky Maulana", role: "UI/UX", email: "rizkymaulana@gmail.com" },
|
||||
{ name: "Sari Utami", role: "UI/UX", email: "sariutami@gmail.com" },
|
||||
];
|
||||
|
||||
// ─── mock data ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const mockProjects: Project[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Internify Project",
|
||||
description:
|
||||
"Internify merupakan website Human Capital (HUMC) yang dirancang sebagai platform pendaftaran dan pengelolaan program magang secara digital. Sistem ini dibuat untuk membantu proses rekrutmen peserta magang mulai dari pendaftaran, seleksi, hingga monitoring aktivitas peserta selama program berlangsung.\n\nFokus utama dari pengembangan Internify adalah pembuatan Learning Management System (LMS) yang digunakan untuk mendukung kegiatan pembelajaran dan pengelolaan peserta magang. LMS pada Internify menyediakan fitur seperti akses materi pembelajaran, pengumpulan tugas, monitoring progress peserta, absensi, sertifikat, hingga komunikasi antara mentor dan peserta magang.",
|
||||
interns: project1Interns,
|
||||
mentor: "Mas A",
|
||||
progress: 12,
|
||||
tasksDone: 1,
|
||||
tasksTotal: 8,
|
||||
tasks: [
|
||||
{
|
||||
id: 2,
|
||||
title: "Laporan tugas 1",
|
||||
description:
|
||||
"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.",
|
||||
type: "file",
|
||||
deadline: { date: new Date(2026, 4, 14), label: "May 14, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 2,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "jonathankristina@gmail.com", submittedAt: "May 12, 2026", status: "Done" },
|
||||
{ internEmail: "anisarahmawati@gmail.com", submittedAt: "May 13, 2026", status: "Done" },
|
||||
{ internEmail: "budisantoso@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "cindypermata@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "dewaaditya@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "ekapratiwi@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Laporan tugas 2",
|
||||
description:
|
||||
"Berisi design UI/UX dan user flow dari aplikasi Internify. Pastikan semua link Figma dapat diakses oleh mentor dan mencakup setidaknya 15 screen utama beserta prototype interaktifnya.",
|
||||
type: "link",
|
||||
deadline: { date: new Date(2026, 4, 28), label: "May 28, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 3,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "jonathankristina@gmail.com", submittedAt: "May 25, 2026", status: "Done" },
|
||||
{ internEmail: "anisarahmawati@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "budisantoso@gmail.com", submittedAt: "May 26, 2026", status: "Done" },
|
||||
{ internEmail: "cindypermata@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "dewaaditya@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
{ internEmail: "ekapratiwi@gmail.com", submittedAt: "May 28, 2026", status: "Done" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 2,
|
||||
name: "Rekrutmen Digital Platform",
|
||||
description:
|
||||
"Platform digital yang memudahkan proses rekrutmen karyawan baru secara end-to-end, mulai dari posting lowongan, seleksi berkas, hingga penjadwalan wawancara.",
|
||||
interns: project2Interns,
|
||||
mentor: "mas B",
|
||||
progress: 37,
|
||||
tasksDone: 3,
|
||||
tasksTotal: 8,
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Analisis Kebutuhan Sistem",
|
||||
description:
|
||||
"Dokumentasikan kebutuhan fungsional dan non-fungsional platform rekrutmen. Sertakan diagram use case dan daftar fitur prioritas.",
|
||||
type: "file",
|
||||
deadline: { date: new Date(2026, 4, 5), label: "May 5, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 4,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "fajarnugroho@gmail.com", submittedAt: "May 4, 2026", status: "Done" },
|
||||
{ internEmail: "gitalestari@gmail.com", submittedAt: "May 4, 2026", status: "Done" },
|
||||
{ internEmail: "hendrawijaya@gmail.com", submittedAt: "May 5, 2026", status: "Done" },
|
||||
{ internEmail: "indahpermata@gmail.com", submittedAt: "May 5, 2026", status: "Done" },
|
||||
{ internEmail: "jokosusilo@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
{ internEmail: "kartikadewi@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Desain Wireframe Aplikasi",
|
||||
description:
|
||||
"Buat wireframe low-fidelity untuk seluruh alur utama rekrutmen menggunakan Figma. Sertakan link yang dapat diakses oleh mentor.",
|
||||
type: "link",
|
||||
deadline: { date: new Date(2026, 4, 15), label: "May 15, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 3,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "fajarnugroho@gmail.com", submittedAt: "May 13, 2026", status: "Done" },
|
||||
{ internEmail: "gitalestari@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "hendrawijaya@gmail.com", submittedAt: "May 14, 2026", status: "Done" },
|
||||
{ internEmail: "indahpermata@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "jokosusilo@gmail.com", submittedAt: "May 15, 2026", status: "Done" },
|
||||
{ internEmail: "kartikadewi@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 3,
|
||||
name: "E-Commerce Analytics Dashboard",
|
||||
description:
|
||||
"Dashboard analitik komprehensif untuk platform e-commerce, mencakup visualisasi data penjualan, perilaku pengguna, dan performa produk secara real-time.",
|
||||
interns: project3Interns,
|
||||
mentor: "Mas C",
|
||||
progress: 50,
|
||||
tasksDone: 4,
|
||||
tasksTotal: 8,
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup Pipeline Data",
|
||||
description:
|
||||
"Konfigurasi ETL pipeline dari database transaksi ke data warehouse. Dokumentasikan arsitektur pipeline dan sertakan screenshot hasil pengujian.",
|
||||
type: "link",
|
||||
deadline: { date: new Date(2026, 4, 3), label: "May 3, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 5,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "mayaanggraeni@gmail.com", submittedAt: "May 2, 2026", status: "Done" },
|
||||
{ internEmail: "nandapratama@gmail.com", submittedAt: "May 2, 2026", status: "Done" },
|
||||
{ internEmail: "oscarfirmansyah@gmail.com", submittedAt: "May 3, 2026", status: "Done" },
|
||||
{ internEmail: "putrihandayani@gmail.com", submittedAt: "May 3, 2026", status: "Done" },
|
||||
{ internEmail: "rizkymaulana@gmail.com", submittedAt: "May 3, 2026", status: "Done" },
|
||||
{ internEmail: "sariutami@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implementasi Dashboard UI",
|
||||
description:
|
||||
"Implementasikan tampilan dashboard menggunakan React dan library chart pilihan. Pastikan responsive di desktop dan tablet.",
|
||||
type: "file",
|
||||
deadline: { date: new Date(2026, 4, 20), label: "May 20, 2026", time: "23:59 PM" },
|
||||
isSubmitted: false,
|
||||
submittedCount: 2,
|
||||
totalInterns: 6,
|
||||
submissions: [
|
||||
{ internEmail: "mayaanggraeni@gmail.com", submittedAt: "May 18, 2026", status: "Done" },
|
||||
{ internEmail: "nandapratama@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "oscarfirmansyah@gmail.com", submittedAt: "May 19, 2026", status: "Done" },
|
||||
{ internEmail: "putrihandayani@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "rizkymaulana@gmail.com", submittedAt: "-", status: "Pending" },
|
||||
{ internEmail: "sariutami@gmail.com", submittedAt: "-", status: "Overdue" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,74 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export const certificateData: CertificateData = {
|
||||
isReceived: false,
|
||||
projectProgress: 90,
|
||||
remainingTasks: 0,
|
||||
certificate: {
|
||||
id: "INF-2024-88392",
|
||||
issuedDate: "24 Oct 2024",
|
||||
recipientName: "JonathanKristina",
|
||||
programName: "Internify Project",
|
||||
duration: "8 Minggu",
|
||||
finalScore: 94,
|
||||
grade: "A",
|
||||
status: "VERIFIED",
|
||||
},
|
||||
};
|
||||
|
||||
export const history: CertificateHistory[] = [
|
||||
{
|
||||
id: 1,
|
||||
month: "Juni 2023",
|
||||
title: "Web Development Fundamentals",
|
||||
grade: "A+",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
month: "Maret 2023",
|
||||
title: "UI/UX Design Essentials",
|
||||
grade: "A",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
month: "Juni 2023",
|
||||
title: "Web Development Advanced",
|
||||
grade: "A+",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
month: "Maret 2023",
|
||||
title: "UI/UX Design Advanced",
|
||||
grade: "A",
|
||||
}
|
||||
]
|
||||
|
||||
export const initialHistoryData = [
|
||||
{ id: 1, name: "Jonathan Kristina", email: "JonathanKristina@gmail.com", project: "Internify Project", date: "24 Oct 2024", initials: "JK" },
|
||||
{ id: 2, name: "Ahmad Faisal", email: "ahmad.faisal@gmail.com", project: "Web Dev Fundamentals", date: "15 Sep 2024", initials: "AF" },
|
||||
{ id: 3, name: "Siti Rahma", email: "siti.rahma@gmail.com", project: "UI/UX Essentials", date: "10 Aug 2024", initials: "SR" },
|
||||
{ id: 4, name: "David Beckham", email: "david.b@gmail.com", project: "Engineering Intern 101", date: "05 Aug 2024", initials: "DB" },
|
||||
];
|
||||
Reference in New Issue
Block a user