Fix Ui Admin & User to Mobile && QC Menu Landing Page, PPID, Desa

This commit is contained in:
2025-09-24 14:50:53 +08:00
parent b5c044df6e
commit 3e4a7a1c0a
47 changed files with 1778 additions and 502 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, ScrollArea, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -23,95 +23,52 @@ function LayoutTabsBerita({
const pathname = usePathname();
const searchParams = useSearchParams();
// Get active tab from URL path
const activeTab = pathname.split('/').pop() || 'semua';
// Get initial search value from URL
const initialSearch = searchParams.get('search') || '';
const [searchValue, setSearchValue] = useState(initialSearch);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// Update active tab state when pathname changes
const [activeTabState, setActiveTabState] = useState(activeTab);
useEffect(() => {
setActiveTabState(activeTab);
}, [activeTab]);
// Clean up timeouts on unmount
useEffect(() => {
return () => {
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
if (searchTimeout !== null) clearTimeout(searchTimeout);
};
}, [searchTimeout]);
// Handle search input change with debounce
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchValue(value);
// Clear previous timeout
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
if (searchTimeout !== null) clearTimeout(searchTimeout);
// Set new timeout
const newTimeout = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set('search', value);
} else {
params.delete('search');
}
if (value) params.set('search', value);
else params.delete('search');
// Only update URL if the search value has actually changed
if (params.toString() !== searchParams.toString()) {
router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
}
}, 500); // 500ms debounce delay
}, 500);
setSearchTimeout(newTimeout);
};
const tabs = [
{
label: "Semua",
value: "semua",
href: "/darmasaba/desa/berita/semua"
},
{
label: "Budaya",
value: "budaya",
href: "/darmasaba/desa/berita/budaya"
},
{
label: "Pemerintahan",
value: "pemerintahan",
href: "/darmasaba/desa/berita/pemerintahan"
},
{
label: "Ekonomi",
value: "ekonomi",
href: "/darmasaba/desa/berita/ekonomi"
},
{
label: "Pembangunan",
value: "pembangunan",
href: "/darmasaba/desa/berita/pembangunan"
},
{
label: "Sosial",
value: "sosial",
href: "/darmasaba/desa/berita/sosial"
},
{
label: "Teknologi",
value: "teknologi",
href: "/darmasaba/desa/berita/teknologi"
},
const tabs = [
{ label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
{ label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
{ label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
{ label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
{ label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
{ label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
{ label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
];
const handleTabChange = (value: string | null) => {
if (!value) return;
const tab = tabs.find(t => t.value === value);
@@ -127,16 +84,29 @@ function LayoutTabsBerita({
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Portal Berita Darmasaba
</Text>
<Text ta="center" px="md">
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
<Group justify='space-between' align="center">
<Stack gap="0">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
Portal Berita Darmasaba
</Text>
<Text>
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Text>
</Stack>
<Box>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</Box>
</Group>
</Box>
<Tabs
color={colors['blue-button']}
@@ -145,33 +115,25 @@ function LayoutTabsBerita({
onChange={handleTabChange}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList>
{tabs.map((tab, index) => (
<TabsTab
key={index}
value={tab.value}
onClick={() => router.push(tab.href)}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</GridCol>
</Grid>
{/* SCROLLABLE TABS */}
<Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
<TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
{tabs.map((tab, index) => (
<TabsTab
key={index}
value={tab.value}
onClick={() => router.push(tab.href)}
style={{
flex: '0 0 auto', // Prevent shrinking
minWidth: 100, // optional: makes them touch-friendly
textAlign: 'center'
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</Box>
</Box>
{children}
@@ -180,4 +142,4 @@ function LayoutTabsBerita({
);
}
export default LayoutTabsBerita;
export default LayoutTabsBerita;

View File

@@ -1,12 +1,12 @@
'use client';
import { useEffect, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text } from '@mantine/core';
import BackButton from '../../layanan/_com/BackButto';
import dynamic from 'next/dynamic';
import type { SearchBarProps } from './searchBar';
import colors from '@/con/colors';
import { Box, Container, Stack, Tabs, TabsList, TabsTab, Text } from '@mantine/core';
import dynamic from 'next/dynamic';
import { usePathname, useRouter } from 'next/navigation';
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
const TABS = [
@@ -35,7 +35,7 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
const router = useRouter();
const pathname = usePathname();
const [isClient, setIsClient] = useState(false);
// Set default active tab to empty string to prevent hydration mismatch
const [activeTab, setActiveTab] = useState('');
@@ -47,7 +47,7 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
// 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);
@@ -75,42 +75,38 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<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">
Galeri Kegiatan Desa Darmasaba
</Text>
</Stack>
</Container>
<Box>
<SearchBar />
</Box>
</Box>
<Tabs
<Tabs
value={isClient ? activeTab : undefined}
defaultValue={TABS[0].value}
onChange={handleTabChange}
color={colors['blue-button']}
color={colors['blue-button']}
variant="pills"
keepMounted={false}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<TabsList>
{TABS.map((tab) => (
<TabsTab
key={tab.value}
value={tab.value}
component="button"
type="button"
>
{tab.label}
</TabsTab>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<SearchBar />
</GridCol>
</Grid>
<TabsList>
{TABS.map((tab) => (
<TabsTab
key={tab.value}
value={tab.value}
component="button"
type="button"
>
{tab.label}
</TabsTab>
))}
</TabsList>
</Box>
<Container size={'xl'}>

View File

@@ -2,7 +2,7 @@
'use client';
import { TextInput } from '@mantine/core';
import { Group, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
@@ -65,13 +65,15 @@ export function SearchBar({
}, []);
return (
<TextInput
<Group justify='center'>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
w={{ base: '100%', md: '50%' }}
value={searchValue}
onChange={handleSearchChange}
/>
</Group>
);
}

View File

@@ -2,19 +2,24 @@
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Center, Pagination, Paper, SimpleGrid, Spoiler, Stack, Text } from '@mantine/core';
import {
Box,
Center,
Pagination,
Paper,
SimpleGrid,
Spoiler,
Stack,
Text,
} from '@mantine/core';
import { useCallback, useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
export default function VideoContent() {
const [expanded, setExpanded] = useState(false);
// ✅ expanded state per index
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
const videoState = useSnapshot(stateGallery.video);
const {
data,
page,
totalPages,
loading,
} = videoState.findMany;
const { data, page, totalPages, loading } = videoState.findMany;
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
@@ -27,24 +32,18 @@ export default function VideoContent() {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
loadData(urlPage, urlSearch);
};
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
loadData(1, search);
};
// Initial load
handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
@@ -57,6 +56,13 @@ export default function VideoContent() {
loadData(newPage, search);
};
const toggleExpanded = (index: number, value: boolean) => {
setExpandedMap((prev) => ({
...prev,
[index]: value,
}));
};
const dataVideo = data || [];
if (loading && !data) {
@@ -72,7 +78,13 @@ export default function VideoContent() {
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataVideo.map((v, k) => (
<Box key={k}>
<Paper mb={50} p="md" radius={26} bg={colors['white-trans-1']} w={{ base: '100%', md: '100%' }}>
<Paper
mb={50}
p="md"
radius={26}
bg={colors['white-trans-1']}
w={{ base: '100%', md: '100%' }}
>
<Box>
<Center>
<Box
@@ -109,8 +121,8 @@ export default function VideoContent() {
Hide details
</Text>
}
expanded={expanded}
onExpandedChange={setExpanded}
expanded={expandedMap[k] || false}
onExpandedChange={(val) => toggleExpanded(k, val)}
>
<Text
ta="justify"
@@ -137,15 +149,15 @@ export default function VideoContent() {
);
}
// ✅ Fix: HAPUS SPASI BERLEBIH DI URL
// ✅ Fix: convert YouTube URL ke embed
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get('v');
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`; // ✅ tanpa spasi!
return `https://www.youtube.com/embed/${videoId}`;
} catch (err) {
console.error('Error converting YouTube URL to embed:', err);
return youtubeUrl;
}
}
}

View File

@@ -2,11 +2,12 @@
'use client'
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Center, Container, Group, Image, Modal, Paper, Select, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../_com/BackButto';
import { useDisclosure } from '@mantine/hooks';
interface LayananData {
id: string;
@@ -31,7 +32,12 @@ function Page() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<LayananData | null>(null);
const [opened, { open, close }] = useDisclosure(false);
const stateCreate = useProxy(stateLayananDesa.ajukanPermohonan);
useEffect(() => {
state.suratKeterangan.findManyAll.load()
const loadData = async () => {
if (!id) return;
try {
@@ -48,6 +54,22 @@ function Page() {
loadData();
}, [id]);
const resetForm = () => {
stateCreate.create.form = {
nama: '',
nik: '',
alamat: '',
nomorKk: '',
kategoriId: '',
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
close();
}
if (loading) {
return (
<Center h="100vh" bg={colors.Bg}>
@@ -105,12 +127,76 @@ function Page() {
size="lg"
variant="gradient"
gradient={{ from: '#1C6EA4', to: '#63B3ED' }}
onClick={open}
>
Ajukan Permohonan
</Button>
</Group>
</Stack>
</Box>
<Modal
opened={opened}
onClose={close}
radius={0}
transitionProps={{ transition: 'fade', duration: 200 }}
>
<Paper p="md" withBorder>
<Stack gap="xs">
<Title order={3}>Ajukan Permohonan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">NIK</Text>}
placeholder="masukkan NIK"
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">Nomor KK</Text>}
placeholder="masukkan Nomor KK"
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
/>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateLayananDesa.suratKeterangan.findManyAll.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={stateCreate.create.form.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
stateCreate.create.form.kategoriId = selected.id;
}
} else {
stateCreate.create.form.kategoriId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>
</Stack>
</Paper>
</Modal>
</Stack>
);
}

View File

@@ -22,7 +22,7 @@ export default function Page() {
<Container w={{ base: "100%", md: "50%" }} >
<Stack align="center" gap={0}>
{/* Bagian Layanan */}
<Text fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Text fz={{ base: "1.8rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Layanan Desa Darmasaba
</Text>
<Text

View File

@@ -1,7 +1,7 @@
'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Container, Flex, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -31,7 +31,7 @@ function Page() {
</Box>
<Container size="lg" px="md">
<Stack gap="xs" >
<Flex justify={"space-between"} align={"center"}>
<Group justify={"space-between"} align={"center"}>
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
{detail.data?.judul}
</Text>
@@ -40,7 +40,7 @@ function Page() {
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
</Paper>
</Group>
</Flex>
</Group>
<Paper bg={colors["white-1"]} p="md">
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >

View File

@@ -0,0 +1,128 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Divider,
Paper,
Skeleton,
Stack,
Text,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
export default function DetailInformasiPublikUser() {
const state = useProxy(daftarInformasiPublik);
const router = useRouter();
const params = useParams();
useEffect(() => {
if (params?.id) state.findUnique.load(params.id as string);
}, [params?.id]);
const data = state.findUnique.data;
if (!state.findUnique.data) {
return (
<Center py="xl">
<Skeleton height={500} radius="md" />
</Center>
);
}
if (!data) {
return (
<Center py="xl">
<Stack align="center" gap="sm">
<Text fz="lg" fw="bold">
Informasi tidak ditemukan
</Text>
<Button variant="light" onClick={() => router.push('/informasi-publik')}>
Kembali ke Daftar
</Button>
</Stack>
</Center>
);
}
return (
<Box py="lg" px={{ base: 'md', md: 100 }} bg={colors.Bg}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={18} color={colors['blue-button']} />}
mb="md"
c={colors['blue-button']}
>
Kembali
</Button>
<Paper
withBorder
radius="lg"
p={{ base: 'md', md: 'xl' }}
mx="auto"
maw={800}
bg="white"
shadow="xs"
>
<Stack gap="xl">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw="bold"
ta="center"
c={colors['blue-button']}
>
Detail Informasi Publik
</Text>
<Divider />
<Stack gap="lg">
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Jenis Informasi
</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
{data.jenisInformasi || '-'}
</Text>
</Box>
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Tanggal Publikasi
</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
{data.tanggal
? new Date(data.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'}
</Text>
</Box>
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Deskripsi
</Text>
<Box
className="prose max-w-none leading-relaxed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
</Stack>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -3,10 +3,13 @@
import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Center,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
@@ -17,21 +20,20 @@ import {
TableTr,
Text,
TextInput,
Paper,
Badge,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconSearch, IconFileInfo, IconMail, IconBrandWhatsapp } from '@tabler/icons-react';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconDeviceImacCog, IconFileInfo, IconMail, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useDebouncedValue } from '@mantine/hooks';
import { useTransitionRouter } from 'next-view-transitions';
function Page() {
const listData = useProxy(daftarInformasiPublik)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useTransitionRouter()
const {
data,
page,
@@ -63,7 +65,7 @@ function Page() {
<BackButton />
</Box>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy"/>
<Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy" />
</Center>
<Text ta="center" fz={{ base: "1.8rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold" lh={1.4}>
Daftar Informasi Publik Desa Darmasaba
@@ -99,38 +101,63 @@ function Page() {
</Stack>
</Center>
) : (
<Table withRowBorders withColumnBorders withTableBorder highlightOnHover verticalSpacing="md">
<TableThead bg={colors['blue-button']}>
<TableTr c={colors['white-1']}>
<TableTh fz="sm" ta="center" w="5%">No</TableTh>
<TableTh fz="sm" ta="center" w="25%">Jenis Informasi</TableTh>
<TableTh fz="sm" ta="center" w="40%">Deskripsi</TableTh>
<TableTh fz="sm" ta="center" w="20%">Tanggal Publikasi</TableTh>
</TableTr>
</TableThead>
<TableTbody bg={colors['white-1']}>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd>
<TableTd>
<Badge variant="light" size="lg" color="blue">
{item.jenisInformasi}
</Badge>
</TableTd>
<TableTd>
<Text fz="sm" c="dark" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd ta="center">
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
</TableTd>
<Box style={{ overflowX: 'auto' }}>
<Table withRowBorders withColumnBorders withTableBorder highlightOnHover verticalSpacing="md">
<TableThead bg={colors['blue-button']}>
<TableTr c={colors['white-1']}>
<TableTh fz="sm" ta="center" w="5%">No</TableTh>
<TableTh fz="sm" ta="center" w="25%">Jenis Informasi</TableTh>
<TableTh fz="sm" ta="center" w="40%">Deskripsi</TableTh>
<TableTh fz="sm" ta="center" w="20%">Tanggal Publikasi</TableTh>
<TableTh fz="sm" ta="center" w="15%">Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody bg={colors['white-1']}>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd>
<TableTd>
<Box w={150}>
<Badge variant="light" size="lg" color="blue">
{item.jenisInformasi}
</Badge>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text lineClamp={1} fz="sm" c="dark" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd ta="center">
<Box w={150}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
</Box>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Box w={150}>
<Tooltip label="Lihat Detail" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
>
Detail
</Button>
</Tooltip>
</Box>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
)}
<Center>

View File

@@ -4,7 +4,7 @@ import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/ind
import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts';
import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
import { useState } from "react";
import { useProxy } from "valtio/utils";
@@ -24,7 +24,8 @@ function Kepuasan() {
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
const [opened, { open, close }] = useDisclosure(false)
const [opened, { open, close }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 768px)");
const resetForm = () => {
state.create.form = {
@@ -140,12 +141,12 @@ function Kepuasan() {
if ((loading && !data) || !data) {
return (
<Stack py={10} px="xl">
<Skeleton height={300} mb="md" />
<SimpleGrid cols={{ base: 1, md: 3 }}>
<Skeleton height={300} />
<Skeleton height={300} />
<Skeleton height={300} />
<Stack py={10} px="sm">
<Skeleton height={200} mb="md" />
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="md">
<Skeleton height={200} />
<Skeleton height={200} />
<Skeleton height={200} />
</SimpleGrid>
</Stack>
);
@@ -412,50 +413,41 @@ function Kepuasan() {
);
}
return (
<Stack p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Stack gap={"xs"}>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Group justify={"center"}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={isMobile ? "md" : "xl"}>
<Stack gap="xs">
<Text ta="center" fz={{ base: "2rem", md: "3rem" }}>Indeks Kepuasan Masyarakat</Text>
<Group justify="center">
<Button radius="lg" bg={colors["blue-button"]} onClick={open}>
Ajukan Responden
</Button>
</Group>
</Stack>
</Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Flex justify={"space-between"} align={"center"}>
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box px={isMobile ? "sm" : "xl"}>
<Paper p="lg" bg={colors.Bg}>
<Paper p={isMobile ? "sm" : "lg"}>
<Stack gap="xs">
<Flex direction={isMobile ? "column" : "row"} justify="space-between" align={isMobile ? "start" : "center"}>
<Text fw="bold" mb={isMobile ? "sm" : 0}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}
<Text fz="sm" fw="bold" c={colors["blue-button"]}>Total Responden</Text>
<Text ta="end" fz="h1" fw="bold" c={colors["blue-button"]}>
{state.findMany.total.toLocaleString("id-ID")}
</Text>
</Box>
</Flex>
<BarChart
h={300}
h={isMobile ? 200 : 300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
series={[{ name: "count", color: colors["blue-button"] }]}
withTooltip
tooltipAnimationDuration={200}
/>
</Stack>
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
<Box py="xl">
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="lg">
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
@@ -465,28 +457,17 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={200}
data={donutDataJenisKelamin}
/>
</Center>
</Box>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
data={donutDataJenisKelamin}
withTooltip
/>
</Center>
</Stack>
</Paper>
)}
</Stack>
@@ -501,35 +482,18 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
data={donutDataRating}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataRating}
/>
</Center>
</Stack>
</Paper>
)}
</Stack>
@@ -544,35 +508,18 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
data={donutDataKelompokUmur}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataKelompokUmur}
/>
</Center>
</Stack>
</Paper>
)}
</Stack>

View File

@@ -32,9 +32,9 @@ function DesaAntiKorupsi() {
<Stack gap={"0"} bg={colors.Bg} p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} >
<Center>
<Text fz={{ base: "2.4rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
</Center>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.4rem" }}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
<Center py={20}>
<Button radius={"lg"} fz={"h4"} bg={colors["blue-button"]} component={Link} href={"/darmasaba/desa-anti-korupsi/detail"}>Selengkapnya</Button>
</Center>
@@ -49,6 +49,7 @@ function DesaAntiKorupsi() {
cols={{ base: 1, sm: 2, md: 3 }}
spacing="lg"
mt="lg"
mb="xl"
>
{data.map((v, k) => (
<Paper

View File

@@ -160,11 +160,11 @@ function Kepuasan() {
</Center>
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Center mt={10}>
<Button
radius={"lg"}
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
<Button
radius={"lg"}
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button>
</Center>
</Container>
@@ -182,7 +182,7 @@ function Kepuasan() {
</Box>
</Flex>
<BarChart
h={300}
h={window.innerWidth < 480 ? 200 : 300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
@@ -196,12 +196,9 @@ function Kepuasan() {
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="md"
verticalSpacing="md"
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
@@ -220,7 +217,7 @@ function Kepuasan() {
withLabels
withTooltip
labelsType="percent"
size={200}
size={250} // Fixed size in pixels
data={donutDataJenisKelamin}
/>
</Center>
@@ -259,7 +256,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
size={250}
data={donutDataRating}
/>
</Center>
@@ -302,7 +299,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
size={250}
data={donutDataKelompokUmur}
/>
</Center>
@@ -419,7 +416,7 @@ function Kepuasan() {
}
return (
<Stack p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Container size="lg" px="md">
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</Center>
@@ -432,9 +429,15 @@ function Kepuasan() {
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Flex justify={"space-between"} align={"center"}>
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Flex
direction={{ base: "column", sm: "row" }}
justify="space-between"
align={{ base: "flex-start", sm: "center" }}
>
<Text fw="bold" ta={{ base: "center", sm: "left" }}>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
<Box mt={{ base: "sm", sm: 0 }}>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}

View File

@@ -100,11 +100,11 @@ function Potensi() {
style={{ zIndex: 1 }}
>
<Tooltip label={v.name} position="top-start">
<Text fw={700} c="white" size="2.2rem" truncate>
<Text fw={700} c="white" fz={{ base: "1.2rem", md: "1.4rem" }} truncate>
{v.name}
</Text>
</Tooltip>
<Text lineClamp={2} c="gray.2" size="sm">
<Text lineClamp={2} c="gray.2" fz={{ base: "0.8rem", md: "1rem" }}>
{v.deskripsi}
</Text>
</Stack>