fix: remove console log and improve notification handling in NotificationList

feat: enhance date formatting in MentorProjectDetailPage and add validation in CreateTaskDialog
refactor: update status handling in StatusSubmissionTable and improve ProjectTaskCard styling
fix: update toast icon for better clarity
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-23 13:46:36 +07:00
parent e396914609
commit f76ab5a3e5
7 changed files with 178 additions and 94 deletions
-1
View File
@@ -46,7 +46,6 @@ export default function InternifyLogin() {
document.cookie = `token=${token}; path=/; SameSite=Strict`; document.cookie = `token=${token}; path=/; SameSite=Strict`;
const decoded = decodeJWT(token); const decoded = decodeJWT(token);
console.log(decoded)
const role = decoded?.role; const role = decoded?.role;
if (role === "intern") { if (role === "intern") {
navigate("/intern"); navigate("/intern");
+140 -72
View File
@@ -1,7 +1,9 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; 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 { LuFileSpreadsheet } from "react-icons/lu";
import { MdOutlineEmail } from "react-icons/md";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import * as NotificationService from "../../services/NotificationService"; import * as NotificationService from "../../services/NotificationService";
import type { UINotification, BackendNotification } from "../../types/notification.types"; import type { UINotification, BackendNotification } from "../../types/notification.types";
@@ -22,8 +24,9 @@ export default function NotificationList() {
const loadNotifications = async () => { const loadNotifications = async () => {
try { try {
const data = await NotificationService.getNotifications(); const data = await NotificationService.getNotifications();
const unread = data.filter((n) => !n.is_read); // const unread = data.filter((n) => !n.is_read);
const mapped = unread.map(NotificationService.mapNotificationToUI); // const mapped = unread.map(NotificationService.mapNotificationToUI);
const mapped = data.map(NotificationService.mapNotificationToUI);
setNotifications(mapped); setNotifications(mapped);
} catch (err) { } catch (err) {
console.error("Failed to load notifications:", err); console.error("Failed to load notifications:", err);
@@ -35,7 +38,7 @@ export default function NotificationList() {
const token = getCookieToken(); const token = getCookieToken();
if (!token) return; 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, { const socket = io(socketUrl, {
auth: { token }, auth: { token },
transports: ["websocket"] transports: ["websocket"]
@@ -47,7 +50,6 @@ export default function NotificationList() {
socket.on("notification", (notif: BackendNotification) => { socket.on("notification", (notif: BackendNotification) => {
console.log("New realtime notification received:", notif); console.log("New realtime notification received:", notif);
if (notif.is_read) return;
const mapped = NotificationService.mapNotificationToUI(notif); const mapped = NotificationService.mapNotificationToUI(notif);
setNotifications(prev => { setNotifications(prev => {
if (prev.some(p => p.id === mapped.id)) { if (prev.some(p => p.id === mapped.id)) {
@@ -69,7 +71,7 @@ export default function NotificationList() {
const handleMarkAllRead = async () => { const handleMarkAllRead = async () => {
try { try {
const res = await NotificationService.markAllAsRead(); const res = await NotificationService.markAllAsRead();
setNotifications([]); setNotifications(prev => prev.map(n => ({ ...n, isNew: false })));
customToast.success("Berhasil", res.message); customToast.success("Berhasil", res.message);
} catch (err: any) { } catch (err: any) {
const msg = err?.response?.data?.message ?? "Gagal menandai semua notifikasi."; const msg = err?.response?.data?.message ?? "Gagal menandai semua notifikasi.";
@@ -80,7 +82,9 @@ export default function NotificationList() {
const handleReadMessage = async (id: number) => { const handleReadMessage = async (id: number) => {
try { try {
const res = await NotificationService.markAsRead(id); 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); customToast.success("Ditandai dibaca", res.message);
} catch (err: any) { } catch (err: any) {
const msg = err?.response?.data?.message ?? "Gagal menandai notifikasi."; const msg = err?.response?.data?.message ?? "Gagal menandai notifikasi.";
@@ -100,9 +104,9 @@ export default function NotificationList() {
</div> </div>
<button <button
onClick={handleMarkAllRead} onClick={handleMarkAllRead}
className="flex items-center gap-1.5 self-start sm:self-center px-4 py-2 border border-gray-200 rounded-lg text-xs font-semibold text-gray-700 bg-white hover:bg-gray-50 transition-colors shadow-sm" className="flex items-center gap-1.5 self-start sm:self-center px-4 py-2 border border-gray-100 rounded-lg text-xs font-semibold text-red bg-white hover:bg-gray-50 transition-colors shadow-sm"
> >
<Check className="w-4 h-4 text-green-600 stroke-[3]" /> <RiCheckDoubleLine className="w-4 h-4 text-red stroke-[3]" />
Mark all as read Mark all as read
</button> </button>
</div> </div>
@@ -115,72 +119,136 @@ export default function NotificationList() {
)} )}
<div className="space-y-6"> <div className="space-y-6">
{/* SECTION TODAY */}
{/* Render Group TODAY */} {notifications.filter((n) => n.group === "TODAY").length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
{notifications.filter(n => n.group === "TODAY").map((item) => ( {notifications
<div .filter((n) => n.group === "TODAY")
key={item.id} .map((item) => (
className={`relative flex items-start gap-4 p-5 bg-box border border-card-outline/40 rounded-xl shadow-[0_2px_10px_rgba(0,0,0,0.01)] ${item.type === 'new-task' ? 'border-l-4 border-l-[#B30000]' : '' <div
}`} key={item.id}
> className={`relative flex items-start gap-4 p-5 bg-white border border-gray-200/80 rounded-xl shadow-sm ${item.isNew ? "border-l-4 border-l-[#B30000]" : ""
{/* Icon Container */} }`}
<div className={`p-2.5 rounded-xl ${item.type === 'new-task' ? 'bg-red-50 text-red-600' : 'bg-gray-100 text-gray-600' >
}`}> {/* Icon Container */}
{item.type === 'new-task' && <LuFileSpreadsheet className="w-5 h-5" />} <div
{item.type === 'achievement' && <Check className="w-5 h-5 stroke-[2.5]" />} className={`p-2.5 rounded-xl ${item.isNew
</div> ? "bg-red-100 text-red-600"
: "bg-gray-100 text-gray-600"
{/* Content text */} }`}
<div className="flex-grow space-y-1"> >
<div className="flex flex-wrap items-center gap-2"> {item.type === "new-task" ? (
{item.isNew && ( <LuFileSpreadsheet className="w-5 h-5" />
<span className="bg-[#B30000] text-white font-extrabold text-[9px] px-2 py-0.5 rounded uppercase tracking-wider"> ) : item.type === "achievement" ? (
NEW <Check className="w-5 h-5 stroke-[2.5]" />
</span> ) : (
)} <MdOutlineEmail className="w-5 h-5" />
<h3 className="text-sm font-bold text-gray-900">{item.title}</h3> )}
</div>
{/* Custom description coloring logic */}
<p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
{item.description}
</p>
{/* Conditional Actions */}
{item.type === 'new-task' && item.link && (
<div className="flex items-center gap-2 pt-3">
<button
className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors"
onClick={() => navigate(`${item.link}`)}>
View Task
</button>
<button
className="px-4 py-1.5 border border-card-outline text-font hover:text-font-shade hover:bg-gray-50 text-xs font-semibold rounded-lg transition-colors"
onClick={() => handleReadMessage(item.id)}
>
Later
</button>
</div> </div>
)}
{item.type === 'achievement' && ( {/* Content Text */}
<button <div className="flex-grow space-y-1">
className="flex items-center gap-1 text-xs font-bold text-[#B30000] hover:underline pt-2" <div className="flex flex-wrap items-center gap-2">
onClick={() => navigate(`/certificates`)}> {item.isNew && (
<Download className="w-3.5 h-3.5" /> <span className="bg-[#B30000] text-white font-extrabold text-[9px] px-2 py-0.5 rounded uppercase tracking-wider">
Ke Halaman NEW
</button> </span>
)} )}
</div> <h3 className="text-sm font-bold text-gray-900">
{item.title}
</h3>
</div>
{/* Time */} <p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap self-start md:absolute md:top-5 md:right-5"> {item.description}
{item.time} </p>
</span>
</div> {/* Actions */}
))} <div className="flex items-center gap-2 pt-3">
</div> {item.link && (
<button
className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors"
onClick={() => navigate(`${item.link}`)}
>
View Task
</button>
)}
{item.isNew && (
<button
className="px-4 py-1.5 border border-gray-200 text-gray-600 hover:bg-gray-50 text-xs font-semibold rounded-lg transition-colors"
onClick={() => handleReadMessage(item.id)}
>
Later
</button>
)}
</div>
</div>
{/* Time */}
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap self-start md:absolute md:top-5 md:right-5">
{item.time}
</span>
</div>
))}
</div>
)}
{/* SECTION YESTERDAY / SEBELUMNYA */}
{notifications.filter((n) => n.group !== "TODAY").length > 0 && (
<div className="space-y-3 pt-4">
{/* Subheader Title */}
<h2 className="text-xs font-bold text-gray-400 tracking-wider uppercase">
YESTERDAY
</h2>
{notifications
.filter((n) => n.group !== "TODAY")
.map((item) => (
<div
key={item.id}
className="relative flex items-start gap-4 p-5 bg-gray-100/70 border border-gray-200/50 rounded-xl"
>
{/* Icon Container Gray */}
<div className="p-2.5 rounded-xl bg-gray-200/60 text-gray-500">
{item.type === "new-task" ? (
<LuFileSpreadsheet className="w-5 h-5" />
) : item.type === "achievement" ? (
<Check className="w-5 h-5 stroke-[2.5]" />
) : (
<MdOutlineEmail className="w-5 h-5" />
)}
</div>
{/* Content Text */}
<div className="flex-grow space-y-1">
<h3 className="text-sm font-bold text-gray-700">
{item.title}
</h3>
<p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
{item.description}
</p>
{/* Action Button untuk Yesterday jika ada */}
{item.link && (
<div className="pt-2">
<button
className="text-xs font-bold text-[#B30000] hover:underline flex items-center gap-1"
onClick={() => navigate(`${item.link}`)}
>
View
</button>
</div>
)}
</div>
{/* Time */}
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap self-start md:absolute md:top-5 md:right-5">
{item.time}
</span>
</div>
))}
</div>
)}
</div> </div>
</div> </div>
); );
@@ -53,12 +53,10 @@ export default function TaskCard({ task }: { task: InternTaskItem }) {
Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"} Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"}
</div> </div>
</div> </div>
<div className={`shrink-0 hidden md:flex items-center rounded-xl border ${statusStyles[displayStatus]}`}>
<div className="shrink-0 hidden md:flex items-center border border-card-outline">
<span <span
className={`font-semibold text-xs px-3.5 py-[5px] rounded-md whitespace-nowrap border ${statusStyles[displayStatus]}`}> className={`font-semibold text-xs px-3.5 py-[5px] whitespace-nowrap`}>
{statusLabels[task.submission_status]} {statusLabels[displayStatus]}
</span> </span>
</div> </div>
</div> </div>
@@ -118,7 +118,11 @@ export default function MentorProjectsDetailPage() {
{/* Kolom Tanggal Kumpul */} {/* Kolom Tanggal Kumpul */}
<td className="px-6 py-4 text-sm text-gray-500"> <td className="px-6 py-4 text-sm text-gray-500">
{sub.submitted_at {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",
})
: "-"} : "-"}
</td> </td>
@@ -126,12 +130,12 @@ export default function MentorProjectsDetailPage() {
<td className="px-6 py-4"> <td className="px-6 py-4">
{sub.display_status === "done" && ( {sub.display_status === "done" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-green-50 text-green-700 border border-green-200"> <span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-green-50 text-green-700 border border-green-200">
<span className="w-1.5 h-1.5 bg-green-500 rounded-full"></span> Done <span className="w-1.5 h-1.5 bg-green-700 rounded-full"></span> Done
</span> </span>
)} )}
{sub.display_status === "pending" && ( {sub.display_status === "pending" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-amber-50 text-amber-700 border border-amber-200"> <span className="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full bg-amber-50 text-amber-700 border border-amber-200">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full"></span> Pending <span className="w-1.5 h-1.5 bg-amber-700 rounded-full"></span> Pending
</span> </span>
)} )}
{sub.display_status === "overdue" && ( {sub.display_status === "overdue" && (
@@ -17,6 +17,7 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
const [taskTitle, setTaskTitle] = useState(""); const [taskTitle, setTaskTitle] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [deadlineDate, setDeadlineDate] = useState(""); const [deadlineDate, setDeadlineDate] = useState("");
const [dateError, setDateError] = useState("");
const [specificTime, setSpecificTime] = useState(""); const [specificTime, setSpecificTime] = useState("");
const [submissionType, setSubmissionType] = useState<SubmissionType>("file_upload"); const [submissionType, setSubmissionType] = useState<SubmissionType>("file_upload");
@@ -24,32 +25,43 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
const dialog = dialogRef.current; const dialog = dialogRef.current;
if (!dialog) return; if (!dialog) return;
if (isOpen) { if (isOpen) {
dialog.showModal(); dialog.showModal();
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
} }
else { else {
dialog.close(); dialog.close();
document.body.style.overflow = "unset"; document.body.style.overflow = "unset";
} }
const handleCancel = (e: Event) => { const handleCancel = (e: Event) => {
e.preventDefault(); e.preventDefault();
}; };
dialog.addEventListener("cancel", handleCancel); dialog.addEventListener("cancel", handleCancel);
return () => { return () => {
dialog.removeEventListener( dialog.removeEventListener(
"cancel", handleCancel "cancel", handleCancel
); );
document.body.style.overflow = "unset"; document.body.style.overflow = "unset";
}; };
}, [isOpen, onClose]); }, [isOpen, onClose]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); 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({ const result = await create({
title: taskTitle, title: taskTitle,
description, description,
@@ -104,8 +116,11 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
type="date" type="date"
required required
value={deadlineDate} 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]" /> 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 && (
<p className="text-xs text-red font-medium">{dateError}</p>
)}
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time</label> <label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time</label>
@@ -37,7 +37,7 @@ interface StatusTableProps {
} }
export function StatusTable({ status, submission }: 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 }[] = [ const rows: { label: string; value: React.ReactNode }[] = [
{ {
+2 -2
View File
@@ -1,7 +1,7 @@
import toast, { type Toast } from 'react-hot-toast'; import toast, { type Toast } from 'react-hot-toast';
import { FiX } from 'react-icons/fi'; import { FiX } from 'react-icons/fi';
import { FaCheck } from "react-icons/fa"; import { FaCheck } from "react-icons/fa";
import { IoAlertOutline } from "react-icons/io5"; import { IoAlertCircleOutline } from "react-icons/io5";
function ToastCard({ t, type, title, description }: { function ToastCard({ t, type, title, description }: {
t: Toast; t: Toast;
@@ -19,7 +19,7 @@ function ToastCard({ t, type, title, description }: {
> >
<div className={`flex items-center justify-center w-6 h-6 rounded-full mt-0.5 shrink-0 <div className={`flex items-center justify-center w-6 h-6 rounded-full mt-0.5 shrink-0
${isSuccess ? 'bg-green-600 text-white' : 'bg-red-600 text-white'}`}> ${isSuccess ? 'bg-green-600 text-white' : 'bg-red-600 text-white'}`}>
{isSuccess ? <FaCheck size={10} /> : <IoAlertOutline size={10} />} {isSuccess ? <FaCheck size={10} /> : <IoAlertCircleOutline size={10} />}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className={`font-semibold text-[10px] ${isSuccess ? 'text-green-700' : 'text-red-700'}`}> <p className={`font-semibold text-[10px] ${isSuccess ? 'text-green-700' : 'text-red-700'}`}>