diff --git a/src/Pages/internify/NotificationList.tsx b/src/Pages/internify/NotificationList.tsx
index 2941442..e3afa68 100644
--- a/src/Pages/internify/NotificationList.tsx
+++ b/src/Pages/internify/NotificationList.tsx
@@ -20,7 +20,8 @@ export default function NotificationList() {
const loadNotifications = async () => {
try {
const data = await NotificationService.getNotifications();
- const mapped = data.map(NotificationService.mapNotificationToUI);
+ const unread = data.filter((n) => !n.is_read);
+ const mapped = unread.map(NotificationService.mapNotificationToUI);
setNotifications(mapped);
} catch (err) {
console.error("Failed to load notifications:", err);
@@ -44,6 +45,7 @@ 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)) {
@@ -66,7 +68,7 @@ export default function NotificationList() {
const handleMarkAllRead = async () => {
try {
await NotificationService.markAllAsRead();
- setNotifications(prev => prev.map(n => ({ ...n, isNew: false })));
+ setNotifications([]);
alert("All notifications marked as read!");
} catch (err) {
console.error("Failed to mark all as read:", err);
@@ -130,16 +132,16 @@ export default function NotificationList() {
{/* Conditional Actions */}
- {item.type === 'new-task' && (
+ {item.type === 'new-task' && item.link &&(
- */}
)}
@@ -148,7 +150,7 @@ export default function NotificationList() {
className="flex items-center gap-1 text-xs font-bold text-[#B30000] hover:underline pt-2"
onClick={() => navigate(`/certificates`)}>
- Download Certificate
+ Ke Halaman
)}
diff --git a/src/Pages/internify/mentor/components/EligibleInternTable.tsx b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
index 6dc11f1..0a52fec 100644
--- a/src/Pages/internify/mentor/components/EligibleInternTable.tsx
+++ b/src/Pages/internify/mentor/components/EligibleInternTable.tsx
@@ -29,6 +29,20 @@ interface EligibleInternTableProps {
const PER_PAGE = 5;
+const BULAN_ID = [
+ "Januari", "Februari", "Maret", "April", "Mei", "Juni",
+ "Juli", "Agustus", "September", "Oktober", "November", "Desember",
+];
+
+function formatDateIndo(dateStr: string): string {
+ const [year, month, day] = dateStr.split("-").map(Number);
+ return `${day} ${BULAN_ID[month - 1]} ${year}`;
+}
+
+function formatDateRange(startDate: string, endDate: string): string {
+ return `${formatDateIndo(startDate)} - ${formatDateIndo(endDate)}`;
+}
+
function StatusBadge({ status }: { status: boolean }) {
return (
@@ -49,7 +63,6 @@ export default function EligibleInternTable({
// Durasi tanggal Proyek
}: EligibleInternTableProps) {
const navigate = useNavigate();
- // const { project } = useProjectDetail(String(projectId));
const { generate: SubmitCertificates } = useGenerateCertificates();
const initialSelected = new Set(
@@ -99,6 +112,8 @@ export default function EligibleInternTable({
const zip = new JSZip();
const templateUrl = resolveImageUrl(project.certificate_template!);
+ const duration = formatDateRange(project.start_date, project.end_date);
+
await Promise.all(
result.map(async (cert) => {
const verifyUrl = `${window.location.origin}/verify-certificate/${cert.uuid}`;
@@ -108,7 +123,7 @@ export default function EligibleInternTable({
cert.project.project_name,
cert.certificate_no,
verifyUrl,
- "20 Juni 2024 - 20 September 2024"
+ duration
);
zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
})
diff --git a/src/Pages/utils/SertificateGenerator.ts b/src/Pages/utils/SertificateGenerator.ts
index ec6d782..d1a82f5 100644
--- a/src/Pages/utils/SertificateGenerator.ts
+++ b/src/Pages/utils/SertificateGenerator.ts
@@ -80,9 +80,9 @@ export async function generateCertificate(
ctx.fillText(certificateNo, canvas.width * 0.825, canvas.height * 0.918);
//Durasi Project — besar (Grenze)
- ctx.font = `50px "Grenze", serif`;
+ ctx.font = `400 48px "Grenze", serif`;
ctx.fillStyle = "#090909";
- ctx.fillText(ProjectDuration, canvas.width * 0.57, canvas.height * 0.715);
+ ctx.fillText("20 April - 20 Juni 2025", canvas.width * 0.68, canvas.height * 0.66);
// QR code — kalau verifyUrl diisi
const qrDataUrl = await QRCode.toDataURL('http://localhost:5172/verify-certificate/' + verifyUrl, {
@@ -93,8 +93,8 @@ export async function generateCertificate(
const qrSize = canvas.width * 0.103;
ctx.drawImage(
qrImg,
- canvas.width * 0.77 - qrSize,
- canvas.height * 0.715 - qrSize,
+ canvas.width * 0.8 - qrSize,
+ canvas.height * 0.75 - qrSize,
qrSize,
qrSize,
);
diff --git a/src/Pages/utils/testlayout.html b/src/Pages/utils/testlayout.html
index b411376..d9c2798 100644
--- a/src/Pages/utils/testlayout.html
+++ b/src/Pages/utils/testlayout.html
@@ -51,6 +51,10 @@
ctx.fillStyle = "#090909";
ctx.fillText("Internify Platform Dev", canvas.width * 0.57, canvas.height * 0.615);
+ ctx.font = `400 48px "Grenze", serif`;
+ ctx.fillStyle = "#090909";
+ ctx.fillText("20 April - 20 Juni 2025", canvas.width * 0.68, canvas.height * 0.66);
+
// 4. Certificate no
ctx.font = `50px Grenze`;
ctx.fillText("CERT/20260609/PRJ2/USR5", canvas.width * 0.665, canvas.height * 0.305);
diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts
index 6914699..a668dfd 100644
--- a/src/services/NotificationService.ts
+++ b/src/services/NotificationService.ts
@@ -90,6 +90,7 @@ export const mapNotificationToUI = (notif: BackendNotification): UINotification
title: notif.title,
time: formatRelativeTime(notif.created_at),
isNew: !notif.is_read,
+ link: notif.link ?? "",
group: mapGroup(notif.created_at),
description: notif.message,
};
diff --git a/src/types/notification.types.ts b/src/types/notification.types.ts
index 6331ea7..d17aef9 100644
--- a/src/types/notification.types.ts
+++ b/src/types/notification.types.ts
@@ -6,6 +6,7 @@ export interface BackendNotification {
message: string;
is_read: boolean;
created_at: string;
+ link: string | null;
}
export interface UINotification {