diff --git a/public/mangupuraaward.jpeg b/public/mangupuraaward.jpeg new file mode 100644 index 00000000..22606461 Binary files /dev/null and b/public/mangupuraaward.jpeg differ diff --git a/src/app/api/check-update/route.ts b/src/app/api/check-update/route.ts deleted file mode 100644 index 167f90a8..00000000 --- a/src/app/api/check-update/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -// 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/api/news/latest/route.ts b/src/app/api/news/latest/route.ts new file mode 100644 index 00000000..a38a1fe6 --- /dev/null +++ b/src/app/api/news/latest/route.ts @@ -0,0 +1,43 @@ +// app/api/news/latest/route.ts +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function GET() { + try { + const berita = await prisma.berita.findMany({ + take: 3, + orderBy: { createdAt: "desc" }, + include: { kategoriBerita: true }, + }); + + const pengumuman = await prisma.pengumuman.findMany({ + take: 3, + orderBy: { createdAt: "desc" }, + include: { CategoryPengumuman: true }, + }); + + const news = [ + ...berita.map((b) => ({ + id: b.id, + type: "berita" as const, + title: b.judul, + content: b.content, + timestamp: b.createdAt, + kategoriBerita: b.kategoriBerita || undefined, + })), + ...pengumuman.map((p) => ({ + id: p.id, + type: "pengumuman" as const, + title: p.judul, + content: p.content, + timestamp: p.createdAt, + kategoriPengumuman: p.CategoryPengumuman || undefined, + })), + ]; + + return NextResponse.json({ success: true, news }); // ✅ ganti 'data' jadi 'news' + } catch (error) { + console.error("API Error:", error); + return NextResponse.json({ success: false, error: "Gagal memuat data" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/darmasaba/_com/ModernNewsNotification.tsx b/src/app/darmasaba/_com/ModernNewsNotification.tsx index 39701cbd..ccf338d9 100644 --- a/src/app/darmasaba/_com/ModernNewsNotification.tsx +++ b/src/app/darmasaba/_com/ModernNewsNotification.tsx @@ -1,74 +1,94 @@ "use client"; -import { useState, useEffect } from "react"; -import { Box, Paper, Text, Group, CloseButton, Badge, ActionIcon, Stack, Transition } from "@mantine/core"; +import { + ActionIcon, + Badge, + Box, + CloseButton, + Group, + Paper, + Stack, + Text, + Transition, +} from "@mantine/core"; import { IconBell, IconChevronRight } from "@tabler/icons-react"; import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; -interface NewsItem { - id: string | number; +// === Tipe yang bisa diimpor di tempat lain === +export interface KategoriBerita { + id: string; + name: string; +} + +export interface KategoriPengumuman { + id: string; + name: string; +} + +export interface NewsItem { + id: string; type: "berita" | "pengumuman"; title: string; content: string; timestamp?: string | Date; + kategoriBerita?: KategoriBerita; + kategoriPengumuman?: KategoriPengumuman; } -interface ModernNewsNotificationProps { +export interface ModernNewsNotificationProps { news: NewsItem[]; - hasNewContent?: boolean; // ✅ TAMBAHAN - newItemCount?: number; // ← tambahkan ini - onSeen?: () => void; // ✅ TAMBAHAN + hasNewContent?: boolean; + newItemCount?: number; + onSeen?: () => void; autoShowDelay?: number; } +// === Helper === function stripHtml(html: string): string { return html - .replace(/<[^>]+>/g, '') - .replace(/ /gi, ' ') - .replace(/&/gi, '&') - .replace(/\s+/g, ' ') + .replace(/<[^>]+>/g, "") + .replace(/ /gi, " ") + .replace(/&/gi, "&") + .replace(/\s+/g, " ") .trim(); } +// === Komponen Utama === export default function ModernNewsNotification({ news = [], hasNewContent = false, - newItemCount = 0, // 👈 tambahkan ini + newItemCount = 0, onSeen, autoShowDelay = 2000, }: ModernNewsNotificationProps) { const router = useRouter(); + const pathname = usePathname(); + const [toastVisible, setToastVisible] = useState(false); const [widgetOpen, setWidgetOpen] = useState(false); const [hasNewNotifications, setHasNewNotifications] = useState(hasNewContent); const [hasShownToast, setHasShownToast] = useState(false); const [iconVisible, setIconVisible] = useState(true); - const pathname = usePathname(); - // Sinkronisasi dari luar + // Sinkronisasi prop eksternal useEffect(() => { - if (hasNewContent) { - setHasNewNotifications(true); - // Jangan otomatis tampilkan toast di sini — biarkan saat page load saja - } + setHasNewNotifications(hasNewContent); }, [hasNewContent]); - // Auto show toast hanya saat page pertama kali load + // Tampilkan toast pertama kali 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?.(); - } + if (hasNewNotifications) onSeen?.(); }, autoShowDelay); return () => clearTimeout(timer); } }, [news.length, autoShowDelay, toastVisible, hasShownToast, hasNewNotifications, onSeen]); - // Auto hide toast + // Sembunyikan toast otomatis useEffect(() => { if (toastVisible) { const timer = setTimeout(() => setToastVisible(false), 8000); @@ -76,7 +96,7 @@ export default function ModernNewsNotification({ } }, [toastVisible]); - // Scroll handler + // Kontrol visibilitas ikon saat scroll useEffect(() => { let lastScrollY = window.scrollY; const HIDE_THRESHOLD = 100; @@ -84,11 +104,11 @@ export default function ModernNewsNotification({ const handleScroll = () => { const currentScrollY = window.scrollY; - const scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up'; + const scrollDirection = currentScrollY > lastScrollY ? "down" : "up"; - if (scrollDirection === 'down' && currentScrollY > HIDE_THRESHOLD) { + if (scrollDirection === "down" && currentScrollY > HIDE_THRESHOLD) { setIconVisible(false); - } else if (scrollDirection === 'up' && currentScrollY < SHOW_THRESHOLD) { + } else if (scrollDirection === "up" && currentScrollY < SHOW_THRESHOLD) { setIconVisible(true); } @@ -99,19 +119,25 @@ export default function ModernNewsNotification({ lastScrollY = currentScrollY; }; - window.addEventListener('scroll', handleScroll, { passive: true }); - return () => window.removeEventListener('scroll', handleScroll); + window.addEventListener("scroll", handleScroll, { passive: true }); + return () => window.removeEventListener("scroll", handleScroll); }, [toastVisible]); const currentNews = news[0]; + // 🔗 Arahkan ke detail dengan kategori aman const handleNotificationClick = (item: NewsItem) => { setWidgetOpen(false); - onSeen?.(); // ✅ tandai sebagai dilihat + onSeen?.(); + if (item.type === "berita") { - router.push("/darmasaba/desa/berita/semua"); + const kategori = item.kategoriBerita?.name || "umum"; + const safeKategori = encodeURIComponent(kategori); + router.push(`/darmasaba/desa/berita/${safeKategori}/${item.id}`); } else if (item.type === "pengumuman") { - router.push("/darmasaba/desa/pengumuman"); + const kategori = item.kategoriPengumuman?.name || "umum"; + const safeKategori = encodeURIComponent(kategori); + router.push(`/darmasaba/desa/pengumuman/${safeKategori}/${item.id}`); } }; @@ -119,35 +145,40 @@ export default function ModernNewsNotification({ setToastVisible(false); setWidgetOpen(true); setHasNewNotifications(false); - onSeen?.(); // ✅ + onSeen?.(); }; const handleDismissToast = (e: React.MouseEvent) => { e.stopPropagation(); setToastVisible(false); - onSeen?.(); // ✅ + onSeen?.(); }; - // Only show on landing page - if (pathname !== '/darmasaba') { - return null; - } + // Hanya tampilkan di landing page + if (pathname !== "/darmasaba") return null; return ( <> {/* Floating Bell Icon */} {(transitionStyles) => ( - + { - setWidgetOpen(!widgetOpen); + setWidgetOpen((open) => !open); setHasNewNotifications(false); - onSeen?.(); // ✅ + onSeen?.(); }} style={{ width: "60px", @@ -168,7 +199,6 @@ export default function ModernNewsNotification({ right: "6px", minWidth: "22px", height: "22px", - padding: "0 6px", display: "flex", alignItems: "center", justifyContent: "center", @@ -208,12 +238,14 @@ export default function ModernNewsNotification({ - Berita & Pengumuman + + Berita & Pengumuman + { setWidgetOpen(false); - onSeen?.(); // ✅ + onSeen?.(); }} variant="transparent" c="white" @@ -224,13 +256,15 @@ export default function ModernNewsNotification({ {news.length === 0 ? ( - Tidak ada berita terbaru + + Tidak ada berita terbaru + ) : ( - {news.map((item, index) => ( + {news.map((item) => ( {/* Toast Notification */} - + {(styles) => ( - {currentNews?.type === "berita" ? "Berita Terbaru" : "Pengumuman"} + {currentNews?.type === "berita" + ? "Berita Terbaru" + : "Pengumuman"} @@ -329,7 +369,7 @@ export default function ModernNewsNotification({ - {news.length > 1 ? `${news.length} berita tersedia` : '1 berita'} + {news.length > 1 ? `${news.length} berita tersedia` : "1 berita"} setShowVideo(true)} style={{ - backgroundImage: "url('/assets/images/award-poster.jpg')", + backgroundImage: "url('/mangupuraaward.jpeg')", backgroundSize: 'cover', backgroundPosition: 'center', cursor: 'pointer', diff --git a/src/app/darmasaba/page.tsx b/src/app/darmasaba/page.tsx index 337dfa1f..fcee95fe 100644 --- a/src/app/darmasaba/page.tsx +++ b/src/app/darmasaba/page.tsx @@ -15,15 +15,17 @@ import Apbdes from "./_com/main-page/apbdes"; import Prestasi from "./_com/main-page/prestasi"; import ScrollToTopButton from "./_com/scrollToTopButton"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useSnapshot } from "valtio"; import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita"; import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman"; import NewsReaderLanding from "./_com/NewsReaderalanding"; import ModernNewsNotification from "./_com/ModernNewsNotification"; +import type { NewsItem } from "./_com/ModernNewsNotification"; // pastikan tipe ini diekspor export default function Page() { + // Tetap gunakan Valtio untuk card utama (NewsReaderLanding) const snap1 = useSnapshot(stateDashboardBerita.berita.findFirst); const snap2 = useSnapshot(stateDesaPengumuman.pengumuman.findFirst); const featured = snap1; @@ -31,13 +33,15 @@ export default function Page() { const loadingFeatured = featured.loading; const loadingPengumuman = pengumuman.loading; + // State untuk notifikasi + const [notificationNews, setNotificationNews] = useState([]); const [hasNewContent, setHasNewContent] = useState(false); const [newItemCount, setNewItemCount] = useState(0); const lastBeritaId = useRef(null); const lastPengumumanId = useRef(null); - // 🔁 Inisialisasi dari localStorage saat mount + // Inisialisasi dari localStorage useEffect(() => { const savedBerita = localStorage.getItem("lastSeenBeritaId"); const savedPengumuman = localStorage.getItem("lastSeenPengumumanId"); @@ -45,13 +49,7 @@ export default function Page() { 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 + // Load data utama (untuk card) useEffect(() => { if (!featured.data && !loadingFeatured) { stateDashboardBerita.berita.findFirst.load(); @@ -64,91 +62,64 @@ 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(); + // 🔁 Fetch berita & pengumuman lengkap untuk notifikasi + const fetchNotificationData = async () => { + try { + const res = await fetch("/api/news/latest"); + const result = await res.json(); + if (result.success && Array.isArray(result.news)) { + const news = result.news as NewsItem[]; - if (!result.success) return; + // Ambil ID terbaru + const latestBerita = news.find((n) => n.type === "berita"); + const latestPengumuman = news.find((n) => n.type === "pengumuman"); - const { berita, pengumuman } = result.data; + const isNewBerita = latestBerita && lastBeritaId.current !== null && latestBerita.id !== lastBeritaId.current; + const isNewPengumuman = latestPengumuman && lastPengumumanId.current !== null && latestPengumuman.id !== lastPengumumanId.current; - // 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; + // Simpan ID terbaru ke ref + if (latestBerita) lastBeritaId.current = (latestBerita.id); + if (latestPengumuman) lastPengumumanId.current = (latestPengumuman.id); - 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(); + // Jika ini bukan inisialisasi pertama, tampilkan notifikasi + if (lastBeritaId.current !== null || lastPengumumanId.current !== null) { + if (isNewBerita || isNewPengumuman) { + const count = (isNewBerita ? 1 : 0) + (isNewPengumuman ? 1 : 0); + setNewItemCount(count); + setHasNewContent(true); + } } 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); - } + // Simpan ke localStorage saat pertama kali + if (latestBerita) localStorage.setItem("lastSeenBeritaId", (latestBerita.id)); + if (latestPengumuman) localStorage.setItem("lastSeenPengumumanId", (latestPengumuman.id)); } - } catch (err) { - console.error("Gagal cek update berita/pengumuman:", err); - } - }; - const interval = setInterval(checkForUpdates, 30_000); + setNotificationNews(news); + } + } catch (err) { + console.error("Gagal fetch data notifikasi:", err); + } + }; + + // Load data notifikasi pertama kali + useEffect(() => { + fetchNotificationData(); + }, []); + + // Polling setiap 30 detik + useEffect(() => { + const interval = setInterval(fetchNotificationData, 30_000); return () => clearInterval(interval); }, []); - const newsData = useMemo(() => { - const items = []; - - if (featured.data) { - items.push({ - id: String(featured.data.id || "berita-1"), - type: "berita" as const, - title: String(featured.data.judul || "Berita Terbaru"), - content: String(featured.data.content || ""), - timestamp: featured.data.createdAt - ? (typeof featured.data.createdAt === 'string' - ? featured.data.createdAt - : new Date(featured.data.createdAt).toISOString()) - : new Date().toISOString(), - }); - } - - if (pengumuman.data) { - items.push({ - id: String(pengumuman.data.id || "pengumuman-1"), - type: "pengumuman" as const, - title: String(pengumuman.data.judul || "Pengumuman Penting"), - content: String(pengumuman.data.content || ""), - timestamp: pengumuman.data.createdAt - ? (typeof pengumuman.data.createdAt === 'string' - ? pengumuman.data.createdAt - : new Date(pengumuman.data.createdAt).toISOString()) - : new Date().toISOString(), - }); - } - - 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); - }; + setHasNewContent(false); + setNewItemCount(0); + const latestBerita = notificationNews.find(n => n.type === "berita"); + const latestPengumuman = notificationNews.find(n => n.type === "pengumuman"); + if (latestBerita) localStorage.setItem("lastSeenBeritaId", String(latestBerita.id)); + if (latestPengumuman) localStorage.setItem("lastSeenPengumumanId", String(latestPengumuman.id)); +}; return ( @@ -168,7 +139,7 @@ export default function Page() {