Fix Ui Admin & User to Mobile && QC Menu Landing Page, PPID, Desa
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" >
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user