Fix Notifikasi saat ada berita atau pengumuman baru, notifikasi baru muncul. Ga setiap masuk landing page ada notifikasi
This commit is contained in:
@@ -828,11 +828,11 @@ model DokterdanTenagaMedis {
|
|||||||
name String
|
name String
|
||||||
specialist String
|
specialist String
|
||||||
jadwal String
|
jadwal String
|
||||||
jadwalLibur String
|
jadwalLibur String?
|
||||||
jamBukaOperasional String
|
jamBukaOperasional String?
|
||||||
jamTutupOperasional String
|
jamTutupOperasional String?
|
||||||
jamBukaLibur String
|
jamBukaLibur String?
|
||||||
jamTutupLibur String
|
jamTutupLibur String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
|
|||||||
36
src/app/api/check-update/route.ts
Normal file
36
src/app/api/check-update/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ interface NewsItem {
|
|||||||
|
|
||||||
interface ModernNewsNotificationProps {
|
interface ModernNewsNotificationProps {
|
||||||
news: NewsItem[];
|
news: NewsItem[];
|
||||||
|
hasNewContent?: boolean; // ✅ TAMBAHAN
|
||||||
|
newItemCount?: number; // ← tambahkan ini
|
||||||
|
onSeen?: () => void; // ✅ TAMBAHAN
|
||||||
autoShowDelay?: number;
|
autoShowDelay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,57 +32,66 @@ function stripHtml(html: string): string {
|
|||||||
|
|
||||||
export default function ModernNewsNotification({
|
export default function ModernNewsNotification({
|
||||||
news = [],
|
news = [],
|
||||||
autoShowDelay = 2000
|
hasNewContent = false,
|
||||||
|
newItemCount = 0, // 👈 tambahkan ini
|
||||||
|
onSeen,
|
||||||
|
autoShowDelay = 2000,
|
||||||
}: ModernNewsNotificationProps) {
|
}: ModernNewsNotificationProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [toastVisible, setToastVisible] = useState(false);
|
const [toastVisible, setToastVisible] = useState(false);
|
||||||
const [widgetOpen, setWidgetOpen] = useState(false);
|
const [widgetOpen, setWidgetOpen] = useState(false);
|
||||||
const [hasNewNotifications, setHasNewNotifications] = useState(true);
|
const [hasNewNotifications, setHasNewNotifications] = useState(hasNewContent);
|
||||||
const [hasShownToast, setHasShownToast] = useState(false);
|
const [hasShownToast, setHasShownToast] = useState(false);
|
||||||
const [iconVisible, setIconVisible] = useState(true);
|
const [iconVisible, setIconVisible] = useState(true);
|
||||||
const pathname = usePathname();
|
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(() => {
|
useEffect(() => {
|
||||||
if (news.length > 0 && !toastVisible && !hasShownToast) {
|
if (news.length > 0 && !toastVisible && !hasShownToast) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setToastVisible(true);
|
setToastVisible(true);
|
||||||
setHasShownToast(true);
|
setHasShownToast(true);
|
||||||
|
// Jika ada new content, anggap sudah "dilihat" setelah toast muncul
|
||||||
|
if (hasNewNotifications) {
|
||||||
|
onSeen?.();
|
||||||
|
}
|
||||||
}, autoShowDelay);
|
}, autoShowDelay);
|
||||||
return () => clearTimeout(timer);
|
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(() => {
|
useEffect(() => {
|
||||||
if (toastVisible) {
|
if (toastVisible) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => setToastVisible(false), 8000);
|
||||||
setToastVisible(false);
|
|
||||||
}, 8000);
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [toastVisible]);
|
}, [toastVisible]);
|
||||||
|
|
||||||
// Enhanced scroll handler with better thresholds
|
// Scroll handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastScrollY = window.scrollY;
|
let lastScrollY = window.scrollY;
|
||||||
const HIDE_THRESHOLD = 100; // Mulai hide saat scroll > 100px
|
const HIDE_THRESHOLD = 100;
|
||||||
const SHOW_THRESHOLD = 50; // Hanya show ketika benar-benar di atas (< 50px)
|
const SHOW_THRESHOLD = 50;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const currentScrollY = window.scrollY;
|
const currentScrollY = window.scrollY;
|
||||||
const scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
|
const scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
|
||||||
|
|
||||||
// Logic untuk hide/show icon
|
|
||||||
if (scrollDirection === 'down' && currentScrollY > HIDE_THRESHOLD) {
|
if (scrollDirection === 'down' && currentScrollY > HIDE_THRESHOLD) {
|
||||||
// Scroll ke bawah dan sudah melewati threshold → hide
|
|
||||||
setIconVisible(false);
|
setIconVisible(false);
|
||||||
} else if (scrollDirection === 'up' && currentScrollY < SHOW_THRESHOLD) {
|
} else if (scrollDirection === 'up' && currentScrollY < SHOW_THRESHOLD) {
|
||||||
// Scroll ke atas dan sudah di posisi paling atas → show
|
|
||||||
setIconVisible(true);
|
setIconVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide toast saat scroll ke bawah melewati 150px
|
|
||||||
if (currentScrollY > 150 && toastVisible) {
|
if (currentScrollY > 150 && toastVisible) {
|
||||||
setToastVisible(false);
|
setToastVisible(false);
|
||||||
}
|
}
|
||||||
@@ -93,9 +105,9 @@ export default function ModernNewsNotification({
|
|||||||
|
|
||||||
const currentNews = news[0];
|
const currentNews = news[0];
|
||||||
|
|
||||||
// Handle notification click
|
|
||||||
const handleNotificationClick = (item: NewsItem) => {
|
const handleNotificationClick = (item: NewsItem) => {
|
||||||
setWidgetOpen(false);
|
setWidgetOpen(false);
|
||||||
|
onSeen?.(); // ✅ tandai sebagai dilihat
|
||||||
if (item.type === "berita") {
|
if (item.type === "berita") {
|
||||||
router.push("/darmasaba/desa/berita/semua");
|
router.push("/darmasaba/desa/berita/semua");
|
||||||
} else if (item.type === "pengumuman") {
|
} else if (item.type === "pengumuman") {
|
||||||
@@ -107,6 +119,13 @@ export default function ModernNewsNotification({
|
|||||||
setToastVisible(false);
|
setToastVisible(false);
|
||||||
setWidgetOpen(true);
|
setWidgetOpen(true);
|
||||||
setHasNewNotifications(false);
|
setHasNewNotifications(false);
|
||||||
|
onSeen?.(); // ✅
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDismissToast = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setToastVisible(false);
|
||||||
|
onSeen?.(); // ✅
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only show on landing page
|
// Only show on landing page
|
||||||
@@ -119,14 +138,7 @@ export default function ModernNewsNotification({
|
|||||||
{/* Floating Bell Icon */}
|
{/* Floating Bell Icon */}
|
||||||
<Transition mounted={iconVisible} transition="slide-down" duration={200}>
|
<Transition mounted={iconVisible} transition="slide-down" duration={200}>
|
||||||
{(transitionStyles) => (
|
{(transitionStyles) => (
|
||||||
<Box
|
<Box style={{ ...transitionStyles, position: "fixed", bottom: "24px", right: "24px" }}>
|
||||||
style={{
|
|
||||||
...transitionStyles,
|
|
||||||
position: "fixed",
|
|
||||||
bottom: "24px",
|
|
||||||
right: "24px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
size="xl"
|
size="xl"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
@@ -135,6 +147,7 @@ export default function ModernNewsNotification({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setWidgetOpen(!widgetOpen);
|
setWidgetOpen(!widgetOpen);
|
||||||
setHasNewNotifications(false);
|
setHasNewNotifications(false);
|
||||||
|
onSeen?.(); // ✅
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: "60px",
|
width: "60px",
|
||||||
@@ -146,20 +159,23 @@ export default function ModernNewsNotification({
|
|||||||
<IconBell size={28} />
|
<IconBell size={28} />
|
||||||
{hasNewNotifications && news.length > 0 && (
|
{hasNewNotifications && news.length > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="red"
|
color="red"
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "6px",
|
top: "6px",
|
||||||
right: "6px",
|
right: "6px",
|
||||||
minWidth: "22px",
|
minWidth: "22px",
|
||||||
height: "22px",
|
height: "22px",
|
||||||
padding: "0 6px",
|
padding: "0 6px",
|
||||||
}}
|
display: "flex",
|
||||||
>
|
alignItems: "center",
|
||||||
{news.length}
|
justifyContent: "center",
|
||||||
</Badge>
|
}}
|
||||||
|
>
|
||||||
|
{newItemCount || news.length}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -195,20 +211,17 @@ export default function ModernNewsNotification({
|
|||||||
<Text c="white" fw={600} size="md">Berita & Pengumuman</Text>
|
<Text c="white" fw={600} size="md">Berita & Pengumuman</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<CloseButton
|
<CloseButton
|
||||||
onClick={() => setWidgetOpen(false)}
|
onClick={() => {
|
||||||
|
setWidgetOpen(false);
|
||||||
|
onSeen?.(); // ✅
|
||||||
|
}}
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
c="white"
|
c="white"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}>
|
||||||
style={{
|
|
||||||
maxHeight: "400px",
|
|
||||||
overflowY: "auto",
|
|
||||||
padding: "12px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{news.length === 0 ? (
|
{news.length === 0 ? (
|
||||||
<Box p="xl" style={{ textAlign: "center" }}>
|
<Box p="xl" style={{ textAlign: "center" }}>
|
||||||
<Text c="dimmed" size="sm">Tidak ada berita terbaru</Text>
|
<Text c="dimmed" size="sm">Tidak ada berita terbaru</Text>
|
||||||
@@ -303,13 +316,7 @@ export default function ModernNewsNotification({
|
|||||||
>
|
>
|
||||||
{currentNews?.type === "berita" ? "Berita Terbaru" : "Pengumuman"}
|
{currentNews?.type === "berita" ? "Berita Terbaru" : "Pengumuman"}
|
||||||
</Badge>
|
</Badge>
|
||||||
<CloseButton
|
<CloseButton onClick={handleDismissToast} size="sm" />
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setToastVisible(false);
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Text fw={600} size="sm" mb={6}>
|
<Text fw={600} size="sm" mb={6}>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
|
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
|
||||||
import { Stack, Box, Container, Button, Text, Loader, Paper } from "@mantine/core";
|
import { Stack, Box, Container, Button, Text, Loader, Paper, Center, ActionIcon } from "@mantine/core";
|
||||||
import { IconAward, IconArrowRight } from "@tabler/icons-react";
|
import { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react";
|
||||||
import { useTransitionRouter } from 'next-view-transitions';
|
import { useTransitionRouter } from 'next-view-transitions';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
@@ -13,6 +13,18 @@ function Penghargaan() {
|
|||||||
const state = useProxy(penghargaanState);
|
const state = useProxy(penghargaanState);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
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(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -31,22 +43,36 @@ function Penghargaan() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }}>
|
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }}>
|
||||||
<video
|
{showVideo ? (
|
||||||
loop
|
<video
|
||||||
autoPlay
|
autoPlay
|
||||||
muted
|
muted
|
||||||
style={{
|
loop
|
||||||
width: "100%",
|
playsInline
|
||||||
height: "100%",
|
webkit-playsinline="true"
|
||||||
objectFit: "cover",
|
onLoadedData={() => setIsVideoLoaded(true)}
|
||||||
position: "absolute",
|
style={{ opacity: isVideoLoaded ? 1 : 0, transition: 'opacity 0.5s' }}
|
||||||
top: 0,
|
>
|
||||||
left: 0,
|
<source src="/assets/videos/award.mp4" type="video/mp4" />
|
||||||
zIndex: 0,
|
</video>
|
||||||
}}
|
) : (
|
||||||
>
|
// Fallback: tampilkan poster + play button
|
||||||
<source src="/assets/videos/award.mp4" type="video/mp4" />
|
<Box
|
||||||
</video>
|
onClick={() => setShowVideo(true)}
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/assets/images/award-poster.jpg')",
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Center h="100%">
|
||||||
|
<ActionIcon size="lg" radius="xl" color="white">
|
||||||
|
<IconPlayerPlay size={32} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import DesaAntiKorupsi from "@/app/darmasaba/_com/main-page/desaantikorupsi";
|
import DesaAntiKorupsi from "@/app/darmasaba/_com/main-page/desaantikorupsi";
|
||||||
import Kepuasan from "@/app/darmasaba/_com/main-page/kepuasan";
|
import Kepuasan from "@/app/darmasaba/_com/main-page/kepuasan";
|
||||||
import LandingPage from "@/app/darmasaba/_com/main-page/landing-page";
|
import LandingPage from "@/app/darmasaba/_com/main-page/landing-page";
|
||||||
@@ -14,23 +15,43 @@ import Apbdes from "./_com/main-page/apbdes";
|
|||||||
import Prestasi from "./_com/main-page/prestasi";
|
import Prestasi from "./_com/main-page/prestasi";
|
||||||
import ScrollToTopButton from "./_com/scrollToTopButton";
|
import ScrollToTopButton from "./_com/scrollToTopButton";
|
||||||
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita";
|
import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita";
|
||||||
import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman";
|
import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman";
|
||||||
import ModernNewsNotification from "./_com/ModernNeewsNotification";
|
|
||||||
import NewsReaderLanding from "./_com/NewsReaderalanding";
|
|
||||||
|
|
||||||
|
import NewsReaderLanding from "./_com/NewsReaderalanding";
|
||||||
|
import ModernNewsNotification from "./_com/ModernNewsNotification";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const snap1 = useSnapshot(stateDashboardBerita.berita.findFirst);
|
const snap1 = useSnapshot(stateDashboardBerita.berita.findFirst);
|
||||||
const snap2 = useSnapshot(stateDesaPengumuman.pengumuman.findFirst);
|
const snap2 = useSnapshot(stateDesaPengumuman.pengumuman.findFirst);
|
||||||
|
|
||||||
const featured = snap1;
|
const featured = snap1;
|
||||||
const pengumuman = snap2;
|
const pengumuman = snap2;
|
||||||
const loadingFeatured = featured.loading;
|
const loadingFeatured = featured.loading;
|
||||||
const loadingPengumuman = pengumuman.loading;
|
const loadingPengumuman = pengumuman.loading;
|
||||||
|
|
||||||
|
const [hasNewContent, setHasNewContent] = useState(false);
|
||||||
|
const [newItemCount, setNewItemCount] = useState(0);
|
||||||
|
|
||||||
|
const lastBeritaId = useRef<string | null>(null);
|
||||||
|
const lastPengumumanId = useRef<string | null>(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(() => {
|
useEffect(() => {
|
||||||
if (!featured.data && !loadingFeatured) {
|
if (!featured.data && !loadingFeatured) {
|
||||||
stateDashboardBerita.berita.findFirst.load();
|
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 newsData = useMemo(() => {
|
||||||
const items = [];
|
const items = [];
|
||||||
@@ -55,8 +119,8 @@ export default function Page() {
|
|||||||
content: String(featured.data.content || ""),
|
content: String(featured.data.content || ""),
|
||||||
timestamp: featured.data.createdAt
|
timestamp: featured.data.createdAt
|
||||||
? (typeof featured.data.createdAt === 'string'
|
? (typeof featured.data.createdAt === 'string'
|
||||||
? featured.data.createdAt
|
? featured.data.createdAt
|
||||||
: new Date(featured.data.createdAt).toISOString())
|
: new Date(featured.data.createdAt).toISOString())
|
||||||
: new Date().toISOString(),
|
: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,8 +133,8 @@ export default function Page() {
|
|||||||
content: String(pengumuman.data.content || ""),
|
content: String(pengumuman.data.content || ""),
|
||||||
timestamp: pengumuman.data.createdAt
|
timestamp: pengumuman.data.createdAt
|
||||||
? (typeof pengumuman.data.createdAt === 'string'
|
? (typeof pengumuman.data.createdAt === 'string'
|
||||||
? pengumuman.data.createdAt
|
? pengumuman.data.createdAt
|
||||||
: new Date(pengumuman.data.createdAt).toISOString())
|
: new Date(pengumuman.data.createdAt).toISOString())
|
||||||
: new Date().toISOString(),
|
: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -78,14 +142,17 @@ export default function Page() {
|
|||||||
return items;
|
return items;
|
||||||
}, [featured.data, pengumuman.data]);
|
}, [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 (
|
return (
|
||||||
<Box id="page-root">
|
<Box id="page-root">
|
||||||
<Stack
|
<Stack bg={colors.grey[1]} gap={0}>
|
||||||
bg={colors.grey[1]}
|
|
||||||
gap={0}
|
|
||||||
>
|
|
||||||
{/* HAPUS RUNNING TEXT, GANTI DENGAN MODERN NOTIFICATION */}
|
|
||||||
<LandingPage />
|
<LandingPage />
|
||||||
<Penghargaan />
|
<Penghargaan />
|
||||||
<Layanan />
|
<Layanan />
|
||||||
@@ -97,13 +164,15 @@ export default function Page() {
|
|||||||
<Prestasi />
|
<Prestasi />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* Tombol Scroll ke Atas */}
|
|
||||||
<ScrollToTopButton />
|
<ScrollToTopButton />
|
||||||
|
|
||||||
<NewsReaderLanding />
|
<NewsReaderLanding />
|
||||||
|
|
||||||
<ModernNewsNotification
|
<ModernNewsNotification
|
||||||
news={newsData}
|
news={newsData}
|
||||||
autoShowDelay={2000} // Muncul 2 detik setelah load
|
hasNewContent={hasNewContent}
|
||||||
|
newItemCount={newItemCount}
|
||||||
|
onSeen={handleSeen}
|
||||||
|
autoShowDelay={2000}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user