Semua tooltips di admin sudah dihilangkan

This commit is contained in:
2025-11-07 14:38:32 +08:00
parent db8909b9ed
commit 417a8937f5
195 changed files with 2479 additions and 3083 deletions

View File

@@ -1,38 +1,91 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import sdgsDesa from '@/app/admin/(dashboard)/_state/landing-page/sdgs-desa';
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, Center, Container, Image, LoadingOverlay, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { Prisma } from '@prisma/client';
import { IconMoodSad } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
function Page() {
const [sdgsDesa, setSdgsDesa] = useState<Prisma.SdgsDesaGetPayload<{ include: { image: true } }>[]>([]);
type SdgsDesa = {
id: string
name: string
jumlah: number
};
const [chartData, setChartData] = useState<SdgsDesa[]>([])
const [mounted, setMounted] = useState(false);
const state = useProxy(sdgsDesa.findManyAll)
const [loading, setLoading] = useState(true);
// Definisikan urutan goal SDGs Desa (sesuai nomor 1-18)
const sdgsOrder = [
"Desa Tanpa Kemiskinan",
"Desa Tanpa Kelaparan",
"Desa Sehat dan Sejahtera",
"Pendidikan Desa Berkualitas",
"Keterlibatan Perempuan Desa",
"Desa Layak Air Bersih dan Sanitasi",
"Desa Berenergi Bersih dan Terbarukan",
"Pertumbuhan Ekonomi Desa Merata",
"Infrastruktur dan Inovasi Desa Sesuai Kebutuhan",
"Desa Tanpa Kesenjangan",
"Kawasan Permukiman Desa Aman dan Nyaman",
"Konsumsi dan Produksi Desa Sadar Lingkungan",
"Desa Tanggap Perubahan Iklim",
"Desa Peduli Lingkungan Laut",
"Desa Peduli Lingkungan Darat",
"Desa Damai Berkeadilan",
"Kemitraan untuk Pembangunan Desa",
"Kelembagaan Desa Dinamis dan Budaya Desa Adaptif"
];
useEffect(() => {
const fetchSdgsDesa = async () => {
if (state.data) {
// Urutkan data sesuai urutan goal 1-18
const sortedData = [...state.data].sort((a, b) => {
const indexA = sdgsOrder.indexOf(a.name);
const indexB = sdgsOrder.indexOf(b.name);
return indexA - indexB;
});
setChartData(sortedData.map((item: any) => ({
id: item.id,
name: item.name,
jumlah: item.jumlah,
})));
}
}, [state.data]);
useEffect(() => {
const loadData = async () => {
try {
const response = await fetch('/api/landingpage/sdgsdesa/findMany?limit=50&page=1');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
let data = [];
if (Array.isArray(result.data)) {
data = result.data;
} else if (Array.isArray(result)) {
data = result;
} else {
console.error('Format data tidak valid:', result);
}
setSdgsDesa(data);
setMounted(true);
setLoading(true)
await state.load()
} catch (error) {
console.error('Gagal mengambil data sdgs desa:', error);
console.error('Error loading data:', error)
} finally {
setLoading(false);
setLoading(false)
}
};
fetchSdgsDesa();
}, []);
}
loadData()
}, [])
const averageScore = useMemo(() => {
if (!state.data?.length) return 0;
const total = state.data.reduce((sum: number, item: any) => {
const val = typeof item.jumlah === 'string'
? parseFloat(item.jumlah.replace(',', '.'))
: Number(item.jumlah);
return isNaN(val) ? sum : sum + val;
}, 0);
return parseFloat((total / state.data.length).toFixed(2));
}, [state.data]);
return (
<Stack pos="relative" py="xl" gap={32}>
@@ -53,13 +106,13 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<Text py={10} ta="justify" fz="md" lh={1.7}>
SDGs Desa adalah upaya terpadu pemerintah dalam percepatan pencapaian tujuan pembangunan berkelanjutan di tingkat desa.
Ini merupakan adaptasi dari SDGs global dalam konteks pembangunan desa di Indonesia, yang bertujuan menciptakan desa
SDGs Desa adalah upaya terpadu pemerintah dalam percepatan pencapaian tujuan pembangunan berkelanjutan di tingkat desa.
Ini merupakan adaptasi dari SDGs global dalam konteks pembangunan desa di Indonesia, yang bertujuan menciptakan desa
inklusif, berkelanjutan, dan tangguh menghadapi tantangan masa depan.
</Text>
<Text ta="justify" pb={20} fz="md" lh={1.7}>
Berdasarkan Permendesa Nomor 21 Tahun 2020, SDGs Desa mencakup 18 tujuan yang harus dicapai pada tahun 2030.
Tujuan-tujuan tersebut meliputi pengentasan kemiskinan, peningkatan kesehatan dan pendidikan, kesetaraan gender,
Berdasarkan Permendesa Nomor 21 Tahun 2020, SDGs Desa mencakup 18 tujuan yang harus dicapai pada tahun 2030.
Tujuan-tujuan tersebut meliputi pengentasan kemiskinan, peningkatan kesehatan dan pendidikan, kesetaraan gender,
pertumbuhan ekonomi, pembangunan infrastruktur, hingga pelestarian lingkungan.
</Text>
</Box>
@@ -67,9 +120,24 @@ function Page() {
<Box py={20} px={{ base: 'md', md: 100 }}>
<Box pos="relative" style={{ minHeight: 200 }}>
<LoadingOverlay visible={loading} overlayProps={{ blur: 2 }} />
{!loading && sdgsDesa.length > 0 ? (
{!loading && state.data && state.data.length > 0 && (
<Center mb="xl">
<Box ta="center">
<Title order={2} c={colors['blue-button']}>Rata-Rata SDGs Desa : </Title>
<Text fw={700} c={colors['blue-button']} fz="2rem" style={{ minHeight: '4rem' }}>
{averageScore} %
</Text>
<Text fs={"italic"} ta="center" fz="lg">
Diambil dari rata-rata 18 Goals SDGs Desa dari Desa Darmasaba
</Text>
</Box>
</Center>
)}
{!loading && state.data && state.data.length > 0 ? (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="xl" verticalSpacing="xl">
{sdgsDesa.map((item) => (
{state.data?.map((item) => (
<Paper
key={item.id}
p="lg"
@@ -114,9 +182,9 @@ function Page() {
/>
</Box>
<Stack gap="xs" align="center" style={{ width: '100%' }}>
<Title order={4} ta="center" c="dark" fw={600} lineClamp={2} style={{ minHeight: '3rem' }}>
{item.name}
</Title>
<Title order={4} ta="center" c="dark" fw={600} lineClamp={2} style={{ minHeight: '3rem' }}>
{item.name}
</Title>
<Text
ta="center"
fw={700}
@@ -139,6 +207,59 @@ function Page() {
</Text>
</Center>
) : null}
{/* Chart */}
<Box mt={30} style={{ width: '100%', minHeight: 400 }}>
<Paper bg={colors['white-1']} pt={50} pb={170} px={90} mb={"xl"} radius="md" withBorder>
<Stack gap={"xs"}>
<Title ta={"center"} pb={10} order={2}>
Grafik APBDes
</Title>
{mounted && chartData.length > 0 ? (
<Box style={{ padding: '0 30px' }}> {/* Tambahkan padding horizontal agar label tidak keluar */}
<BarChart
h={500}
data={chartData}
dataKey="name"
type="stacked"
withBarValueLabel
series={[
{
name: 'jumlah',
color: colors['blue-button'],
// label: 'Jumlah', → HAPUS INI AGAR LEGEND TIDAK MUNCUL
},
]}
withTooltip
tooltipProps={{
labelFormatter: (value) => value,
formatter: (value) => `${value}%`,
}}
xAxisProps={{
angle: -45,
textAnchor: 'end',
interval: 0,
fontSize: 12,
dy: 10,
}}
yAxisProps={{
domain: [0, 100],
tickCount: 6,
}}
style={{
overflowX: 'visible',
paddingBottom: 40, // Tambahkan ruang di bawah untuk label
}}
// Hilangkan legend secara eksplisit
withLegend={false} // ⭐ Ini yang menghilangkan kotak biru + teks "Jumlah"
/>
</Box>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Box>
</Box>
</Stack>

View File

@@ -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>

View File

@@ -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(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#039;/gi, "'")
.replace(/&#8217;/gi, "'")
.replace(/&mdash;/gi, '—')
.replace(/&ndash;/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>
);
}

View File

@@ -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={[

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -13,66 +13,11 @@ import Apbdes from "./_com/main-page/apbdes";
import Prestasi from "./_com/main-page/prestasi";
import ScrollToTopButton from "./_com/scrollToTopButton";
import { useEffect, useMemo } from "react";
import { useProxy } from "valtio/utils";
import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita";
import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman";
import ModernNewsNotification from "./_com/ModernNeewsNotification";
import NewsReaderLanding from "./_com/NewsReaderalanding";
export default function Page() {
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]);
return (
<Box id="page-root">
@@ -95,12 +40,6 @@ export default function Page() {
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
{/* Modern Notification System */}
<ModernNewsNotification
news={newsData}
autoShowDelay={2000} // Muncul 2 detik setelah load
/>
<NewsReaderLanding/>
</Box>