Fix QC Kak Inno Admin, Fix QC Keano UI User, Fix QC Pak jun tabel apbdes

This commit is contained in:
2025-11-12 17:42:31 +08:00
parent 417a8937f5
commit 9622eb5a9a
354 changed files with 11444 additions and 4012 deletions

View File

@@ -66,7 +66,7 @@ function Page() {
</Container>
{/* Tabs Menu */}
<Tabs color={colors['blue-button']} variant="pills" defaultValue="semua">
<Tabs color={colors['blue-button']} variant="pills" value="semua">
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']} >
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>

View File

@@ -8,76 +8,59 @@ import { useEffect, useState } from 'react';
import BackButton from '../../layanan/_com/BackButto';
import type { SearchBarProps } from './searchBar';
// Define tabs outside the component to ensure consistency between server and client
// Definisikan tabs di luar komponen (statis, aman untuk SSR)
const TABS = [
{
label: "Foto",
value: "foto",
href: "/darmasaba/desa/galery/foto",
},
{
label: "Video",
value: "video",
href: "/darmasaba/desa/galery/video",
},
{ label: 'Foto', value: 'foto', href: '/darmasaba/desa/galery/foto' },
{ label: 'Video', value: 'video', href: '/darmasaba/desa/galery/video' },
] as const;
const SearchBar = dynamic<SearchBarProps>(
() => import('./searchBar').then(mod => mod.SearchBar),
() => import('./searchBar').then((mod) => mod.SearchBar),
{ ssr: false }
);
type HeaderSearchProps = {
type LayoutTabsGaleryProps = {
children?: React.ReactNode;
};
function LayoutTabsGalery({ children }: HeaderSearchProps) {
export default function LayoutTabsGalery({ children }: LayoutTabsGaleryProps) {
const router = useRouter();
const pathname = usePathname();
const [isClient, setIsClient] = useState(false);
const [activeTab, setActiveTab] = useState<string>(TABS[0].value);
// Set default active tab to empty string to prevent hydration mismatch
const [activeTab, setActiveTab] = useState('');
// Set client flag on mount
// 🧠 Update tab aktif berdasarkan URL
useEffect(() => {
setIsClient(true);
}, []);
// Update active tab based on current route - only on client side
useEffect(() => {
if (!isClient) return;
const currentTab = TABS.find(tab => pathname.includes(tab.value));
if (currentTab) {
setActiveTab(currentTab.value);
} else {
// Default to first tab if no match found
setActiveTab(TABS[0].value);
}
}, [pathname, isClient]);
const found = TABS.find((tab) => pathname.includes(tab.value));
if (found) setActiveTab(found.value);
else setActiveTab(TABS[0].value);
}, [pathname]);
// 🖱️ Handle perubahan tab
const handleTabChange = (value: string | null) => {
if (!value) return;
const tab = TABS.find(tab => tab.value === value);
if (tab) {
// Only update if we're on the client
if (typeof window !== 'undefined') {
setActiveTab(value);
router.push(tab.href);
}
const selected = TABS.find((tab) => tab.value === value);
if (selected) {
setActiveTab(selected.value);
router.push(selected.href);
}
};
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
{/* 🔙 Header */}
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: "md", md: 100 }}>
{/* 🏷️ Title & Search */}
<Box px={{ base: 'md', md: 100 }}>
<Stack align="center" gap="0">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
<Text
fz={{ base: '2rem', md: '3.4rem' }}
c={colors['blue-button']}
fw="bold"
ta="center"
>
Galeri Kegiatan Desa Darmasaba
</Text>
</Stack>
@@ -86,35 +69,26 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
</Box>
</Box>
{/* 🗂️ Tabs Section */}
<Tabs
value={isClient ? activeTab : undefined}
defaultValue={TABS[0].value}
value={activeTab}
onChange={handleTabChange}
color={colors['blue-button']}
variant="pills"
keepMounted={false}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Box px={{ base: 'md', md: 100 }} py="md" bg={colors['BG-trans']}>
<TabsList>
{TABS.map((tab) => (
<TabsTab
key={tab.value}
value={tab.value}
component="button"
type="button"
>
<TabsTab key={tab.value} value={tab.value}>
{tab.label}
</TabsTab>
))}
</TabsList>
</Box>
<Container size={'xl'}>
{children}
</Container>
<Container size="xl">{children}</Container>
</Tabs>
</Stack>
);
}
export default LayoutTabsGalery;

View File

@@ -166,7 +166,7 @@ function PengaduanMasyarakat() {
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>

View File

@@ -195,7 +195,7 @@ function Page() {
>
<Stack gap="md">
<TextInput
defaultValue={stateLaporan.create.form.judul}
value={stateLaporan.create.form.judul}
onChange={(e) => (stateLaporan.create.form.judul = e.target.value)}
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
placeholder="Masukkan judul laporan publik"
@@ -203,7 +203,7 @@ function Page() {
/>
<TextInput
defaultValue={stateLaporan.create.form.lokasi}
value={stateLaporan.create.form.lokasi}
onChange={(e) => (stateLaporan.create.form.lokasi = e.target.value)}
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
placeholder="Masukkan lokasi laporan publik"
@@ -212,7 +212,7 @@ function Page() {
<DateTimePicker
label={<Text fw="bold" fz="sm">Tanggal Laporan Publik</Text>}
defaultValue={
value={
stateLaporan.create.form.tanggalWaktu
? new Date(stateLaporan.create.form.tanggalWaktu)
: null

View File

@@ -333,7 +333,7 @@ const state = useProxy(indeksKepuasanState.responden);
label="Nama"
type='text'
placeholder="Masukkan nama"
defaultValue={state.create.form.name}
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
@@ -342,7 +342,7 @@ const state = useProxy(indeksKepuasanState.responden);
label="Tanggal"
type="date"
placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal}
value={state.create.form.tanggal}
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
@@ -351,7 +351,7 @@ const state = useProxy(indeksKepuasanState.responden);
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
defaultValue={state.create.form.jenisKelaminId || ""}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
state.create.form.jenisKelaminId = val ?? "";
}}
@@ -369,7 +369,7 @@ const state = useProxy(indeksKepuasanState.responden);
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
defaultValue={state.create.form.ratingId || ""}
value={state.create.form.ratingId || ""}
onChange={(val) => {
state.create.form.ratingId = val ?? "";
}}
@@ -387,7 +387,7 @@ const state = useProxy(indeksKepuasanState.responden);
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
defaultValue={state.create.form.kelompokUmurId || ""}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
state.create.form.kelompokUmurId = val ?? "";
}}
@@ -599,7 +599,7 @@ const state = useProxy(indeksKepuasanState.responden);
label="Nama"
type='text'
placeholder="masukkan nama"
defaultValue={state.create.form.name}
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
@@ -608,7 +608,7 @@ const state = useProxy(indeksKepuasanState.responden);
label="Tanggal Pengisian"
type="date"
placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal}
value={state.create.form.tanggal}
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
@@ -617,7 +617,7 @@ const state = useProxy(indeksKepuasanState.responden);
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
defaultValue={state.create.form.jenisKelaminId || ""}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
state.create.form.jenisKelaminId = val ?? "";
}}
@@ -635,7 +635,7 @@ const state = useProxy(indeksKepuasanState.responden);
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
defaultValue={state.create.form.ratingId || ""}
value={state.create.form.ratingId || ""}
onChange={(val) => {
state.create.form.ratingId = val ?? "";
}}
@@ -653,7 +653,7 @@ const state = useProxy(indeksKepuasanState.responden);
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
defaultValue={state.create.form.kelompokUmurId || ""}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
state.create.form.kelompokUmurId = val ?? "";
}}

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
import colors from '@/con/colors'
import { ActionIcon, BackgroundImage, Box, Center, Container, Group, Loader, SimpleGrid, Stack, Text, Title } from '@mantine/core'
import { ActionIcon, BackgroundImage, Box, Center, Container, Group, Loader, Paper, Progress, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core'
import { IconDownload } from '@tabler/icons-react'
import { Link } from 'next-view-transitions'
import { useEffect, useState } from 'react'
@@ -12,13 +13,14 @@ import BackButton from '../../(pages)/desa/layanan/_com/BackButto'
function Page() {
const state = useProxy(apbdes)
const paDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
const [loading, setLoading] = useState(false)
useEffect(() => {
const loadData = async () => {
try {
setLoading(true)
await state.findMany.load()
await paDesaState.findMany.load()
} catch (error) {
console.error(error)
} finally {
@@ -28,7 +30,7 @@ function Page() {
loadData()
}, [])
const data = state.findMany.data || []
const dataAPBDes = state.findMany.data || []
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={32}>
@@ -52,7 +54,7 @@ function Page() {
<Text fz="lg" c="dimmed">Sedang memuat data APBDes...</Text>
</Stack>
</Center>
) : data.length === 0 ? (
) : dataAPBDes.length === 0 ? (
<Center mih={200}>
<Stack align="center" gap="xs">
<Text fz="xl" fw={600} c="dimmed">Belum ada data APBDes tersedia</Text>
@@ -61,7 +63,7 @@ function Page() {
</Center>
) : (
<SimpleGrid px={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 2, md: 3 }} spacing="xl">
{data.map((v: any, k: number) => (
{dataAPBDes.map((v: any, k: number) => (
<BackgroundImage key={k} src={v.image?.link || ''} h={360} radius="xl" pos="relative">
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 27 }} />
<Stack justify="space-between" h="100%" p="lg" pos="relative">
@@ -82,7 +84,7 @@ function Page() {
bg={colors['blue-button']}
variant="filled"
>
<IconDownload size={20} color="white" />
<IconDownload size={20} color="white" />
</ActionIcon>
</Group>
</Stack>
@@ -90,8 +92,200 @@ function Page() {
))}
</SimpleGrid>
)}
<DetailAPBDesaTable />
<APBDesaProgress />
</Stack>
)
}
function DetailAPBDesaTable() {
// 🔹 Dummy data
const data = {
tahun: 2024,
pendapatan: [
{ id: 1, nama: 'Pendapatan Asli Desa', anggaran: 32000000, realisasi: 6500000 },
{ id: 2, nama: 'Dana Desa', anggaran: 125000000, realisasi: 120000000 },
{ id: 3, nama: 'Bagi Hasil Pajak dan Retribusi', anggaran: 10000000, realisasi: 9000000 },
],
belanja: [
{ id: 1, nama: 'Belanja Pegawai', anggaran: 80000000, realisasi: 75000000 },
{ id: 2, nama: 'Belanja Barang & Jasa', anggaran: 50000000, realisasi: 42000000 },
],
pembiayaan: [
{ id: 1, nama: 'Penerimaan Pembiayaan', anggaran: 15000000, realisasi: 15000000 },
{ id: 2, nama: 'Pengeluaran Pembiayaan', anggaran: 10000000, realisasi: 8000000 },
],
};
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
minimumFractionDigits: 2,
style: 'decimal',
}).format(value);
// 🔹 Helper buat render satu kategori (Pendapatan, Belanja, Pembiayaan)
const renderSection = (title: string, items: any[]) => {
const totalAnggaran = items.reduce((sum, i) => sum + Number(i.anggaran || 0), 0);
const totalRealisasi = items.reduce((sum, i) => sum + Number(i.realisasi || 0), 0);
const totalSelisih = totalAnggaran - totalRealisasi;
const totalPersen = totalAnggaran
? (totalRealisasi / totalAnggaran) * 100
: 0;
return (
<Paper withBorder radius="md" shadow="xs" p="md">
<Title order={5} mb="sm" c={colors['blue-button']}>
{title.toUpperCase()}
</Title>
<Table withColumnBorders highlightOnHover>
<Table.Thead bg={colors['blue-button']}>
<Table.Tr>
<Table.Th c="white">Uraian</Table.Th>
<Table.Th c="white" ta="right">
Anggaran (Rp)
</Table.Th>
<Table.Th c="white" ta="right">
Realisasi (Rp)
</Table.Th>
<Table.Th c="white" ta="right">
Lebih/(Kurang) (Rp)
</Table.Th>
<Table.Th c="white" ta="center">
Persentase (%)
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{items.map((item, index) => {
const selisih = Number(item.anggaran) - Number(item.realisasi);
const persen = item.anggaran
? (item.realisasi / item.anggaran) * 100
: 0;
return (
<Table.Tr key={item.id || index}>
<Table.Td>
<strong>{`${index + 1}. ${item.nama}`}</strong>
</Table.Td>
<Table.Td ta="right">{formatRupiah(item.anggaran)}</Table.Td>
<Table.Td ta="right">{formatRupiah(item.realisasi)}</Table.Td>
<Table.Td ta="right">{formatRupiah(selisih)}</Table.Td>
<Table.Td ta="center">{persen.toFixed(2)}</Table.Td>
</Table.Tr>
);
})}
</Table.Tbody>
<Table.Tfoot bg="#f1f5fb">
<Table.Tr>
<Table.Th>Total {title}</Table.Th>
<Table.Th ta="right">{formatRupiah(totalAnggaran)}</Table.Th>
<Table.Th ta="right">{formatRupiah(totalRealisasi)}</Table.Th>
<Table.Th ta="right">{formatRupiah(totalSelisih)}</Table.Th>
<Table.Th ta="center">{totalPersen.toFixed(2)}</Table.Th>
</Table.Tr>
</Table.Tfoot>
</Table>
</Paper>
);
};
return (
<Box py="md" px={{ base: 'md', md: 100 }}>
<Stack gap="xl">
<Title order={4} c={colors['blue-button']}>
APB Desa Tahun {data.tahun}
</Title>
{renderSection('Pendapatan', data.pendapatan)}
{renderSection('Belanja', data.belanja)}
{renderSection('Pembiayaan', data.pembiayaan)}
</Stack>
</Box>
);
}
function APBDesaProgress() {
const data = {
tahun: 2024,
pendapatan: [
{ id: 1, nama: 'Pendapatan Asli Desa', anggaran: 32000000, realisasi: 6500000 },
{ id: 2, nama: 'Dana Desa', anggaran: 125000000, realisasi: 120000000 },
{ id: 3, nama: 'Bagi Hasil Pajak dan Retribusi', anggaran: 10000000, realisasi: 9000000 },
],
belanja: [
{ id: 1, nama: 'Belanja Pegawai', anggaran: 80000000, realisasi: 75000000 },
{ id: 2, nama: 'Belanja Barang & Jasa', anggaran: 50000000, realisasi: 42000000 },
],
pembiayaan: [
{ id: 1, nama: 'Penerimaan Pembiayaan', anggaran: 15000000, realisasi: 15000000 },
{ id: 2, nama: 'Pengeluaran Pembiayaan', anggaran: 10000000, realisasi: 8000000 },
],
};
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 2,
}).format(value);
const calcProgress = (items: any[]) => {
const anggaran = items.reduce((sum, i) => sum + i.anggaran, 0);
const realisasi = items.reduce((sum, i) => sum + i.realisasi, 0);
const persen = anggaran ? (realisasi / anggaran) * 100 : 0;
return { anggaran, realisasi, persen };
};
const pendapatan = calcProgress(data.pendapatan);
const belanja = calcProgress(data.belanja);
const pembiayaan = calcProgress(data.pembiayaan);
const renderProgress = (label: string, dataset: any) => (
<Box>
<Text fw={600}>{label}</Text>
<Text fw={700} mb="xs">
{formatRupiah(dataset.realisasi)} | {formatRupiah(dataset.anggaran)}
</Text>
<Progress
value={dataset.persen}
size="xl"
radius="xl"
striped={false}
styles={{
root: { backgroundColor: '#d7e3f1' },
section: {
backgroundColor: colors['blue-button'],
position: 'relative',
'&::after': {
content: `'${dataset.persen.toFixed(2)}%'`,
position: 'absolute',
right: 10,
top: '50%',
transform: 'translateY(-50%)',
color: 'white',
fontWeight: 700,
fontSize: '0.8rem',
}
},
}}
/>
</Box>
);
return (
<Paper mx={{ base: 'md', md: 100 }} p="xl" radius="md" shadow="sm" withBorder bg={colors['white-1']}>
<Stack gap="lg">
<Title order={4} c={colors['blue-button']}>
Grafik APB Desa Tahun {data.tahun}
</Title>
{renderProgress('Pendapatan Desa', pendapatan)}
{renderProgress('Belanja Desa', belanja)}
{renderProgress('Pembiayaan Desa', pembiayaan)}
</Stack>
</Paper>
);
}
export default Page

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 { usePathname, useRouter } from "next/navigation"; // 👉 tambahkan ini
import { usePathname, useRouter } from "next/navigation";
interface NewsItem {
id: string | number;
@@ -31,7 +31,7 @@ export default function ModernNewsNotification({
news = [],
autoShowDelay = 2000
}: ModernNewsNotificationProps) {
const router = useRouter(); // 👉 router Next.js
const router = useRouter();
const [toastVisible, setToastVisible] = useState(false);
const [widgetOpen, setWidgetOpen] = useState(false);
const [hasNewNotifications, setHasNewNotifications] = useState(true);
@@ -39,8 +39,7 @@ export default function ModernNewsNotification({
const [iconVisible, setIconVisible] = useState(true);
const pathname = usePathname();
// Auto show toast on page load
useEffect(() => {
if (news.length > 0 && !toastVisible && !hasShownToast) {
const timer = setTimeout(() => {
@@ -51,6 +50,7 @@ export default function ModernNewsNotification({
}
}, [news.length, autoShowDelay, toastVisible, hasShownToast]);
// Auto hide toast after 8 seconds
useEffect(() => {
if (toastVisible) {
const timer = setTimeout(() => {
@@ -60,22 +60,26 @@ export default function ModernNewsNotification({
}
}, [toastVisible]);
// Ganti useEffect scroll yang lama dengan versi berikut:
// Enhanced scroll handler with better thresholds
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 handleScroll = () => {
const currentScrollY = window.scrollY;
const scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up';
// Kontrol ikon lonceng
if (currentScrollY > lastScrollY && currentScrollY > 100) {
// Logic untuk hide/show icon
if (scrollDirection === 'down' && currentScrollY > HIDE_THRESHOLD) {
// Scroll ke bawah dan sudah melewati threshold → hide
setIconVisible(false);
} else if (currentScrollY < lastScrollY) {
} else if (scrollDirection === 'up' && currentScrollY < SHOW_THRESHOLD) {
// Scroll ke atas dan sudah di posisi paling atas → show
setIconVisible(true);
}
// 🔴 BARU: Sembunyikan toast saat scroll ke bawah melewati 150px
// Hide toast saat scroll ke bawah melewati 150px
if (currentScrollY > 150 && toastVisible) {
setToastVisible(false);
}
@@ -85,11 +89,11 @@ export default function ModernNewsNotification({
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [toastVisible]); // 👈 tambahkan toastVisible sebagai dependency
}, [toastVisible]);
const currentNews = news[0];
// 👉 Fungsi baru untuk handle klik notifikasi
// Handle notification click
const handleNotificationClick = (item: NewsItem) => {
setWidgetOpen(false);
if (item.type === "berita") {
@@ -105,13 +109,14 @@ export default function ModernNewsNotification({
setHasNewNotifications(false);
};
// Ganti dengan path landing page Anda
// Only show on landing page
if (pathname !== '/darmasaba') {
return null;
}
return (
<>
{/* Floating Bell Icon */}
<Transition mounted={iconVisible} transition="slide-down" duration={200}>
{(transitionStyles) => (
<Box
@@ -119,7 +124,8 @@ export default function ModernNewsNotification({
...transitionStyles,
position: "fixed",
bottom: "24px",
right: "24px"
right: "24px",
zIndex: 1000,
}}
>
<ActionIcon
@@ -161,6 +167,7 @@ export default function ModernNewsNotification({
)}
</Transition>
{/* Widget Panel */}
<Transition mounted={widgetOpen} transition="slide-up" duration={300}>
{(styles) => (
<Paper
@@ -187,7 +194,7 @@ export default function ModernNewsNotification({
<Group justify="space-between">
<Group gap="xs">
<IconBell size={20} />
<Text c={"white"} fw={600} size="md">Berita & Pengumuman</Text>
<Text c="white" fw={600} size="md">Berita & Pengumuman</Text>
</Group>
<CloseButton
onClick={() => setWidgetOpen(false)}
@@ -257,6 +264,7 @@ export default function ModernNewsNotification({
)}
</Transition>
{/* Toast Notification */}
<Transition mounted={toastVisible && !!currentNews} transition="slide-left" duration={300}>
{(styles) => (
<Paper
@@ -299,7 +307,10 @@ export default function ModernNewsNotification({
{currentNews?.type === "berita" ? "Berita Terbaru" : "Pengumuman"}
</Badge>
<CloseButton
onClick={() => setToastVisible(false)}
onClick={(e) => {
e.stopPropagation();
setToastVisible(false);
}}
size="sm"
/>
</Group>

View File

@@ -339,7 +339,7 @@ function Kepuasan() {
label="Nama"
type='text'
placeholder="Masukkan nama"
defaultValue={state.create.form.name}
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
@@ -348,7 +348,7 @@ function Kepuasan() {
label="Tanggal"
type="date"
placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal}
value={state.create.form.tanggal}
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
@@ -357,7 +357,7 @@ function Kepuasan() {
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
defaultValue={state.create.form.jenisKelaminId || ""}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
state.create.form.jenisKelaminId = val ?? "";
}}
@@ -375,7 +375,7 @@ function Kepuasan() {
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
defaultValue={state.create.form.ratingId || ""}
value={state.create.form.ratingId || ""}
onChange={(val) => {
state.create.form.ratingId = val ?? "";
}}
@@ -393,7 +393,7 @@ function Kepuasan() {
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
defaultValue={state.create.form.kelompokUmurId || ""}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
state.create.form.kelompokUmurId = val ?? "";
}}
@@ -611,7 +611,7 @@ function Kepuasan() {
label="Nama"
type='text'
placeholder="masukkan nama"
defaultValue={state.create.form.name}
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
@@ -620,7 +620,7 @@ function Kepuasan() {
label="Tanggal Pengisian"
type="date"
placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal}
value={state.create.form.tanggal}
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
@@ -629,7 +629,7 @@ function Kepuasan() {
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
defaultValue={state.create.form.jenisKelaminId || ""}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
state.create.form.jenisKelaminId = val ?? "";
}}
@@ -647,7 +647,7 @@ function Kepuasan() {
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
defaultValue={state.create.form.ratingId || ""}
value={state.create.form.ratingId || ""}
onChange={(val) => {
state.create.form.ratingId = val ?? "";
}}
@@ -665,7 +665,7 @@ function Kepuasan() {
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
defaultValue={state.create.form.kelompokUmurId || ""}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
state.create.form.kelompokUmurId = val ?? "";
}}

View File

@@ -18,14 +18,10 @@ import {
} from "@mantine/core";
import { Prisma } from "@prisma/client";
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, 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"];
@@ -72,57 +68,7 @@ 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 () => {
@@ -272,11 +218,6 @@ function LandingPage() {
)}
</Flex>
{/* Modern Notification System */}
<ModernNewsNotification
news={newsData}
autoShowDelay={2000} // Muncul 2 detik setelah load
/>
</Stack>
);
}

View File

@@ -14,15 +14,72 @@ import Prestasi from "./_com/main-page/prestasi";
import ScrollToTopButton from "./_com/scrollToTopButton";
import NewsReaderLanding from "./_com/NewsReaderalanding";
import ModernNewsNotification from "./_com/ModernNeewsNotification";
import { 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 { useEffect } from "react";
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]);
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">
<Stack
bg={colors.grey[1]}
<Stack
bg={colors.grey[1]}
gap={0}
>
{/* HAPUS RUNNING TEXT, GANTI DENGAN MODERN NOTIFICATION */}
@@ -40,8 +97,11 @@ export default function Page() {
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
<NewsReaderLanding/>
<NewsReaderLanding />
<ModernNewsNotification
news={newsData}
autoShowDelay={2000} // Muncul 2 detik setelah load
/>
</Box>
);
}