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:
Kemas M. Aryadary Rasyad
2026-07-06 20:38:30 +07:00
parent 8f5fb82f59
commit 7a331b8e7d
27 changed files with 500 additions and 913 deletions
+1 -1
View File
@@ -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
View File
@@ -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)}
+27 -17
View File
@@ -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' },
+4 -4
View File
@@ -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>
+135 -38
View File
@@ -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"
+8 -2
View File
@@ -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>
);
+15 -6
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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);
}
-330
View File
@@ -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: ""
}
];
-18
View File
@@ -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",
},
];
-59
View File
@@ -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: "",
},
];
-228
View File
@@ -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" },
],
},
],
},
];
-74
View File
@@ -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" },
];
View File