-
-
- {initialLetter}
-
-
-
-
Profile Photo
-
{formData.fullName}
+ {/* Profile Photo Row */}
+
+ {/* Avatar dengan badge edit */}
+
+
+ {photoPreview && !imageError ? (
+

setImageError(true)}
+ />
+ ) : (
+
{initialLetter}
+ )}
+
+ {/* Badge edit icon */}
+ {!isIntern &&
+
+ }
+
+ {/* Info & Action Buttons */}
+ {!isIntern &&
+
+
Profile Photo
+
+ JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K.
+
+ {photoFile && (
+
+ 📎 {photoFile.name}
+
+ )}
+
+
+
+
+
+ }
+
+ {/* Hidden file input */}
+
-
+
- {/* Grid Inputs (Full Name & Email Address) */}
+ {/* Grid Inputs */}
-
- {/* Field: Full Name */}
-
- {/* Field: Email Address */}
- {/* Field: Professional Bio */}
+ {/* Professional Bio */}
- {/* Form Action Footer Row */}
+ {/* Feedback Messages
+ {successMsg && (
+
+ {successMsg}
+
+ )}
+ {errorMsg && (
+
+ {errorMsg}
+
+ )} */}
+
+ {/* Footer Action */}
diff --git a/src/Pages/internify/intern/components/TaskForm-file.tsx b/src/Pages/internify/intern/components/TaskForm-file.tsx
index 19b44c1..4588d6f 100644
--- a/src/Pages/internify/intern/components/TaskForm-file.tsx
+++ b/src/Pages/internify/intern/components/TaskForm-file.tsx
@@ -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 (
diff --git a/src/Pages/internify/intern/components/TaskForm-link.tsx b/src/Pages/internify/intern/components/TaskForm-link.tsx
index ae12080..2967480 100644
--- a/src/Pages/internify/intern/components/TaskForm-link.tsx
+++ b/src/Pages/internify/intern/components/TaskForm-link.tsx
@@ -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 (
diff --git a/src/Pages/internify/mentor/BaseProjectsPage.tsx b/src/Pages/internify/mentor/BaseProjectsPage.tsx
index 8304de2..1dcbfd8 100644
--- a/src/Pages/internify/mentor/BaseProjectsPage.tsx
+++ b/src/Pages/internify/mentor/BaseProjectsPage.tsx
@@ -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() {
Create, manage, and monitor ongoing projects and assignments.
- {/* Tombol pemicu buka modal */}
- {/* Feedback Messages */}
+ {/* Feedback Messages
{successMsg && (
{successMsg}
@@ -233,7 +242,7 @@ export default function MentorSettingsPage() {
{errorMsg}
- )}
+ )} */}
{/* Footer Action */}
diff --git a/src/Pages/internify/mentor/ProjectsInternTab.tsx b/src/Pages/internify/mentor/ProjectsInternTab.tsx
index 006cd8e..a684847 100644
--- a/src/Pages/internify/mentor/ProjectsInternTab.tsx
+++ b/src/Pages/internify/mentor/ProjectsInternTab.tsx
@@ -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
(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 }) {
- {project.members.map((member: ProjectMember, idx) => (
+ {members.map((member: ProjectMember, idx) => (
-
+
{member.email}
-
+
{member.position}
@@ -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)}
/>}
);
diff --git a/src/Pages/internify/mentor/TaskTab.tsx b/src/Pages/internify/mentor/TaskTab.tsx
index 2c6d73b..d882675 100644
--- a/src/Pages/internify/mentor/TaskTab.tsx
+++ b/src/Pages/internify/mentor/TaskTab.tsx
@@ -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(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 }) {
- {error &&
-
- {error}: Sudah berada di project lain
-
- }
+
{/* Sudah jadi member */}
@@ -97,7 +100,7 @@ export default function ManageInternsModal({ isOpen, onClose, projectId, initial
- {member.full_name.slice(0, 2).toUpperCase()}
+ {getInitials(member as unknown as AssignableIntern)}
{member.full_name}
diff --git a/src/Pages/internify/mentor/components/AddInternsToProjects.tsx b/src/Pages/internify/mentor/components/AddInternsToProjects.tsx
index 6a671bf..14bda72 100644
--- a/src/Pages/internify/mentor/components/AddInternsToProjects.tsx
+++ b/src/Pages/internify/mentor/components/AddInternsToProjects.tsx
@@ -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.');
}
};
diff --git a/src/Pages/internify/mentor/components/CertificateProjectCard.tsx b/src/Pages/internify/mentor/components/CertificateProjectCard.tsx
index 3f216bf..cbc5870 100644
--- a/src/Pages/internify/mentor/components/CertificateProjectCard.tsx
+++ b/src/Pages/internify/mentor/components/CertificateProjectCard.tsx
@@ -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 (
{status}
diff --git a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx
index 7492d4a..b053330 100644
--- a/src/Pages/internify/mentor/components/CreateProjectDialog.tsx
+++ b/src/Pages/internify/mentor/components/CreateProjectDialog.tsx
@@ -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(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
-
-
-
-
-
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"
- />
-
+ {/* Input email + dropdown sugesti intern */}
+
+
+
+
+
+ { 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"
+ />
+
+
+
+ {/* Dropdown daftar intern */}
+ {showDropdown && (
+
+ {loadingInterns ? (
+
Memuat data intern...
+ ) : (() => {
+ 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 ? (
+
Tidak ada intern ditemukan.
+ ) : (
+ filtered.map((intern) => (
+
+ ))
+ );
+ })()}
+
+ )}
{/* Badges List Email */}
diff --git a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
index d3da25b..8f7bffd 100644
--- a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
+++ b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
@@ -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 (