diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index 56d417a..e4be35c 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -46,7 +46,6 @@ export default function InternifyLogin() { document.cookie = `token=${token}; path=/; SameSite=Strict`; const decoded = decodeJWT(token); - console.log(decoded) const role = decoded?.role; if (role === "intern") { navigate("/intern"); diff --git a/src/Pages/internify/NotificationList.tsx b/src/Pages/internify/NotificationList.tsx index abd781a..e9e396f 100644 --- a/src/Pages/internify/NotificationList.tsx +++ b/src/Pages/internify/NotificationList.tsx @@ -1,7 +1,9 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import { Check, Download } from "lucide-react"; +import { RiCheckDoubleLine } from "react-icons/ri"; +import { Check } from "lucide-react"; import { LuFileSpreadsheet } from "react-icons/lu"; +import { MdOutlineEmail } from "react-icons/md"; import { io } from "socket.io-client"; import * as NotificationService from "../../services/NotificationService"; import type { UINotification, BackendNotification } from "../../types/notification.types"; @@ -22,8 +24,9 @@ export default function NotificationList() { const loadNotifications = async () => { try { const data = await NotificationService.getNotifications(); - const unread = data.filter((n) => !n.is_read); - const mapped = unread.map(NotificationService.mapNotificationToUI); + // const unread = data.filter((n) => !n.is_read); + // const mapped = unread.map(NotificationService.mapNotificationToUI); + const mapped = data.map(NotificationService.mapNotificationToUI); setNotifications(mapped); } catch (err) { console.error("Failed to load notifications:", err); @@ -35,7 +38,7 @@ export default function NotificationList() { const token = getCookieToken(); if (!token) return; - const socketUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:9000"; + const socketUrl = import.meta.env.VITE_API_BASE_URL; const socket = io(socketUrl, { auth: { token }, transports: ["websocket"] @@ -47,7 +50,6 @@ export default function NotificationList() { socket.on("notification", (notif: BackendNotification) => { console.log("New realtime notification received:", notif); - if (notif.is_read) return; const mapped = NotificationService.mapNotificationToUI(notif); setNotifications(prev => { if (prev.some(p => p.id === mapped.id)) { @@ -69,7 +71,7 @@ export default function NotificationList() { const handleMarkAllRead = async () => { try { const res = await NotificationService.markAllAsRead(); - setNotifications([]); + setNotifications(prev => prev.map(n => ({ ...n, isNew: false }))); customToast.success("Berhasil", res.message); } catch (err: any) { const msg = err?.response?.data?.message ?? "Gagal menandai semua notifikasi."; @@ -80,7 +82,9 @@ export default function NotificationList() { const handleReadMessage = async (id: number) => { try { const res = await NotificationService.markAsRead(id); - setNotifications(prev => prev.filter(n => n.id !== id)); + setNotifications(prev => + prev.map(n => (n.id === id ? { ...n, isNew: false } : n)) + ); customToast.success("Ditandai dibaca", res.message); } catch (err: any) { const msg = err?.response?.data?.message ?? "Gagal menandai notifikasi."; @@ -100,9 +104,9 @@ export default function NotificationList() { @@ -115,72 +119,136 @@ export default function NotificationList() { )}
- - {/* Render Group TODAY */} -
- {notifications.filter(n => n.group === "TODAY").map((item) => ( -
- {/* Icon Container */} -
- {item.type === 'new-task' && } - {item.type === 'achievement' && } -
- - {/* Content text */} -
-
- {item.isNew && ( - - NEW - - )} -

{item.title}

-
- - {/* Custom description coloring logic */} -

- {item.description} -

- - {/* Conditional Actions */} - {item.type === 'new-task' && item.link && ( -
- - + {/* SECTION TODAY */} + {notifications.filter((n) => n.group === "TODAY").length > 0 && ( +
+ {notifications + .filter((n) => n.group === "TODAY") + .map((item) => ( +
+ {/* Icon Container */} +
+ {item.type === "new-task" ? ( + + ) : item.type === "achievement" ? ( + + ) : ( + + )}
- )} - {item.type === 'achievement' && ( - - )} -
+ {/* Content Text */} +
+
+ {item.isNew && ( + + NEW + + )} +

+ {item.title} +

+
- {/* Time */} - - {item.time} - -
- ))} -
+

+ {item.description} +

+ + {/* Actions */} +
+ {item.link && ( + + )} + {item.isNew && ( + + )} +
+
+ + {/* Time */} + + {item.time} + +
+ ))} +
+ )} + + {/* SECTION YESTERDAY / SEBELUMNYA */} + {notifications.filter((n) => n.group !== "TODAY").length > 0 && ( +
+ {/* Subheader Title */} +

+ YESTERDAY +

+ + {notifications + .filter((n) => n.group !== "TODAY") + .map((item) => ( +
+ {/* Icon Container Gray */} +
+ {item.type === "new-task" ? ( + + ) : item.type === "achievement" ? ( + + ) : ( + + )} +
+ + {/* Content Text */} +
+

+ {item.title} +

+

+ {item.description} +

+ + {/* Action Button untuk Yesterday jika ada */} + {item.link && ( +
+ +
+ )} +
+ + {/* Time */} + + {item.time} + +
+ ))} +
+ )}
); diff --git a/src/Pages/internify/intern/components/ProjectTaskCard.tsx b/src/Pages/internify/intern/components/ProjectTaskCard.tsx index ef10285..c9060f3 100644 --- a/src/Pages/internify/intern/components/ProjectTaskCard.tsx +++ b/src/Pages/internify/intern/components/ProjectTaskCard.tsx @@ -53,12 +53,10 @@ export default function TaskCard({ task }: { task: InternTaskItem }) { Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"} - - -
+
diff --git a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx index db9d123..f3b8ce0 100644 --- a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx +++ b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx @@ -118,7 +118,11 @@ export default function MentorProjectsDetailPage() { {/* Kolom Tanggal Kumpul */} {sub.submitted_at - ? new Date(sub.submitted_at).toLocaleDateString("id-ID") + ? new Date(sub.submitted_at).toLocaleString("id-ID", { + day: "numeric", + month: "long", + year: "numeric", + }) : "-"} @@ -126,12 +130,12 @@ export default function MentorProjectsDetailPage() { {sub.display_status === "done" && ( - Done + Done )} {sub.display_status === "pending" && ( - Pending + Pending )} {sub.display_status === "overdue" && ( diff --git a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx index ad70a34..2e0113d 100644 --- a/src/Pages/internify/mentor/components/CreateTaskDialog.tsx +++ b/src/Pages/internify/mentor/components/CreateTaskDialog.tsx @@ -17,6 +17,7 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess const [taskTitle, setTaskTitle] = useState(""); const [description, setDescription] = useState(""); const [deadlineDate, setDeadlineDate] = useState(""); + const [dateError, setDateError] = useState(""); const [specificTime, setSpecificTime] = useState(""); const [submissionType, setSubmissionType] = useState("file_upload"); @@ -24,32 +25,43 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess const dialog = dialogRef.current; if (!dialog) return; - if (isOpen) { - dialog.showModal(); + if (isOpen) { + dialog.showModal(); document.body.style.overflow = "hidden"; } - else { - dialog.close(); + else { + dialog.close(); document.body.style.overflow = "unset"; } - + const handleCancel = (e: Event) => { e.preventDefault(); }; - + dialog.addEventListener("cancel", handleCancel); - + return () => { dialog.removeEventListener( "cancel", handleCancel - ); - document.body.style.overflow = "unset"; + ); + document.body.style.overflow = "unset"; }; }, [isOpen, onClose]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + + setDateError(""); + + if (deadlineDate) { + const today = new Date().toISOString().split("T")[0]; + if (deadlineDate < today) { + setDateError("Deadline date cannot be in the past"); + return; + } + } + const result = await create({ title: taskTitle, description, @@ -104,8 +116,11 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess type="date" required value={deadlineDate} - onChange={(e) => setDeadlineDate(e.target.value)} + onChange={(e) => { setDeadlineDate(e.target.value); setDateError(""); }} className="w-full px-3 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" /> + {dateError && ( +

{dateError}

+ )}
diff --git a/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx b/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx index 59df0cf..13c6da8 100644 --- a/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx +++ b/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx @@ -37,7 +37,7 @@ interface StatusTableProps { } export function StatusTable({ status, submission }: StatusTableProps) { - const hasSubmission = status === "submitted" || status === "done"; + const hasSubmission = status === "submitted" || status == "overdue"; const rows: { label: string; value: React.ReactNode }[] = [ { diff --git a/src/Pages/utils/showToast.tsx b/src/Pages/utils/showToast.tsx index 330ade1..bbb023b 100644 --- a/src/Pages/utils/showToast.tsx +++ b/src/Pages/utils/showToast.tsx @@ -1,7 +1,7 @@ import toast, { type Toast } from 'react-hot-toast'; import { FiX } from 'react-icons/fi'; import { FaCheck } from "react-icons/fa"; -import { IoAlertOutline } from "react-icons/io5"; +import { IoAlertCircleOutline } from "react-icons/io5"; function ToastCard({ t, type, title, description }: { t: Toast; @@ -19,7 +19,7 @@ function ToastCard({ t, type, title, description }: { >
- {isSuccess ? : } + {isSuccess ? : }