Semua tooltips di admin sudah dihilangkan
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, Paper, Text, Group, CloseButton, Badge, ActionIcon, Stack, Transition } from "@mantine/core";
|
||||
import { IconBell, IconChevronRight } from "@tabler/icons-react";
|
||||
import { useRouter } from "next/navigation"; // 👉 tambahkan ini
|
||||
import { usePathname, useRouter } from "next/navigation"; // 👉 tambahkan ini
|
||||
|
||||
interface NewsItem {
|
||||
id: string | number;
|
||||
@@ -27,9 +27,9 @@ function stripHtml(html: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
export default function ModernNewsNotification({
|
||||
export default function ModernNewsNotification({
|
||||
news = [],
|
||||
autoShowDelay = 2000
|
||||
autoShowDelay = 2000
|
||||
}: ModernNewsNotificationProps) {
|
||||
const router = useRouter(); // 👉 router Next.js
|
||||
const [toastVisible, setToastVisible] = useState(false);
|
||||
@@ -37,6 +37,9 @@ export default function ModernNewsNotification({
|
||||
const [hasNewNotifications, setHasNewNotifications] = useState(true);
|
||||
const [hasShownToast, setHasShownToast] = useState(false);
|
||||
const [iconVisible, setIconVisible] = useState(true);
|
||||
const pathname = usePathname();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (news.length > 0 && !toastVisible && !hasShownToast) {
|
||||
@@ -57,25 +60,32 @@ export default function ModernNewsNotification({
|
||||
}
|
||||
}, [toastVisible]);
|
||||
|
||||
// Ganti useEffect scroll yang lama dengan versi berikut:
|
||||
|
||||
useEffect(() => {
|
||||
let lastScrollY = window.scrollY;
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
|
||||
// Kontrol ikon lonceng
|
||||
if (currentScrollY > lastScrollY && currentScrollY > 100) {
|
||||
setIconVisible(false);
|
||||
}
|
||||
else if (currentScrollY < lastScrollY) {
|
||||
} else if (currentScrollY < lastScrollY) {
|
||||
setIconVisible(true);
|
||||
}
|
||||
|
||||
|
||||
// 🔴 BARU: Sembunyikan toast saat scroll ke bawah melewati 150px
|
||||
if (currentScrollY > 150 && toastVisible) {
|
||||
setToastVisible(false);
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
}, [toastVisible]); // 👈 tambahkan toastVisible sebagai dependency
|
||||
|
||||
const currentNews = news[0];
|
||||
|
||||
@@ -95,6 +105,11 @@ export default function ModernNewsNotification({
|
||||
setHasNewNotifications(false);
|
||||
};
|
||||
|
||||
// Ganti dengan path landing page Anda
|
||||
if (pathname !== '/darmasaba') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition mounted={iconVisible} transition="slide-down" duration={200}>
|
||||
@@ -174,7 +189,7 @@ export default function ModernNewsNotification({
|
||||
<IconBell size={20} />
|
||||
<Text c={"white"} fw={600} size="md">Berita & Pengumuman</Text>
|
||||
</Group>
|
||||
<CloseButton
|
||||
<CloseButton
|
||||
onClick={() => setWidgetOpen(false)}
|
||||
variant="transparent"
|
||||
c="white"
|
||||
@@ -283,16 +298,16 @@ export default function ModernNewsNotification({
|
||||
>
|
||||
{currentNews?.type === "berita" ? "Berita Terbaru" : "Pengumuman"}
|
||||
</Badge>
|
||||
<CloseButton
|
||||
<CloseButton
|
||||
onClick={() => setToastVisible(false)}
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
|
||||
|
||||
<Text fw={600} size="sm" mb={6}>
|
||||
{currentNews?.title || "Informasi Terbaru"}
|
||||
</Text>
|
||||
|
||||
|
||||
<Text size="xs" c="dimmed" lineClamp={3}>
|
||||
{stripHtml(currentNews?.content || "")}
|
||||
</Text>
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Box } from "@mantine/core";
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
interface RunningTextProps {
|
||||
news?: string[];
|
||||
speed?: number; // dalam detik (jika mau manual)
|
||||
autoSpeed?: boolean; // otomatis sesuaikan speed dengan panjang text
|
||||
bgColor?: string;
|
||||
textColor?: string;
|
||||
maxLength?: number; // max karakter per item
|
||||
}
|
||||
|
||||
// Utility function untuk strip HTML (works on both server and client)
|
||||
function stripHtmlTags(html: string): string {
|
||||
const text = html
|
||||
.replace(/<style[^>]*>.*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'/gi, "'")
|
||||
.replace(/’/gi, "'")
|
||||
.replace(/—/gi, '—')
|
||||
.replace(/–/gi, '–')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export default function RunningText({
|
||||
news = [
|
||||
"Selamat datang di Portal Desa Darmasaba",
|
||||
"Jam operasional kantor: Senin - Jumat 08:00 - 17:00",
|
||||
],
|
||||
speed = 20,
|
||||
autoSpeed = true,
|
||||
bgColor = "#1e5a7e",
|
||||
textColor = "white",
|
||||
maxLength = 200 // default max 200 karakter per item
|
||||
}: RunningTextProps) {
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
// Process news data
|
||||
const processedNews = useMemo(() => {
|
||||
return news
|
||||
.filter(item => item && item.trim() !== "")
|
||||
.map(item => {
|
||||
let text = stripHtmlTags(item);
|
||||
// Limit panjang per item
|
||||
if (text.length > maxLength) {
|
||||
text = text.substring(0, maxLength) + "...";
|
||||
}
|
||||
return text;
|
||||
})
|
||||
.filter(item => item.length > 0);
|
||||
}, [news, maxLength]);
|
||||
|
||||
const allNews = processedNews.length > 0
|
||||
? processedNews.join(" • ")
|
||||
: "Tidak ada pengumuman";
|
||||
|
||||
// Hitung speed berdasarkan mode
|
||||
const calculatedSpeed = useMemo(() => {
|
||||
if (!autoSpeed) {
|
||||
return speed; // Gunakan speed manual
|
||||
}
|
||||
|
||||
// Auto speed: berdasarkan panjang text
|
||||
const textLength = allNews.length;
|
||||
|
||||
// Formula yang lebih natural:
|
||||
// - Text pendek (< 100 char): 15 detik
|
||||
// - Text sedang (100-300 char): 20-30 detik
|
||||
// - Text panjang (> 300 char): 30-45 detik
|
||||
let calculatedTime;
|
||||
|
||||
if (textLength < 100) {
|
||||
calculatedTime = 15;
|
||||
} else if (textLength < 300) {
|
||||
calculatedTime = 15 + ((textLength - 100) / 200) * 15; // 15-30 detik
|
||||
} else {
|
||||
calculatedTime = 30 + Math.min(((textLength - 300) / 500) * 15, 15); // 30-45 detik max
|
||||
}
|
||||
|
||||
return Math.round(calculatedTime);
|
||||
}, [allNews, speed, autoSpeed]);
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
padding: "12px 0",
|
||||
borderBottom: "2px solid rgba(255, 255, 255, 0.1)"
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
whiteSpace: "nowrap"
|
||||
}}>
|
||||
<IconBell size={20} color={textColor} style={{ flexShrink: 0 }} />
|
||||
<span style={{
|
||||
color: textColor,
|
||||
fontSize: "15px",
|
||||
fontWeight: 500,
|
||||
whiteSpace: "nowrap"
|
||||
}}>
|
||||
Memuat pengumuman...
|
||||
</span>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
padding: "12px 0",
|
||||
borderBottom: "2px solid rgba(255, 255, 255, 0.1)"
|
||||
}}
|
||||
>
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes scrollText {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.running-text-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
animation: scrollText ${calculatedSpeed}s linear infinite;
|
||||
}
|
||||
|
||||
.running-text-wrapper:hover {
|
||||
animation-play-state: paused;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.running-text-content {
|
||||
color: ${textColor};
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
}} />
|
||||
|
||||
<div className="running-text-wrapper">
|
||||
<IconBell size={20} color={textColor} style={{ flexShrink: 0 }} />
|
||||
<span className="running-text-content">
|
||||
{allNews}
|
||||
</span>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -91,9 +91,9 @@ function Apbdes() {
|
||||
if (value >= 1_000_000_000)
|
||||
return `Rp ${(value / 1_000_000_000).toFixed(1)} M`;
|
||||
if (value >= 1_000_000)
|
||||
return `Rp ${(value / 1_000_000).toFixed(1)} Jt`;
|
||||
return `Rp ${(value / 1_000_000).toFixed(1)} JT`;
|
||||
if (value >= 1_000)
|
||||
return `Rp ${(value / 1_000).toFixed(1)} Rb`;
|
||||
return `Rp ${(value / 1_000).toFixed(1)} RB`;
|
||||
return `Rp ${value}`;
|
||||
}}
|
||||
series={[
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function parseJumlah(value: string): number {
|
||||
if (cleaned.includes("T")) return num * 1_000_000_000_000;
|
||||
if (cleaned.includes("M")) return num * 1_000_000_000;
|
||||
if (cleaned.includes("JT")) return num * 1_000_000;
|
||||
if (cleaned.includes("K")) return num * 1_000;
|
||||
if (cleaned.includes("RB")) return num * 1_000;
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,14 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import ModuleView from "./ModuleView";
|
||||
import ProfileView from "./ProfileView";
|
||||
import SosmedView from "./SosmedView";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
|
||||
import stateDesaPengumuman from "@/app/admin/(dashboard)/_state/desa/pengumuman";
|
||||
import ModernNewsNotification from "../../ModernNeewsNotification";
|
||||
|
||||
const getDayOfWeek = () => {
|
||||
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
|
||||
@@ -68,6 +72,58 @@ function LandingPage() {
|
||||
>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const featured = useProxy(stateDashboardBerita.berita.findFirst);
|
||||
const loadingFeatured = featured.loading;
|
||||
const pengumuman = useProxy(stateDesaPengumuman.pengumuman.findFirst);
|
||||
const loadingPengumuman = pengumuman.loading;
|
||||
|
||||
useEffect(() => {
|
||||
if (!featured.data && !loadingFeatured) {
|
||||
stateDashboardBerita.berita.findFirst.load();
|
||||
}
|
||||
}, [featured.data, loadingFeatured]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pengumuman.data && !loadingPengumuman) {
|
||||
stateDesaPengumuman.pengumuman.findFirst.load();
|
||||
}
|
||||
}, [pengumuman.data, loadingPengumuman]);
|
||||
|
||||
// Transform data untuk notification system
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSocialMedia = async () => {
|
||||
try {
|
||||
@@ -215,6 +271,12 @@ function LandingPage() {
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* Modern Notification System */}
|
||||
<ModernNewsNotification
|
||||
news={newsData}
|
||||
autoShowDelay={2000} // Muncul 2 detik setelah load
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user