- {/* Kolom Nama & Profil */}
-
- {internProfile.name}
+ {sub.profile_picture
+ ?
+ :
+ }
+ {sub.full_name}
|
{/* Kolom Tanggal Kumpul */}
- {sub.submittedAt || "-"}
+ {sub.submitted_at
+ ? new Date(sub.submitted_at).toLocaleDateString("id-ID")
+ : "-"}
|
{/* Kolom Badge Status */}
- {sub.status === "Done" && (
+ {sub.display_status === "done" && (
Done
)}
- {sub.status === "Pending" && (
+ {sub.display_status === "pending" && (
Pending
)}
- {sub.status === "Overdue" && (
+ {sub.display_status === "overdue" && (
Overdue
@@ -130,8 +133,8 @@ export default function MentorProjectsDetailPage() {
className="text-sm font-bold text-red-800 hover:text-red-900 transition-colors inline-flex items-center gap-1"
onClick={() =>
navigate(
- `/mentor/projects/${toSlug(targetedProject.name)}/${toSlug(targetedTask.title)}/${sub.internEmail.split("@")[0]}`,
- { state: { status: sub.status === "Done" ? "submitted" : sub.status === "Overdue" ? "overdue" : "pending" } }
+ `/mentor/projects/${slug}/${taskSlug}/${toSlug(sub.full_name)}`,
+ { state: { status: sub.display_status === "done" ? "submitted" : sub.display_status === "overdue" ? "overdue" : "pending" } }
)
}
>
@@ -145,8 +148,6 @@ export default function MentorProjectsDetailPage() {
-
- {/* EditSubmissionModal sekarang berada di dalam induk div utama */}
{openModal && (
(null);
const [photoPreview, setPhotoPreview] = useState(null);
const fileInputRef = useRef(null);
+ // Sync form data ketika user sudah di-fetch
+ useEffect(() => {
+ if (user) {
+ setFormData({
+ fullName: user.full_name || "",
+ email: user.email || "",
+ bio: user.professional_bio || "",
+ });
+ if (user.profile_picture) {
+ setPhotoPreview(user.profile_picture);
+ }
+ }
+ }, [user]);
+
const handleChange = (e: React.ChangeEvent) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
- const handleSaveChanges = (e: React.FormEvent) => {
- e.preventDefault();
- alert("Changes saved successfully!");
- console.log("Saved Data:", formData);
- };
-
const handlePhotoUpload = (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (file) {
+ setPhotoFile(file);
const reader = new FileReader();
reader.onloadend = () => setPhotoPreview(reader.result as string);
reader.readAsDataURL(file);
@@ -32,12 +46,35 @@ export default function MentorSettingsPage() {
};
const handleRemovePhoto = () => {
+ setPhotoFile(null);
setPhotoPreview(null);
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);
+ }
+ };
+
const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U";
+ if (userLoading) {
+ return (
+
+ Loading profile...
+
+ );
+ }
+
return (
{/* Header Title */}
@@ -47,6 +84,7 @@ export default function MentorSettingsPage() {
Manage and edit your profile and account information.
+
{/* White Card Container */}
@@ -91,6 +129,11 @@ export default function MentorSettingsPage() {
JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K.
+ {photoFile && (
+
+ 📎 {photoFile.name}
+
+ )}
+ {/* Feedback Messages */}
+ {successMsg && (
+
+ {successMsg}
+
+ )}
+ {errorMsg && (
+
+ {errorMsg}
+
+ )}
+
{/* Footer Action */}
diff --git a/src/Pages/internify/mentor/ProjectForumTab.tsx b/src/Pages/internify/mentor/ProjectForumTab.tsx
index ba73be3..90ac57b 100644
--- a/src/Pages/internify/mentor/ProjectForumTab.tsx
+++ b/src/Pages/internify/mentor/ProjectForumTab.tsx
@@ -1,27 +1,31 @@
-import type { Project } from '../../../lib/mockProjects';
+import { useParams } from 'react-router-dom';
import ProjectTimeline from '../intern/components/ProjectTimeline';
import HeroProject from '../intern/components/Hero-project';
import MentorTaskCard from './components/MentorTaskCard';
+import { useProjectTasks } from '../../../hooks/useTasks';
+import type { Project } from '../../../types/project.types';
export default function MentorForumTab({ project }: { project: Project }) {
+ const { slug } = useParams<{ slug: string }>();
+ const { tasks, loading, error } = useProjectTasks(slug ?? "");
return (
{/* Hero banner */}
-
+
{/* Timeline + Task list */}
-
-
-
+
+
-
-
- {project.tasks.map((task, idx) => (
-
- ))}
+
+ {tasks.length === 0 ? (
+ Belum ada task.
+ ) : (
+ tasks.map((task) => (
+
+ ))
+ )}
diff --git a/src/Pages/internify/mentor/ProjectsInternTab.tsx b/src/Pages/internify/mentor/ProjectsInternTab.tsx
index 6d1a33c..006cd8e 100644
--- a/src/Pages/internify/mentor/ProjectsInternTab.tsx
+++ b/src/Pages/internify/mentor/ProjectsInternTab.tsx
@@ -1,10 +1,22 @@
import { useState } from 'react';
-import type { Project, Intern } from '../../../lib/mockProjects';
import { Plus } from 'lucide-react';
import ManageInternsModal from './components/AddInternsDialog';
+import type { ProjectDetail, ProjectMember } from '../../../types/project.types';
+import { useRemoveMember } from '../../../hooks/useProjects';
-export default function InternsTab({ project }: { project: Project }) {
+export default function InternsTab({ project }: { project: ProjectDetail }) {
const [isModalOpen, setIsModalOpen] = useState(false);
+ 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();
+ }
+ }
+ };
return (
@@ -15,8 +27,17 @@ export default function InternsTab({ project }: { project: Project }) {
+ {project.admin.profile_picture ? (
+ 
+ ) : (
+
+ )}
- {project.mentor}
+ {project.admin.full_name}
@@ -37,22 +58,26 @@ export default function InternsTab({ project }: { project: Project }) {
- {project.interns.map((intern: Intern, idx: number) => (
+ {project.members.map((member: ProjectMember, idx) => (
- {intern.name}
+ {member.email}
- {intern.role}
+ {member.position}
-
@@ -61,7 +86,9 @@ export default function InternsTab({ project }: { project: Project }) {
{isModalOpen && setIsModalOpen(false)}
+ onClose={() => setIsModalOpen(false)}
+ projectId={project.id}
+ initialMembers={project.members}
/>}
);
diff --git a/src/Pages/internify/mentor/ProjectsPage.tsx b/src/Pages/internify/mentor/ProjectsPage.tsx
index aeef2f9..442fe44 100644
--- a/src/Pages/internify/mentor/ProjectsPage.tsx
+++ b/src/Pages/internify/mentor/ProjectsPage.tsx
@@ -1,17 +1,18 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
-import { mockProjects } from '../../../lib/mockProjects';
+import { useProjectDetail } from '../../../hooks/useProjects';
import MentorForumTab from './ProjectForumTab';
import InternsTab from './ProjectsInternTab';
import TaskTab from './TaskTab';
-const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
-
export default function ProjectDetailPage() {
const { slug } = useParams<{ slug: string }>();
const [activeTab, setActiveTab] = useState<'forum' | 'participants' | 'Task'>('forum');
+ const { project, loading, error } = useProjectDetail(slug ?? "");
- const project = mockProjects.find((p) => toSlug(p.name) === slug);
+ if (loading) return Memuat project... ;
+ if (error) return {error} ;
+ if (!project) return Project tidak ditemukan.
if (!project) {
return (
diff --git a/src/Pages/internify/mentor/TaskTab.tsx b/src/Pages/internify/mentor/TaskTab.tsx
index a17c6a0..2c6d73b 100644
--- a/src/Pages/internify/mentor/TaskTab.tsx
+++ b/src/Pages/internify/mentor/TaskTab.tsx
@@ -1,28 +1,36 @@
-import type { Project, Task } from '../../../lib/mockData';
+import { useProjectTasks, useCreateTask, 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';
-export default function TaskTab({ project }: { project: Project }) {
-
+export default function TaskTab({ project }: { project: ProjectDetail }) {
+ const { tasks, loading, error, refetch } = useProjectTasks(String(project.id));
+ const { remove } = useDeleteTask();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
+ const [selectedTask, setSelectedTask] = useState (null);
- const handleEditTask = (taskTitle: string) => {
- console.log('Edit task:', taskTitle);
+ const handleDeleteTask = async (taskId: number) => {
+ const ok = await remove(taskId, String(project.id));
+ if (ok) refetch();
};
- const handleDeleteTask = (taskTitle: string) => {
- console.log('Delete task:', taskTitle);
- };
+ if (loading) return (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ );
+ if (error) return {error} ;
return (
- {/* Header Section */}
Tasks
-
setIsModalOpen(true)}
className="bg-[#C0392B] hover:bg-[#A93226] text-white px-6 py-2.5 rounded-full font-bold text-sm transition-colors shadow-sm"
@@ -30,39 +38,31 @@ export default function TaskTab({ project }: { project: Project }) {
Add Task
-
- {/* Task List Section */}
- {project.tasks && project.tasks.length > 0 ? (
- project.tasks.map((task: Task, idx: number) => (
+ {tasks.length > 0 ? (
+ tasks.map((task, idx) => (
- {/* Task Title & Icon */}
-
- {task.title}
-
+ {task.title}
-
- {/* Action Buttons (Edit & Delete) */}
setEditModalOpen(true)}
+ onClick={() => { setSelectedTask(task); setEditModalOpen(true); }}
className="hover:text-blue-600 transition-colors p-1"
- title="Edit Task"
>
handleDeleteTask(task.title)}
+ onClick={() => handleDeleteTask(task.id)}
className="hover:text-red-600 transition-colors p-1"
- title="Delete Task"
>
@@ -75,18 +75,23 @@ export default function TaskTab({ project }: { project: Project }) {
)}
- {isModalOpen &&
+
+ {isModalOpen && (
setIsModalOpen(false)}
+ projectId={String(project.id)}
+ onSuccess={() => { setIsModalOpen(false); refetch(); }}
/>
- }
- {editModalOpen &&
+ )}
+ {editModalOpen && selectedTask && (
setEditModalOpen(false)}
+ onClose={() => { setEditModalOpen(false); setSelectedTask(null); }}
+ task={selectedTask}
+ onSuccess={() => { setEditModalOpen(false); setSelectedTask(null); refetch(); }}
/>
- }
+ )}
);
}
\ No newline at end of file
diff --git a/src/Pages/internify/mentor/ViewInternSubmission.tsx b/src/Pages/internify/mentor/ViewInternSubmission.tsx
index 587dd1c..a68b5d3 100644
--- a/src/Pages/internify/mentor/ViewInternSubmission.tsx
+++ b/src/Pages/internify/mentor/ViewInternSubmission.tsx
@@ -1,144 +1,90 @@
import { useParams, useLocation } from "react-router-dom";
-import { FileText, Download, Clock, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react";
-import { mockProjects } from "../../../lib/mockProjects";
-
-const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, "-");
-
-type SubmissionStatus = "submitted" | "pending" | "overdue";
-
-// ─── status badge ───────────────────────────────────────────────────────────
-
-function StatusBadge({ status }: { status: SubmissionStatus }) {
- if (status === "submitted")
- return (
-
-
- Submitted for grading
-
- );
- if (status === "overdue")
- return (
-
-
- Overdue
-
- );
- return (
-
-
- Not submitted
-
- );
-}
-
-// ─── status table ────────────────────────────────────────────────────────────
-
-function StatusTable({ status }: { status: SubmissionStatus }) {
- const rows: { label: string; value: React.ReactNode }[] = [
- {
- label: "Submission status",
- value: ,
- },
- {
- label: "Grading status",
- value: -,
- },
- {
- label: "Time Remaining",
- value:
- status === "overdue" ? (
- Overdue
- ) : (
- -
- ),
- },
- {
- label: "File submission",
- value:
- status === "submitted" ? (
-
-
- -
-
- ) : (
- -
- ),
- },
- ];
-
- return (
-
-
- {rows.map((row) => (
-
-
- {row.label}
-
- {row.value}
-
- ))}
-
-
- );
-}
-
-// ─── main page ───────────────────────────────────────────────────────────────
+import { Download, Clock, AlertCircle } from "lucide-react";
+import { useTaskSubmissions } from "../../../hooks/useTasks";
+import type { DisplayStatus } from "../../../types/task.types";
+import { StatusTable } from "./components/StatusSubmissionTable";
export default function ViewInternSubmission() {
- const { slug, taskSlug, internName } = useParams<{ slug: string; taskSlug: string; internName: string }>();
- const location = useLocation();
- const status: SubmissionStatus = location.state?.status ?? "pending";
+ const { slug, taskSlug, nameIntern } = useParams<{
+ slug: string;
+ taskSlug: string;
+ nameIntern: string;
+ }>();
+ const location = useLocation();
+ const status: DisplayStatus = location.state?.status ?? "pending";
- const project = mockProjects.find((p) => toSlug(p.name) === slug);
- const task = project?.tasks.find((t) => toSlug(t.title) === taskSlug);
-
- if (!task) {
- return (
-
- );
- }
+ const { task, loading, error } = useTaskSubmissions(taskSlug!, slug);
+ if (loading)
return (
-
- {/* Header */}
-
-
-
-
- {task.title}
-
-
-
- Due {task.deadline.label} at {task.deadline.time}
-
-
-
-
-
- {/* Task description */}
-
- Task Description
-
- {task.description}
-
-
-
- {/* Status table */}
-
-
- Submission Detail
- {status === "submitted" && (
-
-
- Download
-
- )}
-
-
-
-
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
);
+
+ if (error) return {error} ;
+ if (!task) return null;
+
+ const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
+
+ const internSubmission = task.submissions.find(
+ (s) => toSlug(s.full_name) === nameIntern
+ );
+
+ if (!internSubmission)
+ return (
+
+
+ Submission not found.
+
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+ {task.title}
+
+
+
+ {internSubmission.full_name} — {internSubmission.email}
+
+
+
+
+ {/* Task Description */}
+
+ Task Description
+
+ {task.description}
+
+
+
+ {/* Submission Detail */}
+
+
+ Submission Detail
+ {internSubmission.submission?.file_path && (
+
+
+ Download
+
+ )}
+
+
+
+
+ );
}
\ No newline at end of file
diff --git a/src/Pages/internify/mentor/components/AddInternsDialog.tsx b/src/Pages/internify/mentor/components/AddInternsDialog.tsx
index e7541f9..0b48c78 100644
--- a/src/Pages/internify/mentor/components/AddInternsDialog.tsx
+++ b/src/Pages/internify/mentor/components/AddInternsDialog.tsx
@@ -1,28 +1,34 @@
+// ManageInternsModal.tsx
import { useRef, useEffect, useState } from "react";
-import { X, Search, Plus, Trash2 } from "lucide-react";
+import { Search, Plus, Trash2 } from "lucide-react";
+import { useMahasiswa } from "../../../../hooks/useInterns";
+import { useAssignMember } from '../../../../hooks/useProjects';
+import type { ProjectMember } from '../../../../types/project.types';
+import type { Mahasiswa } from '../../../../types/project.types';
interface ManageInternsModalProps {
isOpen: boolean;
onClose: () => void;
+ projectId: number;
+ initialMembers: ProjectMember[];
}
-export default function ManageInternsModal({ isOpen, onClose }: ManageInternsModalProps) {
+export default function ManageInternsModal({ isOpen, onClose, projectId, initialMembers }: ManageInternsModalProps) {
const dialogRef = useRef (null);
- const [searchQuery, setSearchQuery] = useState("Jonathankristin@gmail.com");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [invitedList, setInvitedList] = useState(initialMembers);
- const [invitedList, setInvitedList] = useState([
- { id: 1, email: "Jonathankristina@gmail.com", initials: "JK" }
- ]);
+ const { mahasiswa, loading: loadingMahasiswa } = useMahasiswa();
+ const { assign, loading: assigning } = useAssignMember();
- // Data Rekomendasi/Hasil Pencarian (Simulasi list bawah yang ada ikon plus)
- const [searchResult, setSearchResult] = useState([
- { id: 2, email: "Jonathankristin@gmail.com", initials: "JK" }
- ]);
+ // Sync saat modal dibuka
+ useEffect(() => {
+ if (isOpen) setInvitedList(initialMembers);
+ }, [isOpen, initialMembers]);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
-
if (isOpen) {
dialog.showModal();
document.body.style.overflow = "hidden";
@@ -30,12 +36,7 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
dialog.close();
document.body.style.overflow = "unset";
}
-
- const handleCancel = (e: Event) => {
- e.preventDefault();
- onClose();
- };
-
+ const handleCancel = (e: Event) => { e.preventDefault(); onClose(); };
dialog.addEventListener("cancel", handleCancel);
return () => {
dialog.removeEventListener("cancel", handleCancel);
@@ -43,38 +44,66 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
};
}, [isOpen, onClose]);
+ // Filter mahasiswa yang belum jadi member & sesuai search
+ const invitedIds = new Set(invitedList.map(m => m.id));
+ const searchResult = mahasiswa.filter(m =>
+ !invitedIds.has(m.id) &&
+ (m.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ `${m.nama_depan} ${m.nama_belakang}`.toLowerCase().includes(searchQuery.toLowerCase()))
+ );
+
+ const handleAssign = async (intern: Mahasiswa) => {
+ const result = await assign({ id_project: projectId, id_user: intern.id });
+ if (result) {
+ setInvitedList(prev => [...prev, {
+ id: intern.id,
+ id_user: intern.id,
+ full_name: `${intern.nama_depan} ${intern.nama_belakang}`,
+ email: intern.email,
+ avatar: "",
+ kelompok_peminatan: intern.jurusan,
+ professional_bio: "",
+ position: intern.role,
+ } as ProjectMember]);
+ }
+ };
+
+ const getInitials = (m: Mahasiswa) =>
+ `${m.nama_depan[0]}${m.nama_belakang[0]}`.toUpperCase();
+
return (
-
+ {error && (
+ {error}
+ )}
+
{/* Action Row Buttons */}
- Launch Project
+ {loading ? "Launching..." : "Launch Project"}
diff --git a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
index 41f2c7f..d3da25b 100644
--- a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
+++ b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx
@@ -1,76 +1,59 @@
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';
interface CreateTaskModalProps {
isOpen: boolean;
onClose: () => void;
+ projectId: string;
+ onSuccess: () => void;
}
-export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProps) {
+export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess }: CreateTaskModalProps) {
const dialogRef = useRef (null);
+ const { create, loading } = useCreateTask(projectId);
- // Form States
const [taskTitle, setTaskTitle] = useState("");
const [description, setDescription] = useState("");
const [deadlineDate, setDeadlineDate] = useState("");
const [specificTime, setSpecificTime] = useState("");
- const [submissionType, setSubmissionType] = useState<"file" | "url">("file");
+ const [submissionType, setSubmissionType] = useState("file_upload");
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
-
- if (isOpen) {
- dialog.showModal();
- document.body.style.overflow = "hidden";
- } else {
- dialog.close();
- document.body.style.overflow = "unset";
- }
-
- const handleCancel = (e: Event) => {
- e.preventDefault();
- onClose();
- };
-
+ if (isOpen) { dialog.showModal(); document.body.style.overflow = "hidden"; }
+ else { dialog.close(); document.body.style.overflow = "unset"; }
+ const handleCancel = (e: Event) => { e.preventDefault(); onClose(); };
dialog.addEventListener("cancel", handleCancel);
- return () => {
- dialog.removeEventListener("cancel", handleCancel);
- document.body.style.overflow = "unset";
- };
+ return () => { dialog.removeEventListener("cancel", handleCancel); document.body.style.overflow = "unset"; };
}, [isOpen, onClose]);
- const handleSubmit = (e: React.FormEvent) => {
+ const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- const payload = { taskTitle, description, deadlineDate, specificTime, submissionType };
- console.log("Saving Task:", payload);
- alert("Task Saved Successfully!");
- onClose();
+ const result = await create({
+ title: taskTitle,
+ description,
+ deadline_date: deadlineDate,
+ specific_time: specificTime,
+ submission_type: submissionType,
+ });
+ if (result) onSuccess();
};
-
return (
-
- {/* Header Modal */}
+
Create New Task
-
- Fill in the details for the next intern assignment.
-
+ Fill in the details for the next intern assignment.
-
- {/* Body Form Modal (Scrollable jika layar pendek) */}
- |