From ec3ad12531b753f045ce06a60fff84107180dd48 Mon Sep 17 00:00:00 2001 From: nico Date: Fri, 5 Dec 2025 14:30:53 +0800 Subject: [PATCH] Fix Notifikasi saat ada berita atau pengumuman baru, notifikasi baru muncul. Ga setiap masuk landing page ada notifikasi --- prisma/schema.prisma | 10 +- src/app/api/check-update/route.ts | 36 ++++++ ...ication.tsx => ModernNewsNotification.tsx} | 113 ++++++++++-------- .../_com/main-page/penghargaan/index.tsx | 62 +++++++--- src/app/darmasaba/page.tsx | 103 +++++++++++++--- 5 files changed, 231 insertions(+), 93 deletions(-) create mode 100644 src/app/api/check-update/route.ts rename src/app/darmasaba/_com/{ModernNeewsNotification.tsx => ModernNewsNotification.tsx} (81%) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5d8b95af..23535296 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -828,11 +828,11 @@ model DokterdanTenagaMedis { name String specialist String jadwal String - jadwalLibur String - jamBukaOperasional String - jamTutupOperasional String - jamBukaLibur String - jamTutupLibur String + jadwalLibur String? + jamBukaOperasional String? + jamTutupOperasional String? + jamBukaLibur String? + jamTutupLibur String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) diff --git a/src/app/api/check-update/route.ts b/src/app/api/check-update/route.ts new file mode 100644 index 00000000..167f90a8 --- /dev/null +++ b/src/app/api/check-update/route.ts @@ -0,0 +1,36 @@ +// app/api/check-update/route.ts +import prisma from "@/lib/prisma"; + +export async function GET() { + try { + // Ambil berita terbaru + const latestBerita = await prisma.berita.findFirst({ + orderBy: { createdAt: "desc" }, + select: { id: true, createdAt: true }, + }); + + // Ambil pengumuman terbaru + const latestPengumuman = await prisma.pengumuman.findFirst({ + orderBy: { createdAt: "desc" }, + select: { id: true, createdAt: true }, + }); + + return Response.json({ + success: true, + data: { + berita: latestBerita + ? { id: latestBerita.id, createdAt: latestBerita.createdAt.toISOString() } + : null, + pengumuman: latestPengumuman + ? { id: latestPengumuman.id, createdAt: latestPengumuman.createdAt.toISOString() } + : null, + }, + }); + } catch (error) { + console.error("Error in /api/check-update:", error); + return Response.json( + { success: false, message: "Gagal cek update" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/darmasaba/_com/ModernNeewsNotification.tsx b/src/app/darmasaba/_com/ModernNewsNotification.tsx similarity index 81% rename from src/app/darmasaba/_com/ModernNeewsNotification.tsx rename to src/app/darmasaba/_com/ModernNewsNotification.tsx index 6c91c206..39701cbd 100644 --- a/src/app/darmasaba/_com/ModernNeewsNotification.tsx +++ b/src/app/darmasaba/_com/ModernNewsNotification.tsx @@ -15,6 +15,9 @@ interface NewsItem { interface ModernNewsNotificationProps { news: NewsItem[]; + hasNewContent?: boolean; // ✅ TAMBAHAN + newItemCount?: number; // ← tambahkan ini + onSeen?: () => void; // ✅ TAMBAHAN autoShowDelay?: number; } @@ -29,57 +32,66 @@ function stripHtml(html: string): string { export default function ModernNewsNotification({ news = [], - autoShowDelay = 2000 + hasNewContent = false, + newItemCount = 0, // 👈 tambahkan ini + onSeen, + autoShowDelay = 2000, }: ModernNewsNotificationProps) { const router = useRouter(); const [toastVisible, setToastVisible] = useState(false); const [widgetOpen, setWidgetOpen] = useState(false); - const [hasNewNotifications, setHasNewNotifications] = useState(true); + const [hasNewNotifications, setHasNewNotifications] = useState(hasNewContent); const [hasShownToast, setHasShownToast] = useState(false); const [iconVisible, setIconVisible] = useState(true); const pathname = usePathname(); - // Auto show toast on page load + // Sinkronisasi dari luar + useEffect(() => { + if (hasNewContent) { + setHasNewNotifications(true); + // Jangan otomatis tampilkan toast di sini — biarkan saat page load saja + } + }, [hasNewContent]); + + // Auto show toast hanya saat page pertama kali load useEffect(() => { if (news.length > 0 && !toastVisible && !hasShownToast) { const timer = setTimeout(() => { setToastVisible(true); setHasShownToast(true); + // Jika ada new content, anggap sudah "dilihat" setelah toast muncul + if (hasNewNotifications) { + onSeen?.(); + } }, autoShowDelay); return () => clearTimeout(timer); } - }, [news.length, autoShowDelay, toastVisible, hasShownToast]); + }, [news.length, autoShowDelay, toastVisible, hasShownToast, hasNewNotifications, onSeen]); - // Auto hide toast after 8 seconds + // Auto hide toast useEffect(() => { if (toastVisible) { - const timer = setTimeout(() => { - setToastVisible(false); - }, 8000); + const timer = setTimeout(() => setToastVisible(false), 8000); return () => clearTimeout(timer); } }, [toastVisible]); - // Enhanced scroll handler with better thresholds + // Scroll handler useEffect(() => { let lastScrollY = window.scrollY; - const HIDE_THRESHOLD = 100; // Mulai hide saat scroll > 100px - const SHOW_THRESHOLD = 50; // Hanya show ketika benar-benar di atas (< 50px) + const HIDE_THRESHOLD = 100; + const SHOW_THRESHOLD = 50; const handleScroll = () => { const currentScrollY = window.scrollY; const scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up'; - // Logic untuk hide/show icon if (scrollDirection === 'down' && currentScrollY > HIDE_THRESHOLD) { - // Scroll ke bawah dan sudah melewati threshold → hide setIconVisible(false); } else if (scrollDirection === 'up' && currentScrollY < SHOW_THRESHOLD) { - // Scroll ke atas dan sudah di posisi paling atas → show setIconVisible(true); } - // Hide toast saat scroll ke bawah melewati 150px if (currentScrollY > 150 && toastVisible) { setToastVisible(false); } @@ -93,9 +105,9 @@ export default function ModernNewsNotification({ const currentNews = news[0]; - // Handle notification click const handleNotificationClick = (item: NewsItem) => { setWidgetOpen(false); + onSeen?.(); // ✅ tandai sebagai dilihat if (item.type === "berita") { router.push("/darmasaba/desa/berita/semua"); } else if (item.type === "pengumuman") { @@ -107,6 +119,13 @@ export default function ModernNewsNotification({ setToastVisible(false); setWidgetOpen(true); setHasNewNotifications(false); + onSeen?.(); // ✅ + }; + + const handleDismissToast = (e: React.MouseEvent) => { + e.stopPropagation(); + setToastVisible(false); + onSeen?.(); // ✅ }; // Only show on landing page @@ -119,14 +138,7 @@ export default function ModernNewsNotification({ {/* Floating Bell Icon */} {(transitionStyles) => ( - + { setWidgetOpen(!widgetOpen); setHasNewNotifications(false); + onSeen?.(); // ✅ }} style={{ width: "60px", @@ -146,20 +159,23 @@ export default function ModernNewsNotification({ {hasNewNotifications && news.length > 0 && ( - {news.length} - + size="sm" + variant="filled" + color="red" + style={{ + position: "absolute", + top: "6px", + right: "6px", + minWidth: "22px", + height: "22px", + padding: "0 6px", + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + > + {newItemCount || news.length} + )} @@ -195,20 +211,17 @@ export default function ModernNewsNotification({ Berita & Pengumuman setWidgetOpen(false)} + onClick={() => { + setWidgetOpen(false); + onSeen?.(); // ✅ + }} variant="transparent" c="white" /> - + {news.length === 0 ? ( Tidak ada berita terbaru @@ -303,13 +316,7 @@ export default function ModernNewsNotification({ > {currentNews?.type === "berita" ? "Berita Terbaru" : "Pengumuman"} - { - e.stopPropagation(); - setToastVisible(false); - }} - size="sm" - /> + diff --git a/src/app/darmasaba/_com/main-page/penghargaan/index.tsx b/src/app/darmasaba/_com/main-page/penghargaan/index.tsx index 46ef6f5c..1272be84 100644 --- a/src/app/darmasaba/_com/main-page/penghargaan/index.tsx +++ b/src/app/darmasaba/_com/main-page/penghargaan/index.tsx @@ -1,8 +1,8 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan"; -import { Stack, Box, Container, Button, Text, Loader, Paper } from "@mantine/core"; -import { IconAward, IconArrowRight } from "@tabler/icons-react"; +import { Stack, Box, Container, Button, Text, Loader, Paper, Center, ActionIcon } from "@mantine/core"; +import { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react"; import { useTransitionRouter } from 'next-view-transitions'; import { useEffect, useState } from "react"; import { useProxy } from "valtio/utils"; @@ -13,6 +13,18 @@ function Penghargaan() { const state = useProxy(penghargaanState); const [loading, setLoading] = useState(false); const isMobile = useMediaQuery('(max-width: 768px)'); + const [isVideoLoaded, setIsVideoLoaded] = useState(false); + const [showVideo, setShowVideo] = useState(true); + + // Opsional: deteksi iOS + const isIOS = typeof window !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent); + + useEffect(() => { + if (isIOS) { + // Di iOS, jangan andalkan autoplay — tampilkan kontrol + setShowVideo(false); + } + }, []); useEffect(() => { const loadData = async () => { @@ -31,22 +43,36 @@ function Penghargaan() { return ( - + {showVideo ? ( + + ) : ( + // Fallback: tampilkan poster + play button + setShowVideo(true)} + style={{ + backgroundImage: "url('/assets/images/award-poster.jpg')", + backgroundSize: 'cover', + backgroundPosition: 'center', + cursor: 'pointer', + }} + > +
+ + + +
+
+ )} (null); + const lastPengumumanId = useRef(null); + + // 🔁 Inisialisasi dari localStorage saat mount + useEffect(() => { + const savedBerita = localStorage.getItem("lastSeenBeritaId"); + const savedPengumuman = localStorage.getItem("lastSeenPengumumanId"); + if (savedBerita) lastBeritaId.current = savedBerita; + if (savedPengumuman) lastPengumumanId.current = savedPengumuman; + }, []); + + // Simpan ID saat data dimuat (termasuk dari API) + useEffect(() => { + if (featured.data?.id) lastBeritaId.current = featured.data.id; + if (pengumuman.data?.id) lastPengumumanId.current = pengumuman.data.id; + }, [featured.data?.id, pengumuman.data?.id]); + + // Load data awal useEffect(() => { if (!featured.data && !loadingFeatured) { stateDashboardBerita.berita.findFirst.load(); @@ -43,6 +64,49 @@ export default function Page() { } }, []); + // 🔁 Polling untuk cek update setiap 30 detik + useEffect(() => { + const checkForUpdates = async () => { + try { + const res = await fetch("/api/check-update"); + const result = await res.json(); + + if (!result.success) return; + + const { berita, pengumuman } = result.data; + + // Deteksi hanya jika sudah pernah ada data sebelumnya + const isNewBerita = berita && lastBeritaId.current !== null && berita.id !== lastBeritaId.current; + const isNewPengumuman = pengumuman && lastPengumumanId.current !== null && pengumuman.id !== lastPengumumanId.current; + + if (isNewBerita || isNewPengumuman) { + // Hitung berapa yang benar-benar baru + const count = (isNewBerita ? 1 : 0) + (isNewPengumuman ? 1 : 0); + setNewItemCount(count); + setHasNewContent(true); + + // Reload hanya yang berubah + if (isNewBerita) stateDashboardBerita.berita.findFirst.load(); + if (isNewPengumuman) stateDesaPengumuman.pengumuman.findFirst.load(); + } else { + // Jika ini adalah pertama kali (masih null), simpan ID tanpa notifikasi + if (lastBeritaId.current === null && berita) { + lastBeritaId.current = berita.id; + localStorage.setItem("lastSeenBeritaId", berita.id); + } + if (lastPengumumanId.current === null && pengumuman) { + lastPengumumanId.current = pengumuman.id; + localStorage.setItem("lastSeenPengumumanId", pengumuman.id); + } + } + } catch (err) { + console.error("Gagal cek update berita/pengumuman:", err); + } + }; + + const interval = setInterval(checkForUpdates, 30_000); + return () => clearInterval(interval); + }, []); const newsData = useMemo(() => { const items = []; @@ -55,8 +119,8 @@ export default function Page() { content: String(featured.data.content || ""), timestamp: featured.data.createdAt ? (typeof featured.data.createdAt === 'string' - ? featured.data.createdAt - : new Date(featured.data.createdAt).toISOString()) + ? featured.data.createdAt + : new Date(featured.data.createdAt).toISOString()) : new Date().toISOString(), }); } @@ -69,8 +133,8 @@ export default function Page() { content: String(pengumuman.data.content || ""), timestamp: pengumuman.data.createdAt ? (typeof pengumuman.data.createdAt === 'string' - ? pengumuman.data.createdAt - : new Date(pengumuman.data.createdAt).toISOString()) + ? pengumuman.data.createdAt + : new Date(pengumuman.data.createdAt).toISOString()) : new Date().toISOString(), }); } @@ -78,14 +142,17 @@ export default function Page() { return items; }, [featured.data, pengumuman.data]); + const handleSeen = () => { + setHasNewContent(false); + setNewItemCount(0); + // Simpan ke localStorage saat dilihat + if (featured.data?.id) localStorage.setItem("lastSeenBeritaId", featured.data.id); + if (pengumuman.data?.id) localStorage.setItem("lastSeenPengumumanId", pengumuman.data.id); + }; return ( - - {/* HAPUS RUNNING TEXT, GANTI DENGAN MODERN NOTIFICATION */} + @@ -97,13 +164,15 @@ export default function Page() { - {/* Tombol Scroll ke Atas */} - + );