Merge pull request 'nico/10-des-25' (#40) from nico/10-des-25 into staggingweb

Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/40
This commit is contained in:
2025-12-10 17:45:16 +08:00
55 changed files with 1736 additions and 868 deletions

View File

@@ -11,21 +11,21 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
const pathname = usePathname() const pathname = usePathname()
const tabs = [ const tabs = [
{ {
label: "Profile Desa", label: "Profil Desa",
value: "profiledesa", value: "profildesa",
href: "/admin/desa/profile/profile-desa", href: "/admin/desa/profil/profil-desa",
icon: <IconUser size={18} stroke={1.8} /> icon: <IconUser size={18} stroke={1.8} />
}, },
{ {
label: "Profile Perbekel", label: "Profil Perbekel",
value: "profileperbekel", value: "profilperbekel",
href: "/admin/desa/profile/profile-perbekel", href: "/admin/desa/profil/profil-perbekel",
icon: <IconUsers size={18} stroke={1.8} /> icon: <IconUsers size={18} stroke={1.8} />
}, },
{ {
label: "Profile Perbekel Dari Masa Ke Masa", label: "Profil Perbekel Dari Masa Ke Masa",
value: "profile-perbekel-dari-masa-ke-masa", value: "profilperbekeldarimasakemasa",
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa", href: "/admin/desa/profil/profil-perbekel-dari-masa-ke-masa",
icon: <IconCalendar size={18} stroke={1.8} /> icon: <IconCalendar size={18} stroke={1.8} />
} }
]; ];

View File

@@ -12,22 +12,22 @@ function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
{ {
label: "Sejarah Desa", label: "Sejarah Desa",
value: "sejarahdesa", value: "sejarahdesa",
href: "/admin/desa/profile/edit/sejarah_desa" href: "/admin/desa/profil/edit/sejarah_desa"
}, },
{ {
label: "Visi Misi Desa", label: "Visi Misi Desa",
value: "visimisidesa", value: "visimisidesa",
href: "/admin/desa/profile/edit/visi_misi_desa" href: "/admin/desa/profil/edit/visi_misi_desa"
}, },
{ {
label: "Lambang Desa", label: "Lambang Desa",
value: "lambangdesa", value: "lambangdesa",
href: "/admin/desa/profile/edit/lambang_desa" href: "/admin/desa/profil/edit/lambang_desa"
}, },
{ {
label: "Maskot Desa", label: "Maskot Desa",
value: "maskotdesa", value: "maskotdesa",
href: "/admin/desa/profile/edit/maskot_desa" href: "/admin/desa/profil/edit/maskot_desa"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname) const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -43,7 +43,7 @@ function Page() {
const id = params?.id as string; const id = params?.id as string;
if (!id) { if (!id) {
toast.error('ID tidak valid'); toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa'); router.push('/admin/desa/profil/profil-desa');
return; return;
} }
@@ -106,7 +106,7 @@ function Page() {
if (success) { if (success) {
toast.success('Data berhasil disimpan'); toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa'); router.push('/admin/desa/profil/profil-desa');
} else { } else {
toast.error('Gagal menyimpan data'); toast.error('Gagal menyimpan data');
} }
@@ -156,7 +156,7 @@ function Page() {
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md"> <Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
{loadError} {loadError}
</Alert> </Alert>
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline"> <Button onClick={() => router.push('/admin/desa/profil/profil-desa')} variant="outline">
Kembali ke Halaman Utama Kembali ke Halaman Utama
</Button> </Button>
</Stack> </Stack>

View File

@@ -40,7 +40,7 @@ function Page() {
const id = params?.id as string; const id = params?.id as string;
if (!id) { if (!id) {
toast.error("ID tidak valid"); toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa"); router.push("/admin/desa/profil/profil-desa");
return; return;
} }
@@ -157,7 +157,7 @@ function Page() {
if (success) { if (success) {
toast.success("Maskot berhasil diperbarui!"); toast.success("Maskot berhasil diperbarui!");
router.push("/admin/desa/profile/profile-desa"); router.push("/admin/desa/profil/profil-desa");
} }
} catch (error) { } catch (error) {
console.error("Error update maskot:", error); console.error("Error update maskot:", error);

View File

@@ -50,7 +50,7 @@ function Page() {
const id = params?.id as string; const id = params?.id as string;
if (!id) { if (!id) {
toast.error('ID tidak valid'); toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa'); router.push('/admin/desa/profil/profil-desa');
return; return;
} }
@@ -122,7 +122,7 @@ function Page() {
if (success) { if (success) {
toast.success('Data berhasil disimpan'); toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa'); router.push('/admin/desa/profil/profil-desa');
} else { } else {
toast.error('Gagal menyimpan data'); toast.error('Gagal menyimpan data');
} }
@@ -179,7 +179,7 @@ function Page() {
{loadError} {loadError}
</Alert> </Alert>
<Button <Button
onClick={() => router.push('/admin/desa/profile/profile-desa')} onClick={() => router.push('/admin/desa/profil/profil-desa')}
variant="outline" variant="outline"
> >
Kembali ke Halaman Utama Kembali ke Halaman Utama

View File

@@ -42,7 +42,7 @@ function Page() {
const id = params?.id as string; const id = params?.id as string;
if (!id) { if (!id) {
toast.error('ID tidak valid'); toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa'); router.push('/admin/desa/profil/profil-desa');
return; return;
} }
@@ -106,7 +106,7 @@ function Page() {
if (success) { if (success) {
toast.success('Data berhasil disimpan'); toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa'); router.push('/admin/desa/profil/profil-desa');
} else { } else {
toast.error('Gagal menyimpan data'); toast.error('Gagal menyimpan data');
} }
@@ -156,7 +156,7 @@ function Page() {
{loadError} {loadError}
</Alert> </Alert>
<Button <Button
onClick={() => router.push('/admin/desa/profile/profile-desa')} onClick={() => router.push('/admin/desa/profil/profil-desa')}
variant="outline" variant="outline"
> >
Kembali ke Halaman Utama Kembali ke Halaman Utama

View File

@@ -27,7 +27,7 @@ function Page() {
return ( return (
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm"> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="lg"> <Stack gap="lg">
<Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title> <Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
{/* Sejarah Desa */} {/* Sejarah Desa */}
{sejarah && ( {sejarah && (
@@ -42,7 +42,7 @@ function Page() {
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
radius="md" radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)} onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
> >
Edit Edit
</Button> </Button>
@@ -87,7 +87,7 @@ function Page() {
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
radius="md" radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)} onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
> >
Edit Edit
</Button> </Button>
@@ -135,7 +135,7 @@ function Page() {
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
radius="md" radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)} onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
> >
Edit Edit
</Button> </Button>
@@ -180,7 +180,7 @@ function Page() {
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
radius="md" radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)} onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
> >
Edit Edit
</Button> </Button>

View File

@@ -117,7 +117,7 @@ function EditPerbekelDariMasaKeMasa() {
await state.update.update(); await state.update.update();
toast.success('Perbekel dari masa ke masa berhasil diperbarui!'); toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
} catch (error) { } catch (error) {
console.error('Error updating perbekel dari masa ke masa:', error); console.error('Error updating perbekel dari masa ke masa:', error);
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa'); toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');

View File

@@ -25,7 +25,7 @@ function DetailPerbekelDariMasa() {
state.delete.byId(selectedId); state.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa"); router.push("/admin/desa/profil/profil-perbekel-dari-masa-ke-masa");
} }
}; };
@@ -113,7 +113,7 @@ function DetailPerbekelDariMasa() {
<Button <Button
color="green" color="green"
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)} onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
variant="light" variant="light"
radius="md" radius="md"
size="md" size="md"

View File

@@ -46,7 +46,7 @@ function CreatePerbekelDariMasaKeMasa() {
state.create.form.imageId = uploaded.id; state.create.form.imageId = uploaded.id;
await state.create.create(); await state.create.create();
resetForm(); resetForm();
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.error('Gagal menambahkan perbekel dari masa ke masa'); toast.error('Gagal menambahkan perbekel dari masa ke masa');

View File

@@ -53,7 +53,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')} onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')}
> >
Tambah Baru Tambah Baru
</Button> </Button>
@@ -90,7 +90,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)} onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
> >
Detail Detail
</Button> </Button>

View File

@@ -25,7 +25,7 @@ function ProfilePerbekel() {
const id = params?.id as string; const id = params?.id as string;
if (!id) { if (!id) {
toast.error("ID tidak valid"); toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-perbekel"); router.push("/admin/desa/profil/profil-perbekel");
return; return;
} }
@@ -74,7 +74,7 @@ function ProfilePerbekel() {
const success = await perbekelState.edit.submit() const success = await perbekelState.edit.submit()
if (success) { if (success) {
toast.success("Data berhasil disimpan"); toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-perbekel"); router.push("/admin/desa/profil/profil-perbekel");
} }
} catch (error) { } catch (error) {
console.error("Error update sejarah desa:", error); console.error("Error update sejarah desa:", error);

View File

@@ -41,7 +41,7 @@ function Page() {
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
radius="md" radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)} onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
> >
Edit Edit
</Button> </Button>

View File

@@ -91,8 +91,8 @@ export const devBar = [
children: [ children: [
{ {
id: "Desa_1", id: "Desa_1",
name: "Profile", name: "Profil",
path: "/admin/desa/profile/profile-desa" path: "/admin/desa/profil/profil-desa"
}, },
{ {
id: "Desa_2", id: "Desa_2",
@@ -495,8 +495,8 @@ export const navBar = [
children: [ children: [
{ {
id: "Desa_1", id: "Desa_1",
name: "Profile", name: "Profil",
path: "/admin/desa/profile/profile-desa" path: "/admin/desa/profil/profil-desa"
}, },
{ {
id: "Desa_2", id: "Desa_2",
@@ -899,8 +899,8 @@ export const role1 = [
children: [ children: [
{ {
id: "Desa_1", id: "Desa_1",
name: "Profile", name: "Profil",
path: "/admin/desa/profile/profile-desa" path: "/admin/desa/profil/profil-desa"
}, },
{ {
id: "Desa_2", id: "Desa_2",

View File

@@ -12,6 +12,7 @@ import {
Skeleton, Skeleton,
Stack, Stack,
Text, Text,
Title,
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -41,9 +42,9 @@ export default function DetailInformasiPublikUser() {
return ( return (
<Center py="xl"> <Center py="xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Text fz="lg" fw="bold"> <Title order={4} fz={{ base: 'lg', md: 'xl' }} lh={1.3}>
Informasi tidak ditemukan Informasi tidak ditemukan
</Text> </Title>
<Button variant="light" onClick={() => router.push('/informasi-publik')}> <Button variant="light" onClick={() => router.push('/informasi-publik')}>
Kembali ke Daftar Kembali ke Daftar
</Button> </Button>
@@ -75,53 +76,60 @@ export default function DetailInformasiPublikUser() {
shadow="xs" shadow="xs"
> >
<Stack gap="xl"> <Stack gap="xl">
<Text {/* MAIN TITLE */}
fz={{ base: 'xl', md: '2xl' }} <Title
fw="bold" order={2}
lh={1.2}
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
> >
Detail Informasi Publik Detail Informasi Publik
</Text> </Title>
<Divider /> <Divider />
{/* CONTENT */}
<Stack gap="lg"> <Stack gap="lg">
{/* Jenis Informasi */}
<Box px="lg"> <Box px="lg">
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}> <Title order={5} fz={{ base: 'lg', md: 'xl' }} lh={1.3} mb={4}>
Jenis Informasi Jenis Informasi
</Text> </Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed"> <Text fz={{ base: 'sm', md: 'md' }} lh={1.5} c="black">
{data.jenisInformasi || '-'} {data.jenisInformasi || '-'}
</Text> </Text>
</Box> </Box>
{/* Tanggal Publikasi */}
<Box px="lg"> <Box px="lg">
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}> <Title order={5} fz={{ base: 'lg', md: 'xl' }} lh={1.3} mb={4}>
Tanggal Publikasi Tanggal Publikasi
</Text> </Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed"> <Text fz={{ base: 'sm', md: 'md' }} lh={1.5} c="black">
{data.tanggal {data.tanggal
? new Date(data.tanggal).toLocaleDateString('id-ID', { ? new Date(data.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
}) })
: '-'} : '-'}
</Text> </Text>
</Box> </Box>
{/* Deskripsi */}
<Box px="lg"> <Box px="lg">
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}> <Title order={5} fz={{ base: 'lg', md: 'xl' }} lh={1.3} mb={4}>
Deskripsi Deskripsi
</Text> </Title>
<Box> <Box>
<Text <Text
ta={"justify"} ta="justify"
className="prose max-w-none leading-relaxed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} fz={{ base: 'sm', md: 'md' }}
fz={{ base: 'md', md: 'lg' }} lh={1.6}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
className="prose max-w-none"
/> />
</Box> </Box>
</Box> </Box>
@@ -130,4 +138,4 @@ export default function DetailInformasiPublikUser() {
</Paper> </Paper>
</Box> </Box>
); );
} }

View File

@@ -21,7 +21,8 @@ import {
TableTr, TableTr,
Text, Text,
TextInput, TextInput,
Tooltip Tooltip,
Title
} from '@mantine/core'; } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconDeviceImacCog, IconFileInfo, IconMail, IconSearch } from '@tabler/icons-react'; import { IconBrandWhatsapp, IconDeviceImacCog, IconFileInfo, IconMail, IconSearch } from '@tabler/icons-react';
@@ -33,7 +34,7 @@ import { useTransitionRouter } from 'next-view-transitions';
function Page() { function Page() {
const listData = useProxy(daftarInformasiPublik) const listData = useProxy(daftarInformasiPublik)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 1000ms delay
const router = useTransitionRouter() const router = useTransitionRouter()
const { const {
data, data,
@@ -65,20 +66,49 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Center> <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> </Center>
<Text ta="center" fz={{ base: "1.8rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold" lh={1.4}>
<Title
order={2}
ta="center"
fz={{ base: '1.6rem', md: '2.4rem' }}
c={colors['blue-button']}
lh={1.35}
style={{ fontWeight: 700 }}
>
Daftar Informasi Publik Daftar Informasi Publik
</Text> </Title>
<Box px={{ base: "md", md: 100 }}>
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="lg"> <Stack gap="lg">
<Paper p="lg" radius="xl" shadow="sm" withBorder> <Paper p="lg" radius="xl" shadow="sm" withBorder>
<Stack gap="sm"> <Stack gap="sm">
<Text ta={"center"} fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors["blue-button"]}> <Title
order={4}
ta="center"
fz={{ base: 'lg', md: 'xl' }}
c={colors['blue-button']}
lh={1.2}
style={{ fontWeight: 700 }}
>
Tentang Informasi Publik Tentang Informasi Publik
</Text> </Title>
<Text ta={"center"} fz={{ base: 'sm', md: 'md' }} c="dimmed">
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
c="black"
lh={1.6}
style={{ maxWidth: 900, margin: '0 auto' }}
>
Daftar Informasi Publik Desa Darmasaba adalah kumpulan data yang dapat diakses oleh masyarakat sesuai dengan ketentuan peraturan yang berlaku. Daftar Informasi Publik Desa Darmasaba adalah kumpulan data yang dapat diakses oleh masyarakat sesuai dengan ketentuan peraturan yang berlaku.
</Text> </Text>
</Stack> </Stack>
@@ -97,8 +127,8 @@ function Page() {
{data.length === 0 ? ( {data.length === 0 ? (
<Center py="xl"> <Center py="xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconFileInfo size={48} stroke={1.5} color={colors["blue-button"]} /> <IconFileInfo size={48} stroke={1.5} color={colors['blue-button']} />
<Text fz="md" c="dimmed">Tidak ada informasi publik yang ditemukan.</Text> <Text fz="md" c="dimmed" lh={1.5}>Tidak ada informasi publik yang ditemukan.</Text>
</Stack> </Stack>
</Center> </Center>
) : ( ) : (
@@ -113,27 +143,42 @@ function Page() {
<TableTh fz="sm" ta="center" w="15%">Aksi</TableTh> <TableTh fz="sm" ta="center" w="15%">Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody bg={colors['white-1']}> <TableTbody bg={colors['white-1']}>
{data.map((item, index) => ( {data.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd> <TableTd ta="center">
<Text fz="sm" lh={1.4}>
{(page - 1) * 5 + index + 1}
</Text>
</TableTd>
<TableTd> <TableTd>
<Box> <Box>
<Badge variant="light" size="lg" color="blue"> <Badge variant="light" size="lg" color="blue">
<Text fw={650} fz={"sm"} c={'blue'} lineClamp={1}> <Text fw={650} fz="sm" c="blue" lineClamp={1} lh={1.2}>
{item.jenisInformasi} {item.jenisInformasi}
</Text> </Text>
</Badge> </Badge>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box> <Box>
<Text lineClamp={1} fz="sm" c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text
lineClamp={1}
fz={{ base: 'sm', md: 'md' }}
c="dark"
lh={1.5}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box> </Box>
</TableTd> </TableTd>
<TableTd ta="center"> <TableTd ta="center">
<Box> <Box>
<Text ta={"center"}> <Text ta="center" fz="sm" lh={1.4}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', { {item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
@@ -142,6 +187,7 @@ function Page() {
</Text> </Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd style={{ textAlign: 'center' }}> <TableTd style={{ textAlign: 'center' }}>
<Box> <Box>
<Tooltip label="Lihat Detail" withArrow> <Tooltip label="Lihat Detail" withArrow>
@@ -152,8 +198,9 @@ function Page() {
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik/${item.id}`)} onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik/${item.id}`)}
aria-label={`Detail ${item.jenisInformasi}`}
> >
Detail <Text fz="xs" lh={1.2}>Detail</Text>
</Button> </Button>
</Tooltip> </Tooltip>
</Box> </Box>
@@ -178,17 +225,27 @@ function Page() {
<Paper p="lg" radius="xl" shadow="xs" withBorder> <Paper p="lg" radius="xl" shadow="xs" withBorder>
<Stack gap="xs"> <Stack gap="xs">
<Text fz="lg" fw="bold" c={colors["blue-button"]}>Kontak PPID</Text> <Title order={5} fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors['blue-button']} lh={1.2}>
Kontak PPID
</Title>
<Group> <Group>
<IconMail color='gray' size={16} style={{ marginRight: 6 }} /> <IconMail color="gray" size={16} style={{ marginRight: 6 }} />
<Text c={"dimmed"} fz="sm" lh={1.6}> <Text c="dimmed" fz="sm" lh={1.6}>
Email: <Text c={"dimmed"} span fw="500">ppid@desadarmasaba.id</Text> Email:{' '}
<Text c="dimmed" span fw={500} fz="sm" lh={1.6}>
ppid@desadarmasaba.id
</Text>
</Text> </Text>
</Group> </Group>
<Group> <Group>
<IconBrandWhatsapp color='gray' size={16} style={{ marginRight: 6 }} /> <IconBrandWhatsapp color="gray" size={16} style={{ marginRight: 6 }} />
<Text c={"dimmed"} fz="sm" lh={1.6}> <Text c="dimmed" fz="sm" lh={1.6}>
WhatsApp: <Text c={"dimmed"} span fw="500">081-xxx-xxx-xxx</Text> WhatsApp:{' '}
<Text c="dimmed" span fw={500} fz="sm" lh={1.6}>
081-xxx-xxx-xxx
</Text>
</Text> </Text>
</Group> </Group>
</Stack> </Stack>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import stateDasarHukum from '@/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum'; import stateDasarHukum from '@/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Paper, Skeleton, Stack, Text, Transition } from '@mantine/core'; import { Box, Paper, Skeleton, Stack, Text, Transition, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconBook2 } from '@tabler/icons-react'; import { IconBook2 } from '@tabler/icons-react';
@@ -31,31 +31,39 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Stack
align="center" {/* HEADER */}
gap="xs" <Stack align="center" gap="xs" px={{ base: 'md', md: 100 }}>
px={{ base: 'md', md: 100 }}
>
<IconBook2 size={42} stroke={1.5} color={colors["blue-button"]} /> <IconBook2 size={42} stroke={1.5} color={colors["blue-button"]} />
<Text
<Title
order={1}
ta="center" ta="center"
fz={{ base: "2rem", md: "2.5rem" }}
c={colors["blue-button"]} c={colors["blue-button"]}
fw="bold" fz={{ base: "1.8rem", md: "2.3rem" }}
lh={1.2}
style={{ letterSpacing: "-0.5px" }} style={{ letterSpacing: "-0.5px" }}
> >
Dasar Hukum Dasar Hukum
</Text> </Title>
<Text ta="center" fz="md" c={"black"}>
<Text
ta="center"
fz={{ base: "sm", md: "md" }}
lh={1.6}
c="black"
>
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
</Text> </Text>
</Stack> </Stack>
{/* CONTENT */}
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap="lg"> <Stack gap="lg">
{dataArray.map((item, k) => ( {dataArray.map((item, k) => (
<Transition <Transition
key={k} key={k}
mounted={true} mounted
transition="fade-up" transition="fade-up"
duration={400} duration={400}
timingFunction="ease" timingFunction="ease"
@@ -73,19 +81,27 @@ function Page() {
}} }}
> >
<Stack gap="md"> <Stack gap="md">
<Text
{/* JUDUL */}
<Title
order={3}
ta="center" ta="center"
c={"black"} c="black"
fw="bold" fz={{ base: "lg", md: "xl" }}
fz={{ base: 'lg', md: 'xl' }} lh={1.3}
style={{ lineHeight: 1.4 }}
dangerouslySetInnerHTML={{ __html: item.judul }} dangerouslySetInnerHTML={{ __html: item.judul }}
/> />
{/* CONTENT */}
<Text <Text
c={"black"} c="black"
ta={"justify"} ta="justify"
fz={{ base: 'sm', md: 'md' }} fz={{ base: "sm", md: "md" }}
style={{ lineHeight: 1.7, wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7}
style={{
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{ __html: item.content }} dangerouslySetInnerHTML={{ __html: item.content }}
/> />
</Stack> </Stack>

View File

@@ -3,7 +3,22 @@
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"; import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts'; import { BarChart, PieChart } from '@mantine/charts';
import { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core"; import {
Box,
Button,
Center,
Container,
Flex,
Modal,
Paper,
Select,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useState } from "react"; import { useState } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
@@ -15,16 +30,14 @@ interface ChartDataItem {
label?: string; label?: string;
} }
function Kepuasan() { function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden); const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany; const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]); const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]); const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]); const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]); const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
const [opened, { open, close }] = useDisclosure(false) const [opened, { open, close }] = useDisclosure(false);
const resetForm = () => { const resetForm = () => {
state.create.form = { state.create.form = {
@@ -34,14 +47,14 @@ const state = useProxy(indeksKepuasanState.responden);
jenisKelaminId: "", jenisKelaminId: "",
ratingId: "", ratingId: "",
kelompokUmurId: "", kelompokUmurId: "",
} };
} };
useShallowEffect(() => { useShallowEffect(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load() indeksKepuasanState.jenisKelaminResponden.findMany.load();
indeksKepuasanState.pilihanRatingResponden.findMany.load() indeksKepuasanState.pilihanRatingResponden.findMany.load();
indeksKepuasanState.kelompokUmurResponden.findMany.load() indeksKepuasanState.kelompokUmurResponden.findMany.load();
},[]) }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@@ -51,11 +64,11 @@ const state = useProxy(indeksKepuasanState.responden);
await state.findUnique.load(idStr); await state.findUnique.load(idStr);
} }
resetForm(); resetForm();
close() close();
} catch (error) { } catch (error) {
console.error('Error submitting form:', error); console.error('Error submitting form:', error);
} }
} };
// Load data on component mount // Load data on component mount
useShallowEffect(() => { useShallowEffect(() => {
@@ -154,33 +167,52 @@ const state = useProxy(indeksKepuasanState.responden);
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Stack p="sm"> <Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}> <Container w={{ base: "100%", md: "80%" }} p="xl">
<Center> <Center>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text> {/* Main page title — converted to Title, use order (don't set fz according to rules) */}
<Title order={2} ta="center" c="dark">
Indeks Kepuasan Masyarakat
</Title>
</Center> </Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>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>
{/* Body lead text — responsive fz & lh */}
<Text ta="center" fz={{ base: "1rem", md: "1.25rem" }} lh={{ base: 1.4, md: 1.6 }} c="dimmed" mt="sm">
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}> <Center mt={10}>
<Button <Button
radius={"lg"} radius="lg"
onClick={open} onClick={open}
variant="gradient" variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }} gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button> >
Ajukan Responden
</Button>
</Center> </Center>
</Container> </Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}> <Box px="xl">
<Paper p={"lg"}> <Paper p="lg" bg={colors.Bg}>
<Stack gap={"xs"}> <Paper p="lg">
<Flex justify={"space-between"} align={"center"}> <Stack gap="xs">
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text> <Flex justify="space-between" align="center">
{/* Section heading — use Title order for hierarchy */}
<Title order={4}>
Pelayanan Terhadap Publik Desa Darmasaba
</Title>
<Box> <Box>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text> <Text fz={{ base: "0.9rem", md: "1rem" }} fw="bold" c={colors["blue-button"]}>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}> Total Responden
{state.findMany.total.toLocaleString('id-ID')}
</Text> </Text>
{/* Big number — use Title for emphasis */}
<Title order={3} ta="end" c={colors["blue-button"]} fw="bold" mt="xs">
{state.findMany.total.toLocaleString('id-ID')}
</Title>
</Box> </Box>
</Flex> </Flex>
<BarChart <BarChart
h={window.innerWidth < 480 ? 200 : 300} h={window.innerWidth < 480 ? 200 : 300}
data={barChartData} data={barChartData}
@@ -194,18 +226,16 @@ const state = useProxy(indeksKepuasanState.responden);
/> />
</Stack> </Stack>
</Paper> </Paper>
<Box py={"xl"}>
<SimpleGrid <Box py="xl">
cols={{ base: 1, sm: 2, lg: 3 }} <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" verticalSpacing="md">
spacing="md"
verticalSpacing="md"
>
{/* Chart Jenis Kelamin */} {/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Jenis Kelamin</Title> <Title order={5}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? ( {donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
@@ -218,7 +248,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip withTooltip
labelsPosition="inside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
size={250} // Fixed size in pixels size={250}
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
/> />
</Center> </Center>
@@ -227,7 +257,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataJenisKelamin.map((entry) => ( {donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center"> <Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text> <Text fz={{ base: "0.95rem", md: "1rem" }}>{entry.name}: {entry.value}</Text>
</Flex> </Flex>
))} ))}
</Stack> </Stack>
@@ -240,9 +270,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Ulasan</Title> <Title order={5}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
@@ -267,7 +298,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataRating.map((entry) => ( {donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -283,9 +314,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Kelompok Umur */} {/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Umur</Title> <Title order={5}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? ( {donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
@@ -310,7 +342,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataKelompokUmur.map((entry) => ( {donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -326,18 +358,21 @@ const state = useProxy(indeksKepuasanState.responden);
</Box> </Box>
</Paper> </Paper>
</Box> </Box>
{/* Modal */} {/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered> <Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="md">
<Stack> <Stack>
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type="text"
placeholder="Masukkan nama" placeholder="Masukkan nama"
value={state.create.form.name} value={state.create.form.name}
onChange={(val) => { onChange={(val) => {
state.create.form.name = val.currentTarget.value; state.create.form.name = val.currentTarget.value;
}} }}
// label typography
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<TextInput <TextInput
label="Tanggal" label="Tanggal"
@@ -347,10 +382,11 @@ const state = useProxy(indeksKepuasanState.responden);
onChange={(val) => { onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value; state.create.form.tanggal = val.currentTarget.value;
}} }}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Select <Select
key={"jenisKelamin"} key="jenisKelamin"
label={"Jenis Kelamin"} label="Jenis Kelamin"
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'} placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={state.create.form.jenisKelaminId || ""} value={state.create.form.jenisKelaminId || ""}
onChange={(val) => { onChange={(val) => {
@@ -358,17 +394,19 @@ const state = useProxy(indeksKepuasanState.responden);
}} }}
data={ data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || []) (indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll .filter(Boolean)
.map((item) => ({ .map((item) => ({
value: item.id, value: item.id,
label: item.name || 'Tanpa Nama', label: item.name || 'Tanpa Nama',
})) }))
} }
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
// label typography
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Select <Select
key={"rating_responden"} key="rating_responden"
label={"Rating"} label="Rating"
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={state.create.form.ratingId || ""} value={state.create.form.ratingId || ""}
onChange={(val) => { onChange={(val) => {
@@ -376,17 +414,18 @@ const state = useProxy(indeksKepuasanState.responden);
}} }}
data={ data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || []) (indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll .filter(Boolean)
.map((item) => ({ .map((item) => ({
value: item.id, value: item.id,
label: item.name || 'Tanpa Nama', label: item.name || 'Tanpa Nama',
})) }))
} }
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading} disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Select <Select
key={"kelompokUmur"} key="kelompokUmur"
label={"Kelompok Umur"} label="Kelompok Umur"
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'} placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={state.create.form.kelompokUmurId || ""} value={state.create.form.kelompokUmurId || ""}
onChange={(val) => { onChange={(val) => {
@@ -394,19 +433,16 @@ const state = useProxy(indeksKepuasanState.responden);
}} }}
data={ data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || []) (indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll .filter(Boolean)
.map((item) => ({ .map((item) => ({
value: item.id, value: item.id,
label: item.name || 'Tanpa Nama', label: item.name || 'Tanpa Nama',
})) }))
} }
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading} disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Button <Button mt={10} bg={colors['blue-button']} onClick={handleSubmit}>
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit Submit
</Button> </Button>
</Stack> </Stack>
@@ -415,36 +451,47 @@ const state = useProxy(indeksKepuasanState.responden);
</Stack> </Stack>
); );
} }
return ( return (
<Stack p={"sm"}> <Stack p="sm">
<Container size="lg" px="md"> <Container size="lg" px="md">
<Center> <Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text> {/* Main page title — Title with order */}
<Title order={2} ta="center" c="dark">
Indeks Kepuasan Masyarakat
</Title>
</Center> </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>
<Text fz={{ base: "1rem", md: "1.125rem" }} lh={{ base: 1.4, md: 1.6 }} ta="center" c="dimmed" mt="sm">
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}> <Center mt={10}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button> <Button radius="lg" bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
</Center> </Center>
</Container> </Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}> <Box px="xl">
<Paper p={"lg"}> <Paper p="lg" bg={colors.Bg}>
<Stack gap={"xs"}> <Paper p="lg">
<Stack gap="xs">
<Flex <Flex
direction={{ base: "column", sm: "row" }} direction={{ base: "column", sm: "row" }}
justify="space-between" justify="space-between"
align={{ base: "flex-start", sm: "center" }} align={{ base: "flex-start", sm: "center" }}
> >
<Text fw="bold" ta={{ base: "center", sm: "left" }}> <Title order={4} ta={{ base: "center", sm: "left" }}>
Pelayanan Terhadap Publik Desa Darmasaba Pelayanan Terhadap Publik Desa Darmasaba
</Text> </Title>
<Box mt={{ base: "sm", sm: 0 }}> <Box mt={{ base: "sm", sm: 0 }}>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text> <Text fz={{ base: "0.9rem", md: "1rem" }} fw="bold" c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}> <Title order={3} ta="end" c={colors["blue-button"]} fw="bold" mt="xs">
{state.findMany.total.toLocaleString('id-ID')} {state.findMany.total.toLocaleString('id-ID')}
</Text> </Title>
</Box> </Box>
</Flex> </Flex>
<BarChart <BarChart
h={300} h={300}
data={barChartData} data={barChartData}
@@ -458,21 +505,18 @@ const state = useProxy(indeksKepuasanState.responden);
/> />
</Stack> </Stack>
</Paper> </Paper>
<Box py={"xl"}>
<Box py="xl">
<SimpleGrid <SimpleGrid
cols={{ cols={{ base: 1, md: 1, lg: 1, xl: 3 }}
base: 1,
md: 1,
lg: 1,
xl: 3
}}
> >
{/* Chart Jenis Kelamin */} {/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Jenis Kelamin</Title> <Title order={5}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? ( {donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
@@ -494,7 +538,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataJenisKelamin.map((entry) => ( {donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center"> <Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text> <Text fz={{ base: "0.95rem", md: "1rem" }}>{entry.name}: {entry.value}</Text>
</Flex> </Flex>
))} ))}
</Stack> </Stack>
@@ -507,9 +551,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Ulasan</Title> <Title order={5}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
@@ -534,7 +579,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataRating.map((entry) => ( {donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -550,9 +595,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Kelompok Umur */} {/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Umur</Title> <Title order={5}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? ( {donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
@@ -577,7 +623,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataKelompokUmur.map((entry) => ( {donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -593,18 +639,20 @@ const state = useProxy(indeksKepuasanState.responden);
</Box> </Box>
</Paper> </Paper>
</Box> </Box>
{/* Modal */} {/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered> <Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="md">
<Stack> <Stack>
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type="text"
placeholder="Masukkan nama" placeholder="Masukkan nama"
value={state.create.form.name} value={state.create.form.name}
onChange={(val) => { onChange={(val) => {
state.create.form.name = val.currentTarget.value; state.create.form.name = val.currentTarget.value;
}} }}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<TextInput <TextInput
label="Tanggal Pengisian" label="Tanggal Pengisian"
@@ -614,10 +662,11 @@ const state = useProxy(indeksKepuasanState.responden);
onChange={(val) => { onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value; state.create.form.tanggal = val.currentTarget.value;
}} }}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Select <Select
key={"jenisKelamin"} key="jenisKelamin"
label={"Jenis Kelamin"} label="Jenis Kelamin"
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'} placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={state.create.form.jenisKelaminId || ""} value={state.create.form.jenisKelaminId || ""}
onChange={(val) => { onChange={(val) => {
@@ -625,17 +674,18 @@ const state = useProxy(indeksKepuasanState.responden);
}} }}
data={ data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || []) (indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll .filter(Boolean)
.map((item) => ({ .map((item) => ({
value: item.id, value: item.id,
label: item.name || 'Tanpa Nama', label: item.name || 'Tanpa Nama',
})) }))
} }
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Select <Select
key={"rating_responden"} key="rating_responden"
label={"Rating"} label="Rating"
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={state.create.form.ratingId || ""} value={state.create.form.ratingId || ""}
onChange={(val) => { onChange={(val) => {
@@ -643,17 +693,18 @@ const state = useProxy(indeksKepuasanState.responden);
}} }}
data={ data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || []) (indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll .filter(Boolean)
.map((item) => ({ .map((item) => ({
value: item.id, value: item.id,
label: item.name || 'Tanpa Nama', label: item.name || 'Tanpa Nama',
})) }))
} }
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading} disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Select <Select
key={"kelompokUmur"} key="kelompokUmur"
label={"Kelompok Umur"} label="Kelompok Umur"
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'} placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={state.create.form.kelompokUmurId || ""} value={state.create.form.kelompokUmurId || ""}
onChange={(val) => { onChange={(val) => {
@@ -661,19 +712,16 @@ const state = useProxy(indeksKepuasanState.responden);
}} }}
data={ data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || []) (indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll .filter(Boolean)
.map((item) => ({ .map((item) => ({
value: item.id, value: item.id,
label: item.name || 'Tanpa Nama', label: item.name || 'Tanpa Nama',
})) }))
} }
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading} disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/> />
<Button <Button mt={10} bg={colors['blue-button']} onClick={handleSubmit}>
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit Submit
</Button> </Button>
</Stack> </Stack>

View File

@@ -12,7 +12,8 @@ import {
SimpleGrid, SimpleGrid,
Stack, Stack,
Text, Text,
TextInput TextInput,
Title
} from '@mantine/core'; } from '@mantine/core';
import { IconSend2 } from '@tabler/icons-react'; import { IconSend2 } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -55,7 +56,7 @@ function Page() {
const submitForms = async () => { const submitForms = async () => {
const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik; const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik;
const hasil = await create.create(); // tunggu hasilnya const hasil = await create.create();
if (hasil) { if (hasil) {
router.push('/darmasaba/permohonan/berhasil'); router.push('/darmasaba/permohonan/berhasil');
} }
@@ -67,14 +68,17 @@ function Page() {
<BackButton /> <BackButton />
</Box> </Box>
<Text {/* MAIN PAGE TITLE */}
<Title
order={1}
ta="center" ta="center"
fz={{ base: '2rem', md: '2.5rem' }} fz={{ base: '1.8rem', sm: '2.2rem', md: '2.6rem' }}
lh={1.2}
c={colors['blue-button']} c={colors['blue-button']}
fw="bold" style={{ fontWeight: 700 }}
> >
Permohonan Informasi Publik Permohonan Informasi Publik
</Text> </Title>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Stack gap="xl"> <Stack gap="xl">
@@ -85,15 +89,18 @@ function Page() {
shadow="sm" shadow="sm"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
> >
<Text {/* SUBTITLE */}
<Title
order={2}
pb={30} pb={30}
ta="center" ta="center"
fw="bold" fz={{ base: '1.4rem', md: '1.8rem' }}
fz={{ base: 'h4', md: 'h3' }} lh={1.3}
c={colors['blue-button']} c={colors['blue-button']}
style={{ fontWeight: 700 }}
> >
Tata Cara Permohonan Tata Cara Permohonan
</Text> </Title>
<SimpleGrid pb={30} cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg"> <SimpleGrid pb={30} cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
{steps.map((v) => ( {steps.map((v) => (
@@ -116,27 +123,38 @@ function Page() {
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw="bold" fw="bold"
fz="h3" fz="h2"
lh={1}
> >
{v.number} {v.number}
</Text> </Text>
</ActionIcon> </ActionIcon>
</Center> </Center>
<Title
order={4}
ta="center"
c={colors['white-1']}
fz="lg"
lh={1.3}
style={{ fontWeight: 700 }}
>
{v.title}
</Title>
<Text <Text
ta="center" ta="center"
c={colors['white-1']} c={colors['white-1']}
fw="bold" fz="sm"
fz="lg" lh={1.4}
> >
{v.title}
</Text>
<Text ta="center" c={colors['white-1']} fz="sm">
{v.desc} {v.desc}
</Text> </Text>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
<Group justify="center"> <Group justify="center">
<Paper <Paper
p="xl" p="xl"
@@ -148,15 +166,20 @@ function Page() {
maw={800} maw={800}
> >
<Stack gap="md"> <Stack gap="md">
<Text
fw="bold" {/* FORM TITLE */}
fz={{ base: 'h4', md: 'h3' }} <Title
order={2}
ta="center" ta="center"
fz={{ base: '1.4rem', md: '1.8rem' }}
lh={1.3}
c={colors['blue-button']} c={colors['blue-button']}
style={{ fontWeight: 700 }}
> >
Formulir Permohonan Informasi Formulir Permohonan Informasi
</Text> </Title>
{/* INPUTS */}
<TextInput <TextInput
label="Nama Lengkap" label="Nama Lengkap"
placeholder="Masukkan nama lengkap Anda" placeholder="Masukkan nama lengkap Anda"
@@ -166,6 +189,7 @@ function Page() {
val.target.value; val.target.value;
}} }}
/> />
<TextInput <TextInput
label="Nomor Induk Kependudukan (NIK)" label="Nomor Induk Kependudukan (NIK)"
placeholder="Masukkan NIK" placeholder="Masukkan NIK"
@@ -175,6 +199,7 @@ function Page() {
val.target.value; val.target.value;
}} }}
/> />
<TextInput <TextInput
label="Nomor Telepon" label="Nomor Telepon"
placeholder="Masukkan nomor telepon aktif" placeholder="Masukkan nomor telepon aktif"
@@ -184,6 +209,7 @@ function Page() {
val.target.value; val.target.value;
}} }}
/> />
<TextInput <TextInput
label="Alamat Lengkap" label="Alamat Lengkap"
placeholder="Masukkan alamat sesuai identitas" placeholder="Masukkan alamat sesuai identitas"
@@ -193,6 +219,7 @@ function Page() {
val.target.value; val.target.value;
}} }}
/> />
<TextInput <TextInput
label="Alamat Email" label="Alamat Email"
placeholder="Masukkan alamat email aktif" placeholder="Masukkan alamat email aktif"
@@ -209,12 +236,14 @@ function Page() {
val.id; val.id;
}} }}
/> />
<MemperolehInformasi <MemperolehInformasi
onChange={(val) => { onChange={(val) => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form.caraMemperolehInformasiId = permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form.caraMemperolehInformasiId =
val.id; val.id;
}} }}
/> />
<MemperolehSalinan <MemperolehSalinan
onChange={(val) => { onChange={(val) => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form.caraMemperolehSalinanInformasiId = permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form.caraMemperolehSalinanInformasiId =
@@ -241,6 +270,7 @@ function Page() {
Kirim Permohonan Kirim Permohonan
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Group> </Group>

View File

@@ -12,6 +12,7 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title,
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { import {
@@ -56,13 +57,8 @@ function Page() {
const router = useRouter(); const router = useRouter();
const submit = async () => { const submit = async () => {
const { create } = stateKeberatan; const hasil = await stateKeberatan.create.create();
if (hasil) router.push('/darmasaba/permohonan/berhasil');
const hasil = await create.create(); // tunggu hasilnya
if (hasil) {
router.push('/darmasaba/permohonan/berhasil');
}
}; };
return ( return (
@@ -72,15 +68,16 @@ function Page() {
</Box> </Box>
<Stack align="center" px={{ base: 'md', md: 100 }}> <Stack align="center" px={{ base: 'md', md: 100 }}>
<Text <Title
order={1}
ta="center" ta="center"
fz={{ base: '2rem', md: '2.8rem' }} fz={{ base: '1.8rem', md: '2.6rem' }}
lh={1.2}
c={colors['blue-button']} c={colors['blue-button']}
fw={800} style={{ letterSpacing: -0.5 }}
style={{ letterSpacing: '-0.5px' }}
> >
Permohonan Keberatan Informasi Publik Permohonan Keberatan Informasi Publik
</Text> </Title>
<Paper <Paper
p="xl" p="xl"
@@ -90,26 +87,36 @@ function Page() {
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
{/* Tentang */}
<Box> <Box>
<Text fw={700} fz={{ base: 'lg', md: 'xl' }} mb={8}> <Title order={3} fz={{ base: 'lg', md: 'xl' }} lh={1.3} mb={8}>
Tentang Permohonan Keberatan Tentang Permohonan Keberatan
</Text> </Title>
<Text ta="justify" fz={{ base: 'sm', md: 'md' }} lh={1.6}>
<Text
ta="justify"
fz={{ base: 'sm', md: 'md' }}
lh={1.7}
c="black"
>
Jika Anda merasa permohonan informasi tidak ditanggapi dengan Jika Anda merasa permohonan informasi tidak ditanggapi dengan
baik atau ditolak, Anda berhak mengajukan keberatan melalui baik atau ditolak, Anda berhak mengajukan keberatan melalui
formulir berikut. formulir berikut.
</Text> </Text>
</Box> </Box>
{/* Alur */}
<Stack> <Stack>
<Text <Title
order={3}
ta="center" ta="center"
fw={700} fw={700}
fz={{ base: 'xl', md: '2xl' }} fz={{ base: 'xl', md: '2xl' }}
style={{ letterSpacing: '-0.5px' }} lh={1.2}
style={{ letterSpacing: -0.5 }}
> >
Alur Pengajuan Keberatan Alur Pengajuan Keberatan
</Text> </Title>
<SimpleGrid cols={{ base: 1, md: 4 }} spacing="lg"> <SimpleGrid cols={{ base: 1, md: 4 }} spacing="lg">
{data.map((v) => ( {data.map((v) => (
@@ -124,15 +131,23 @@ function Page() {
<Center> <Center>
<v.icon size={48} color={colors['white-1']} /> <v.icon size={48} color={colors['white-1']} />
</Center> </Center>
<Text <Text
ta="center" ta="center"
c={colors['white-1']} c={colors['white-1']}
fw={700} fw={700}
fz="lg" fz="lg"
lh={1.3}
> >
{v.title} {v.title}
</Text> </Text>
<Text ta="center" c={colors['white-1']} fz="sm">
<Text
ta="center"
c={colors['white-1']}
fz="sm"
lh={1.6}
>
{v.desc} {v.desc}
</Text> </Text>
</Stack> </Stack>
@@ -141,6 +156,7 @@ function Page() {
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
{/* Form */}
<Group justify="center"> <Group justify="center">
<Paper <Paper
p="xl" p="xl"
@@ -152,14 +168,16 @@ function Page() {
w="100%" w="100%"
> >
<Stack gap="md"> <Stack gap="md">
<Text <Title
order={3}
ta="center"
fw={700} fw={700}
fz={{ base: 'lg', md: 'xl' }} fz={{ base: 'lg', md: 'xl' }}
ta="center" lh={1.3}
mb={4} mb={4}
> >
Formulir Keberatan Formulir Keberatan
</Text> </Title>
<TextInput <TextInput
label="Nama Lengkap" label="Nama Lengkap"
@@ -196,7 +214,7 @@ function Page() {
/> />
<Box> <Box>
<Text fw={600} fz="sm" mb={6}> <Text fw={600} fz="sm" lh={1.4} mb={6}>
Alasan Keberatan Alasan Keberatan
</Text> </Text>
<PPIDTextEditor <PPIDTextEditor
@@ -222,11 +240,13 @@ function Page() {
</Paper> </Paper>
</Group> </Group>
{/* Kontak */}
<Stack gap={4} pt="lg" align="center"> <Stack gap={4} pt="lg" align="center">
<Text fw={700} fz="lg"> <Title order={4} fw={700} fz="lg" lh={1.3}>
Kontak PPID Kontak PPID
</Text> </Title>
<Text fz="sm" c="dimmed">
<Text fz="sm" lh={1.5} c="dimmed" ta="center">
Email: desadarmasaba@badungkab.go.id | WhatsApp: 081-xxx-xxx-xxx Email: desadarmasaba@badungkab.go.id | WhatsApp: 081-xxx-xxx-xxx
</Text> </Text>
</Stack> </Stack>

View File

@@ -0,0 +1,257 @@
'use client'
import stateProfilePPID from '@/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID';
import colors from '@/con/colors';
import {
Box,
Center,
Divider,
Flex,
Image,
List,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import {
IconBuildingCommunity,
IconTargetArrow,
IconTimeline,
IconUser,
} from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() {
const allList = useProxy(stateProfilePPID);
useShallowEffect(() => {
allList.profile.load('edit');
}, []);
// LOADING SKELETON
if (!allList.profile.data)
return (
<Stack bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<Skeleton h={40} />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Skeleton h={80} />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']}>
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} h={40} mb="sm" />
))}
</Paper>
</Box>
</Stack>
);
const dataArray = Array.isArray(allList.profile.data)
? allList.profile.data
: [allList.profile.data];
return (
<Box>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* Back Button */}
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
{/* Page Title */}
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: '2rem', md: '2.7rem', lg: '3.2rem', xl: '3.6rem' }}
lh={{ base: 1.1, md: 1.1 }}
fw={900}
>
Profil PPID Desa Darmasaba
</Title>
</Box>
{dataArray.map((item) => (
<Box key={item.id} px={{ base: 'md', md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
{/* LOGO & TITLE */}
<Box px={{ base: 'md', md: 100 }}>
<Center>
<Image
loading="lazy"
src="/darmasaba-icon.png"
h={{ base: 70, md: 120 }}
w={{ base: 70, md: 120 }}
alt="Logo Desa"
/>
</Center>
<Title
order={2}
ta="center"
fz={{ base: '1.4rem', md: '2.2rem', lg: '2.6rem', xl: '3rem' }}
lh={1.1}
fw={800}
mt="md"
>
Pejabat Pengelola Informasi dan Dokumentasi
</Title>
</Box>
<Divider my="lg" />
{/* GRID BLOCK */}
<Box px={{ base: 0, md: 50 }} pb={40}>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
{/* FOTO + NAMA */}
<Box px={{ base: 0, md: 50 }}>
<Paper bg={colors['white-trans-1']} radius="xl" shadow="md" withBorder>
<Stack gap={0}>
<Image
pt={{ base: 0, md: 100 }}
px="lg"
src={
item.image?.link
? `${item.image.link}?t=${Date.now()}`
: '/perbekel.png'
}
alt="Foto Pimpinan"
radius="lg"
onError={(e) => (e.currentTarget.src = '/perbekel.png')}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="lg"
radius="0 0 var(--mantine-radius-xl) var(--mantine-radius-xl)"
className="glass3"
py={{ base: 20, md: 50 }}
>
<Title
order={3}
ta="center"
c={colors['white-1']}
fz={{ base: '1.4rem', md: '2.2rem' }}
lh={1.1}
fw={900}
>
{item.name}
</Title>
</Paper>
</Stack>
</Paper>
</Box>
{/* BIOGRAFI & RIWAYAT */}
<Box>
<Stack gap="xl">
{/* BIO */}
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconUser size={28} />
<Title order={3} fz={{ base: '1.3rem', md: '1.6rem' }} lh={1.2} fw={800}>
Biografi
</Title>
</Flex>
<Box px={20}>
<Text
fz={{ base: '1rem', md: '1.1rem', lg: '1.2rem' }}
lh={1.6}
ta="justify"
dangerouslySetInnerHTML={{ __html: item.biodata }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</Box>
{/* RIWAYAT */}
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTimeline size={28} />
<Title order={3} fz={{ base: '1.3rem', md: '1.6rem' }} lh={1.2} fw={800}>
Riwayat Karir
</Title>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text
fz={{ base: '1rem', md: '1.1rem', lg: '1.2rem' }}
lh={1.6}
ta="justify"
dangerouslySetInnerHTML={{ __html: item.riwayat }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</List>
</Box>
</Stack>
</Box>
</SimpleGrid>
</Box>
{/* ORGANISASI */}
<Box pb={40}>
<Flex align="center" gap="sm" mb="sm">
<IconBuildingCommunity size={28} />
<Title order={3} fz={{ base: '1.3rem', md: '1.6rem' }} lh={1.2} fw={800}>
Pengalaman Organisasi
</Title>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text
fz={{ base: '1rem', md: '1.1rem' }}
lh={1.6}
ta="justify"
dangerouslySetInnerHTML={{ __html: item.pengalaman }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</List>
</Box>
{/* PROGRAM UNGGULAN */}
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTargetArrow size={28} />
<Title order={3} fz={{ base: '1.3rem', md: '1.6rem' }} lh={1.2} fw={800}>
Program Unggulan
</Title>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text
fz={{ base: '1rem', md: '1.1rem' }}
lh={1.6}
ta="justify"
dangerouslySetInnerHTML={{ __html: item.unggulan }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</List>
</Box>
</Paper>
</Box>
))}
</Stack>
{/* tombol scroll */}
<ScrollToTopButton />
</Box>
);
}
export default Page;

View File

@@ -1,152 +0,0 @@
'use client'
import stateProfilePPID from '@/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID';
import colors from '@/con/colors';
import { Box, Center, Divider, Flex, Image, List, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() {
const allList = useProxy(stateProfilePPID)
useShallowEffect(() => {
allList.profile.load("edit")
}, [])
if (!allList.profile.data) return (
<Stack bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<Skeleton h={40} />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Skeleton h={80} />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']}>
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} h={40} mb="sm" />
))}
</Paper>
</Box>
</Stack>
)
const dataArray = Array.isArray(allList.profile.data)
? allList.profile.data
: [allList.profile.data]
return (
<Box>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Profil PPID Desa Darmasaba
</Text>
</Box>
{dataArray.map((item) => (
<Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} w={{ base: 70, md: 120 }} alt="Logo Desa" />
</Center>
<Text ta="center" fz={{ base: "1.2rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi dan Dokumentasi
</Text>
</Box>
<Divider my="lg" />
<Box px={{ base: 0, md: 50 }} pb={40}>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
<Box px={{ base: 0, md: 50 }}>
<Paper bg={colors['white-trans-1']} radius="xl" shadow="md" withBorder>
<Stack gap={0}>
<Image
pt={{ base: 0, md: 100 }}
px="lg"
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
alt="Foto Pimpinan"
radius="lg"
onError={(e) => e.currentTarget.src = "/perbekel.png"}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="lg"
radius="0 0 var(--mantine-radius-xl) var(--mantine-radius-xl)"
className="glass3"
py={{ base: 20, md: 50 }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "xl", md: "h2" }}>
{item.name}
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box>
<Stack gap="xl">
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconUser size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
</Flex>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTimeline size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box>
</Stack>
</Box>
</SimpleGrid>
</Box>
<Box pb={40}>
<Flex align="center" gap="sm" mb="sm">
<IconBuildingCommunity size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTargetArrow size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box>
</Paper>
</Box>
))}
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
)
}
export default Page

View File

@@ -27,7 +27,6 @@ function DetailPegawaiUser() {
statePegawai.findUnique.load(params?.id as string); statePegawai.findUnique.load(params?.id as string);
}, []); }, []);
if (!statePegawai.findUnique.data) { if (!statePegawai.findUnique.data) {
return ( return (
<Stack py="lg"> <Stack py="lg">
@@ -52,7 +51,7 @@ function DetailPegawaiUser() {
}} }}
> >
<IconArrowBack size={22} color={colors['blue-button']} /> <IconArrowBack size={22} color={colors['blue-button']} />
<Text c={colors['blue-button']} fw={500}> <Text fz={{ base: 'sm', md: 'md' }} lh="1.4" fw={500} c={colors['blue-button']}>
Kembali Kembali
</Text> </Text>
</Box> </Box>
@@ -65,9 +64,7 @@ function DetailPegawaiUser() {
radius="lg" radius="lg"
shadow="sm" shadow="sm"
bg="white" bg="white"
style={{ style={{ border: '1px solid #eaeaea' }}
border: '1px solid #eaeaea',
}}
> >
<Stack align="center" gap="md"> <Stack align="center" gap="md">
{/* Foto Profil */} {/* Foto Profil */}
@@ -84,10 +81,23 @@ function DetailPegawaiUser() {
{/* Nama & Jabatan */} {/* Nama & Jabatan */}
<Stack align="center" gap={2}> <Stack align="center" gap={2}>
<Title order={3} fw={700} c={colors['blue-button']}> <Title
order={2}
c={colors['blue-button']}
fw={700}
fz={{ base: 'xl', md: '28px' }}
lh="1.2"
ta="center"
>
{data.namaLengkap || '-'} {data.gelarAkademik || ''} {data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Title> </Title>
<Text fz="sm" c="dimmed">
<Text
fz={{ base: 'sm', md: 'md' }}
lh="1.4"
c="dimmed"
ta="center"
>
{data.posisi?.nama || 'Posisi tidak tersedia'} {data.posisi?.nama || 'Posisi tidak tersedia'}
</Text> </Text>
</Stack> </Stack>
@@ -105,10 +115,10 @@ function DetailPegawaiUser() {
value={ value={
data.tanggalMasuk data.tanggalMasuk
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', { ? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
}) })
: '-' : '-'
} }
/> />
@@ -123,7 +133,7 @@ function DetailPegawaiUser() {
); );
} }
/* Komponen kecil untuk menampilkan baris informasi */ /* Komponen Baris Informasi */
function InfoRow({ function InfoRow({
label, label,
value, value,
@@ -137,11 +147,18 @@ function InfoRow({
}) { }) {
return ( return (
<Box> <Box>
<Text fz="sm" fw={600} c="dark"> <Text
fz={{ base: 'sm', md: 'md' }}
fw={600}
lh="1.3"
c="dark"
>
{label} {label}
</Text> </Text>
<Text <Text
fz="sm" fz={{ base: 'sm', md: 'md' }}
lh="1.5"
c={valueColor || 'dimmed'} c={valueColor || 'dimmed'}
style={{ style={{
whiteSpace: multiline ? 'normal' : 'nowrap', whiteSpace: multiline ? 'normal' : 'nowrap',

View File

@@ -59,10 +59,11 @@ export default function Page() {
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }} fz={{ base: 28, md: 36, lg: 44 }}
lh={{ base: 1.05, md: 1.03 }}
> >
Struktur Organisasi PPID Struktur Organisasi PPID
</Title> </Title>
<Text ta="center" c="black" maw={800}> <Text ta="center" c="black" maw={800} fz={{ base: 13, md: 15 }} lh={1.45}>
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
untuk melihat detail atau klik node untuk fokus tampilan. untuk melihat detail atau klik node untuk fokus tampilan.
</Text> </Text>
@@ -105,8 +106,8 @@ function StrukturOrganisasiPPID() {
<Center py={48}> <Center py={48}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Loader size="lg" /> <Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text> <Text fw={600} fz={{ base: 15, md: 16 }} lh={1.2}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm"> <Text c="dimmed" fz={{ base: 12, md: 13 }} lh={1.4}>
Mengambil data pegawai dan posisi. Mohon tunggu sebentar. Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
</Text> </Text>
</Stack> </Stack>
@@ -132,10 +133,10 @@ function StrukturOrganisasiPPID() {
<Center> <Center>
<IconUsers size={56} /> <IconUsers size={56} />
</Center> </Center>
<Title order={3} mt="md"> <Title order={3} mt="md" fz={{ base: 16, md: 18 }} lh={1.15}>
Data pegawai belum tersedia Data pegawai belum tersedia
</Title> </Title>
<Text c="dimmed" mt="xs"> <Text c="dimmed" mt="xs" fz={{ base: 13, md: 14 }} lh={1.4}>
Belum ada data pegawai yang tercatat untuk PPID. Belum ada data pegawai yang tercatat untuk PPID.
</Text> </Text>
<Group justify="center" mt="lg"> <Group justify="center" mt="lg">
@@ -232,11 +233,18 @@ function StrukturOrganisasiPPID() {
{/* 🔍 Controls */} {/* 🔍 Controls */}
<Paper <Paper
shadow="xs" shadow="xs"
w={{
base: '100%', // Mobile: 100%
sm: '40%', // Tablet: 95%
md: '39%', // Desktop: 70%
lg: '38%', // Desktop L: 60%
xl: '37%', // 4K: 50%
'2xl': '36%', // Ultra-wide: 45%
}}
p="md" p="md"
radius="md" radius="md"
style={{ style={{
background: colors['blue-button'], background: colors['blue-button'], // ⬅️ penting
width: '100%', // ⬅️ penting
maxWidth: '100%', // ⬅️ penting maxWidth: '100%', // ⬅️ penting
overflowX: 'auto' // ⬅️ untuk mencegah overflow overflowX: 'auto' // ⬅️ untuk mencegah overflow
}} }}
@@ -269,30 +277,33 @@ function StrukturOrganisasiPPID() {
fontSize: '0.875rem', fontSize: '0.875rem',
padding: '6px 12px', padding: '6px 12px',
minHeight: 'auto', minHeight: 'auto',
flexShrink: 0, // 👈 PENTING: mencegah tab mengecil flexShrink: 0,
}, },
}} }}
style={{ width: '100%' }} // 👈 penting
> >
<TabsList <TabsList
style={{ style={{
display: 'flex', display: 'flex',
overflowX: 'auto', overflowX: 'auto',
overflowY: 'hidden', // 👈 tambahkan ini overflowY: 'hidden',
gap: '4px', gap: '4px',
paddingBottom: '4px', paddingBottom: '4px',
flexWrap: 'nowrap', flexWrap: 'nowrap',
WebkitOverflowScrolling: 'touch', // 👈 smooth scroll di iOS WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'thin', // 👈 scrollbar tipis di Firefox scrollbarWidth: 'thin',
msOverflowStyle: '-ms-autohiding-scrollbar', // 👈 untuk IE/Edge msOverflowStyle: '-ms-autohiding-scrollbar',
maxWidth: '100%',
scrollBehavior: 'smooth', // 👈 smooth scroll
}} }}
> >
<TabsTab <TabsTab
value="zoom-out" value="zoom-out"
onClick={handleZoomOut} onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />} leftSection={<IconZoomOut size={16} />}
style={{ flexShrink: 0 }} // 👈 pastikan tidak mengecil style={{ flexShrink: 0 }}
> >
Zoom Out <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom Out</Text>
</TabsTab> </TabsTab>
<Box <Box
@@ -301,7 +312,6 @@ function StrukturOrganisasiPPID() {
px={12} px={12}
py={6} py={6}
style={{ style={{
fontSize: 14,
fontWeight: 700, fontWeight: 700,
borderRadius: '6px', borderRadius: '6px',
minWidth: 60, minWidth: 60,
@@ -310,10 +320,12 @@ function StrukturOrganisasiPPID() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0, flexShrink: 0,
whiteSpace: 'nowrap', // 👈 mencegah text wrap whiteSpace: 'nowrap',
}} }}
> >
{Math.round(scale * 100)}% <Text fz={{ base: 12, sm: 13 }} lh={1} c={colors['blue-button']}>
{Math.round(scale * 100)}%
</Text>
</Box> </Box>
<TabsTab <TabsTab
@@ -322,7 +334,7 @@ function StrukturOrganisasiPPID() {
leftSection={<IconZoomIn size={16} />} leftSection={<IconZoomIn size={16} />}
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
> >
Zoom In <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom In</Text>
</TabsTab> </TabsTab>
<TabsTab <TabsTab
@@ -330,7 +342,7 @@ function StrukturOrganisasiPPID() {
onClick={resetZoom} onClick={resetZoom}
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
> >
Reset <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Reset</Text>
</TabsTab> </TabsTab>
<TabsTab <TabsTab
@@ -345,7 +357,9 @@ function StrukturOrganisasiPPID() {
} }
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
> >
{isFullscreen ? 'Exit' : 'Fullscreen'} <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">
{isFullscreen ? 'Exit' : 'Fullscreen'}
</Text>
</TabsTab> </TabsTab>
</TabsList> </TabsList>
</Tabs> </Tabs>
@@ -451,18 +465,17 @@ function NodeCard({ node, router }: any) {
{/* Name */} {/* Name */}
<Text <Text
fw={700} fw={700}
size="sm"
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
lineClamp={2} lineClamp={2}
fz={{ base: 13, md: 15 }}
lh={1.2}
style={{ style={{
// fontSize: 'clamp(12px, 4vw, 16px)', // 👈 responsif font size
minHeight: 40, minHeight: 40,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
wordBreak: 'break-word', wordBreak: 'break-word',
lineHeight: 1.3,
}} }}
> >
{name} {name}
@@ -470,18 +483,18 @@ function NodeCard({ node, router }: any) {
{/* Title/Position */} {/* Title/Position */}
<Text <Text
size="xs"
c="dimmed" c="dimmed"
ta="center" ta="center"
fw={500} fw={500}
lineClamp={2} lineClamp={2}
fz={{ base: 12, md: 13 }}
lh={1.3}
style={{ style={{
minHeight: 32, minHeight: 32,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
wordBreak: 'break-word', wordBreak: 'break-word',
lineHeight: 1.2,
}} }}
> >
{title} {title}
@@ -504,7 +517,7 @@ function NodeCard({ node, router }: any) {
fontWeight: 600, fontWeight: 600,
}} }}
> >
Lihat Detail <Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button> </Button>
)} )}
</Stack> </Stack>

View File

@@ -1,7 +1,18 @@
'use client' 'use client'
import stateVisiMisiPPID from '@/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID'; import stateVisiMisiPPID from '@/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Divider, Transition } from '@mantine/core'; import {
Box,
Center,
Image,
Paper,
Skeleton,
Stack,
Text,
Divider,
Transition,
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconSparkles } from '@tabler/icons-react'; import { IconSparkles } from '@tabler/icons-react';
@@ -9,6 +20,7 @@ import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const allList = useProxy(stateVisiMisiPPID); const allList = useProxy(stateVisiMisiPPID);
useShallowEffect(() => { useShallowEffect(() => {
allList.findById.load("1"); allList.findById.load("1");
}, []); }, []);
@@ -35,7 +47,7 @@ function Page() {
{dataArray.map((item) => ( {dataArray.map((item) => (
<Box key={item.id} px={{ base: 'md', md: 100 }}> <Box key={item.id} px={{ base: 'md', md: 100 }}>
<Transition mounted={true} transition="fade" duration={500} timingFunction="ease"> <Transition mounted transition="fade" duration={500} timingFunction="ease">
{(styles) => ( {(styles) => (
<Paper <Paper
style={styles} style={styles}
@@ -46,56 +58,93 @@ function Page() {
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
{/* ==== MOTTO SECTION ==== */}
<Box> <Box>
<Center mb="md"> <Center mb="md">
<Image src="/darmasaba-icon.png" w={{ base: 80, md: 130 }} alt="Logo Desa Darmasaba" loading='lazy' /> <Image
src="/darmasaba-icon.png"
w={{ base: 80, md: 130 }}
alt="Logo Desa Darmasaba"
loading="lazy"
/>
</Center> </Center>
<Text
<Title
order={2}
ta="center" ta="center"
fz={{ base: 28, md: 36 }} fz={{ base: 26, md: 34 }}
fw={800} lh={1.2}
c={colors['blue-button']} c={colors['blue-button']}
> >
Moto PPID Desa Darmasaba Moto PPID Desa Darmasaba
</Text> </Title>
<Text ta="center" fz={{ base: 16, md: 20 }} mt="xs">
<Text
ta="center"
fz={{ base: 15, md: 18 }}
lh={1.5}
c={"black"}
mt="xs"
>
Memberikan informasi yang cepat, mudah, tepat, dan transparan Memberikan informasi yang cepat, mudah, tepat, dan transparan
</Text> </Text>
</Box> </Box>
<Divider my="sm" labelPosition="center" label={<IconSparkles size={18} />} /> <Divider
my="sm"
labelPosition="center"
label={<IconSparkles size={18} />}
/>
{/* ==== VISI SECTION ==== */}
<Box> <Box>
<Text ta="center" fz={{ base: 24, md: 30 }} fw={800} <Title
c={colors['blue-button']} mb="sm"> order={3}
Visi PPID
</Text>
<Text
fz={{ base: 'md', md: 'lg' }}
lh={1.7}
ta="center" ta="center"
fz={{ base: 22, md: 28 }}
lh={1.2}
c={colors['blue-button']}
mb="sm"
>
Visi PPID
</Title>
<Text
ta="center"
fz={{ base: 15, md: 18 }}
lh={1.7}
dangerouslySetInnerHTML={{ __html: item.visi }} dangerouslySetInnerHTML={{ __html: item.visi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
</Box> </Box>
<Divider my="sm" /> <Divider my="sm" />
{/* ==== MISI SECTION ==== */}
<Box> <Box>
<Text ta="center" fz={{ base: 24, md: 30 }} fw={800} <Title
c={colors['blue-button']} mb="sm"> order={3}
ta="center"
fz={{ base: 22, md: 28 }}
lh={1.2}
c={colors['blue-button']}
mb="sm"
>
Misi PPID Misi PPID
</Text> </Title>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Text <Text
ta={"justify"} ta="justify"
fz={{ base: 'md', md: 'lg' }} fz={{ base: 15, md: 18 }}
lh={1.7} lh={1.7}
dangerouslySetInnerHTML={{ __html: item.misi }} dangerouslySetInnerHTML={{ __html: item.misi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
</Box> </Box>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
)} )}

View File

@@ -168,6 +168,7 @@ export default function ModernNewsNotification({
position: "fixed", position: "fixed",
bottom: "24px", bottom: "24px",
right: "24px", right: "24px",
zIndex: 1
}} }}
> >
<ActionIcon <ActionIcon

View File

@@ -100,6 +100,7 @@ const NewsReaderLanding = () => {
borderBottomRightRadius: '20px', borderBottomRightRadius: '20px',
borderTopRightRadius: '20px', borderTopRightRadius: '20px',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
zIndex: 1
}} }}
> >
{isPointerMode ? <IconMusicOff /> : <IconMusic />} {isPointerMode ? <IconMusicOff /> : <IconMusic />}

View File

@@ -5,7 +5,20 @@ import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
import APBDesProgress from '@/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress' import APBDesProgress from '@/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress'
import { transformAPBDesData } from '@/app/darmasaba/(tambahan)/apbdes/lib/types' import { transformAPBDesData } from '@/app/darmasaba/(tambahan)/apbdes/lib/types'
import colors from '@/con/colors' import colors from '@/con/colors'
import { ActionIcon, BackgroundImage, Box, Button, Center, Group, Loader, Select, SimpleGrid, Stack, Text } from '@mantine/core' import {
ActionIcon,
BackgroundImage,
Box,
Button,
Center,
Group,
Loader,
Select,
SimpleGrid,
Stack,
Text,
Title,
} from '@mantine/core'
import { IconDownload } from '@tabler/icons-react' import { IconDownload } from '@tabler/icons-react'
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@@ -38,17 +51,15 @@ function Apbdes() {
const dataAPBDes = state.findMany.data || [] const dataAPBDes = state.findMany.data || []
const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun))) const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun)))
.sort((a, b) => b - a) // urutkan descending .sort((a, b) => b - a)
.map(year => ({ value: year.toString(), label: `Tahun ${year}` })) .map(year => ({ value: year.toString(), label: `Tahun ${year}` }))
// Pilih tahun pertama sebagai default jika belum ada yang dipilih
useEffect(() => { useEffect(() => {
if (years.length > 0 && !selectedYear) { if (years.length > 0 && !selectedYear) {
setSelectedYear(years[0].value) setSelectedYear(years[0].value)
} }
}, [years, selectedYear]) }, [years, selectedYear])
// Transform and filter data based on selected year
const currentApbdes = dataAPBDes.length > 0 const currentApbdes = dataAPBDes.length > 0
? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0]) ? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
: null : null
@@ -57,17 +68,31 @@ function Apbdes() {
return ( return (
<Stack p="sm" gap="xl" bg={colors.Bg}> <Stack p="sm" gap="xl" bg={colors.Bg}>
<Box mt={"xl"}> {/* 📌 HEADING */}
<Box mt="xl">
<Stack gap="sm"> <Stack gap="sm">
<Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}> <Title
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: '2rem', md: '3.6rem' }}
lh={{ base: 1.2, md: 1.1 }}
>
{textHeading.title} {textHeading.title}
</Text> </Title>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
<Text
ta="center"
fz={{ base: '1rem', md: '1.25rem' }}
lh={{ base: 1.5, md: 1.55 }}
c="black"
>
{textHeading.des} {textHeading.des}
</Text> </Text>
</Stack> </Stack>
</Box> </Box>
{/* Button Lihat Semua */}
<Group justify="center"> <Group justify="center">
<Button <Button
component={Link} component={Link}
@@ -81,32 +106,39 @@ function Apbdes() {
</Button> </Button>
</Group> </Group>
{/* 🔥 COMBOBOX UNTUK PILIH TAHUN */} {/* COMBOBOX */}
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Select <Select
label="Pilih Tahun APBDes" label={<Text fw={600} fz="sm">Pilih Tahun APBDes</Text>}
placeholder="Pilih tahun" placeholder="Pilih tahun"
value={selectedYear} value={selectedYear}
onChange={setSelectedYear} onChange={setSelectedYear}
data={years} data={years}
w={{ base: '100%', sm: 200 }} w={{ base: '100%', sm: 220 }}
searchable searchable
clearable clearable
nothingFoundMessage="Tidak ada tahun tersedia" nothingFoundMessage="Tidak ada tahun tersedia"
/> />
</Box> </Box>
{/* Progress */}
{currentApbdes ? ( {currentApbdes ? (
<> <APBDesProgress apbdesData={currentApbdes} />
<APBDesProgress apbdesData={currentApbdes} />
</>
) : ( ) : (
<Box px={{ base: 'md', md: 100 }} py="md"> <Box px={{ base: 'md', md: 100 }} py="md">
<Text c="dimmed">Tidak ada data APBDes untuk tahun yang dipilih.</Text> <Text fz="sm" c="dimmed" ta="center" lh={1.5}>
Tidak ada data APBDes untuk tahun yang dipilih.
</Text>
</Box> </Box>
)} )}
<SimpleGrid mx={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 3 }} spacing="lg" pb={"xl"}> {/* GRID */}
<SimpleGrid
mx={{ base: 'md', md: 100 }}
cols={{ base: 1, sm: 3 }}
spacing="lg"
pb="xl"
>
{loading ? ( {loading ? (
<Center mih={200}> <Center mih={200}>
<Loader size="lg" color="blue" /> <Loader size="lg" color="blue" />
@@ -114,10 +146,10 @@ function Apbdes() {
) : data.length === 0 ? ( ) : data.length === 0 ? (
<Center mih={200}> <Center mih={200}>
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<Text fz="lg" c="dimmed"> <Text fz="lg" c="dimmed" lh={1.4}>
Belum ada data APBDes yang tersedia Belum ada data APBDes yang tersedia
</Text> </Text>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed" lh={1.4}>
Data akan ditampilkan di sini setelah diunggah Data akan ditampilkan di sini setelah diunggah
</Text> </Text>
</Stack> </Stack>
@@ -133,25 +165,30 @@ function Apbdes() {
style={{ overflow: 'hidden' }} style={{ overflow: 'hidden' }}
> >
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} /> <Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
<Stack gap={"xs"} justify="space-between" h="100%" p="xl" pos="relative">
<Stack gap="xs" justify="space-between" h="100%" p="xl" pos="relative">
<Text <Text
c="white" c="white"
fw={600} fw={600}
fz="lg" fz={{ base: 'lg', md: 'xl' }}
ta="center" ta="center"
lh={1.35}
lineClamp={2} lineClamp={2}
> >
{v.name} {v.name}
</Text> </Text>
<Text <Text
fw="bold" fw={700}
c="white" c="white"
fz="3rem" fz={{ base: '2.4rem', md: '3.2rem' }}
ta="center" ta="center"
lh={1}
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.6)' }} style={{ textShadow: '0 2px 8px rgba(0,0,0,0.6)' }}
> >
{v.jumlah} {v.jumlah}
</Text> </Text>
<Center> <Center>
<ActionIcon <ActionIcon
component={Link} component={Link}
@@ -163,29 +200,12 @@ function Apbdes() {
> >
<IconDownload size={20} color="white" /> <IconDownload size={20} color="white" />
</ActionIcon> </ActionIcon>
</Center> </Center>
{/* <Group justify="center">
<ActionIcon
component={Link}
href={v.file?.link || ''}
radius="xl"
size="lg"
variant="gradient"
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
>
<Group align="center" gap="xs" px="md" py={6}>
<IconDownload size={25} color="white" />
</Group>
</ActionIcon>
</Group> */}
</Stack> </Stack>
</BackgroundImage> </BackgroundImage>
)) ))
)} )}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
) )
} }

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import korupsiState from "@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi"; import korupsiState from "@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Button, Center, Container, Flex, Paper, SimpleGrid, Stack, Text } from "@mantine/core"; import {
Button,
Center,
Container,
Flex,
Paper,
SimpleGrid,
Stack,
Text
} from "@mantine/core";
import { IconClipboardText } from "@tabler/icons-react"; import { IconClipboardText } from "@tabler/icons-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -11,7 +20,6 @@ import { useProxy } from "valtio/utils";
function DesaAntiKorupsi() { function DesaAntiKorupsi() {
const state = useProxy(korupsiState); const state = useProxy(korupsiState);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -19,30 +27,64 @@ function DesaAntiKorupsi() {
setLoading(true); setLoading(true);
await state.desaAntikorupsi.findMany.load(); await state.desaAntikorupsi.findMany.load();
} catch (error) { } catch (error) {
console.error('Error loading data:', error); console.error("Error loading data:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
} };
loadData(); loadData();
}, []) }, []);
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6); const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
return ( return (
<Stack gap={"0"} bg={colors.Bg} p={"sm"} my={"xs"}> <Stack gap="0" bg={colors.Bg} p="sm" my="xs">
<Container w={{ base: "100%", md: "80%" }} p={"md"} > {/* ===================== HEADER ===================== */}
<Container w={{ base: "100%", md: "80%" }} p="md">
<Center> <Center>
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text> <Text
fw={700}
ta="center"
c={colors["blue-button"]}
fz={{ base: "1.8rem", md: "3.2rem" }}
lh={{ base: "2.2rem", md: "3.4rem" }}
>
Desa Anti Korupsi
</Text>
</Center> </Center>
<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}> <Text
<Button radius={"lg"} fz={"h4"} bg={colors["blue-button"]} component={Link} href={"/darmasaba/desa-anti-korupsi/detail"}>Selengkapnya</Button> ta="center"
c="black"
fz={{ base: "1rem", md: "1.25rem" }}
lh={{ base: "1.5rem", md: "1.8rem" }}
mt="sm"
>
Desa antikorupsi mendorong pemerintahan jujur dan transparan.
Keuangan desa dikelola secara terbuka dengan melibatkan warga
dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan
sesuai kebutuhan masyarakat.
</Text>
<Center py={25}>
<Button
radius="lg"
fz={{ base: "md", md: "lg" }}
bg={colors["blue-button"]}
component={Link}
href="/darmasaba/desa-anti-korupsi/detail"
style={{ paddingInline: "2rem" }}
>
Selengkapnya
</Button>
</Center> </Center>
</Container> </Container>
{/* ===================== LIST ===================== */}
<Container w="100%" maw="80rem" px="md"> <Container w="100%" maw="80rem" px="md">
{loading ? ( {loading ? (
<Center mih={200}> <Center mih={200}>
<Text fz="lg">Memuat Data...</Text> <Text fz={{ base: "md", md: "lg" }}>Memuat Data...</Text>
</Center> </Center>
) : ( ) : (
<SimpleGrid <SimpleGrid
@@ -64,26 +106,35 @@ function DesaAntiKorupsi() {
<IconClipboardText <IconClipboardText
color={colors["blue-button"]} color={colors["blue-button"]}
size={40} size={40}
style={{ flexShrink: 0 }} // biar icon nggak ketekan style={{ flexShrink: 0 }}
/> />
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
<Stack gap={6} style={{ flex: 1, minWidth: 0 }}>
{/* Title */}
<Text <Text
fz={{ base: "sm", sm: "md", md: "lg", lg: "xl" }} // lebih besar di desktop fw={700}
c={colors["blue-button"]} c={colors["blue-button"]}
fw={600} fz={{ base: "1rem", sm: "1.1rem", md: "1.25rem" }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={{ base: "1.3rem", md: "1.5rem" }}
style={{
wordBreak: "break-word",
whiteSpace: "normal"
}}
> >
{v.kategori?.name || "Kategori"} {v.kategori?.name || "Kategori"}
</Text> </Text>
{/* Description */}
<Text <Text
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: v.name || "Name", __html: v.name || "Name"
}} }}
fz={{ base: "sm", sm: "md", md: "lg", lg: "xl" }} // sama, scaling responsif
c="dark" c="dark"
fz={{ base: "0.9rem", sm: "1rem", md: "1.15rem" }}
lh={{ base: "1.3rem", md: "1.6rem" }}
style={{ style={{
wordBreak: "break-word", wordBreak: "break-word",
whiteSpace: "normal", whiteSpace: "normal"
}} }}
/> />
</Stack> </Stack>
@@ -91,7 +142,6 @@ function DesaAntiKorupsi() {
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
)} )}
</Container> </Container>
</Stack> </Stack>

View File

@@ -15,8 +15,6 @@ interface ChartDataItem {
label?: string; label?: string;
} }
function Kepuasan() { function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden); const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany; const { data, loading } = state.findMany;
@@ -154,67 +152,118 @@ function Kepuasan() {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Stack p="sm" my={"xs"}> <Stack p="sm" my="xs">
<Container w={{ base: "100%", md: "80%" }} p={"sm"}> <Container w={{ base: "100%", md: "80%" }} p="sm">
<Center> <Center>
<Text <Title
order={2}
ta="center" ta="center"
fz={{ base: '2rem', md: '2.8rem' }} fz={{ base: '2rem', md: '2.8rem' }}
lh={{ base: 1.05, md: 1.04 }}
c={colors['blue-button']} c={colors['blue-button']}
fw={800} fw={800}
style={{ letterSpacing: '-0.5px' }} style={{ letterSpacing: '-0.5px' }}
>Indeks Kepuasan Masyarakat</Text> >
Indeks Kepuasan Masyarakat
</Title>
</Center> </Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>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}> <Text
ta="center"
fz={{ base: "0.95rem", md: "1.25rem" }}
lh={{ base: 1.45, md: 1.5 }}
c="black"
mt="sm"
>
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={12}>
<Button <Button
radius={"lg"} radius="lg"
onClick={open} onClick={open}
variant="gradient" variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }} gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button> style={{ paddingLeft: 20, paddingRight: 20, fontWeight: 600 }}
>
<Text fz={{ base: "0.95rem", md: "1rem" }} ta="center" c="white">Ajukan Responden</Text>
</Button>
</Center> </Center>
</Container> </Container>
<Box px={"sm"}>
<Paper p={"lg"} bg={colors.Bg}> <Box px="sm">
<Paper p={"lg"}> <Paper p="lg" bg={colors.Bg}>
<Stack gap={"xs"}> <Paper p="lg">
<Flex justify={"space-between"} align={"center"}> <Stack gap="xs">
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text> <Flex
<Box> direction={{ base: "column", sm: "row" }}
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text> justify="space-between"
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}> align={{ base: "flex-start", sm: "center" }}
gap={{ base: "xs", sm: "md" }}
>
<Text
fw={700}
ta={{ base: "center", sm: "left" }}
fz={{ base: "0.95rem", sm: "1rem" }}
lh={1.3}
>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
<Box
mt={{ base: "sm", sm: 0 }}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
textAlign: 'right',
}}
>
<Text fz={{ base: "0.8rem", sm: "0.95rem" }} fw={700} c={colors["blue-button"]} lh={1.2}>
Total Responden
</Text>
<Text
ta="end"
fz={{ base: "1.6rem", sm: "2rem" }}
fw={800}
c={colors["blue-button"]}
lh={1.02}
>
{state.findMany.total.toLocaleString('id-ID')} {state.findMany.total.toLocaleString('id-ID')}
</Text> </Text>
</Box> </Box>
</Flex> </Flex>
<BarChart
h={window.innerWidth < 480 ? 200 : 300} <Box style={{ overflowX: 'auto', width: '100%' }}>
data={barChartData} <BarChart
dataKey="month" h={300}
series={[{ name: 'Responden', color: colors['blue-button'] }]} data={barChartData}
tickLine="y" dataKey="month"
xAxisLabel="Bulan" series={[{ name: 'Responden', color: colors['blue-button'] }]}
yAxisLabel="Jumlah Responden" tickLine="y"
withTooltip xAxisLabel="Bulan"
tooltipAnimationDuration={200} yAxisLabel="Jumlah Responden"
/> withTooltip
tooltipAnimationDuration={200}
xAxisProps={{
angle: -45,
textAnchor: 'end',
fontSize: 12,
}}
style={{ minWidth: 'fit-content' }}
/>
</Box>
</Stack> </Stack>
</Paper> </Paper>
<Box py={"xl"}>
<SimpleGrid <Box py="xl">
cols={{ base: 1, sm: 2, lg: 3 }} <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" verticalSpacing="md">
spacing="md"
verticalSpacing="md"
>
{/* Chart Jenis Kelamin */} {/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Jenis Kelamin</Title> <Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? ( {donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : ( ) : (
<Paper p="md" radius="md" withBorder> <Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -224,19 +273,20 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="inside" // 👈 ini yang penting! labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile size={isMobile ? 180 : 250}
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
/> />
</Center> </Center>
</Box> </Box>
<Stack gap="sm" mt="md"> <Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => ( {donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center"> <Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text> <Text fz="sm" lh={1.25}>{entry.name}: {entry.value}</Text>
</Flex> </Flex>
))} ))}
</Stack> </Stack>
@@ -249,11 +299,9 @@ function Kepuasan() {
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Ulasan</Title> <Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : ( ) : (
<Paper p="md" radius="md" withBorder> <Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -263,20 +311,21 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="inside" // 👈 ini yang penting! labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile size={isMobile ? 180 : 250}
data={donutDataRating} data={donutDataRating}
/> />
</Center> </Center>
</Box> </Box>
<Box mt="md" style={{ width: '100%' }}> <Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs"> <SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => ( {donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -292,11 +341,9 @@ function Kepuasan() {
{/* Chart Kelompok Umur */} {/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Umur</Title> <Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? ( {donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : ( ) : (
<Paper p="md" radius="md" withBorder> <Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -306,20 +353,21 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="inside"// 👈 ini yang penting! labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile size={isMobile ? 180 : 250}
data={donutDataKelompokUmur} data={donutDataKelompokUmur}
/> />
</Center> </Center>
</Box> </Box>
<Box mt="md" style={{ width: '100%' }}> <Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs"> <SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => ( {donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -331,17 +379,19 @@ function Kepuasan() {
)} )}
</Stack> </Stack>
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Paper> </Paper>
</Box> </Box>
{/* Modal */} {/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered> <Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="md">
<Stack> <Stack>
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type="text"
placeholder="Masukkan nama" placeholder="Masukkan nama"
value={state.create.form.name} value={state.create.form.name}
onChange={(val) => { onChange={(val) => {
@@ -415,8 +465,9 @@ function Kepuasan() {
mt={10} mt={10}
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit} onClick={handleSubmit}
style={{ fontWeight: 700 }}
> >
Submit <Text fz="sm" ta="center" c="white">Submit</Text>
</Button> </Button>
</Stack> </Stack>
</Paper> </Paper>
@@ -424,72 +475,108 @@ function Kepuasan() {
</Stack> </Stack>
); );
} }
return ( return (
<Stack p={"sm"} my={"xs"}> <Stack p="sm" my="xs">
<Container size="lg" px="sm"> <Container size="lg" px="sm">
<Center> <Center>
<Text <Title
order={2}
ta="center" ta="center"
fz={{ base: '2rem', md: '2.8rem' }} fz={{ base: '2rem', md: '2.8rem' }}
lh={{ base: 1.05, md: 1.04 }}
c={colors['blue-button']} c={colors['blue-button']}
fw={800} fw={800}
style={{ letterSpacing: '-0.5px' }} style={{ letterSpacing: '-0.5px' }}
>Indeks Kepuasan Masyarakat</Text> >
Indeks Kepuasan Masyarakat
</Title>
</Center> </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}> <Text fz={{ base: "1rem", md: "1.25rem" }} ta="center" c="black" lh={1.5} mt="sm">
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button> 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={12}>
<Button radius="lg" bg={colors["blue-button"]} onClick={open} style={{ paddingLeft: 20, paddingRight: 20, fontWeight: 600 }}>
<Text fz={{ base: "0.95rem", md: "1rem" }} ta="center" c="white">Ajukan Responden</Text>
</Button>
</Center> </Center>
</Container> </Container>
<Box px={"md"}>
<Paper p={"lg"} bg={colors.Bg}> <Box px="md">
<Paper p={"lg"}> <Paper p="lg" bg={colors.Bg}>
<Stack gap={"xs"}> <Paper p="lg">
<Stack gap="xs">
<Flex <Flex
direction={{ base: "column", sm: "row" }} direction={{ base: "column", sm: "row" }}
justify="space-between" justify="space-between"
align={{ base: "flex-start", sm: "center" }} align={{ base: "flex-start", sm: "center" }}
gap={{ base: "xs", sm: "md" }}
> >
<Text fw="bold" ta={{ base: "center", sm: "left" }}> <Text
fw={700}
ta={{ base: "center", sm: "left" }}
fz={{ base: "0.95rem", sm: "1rem" }}
lh={1.3}
>
Pelayanan Terhadap Publik Desa Darmasaba Pelayanan Terhadap Publik Desa Darmasaba
</Text> </Text>
<Box mt={{ base: "sm", sm: 0 }}>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text> <Box
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}> mt={{ base: "sm", sm: 0 }}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
textAlign: 'right',
}}
>
<Text fz={{ base: "0.8rem", sm: "0.95rem" }} fw={700} c={colors["blue-button"]} lh={1.2}>
Total Responden
</Text>
<Text
ta="end"
fz={{ base: "1.6rem", sm: "2rem" }}
fw={800}
c={colors["blue-button"]}
lh={1.02}
>
{state.findMany.total.toLocaleString('id-ID')} {state.findMany.total.toLocaleString('id-ID')}
</Text> </Text>
</Box> </Box>
</Flex> </Flex>
<BarChart
h={300} <Box style={{ overflowX: 'auto', width: '100%' }} pb={50}>
data={barChartData} <BarChart
dataKey="month" h={300}
series={[{ name: 'Responden', color: colors['blue-button'] }]} data={barChartData}
tickLine="y" dataKey="month"
xAxisLabel="Bulan" series={[{ name: 'Responden', color: colors['blue-button'] }]}
yAxisLabel="Jumlah Responden" tickLine="y"
withTooltip xAxisLabel=""
tooltipAnimationDuration={200} yAxisLabel="Jumlah Responden"
/> withTooltip
tooltipAnimationDuration={200}
xAxisProps={{
angle: -45,
textAnchor: 'end',
fontSize: 12,
}}
style={{ minWidth: 'fit-content' }}
/>
</Box>
</Stack> </Stack>
</Paper> </Paper>
<Box py={"xl"}>
<SimpleGrid <Box py="xl">
cols={{ <SimpleGrid cols={{ base: 1, md: 1, lg: 1, xl: 3 }}>
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
{/* Chart Jenis Kelamin */} {/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Jenis Kelamin</Title> <Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? ( {donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : ( ) : (
<Paper p="md" radius="md" withBorder> <Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -499,18 +586,18 @@ function Kepuasan() {
withLabels withLabels
withTooltip withTooltip
labelsPosition="inside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
size={200} size={200}
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
/> />
</Center> </Center>
</Box> </Box>
<Stack gap="sm" mt="md"> <Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => ( {donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center"> <Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text> <Text fz="sm" lh={1.25}>{entry.name}: {entry.value}</Text>
</Flex> </Flex>
))} ))}
</Stack> </Stack>
@@ -523,11 +610,9 @@ function Kepuasan() {
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Ulasan</Title> <Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : ( ) : (
<Paper p="md" radius="md" withBorder> <Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -537,7 +622,6 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="inside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
@@ -546,12 +630,13 @@ function Kepuasan() {
/> />
</Center> </Center>
</Box> </Box>
<Box mt="md" style={{ width: '100%' }}> <Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs"> <SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => ( {donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -567,11 +652,9 @@ function Kepuasan() {
{/* Chart Kelompok Umur */} {/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Umur</Title> <Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? ( {donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : ( ) : (
<Paper p="md" radius="md" withBorder> <Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -581,7 +664,6 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="inside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
@@ -590,12 +672,13 @@ function Kepuasan() {
/> />
</Center> </Center>
</Box> </Box>
<Box mt="md" style={{ width: '100%' }}> <Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs"> <SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => ( {donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}> <Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} /> <Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value} {entry.name}: {entry.value}
</Text> </Text>
</Flex> </Flex>
@@ -607,13 +690,15 @@ function Kepuasan() {
)} )}
</Stack> </Stack>
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Paper> </Paper>
</Box> </Box>
{/* Modal */} {/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered> <Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="md">
<Stack> <Stack>
<TextInput <TextInput
label="Nama" label="Nama"
@@ -691,8 +776,9 @@ function Kepuasan() {
mt={10} mt={10}
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit} onClick={handleSubmit}
style={{ fontWeight: 700 }}
> >
Submit <Text fz="sm" ta="center" c="white">Submit</Text>
</Button> </Button>
</Stack> </Stack>
</Paper> </Paper>
@@ -701,4 +787,4 @@ function Kepuasan() {
); );
} }
export default Kepuasan; export default Kepuasan;

View File

@@ -53,14 +53,23 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
) : ( ) : (
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<IconPhotoOff size={38} stroke={1.5} /> <IconPhotoOff size={38} stroke={1.5} />
<Text size="sm" c="dimmed">
{/* ❗ Caption konsisten */}
<Text fz={{ base: 13, md: 14 }} c="dimmed">
Belum ada gambar Belum ada gambar
</Text> </Text>
</Stack> </Stack>
)} )}
</Center> </Center>
<Box mt="md"> <Box mt="md">
<Text fw={600} ta="center" size="md"> {/* ❗ Responsive Title */}
<Text
fw={600}
ta="center"
fz={{ base: 16, md: 18 }} // mobile → desktop
lh={1.3}
>
{data.name} {data.name}
</Text> </Text>
</Box> </Box>
@@ -91,10 +100,14 @@ function ModuleView() {
<Center h={320}> <Center h={320}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconPhotoOff size={54} stroke={1.5} /> <IconPhotoOff size={54} stroke={1.5} />
<Text size="lg" fw={600}>
{/* ❗ Empty title lebih besar */}
<Text fw={600} fz={{ base: 18, md: 22 }}>
Belum ada program inovasi Belum ada program inovasi
</Text> </Text>
<Text size="sm" c="dimmed">
{/* ❗ Deskripsi kecil & lembut */}
<Text fz={{ base: 14, md: 16 }} c="dimmed" ta="center" lh={1.4}>
Tambahkan program inovasi untuk ditampilkan di sini Tambahkan program inovasi untuk ditampilkan di sini
</Text> </Text>
</Stack> </Stack>
@@ -103,11 +116,12 @@ function ModuleView() {
} }
return ( return (
<ScrollArea h={280} // ✅ tinggi fixed, bisa disesuaikan <ScrollArea
h={280}
scrollbarSize={2} scrollbarSize={2}
offsetScrollbars offsetScrollbars
styles={{ styles={{
viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet viewport: { paddingRight: 8 },
}} }}
> >
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">

View File

@@ -13,10 +13,23 @@ export default function ProfileView({ data }: ProfileViewProps) {
<Card radius="2xl" className="glass3" py="xl" px="lg" withBorder> <Card radius="2xl" className="glass3" py="xl" px="lg" withBorder>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconUserCircle size={72} stroke={1.4} /> <IconUserCircle size={72} stroke={1.4} />
<Text fw={500} c="dimmed">
{/* TITLE EMPTY */}
<Text
fw={600}
c="dimmed"
fz={{ base: 'lg', sm: 'xl', md: 'xl' }}
ta="center"
>
Profil belum tersedia Profil belum tersedia
</Text> </Text>
<Text fz="sm" c="dimmed">
{/* DESCRIPTION EMPTY */}
<Text
fz={{ base: 'sm', sm: 'md' }}
c="dimmed"
ta="center"
>
Data pejabat desa akan muncul di sini Data pejabat desa akan muncul di sini
</Text> </Text>
</Stack> </Stack>
@@ -30,12 +43,12 @@ export default function ProfileView({ data }: ProfileViewProps) {
align="end" align="end"
pos="relative" pos="relative"
w={{ w={{
base: '100%', // mobile: full width base: '100%',
xs: '100%', // small mobile xs: '100%',
sm: '85%', // tablet: 85% sm: '85%',
md: '60%', // laptop: 60% md: '60%',
lg: '55%', // laptop large: 55% lg: '55%',
xl: '50%' // extra large (4K): 50% xl: '50%',
}} }}
px={{ base: 'md', sm: 'lg', md: 'xl', xl: '2xl' }} px={{ base: 'md', sm: 'lg', md: 'xl', xl: '2xl' }}
h={{ base: 'auto', sm: '500px', md: '600px', lg: '650px', xl: '700px' }} h={{ base: 'auto', sm: '500px', md: '600px', lg: '650px', xl: '700px' }}
@@ -67,13 +80,17 @@ export default function ProfileView({ data }: ProfileViewProps) {
) : ( ) : (
<Stack align="center" gap="xs" w="100%" py="xl"> <Stack align="center" gap="xs" w="100%" py="xl">
<IconUserCircle size={96} stroke={1.5} /> <IconUserCircle size={96} stroke={1.5} />
<Text c="dimmed" fz="sm"> <Text
c="dimmed"
fz={{ base: 'sm', sm: 'md' }}
ta="center"
>
Belum ada foto Belum ada foto
</Text> </Text>
</Stack> </Stack>
)} )}
{/* Box nama dan jabatan - responsive positioning */} {/* Box nama & jabatan */}
<Box <Box
pos="absolute" pos="absolute"
bottom={{ base: -30, sm: -25, md: -20 }} bottom={{ base: -30, sm: -25, md: -20 }}
@@ -94,17 +111,21 @@ export default function ProfileView({ data }: ProfileViewProps) {
backgroundColor: 'rgba(255, 255, 255, 0.95)', backgroundColor: 'rgba(255, 255, 255, 0.95)',
}} }}
> >
<Text
fz={{ base: 'xs', sm: 'sm' }} {/* POSITION / JABATAN */}
c="dimmed" <Text
lineClamp={1} fz={{ base: 'xs', sm: 'sm', md: 'md' }}
> c="dimmed"
{data.position || 'Tidak ada jabatan'} lineClamp={1}
</Text> >
{data.position || 'Tidak ada jabatan'}
</Text>
{/* NAME */}
<Text <Text
c={colors['blue-button']} c={colors['blue-button']}
fw={700} fw={700}
fz={{ base: 'lg', sm: 'xl' }} fz={{ base: 'lg', sm: 'xl', md: 'xl', lg: '2xl' }}
mt={4} mt={4}
lineClamp={2} lineClamp={2}
> >
@@ -114,4 +135,4 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Box> </Box>
</Stack> </Stack>
); );
} }

View File

@@ -26,7 +26,11 @@ function SosmedView({
data.map((item, k) => ( data.map((item, k) => (
<Tooltip <Tooltip
key={k} key={k}
label={item.name || "Tautan Sosial"} label={
<Text fz={{ base: 12, md: 14 }}>
{item.name || "Tautan Sosial"}
</Text>
}
withArrow withArrow
position="top" position="top"
transitionProps={{ transition: "pop", duration: 150 }} transitionProps={{ transition: "pop", duration: 150 }}
@@ -57,7 +61,7 @@ function SosmedView({
); );
} }
return <Box bg={colors['blue-button']} w="100%" h="100%" />; return <Box bg={colors["blue-button"]} w="100%" h="100%" />;
})()} })()}
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@@ -72,7 +76,12 @@ function SosmedView({
background: "linear-gradient(135deg, #1C6EA4 0%, #000 100%)", background: "linear-gradient(135deg, #1C6EA4 0%, #000 100%)",
}} }}
> >
<Text ta="center" c="dimmed" size="sm"> <Text
ta="center"
c="dimmed"
fz={{ base: 13, md: 15 }}
lh={1.4}
>
Belum ada media sosial yang terhubung Belum ada media sosial yang terhubung
</Text> </Text>
</Card> </Card>

View File

@@ -59,7 +59,7 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
: { status: "Tutup", message: "08:00 - 17:00" }; : { status: "Tutup", message: "08:00 - 17:00" };
}; };
// Skeleton component untuk Social Media // 🟦 Skeleton component untuk Social Media
const SosmedSkeleton = () => ( const SosmedSkeleton = () => (
<Flex gap="md" justify="center" wrap="wrap"> <Flex gap="md" justify="center" wrap="wrap">
{[1, 2, 3, 4].map((i) => ( {[1, 2, 3, 4].map((i) => (
@@ -68,7 +68,7 @@ const SosmedSkeleton = () => (
</Flex> </Flex>
); );
// Skeleton component untuk Profile // 🟦 Skeleton component untuk Profile
const ProfileSkeleton = () => ( const ProfileSkeleton = () => (
<Card <Card
radius="xl" radius="xl"
@@ -158,6 +158,8 @@ function LandingPage() {
<Stack w={{ base: "100%", md: "65%" }} gap="lg"> <Stack w={{ base: "100%", md: "65%" }} gap="lg">
<Card radius="xl" bg={colors.grey[1]} p="lg" mt={10} shadow="xl"> <Card radius="xl" bg={colors.grey[1]} p="lg" mt={10} shadow="xl">
<Stack gap="xl"> <Stack gap="xl">
{/* Header Logo */}
<Flex gap="md" wrap="wrap"> <Flex gap="md" wrap="wrap">
<Group> <Group>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}> <Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
@@ -167,6 +169,8 @@ function LandingPage() {
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" /> <Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
</Box> </Box>
</Group> </Group>
{/* Jam Operasional */}
<Grid w="100%"> <Grid w="100%">
<Grid.Col span={12}> <Grid.Col span={12}>
<Paper <Paper
@@ -177,36 +181,58 @@ function LandingPage() {
style={{ position: "relative", overflow: "hidden" }} style={{ position: "relative", overflow: "hidden" }}
> >
<Grid gutter="md"> <Grid gutter="md">
{/* Kolom 1 */}
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }}>
<Stack gap="xs"> <Stack gap="xs">
<Flex align="center" gap="xs"> <Flex align="center" gap="xs">
<IconCalendarTime size={16} color="white" /> <IconCalendarTime size={16} color="white" />
<Text c="white" fz="sm">Jam Operasional</Text> <Text c="white" fz={{ base: "xs", md: "sm" }}>
Jam Operasional
</Text>
</Flex> </Flex>
<Paper p="sm" radius="md" bg="white"> <Paper p="sm" radius="md" bg="white">
<Tooltip label="Status saat ini berdasarkan jam operasional kantor"> <Tooltip label="Status saat ini berdasarkan jam operasional kantor">
<Badge <Badge
color={workStatus.status === "Buka" ? "green" : "red"} color={workStatus.status === "Buka" ? "green" : "red"}
radius="sm" radius="sm"
variant="filled" variant="filled"
size="md"
> >
{workStatus.status} {workStatus.status}
</Badge> </Badge>
</Tooltip> </Tooltip>
<Text fw="bold" fz="lg">{workStatus.message}</Text>
<Text
fw={700}
fz={{ base: "md", md: "lg" }}
mt={4}
>
{workStatus.message}
</Text>
</Paper> </Paper>
</Stack> </Stack>
</GridCol> </GridCol>
{/* Kolom 2 */}
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }}>
<Stack gap="xs"> <Stack gap="xs">
<Flex align="center" gap="xs"> <Flex align="center" gap="xs">
<IconInfoCircle size={16} color="white" /> <IconInfoCircle size={16} color="white" />
<Text c="white" fz="sm">Hari Ini</Text> <Text c="white" fz={{ base: "xs", md: "sm" }}>
Hari Ini
</Text>
</Flex> </Flex>
<Paper p="sm" radius="md" bg="white"> <Paper p="sm" radius="md" bg="white">
<Text fz="sm">Status Kantor</Text> <Text fz={{ base: "xs", md: "sm" }} c="dimmed">
<Text fw="bold" fz="lg"> Status Kantor
{workStatus.status === "Buka" ? "Sedang Beroperasi" : "Tidak Beroperasi"} </Text>
<Text fw={700} fz={{ base: "md", md: "lg" }}>
{workStatus.status === "Buka"
? "Sedang Beroperasi"
: "Tidak Beroperasi"}
</Text> </Text>
</Paper> </Paper>
</Stack> </Stack>
@@ -217,19 +243,29 @@ function LandingPage() {
</Grid> </Grid>
</Flex> </Flex>
{/* MODULE VIEW */}
<ModuleView /> <ModuleView />
{/* Sosmed */}
{isLoadingSosmed ? ( {isLoadingSosmed ? (
<SosmedSkeleton /> <SosmedSkeleton />
) : socialMedia.length > 0 ? ( ) : socialMedia.length > 0 ? (
<SosmedView data={socialMedia} /> <SosmedView data={socialMedia} />
) : ( ) : (
<Center> <Center>
<Text c="dimmed">Belum ada tautan media sosial yang tersedia</Text> <Text fz={{ base: "sm", md: "md" }} c="dimmed">
Belum ada tautan media sosial yang tersedia
</Text>
</Center> </Center>
)} )}
<Text ta="center" c={colors.trans.dark[2]}> {/* CTA Text */}
<Text
ta="center"
c={colors.trans.dark[2]}
fz={{ base: "sm", md: "md" }}
lh={1.5}
>
Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa.
Semua lebih mudah dengan fitur interaktif yang kami sediakan. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
</Text> </Text>
@@ -237,6 +273,7 @@ function LandingPage() {
</Card> </Card>
</Stack> </Stack>
{/* PROFIL */}
{isLoadingProfile ? ( {isLoadingProfile ? (
<ProfileSkeleton /> <ProfileSkeleton />
) : profile ? ( ) : profile ? (
@@ -251,7 +288,9 @@ function LandingPage() {
style={{ height: "fit-content" }} style={{ height: "fit-content" }}
> >
<Center h={300}> <Center h={300}>
<Text c="dimmed">Informasi profil belum tersedia</Text> <Text fz={{ base: "sm", md: "md" }} c="dimmed">
Informasi profil belum tersedia
</Text>
</Center> </Center>
</Card> </Card>
)} )}
@@ -260,4 +299,4 @@ function LandingPage() {
); );
} }
export default LandingPage; export default LandingPage;

View File

@@ -28,20 +28,41 @@ const textHeading = {
const HEIGHT = 720; const HEIGHT = 720;
function Layanan() { function Layanan() {
// responsive breakpoints: base = mobile, md = desktop/tablet landscape
return ( return (
<Stack pos="relative" bg={colors.grey[1]} gap="xl" py="md"> <Stack pos="relative" bg={colors.grey[1]} gap="xl" py="md">
<Container w={{ base: "100%", md: "80%" }} p="md"> <Container w={{ base: "100%", md: "80%" }} p="md">
<Stack align="center" gap="0"> <Stack align="center" gap="0">
{/* Main title - semantic h1 */}
<Text <Text
fw="bold" component="h1"
fw={700}
c={colors["blue-button"]} c={colors["blue-button"]}
fz={{ base: "1.8rem", md: "3.4rem" }} ta="center"
// responsive sizes: mobile ~28px, desktop ~48px
fz={{ base: "1.75rem", md: "3rem" }}
// tighter line-height for large headings, slightly more compact on desktop
style={{ lineHeight: "1.05" }}
> >
{textHeading.title} {textHeading.title}
</Text> </Text>
<Text ta="center" fz={{ base: "1rem", md: "1.3rem" }}>
{/* Description - readable line-height and constrained width on desktop */}
<Text
component="p"
ta="center"
fz={{ base: "0.95rem", md: "1.15rem" }}
// more comfortable line-height for paragraphs
style={{
lineHeight: "1.6",
maxWidth: "70ch",
marginTop: 8,
}}
c="black"
>
{textHeading.des} {textHeading.des}
</Text> </Text>
<Box p="md"> <Box p="md">
<Button <Button
component={Link} component={Link}
@@ -49,6 +70,14 @@ function Layanan() {
variant="filled" variant="filled"
bg={colors["blue-button"]} bg={colors["blue-button"]}
radius={100} radius={100}
// accessible sizing: slightly smaller on mobile, comfortable on desktop
style={{
paddingLeft: 20,
paddingRight: 20,
fontSize: "md",
// ensure button text doesn't overflow on very narrow screens
whiteSpace: "nowrap",
}}
> >
Detail Detail
</Button> </Button>
@@ -175,7 +204,7 @@ function Slider() {
startXRef.current = e.pageX - containerRef.current.offsetLeft; startXRef.current = e.pageX - containerRef.current.offsetLeft;
scrollLeftRef.current = containerRef.current.scrollLeft; scrollLeftRef.current = containerRef.current.scrollLeft;
velocityRef.current = 0; velocityRef.current = 0;
containerRef.current.style.cursor = 'grabbing'; containerRef.current.style.cursor = "grabbing";
}; };
const handleMouseMove = (e: React.MouseEvent) => { const handleMouseMove = (e: React.MouseEvent) => {
@@ -196,7 +225,7 @@ function Slider() {
if (!containerRef.current || mobile) return; if (!containerRef.current || mobile) return;
isDraggingRef.current = false; isDraggingRef.current = false;
containerRef.current.style.cursor = 'grab'; containerRef.current.style.cursor = "grab";
}; };
const handleWheel = (e: React.WheelEvent) => { const handleWheel = (e: React.WheelEvent) => {
@@ -215,7 +244,7 @@ function Slider() {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Container> <Container>
<Text ta="center" c="dimmed"> <Text ta="center" c="dimmed" fz={{ base: "0.95rem", md: "1rem" }}>
Tidak ada layanan tersedia Tidak ada layanan tersedia
</Text> </Text>
</Container> </Container>
@@ -240,6 +269,8 @@ function Slider() {
scrollbarWidth: "none", scrollbarWidth: "none",
msOverflowStyle: "none", msOverflowStyle: "none",
}} }}
// ensure keyboard accessibility: allow focus outline when focused
tabIndex={0}
> >
<Box <Box
style={{ style={{
@@ -287,26 +318,56 @@ function Slider() {
pos="relative" pos="relative"
> >
<Box p="lg"> <Box p="lg">
{/* slide title - semantic h2 */}
<Text <Text
fw="bold" component="h2"
fw={700}
c="white" c="white"
fz={{base: "xl", md: "3.5rem"}} fz={{ base: "1.25rem", md: "2.4rem" }}
// tighter heading line-height but ensure readability on mobile
style={{ style={{
textAlign: "center", textAlign: "center",
lineHeight: mobile ? "1.15" : "1.02",
// clamp long names visually
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}} }}
title={_.startCase(item.name)}
> >
{_.startCase(item.name)} {_.startCase(item.name)}
</Text> </Text>
{/* optional short description - rendered if exists */}
{item.description ? (
<Text
component="p"
mt="sm"
c="white"
fz={{ base: "0.9rem", md: "1rem" }}
style={{ lineHeight: "1.5", textAlign: "center" }}
>
{item.description}
</Text>
) : null}
</Box> </Box>
<Group justify="center">
<Group justify="center" mb="lg">
<Button <Button
onClick={() => onClick={() =>
router.push(`/darmasaba/desa/layanan/${item.id}`) router.push(`/darmasaba/desa/layanan/${item.id}`)
} }
px={46} px={mobile ? 20 : 46}
radius="100" radius="100"
size="md" size={mobile ? "sm" : "md"}
bg={colors["blue-button"]} bg={colors["blue-button"]}
// ensure button text readable on all sizes
style={{
fontSize: mobile ? "0.95rem" : "1rem",
whiteSpace: "nowrap",
}}
aria-label={`Detail layanan ${_.startCase(item.name)}`}
> >
Detail Detail
</Button> </Button>
@@ -320,4 +381,4 @@ function Slider() {
); );
} }
export default Layanan; export default Layanan;

View File

@@ -1,9 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client'; 'use client';
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan"; import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
import { Stack, Box, Container, Button, Text, Loader, Paper, Center, ActionIcon } from "@mantine/core"; import {
Stack,
Box,
Container,
Button,
Text,
Loader,
Paper,
Center,
ActionIcon,
Title,
} from "@mantine/core";
import { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react"; import { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react";
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from "next-view-transitions";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import { useMediaQuery } from "@mantine/hooks"; import { useMediaQuery } from "@mantine/hooks";
@@ -12,43 +24,33 @@ function Penghargaan() {
const router = useTransitionRouter(); const router = useTransitionRouter();
const state = useProxy(penghargaanState); const state = useProxy(penghargaanState);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery("(max-width: 768px)");
const [isVideoLoaded, setIsVideoLoaded] = useState(false); const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [showVideo, setShowVideo] = useState(true);
const [videoError, setVideoError] = useState(false); const [videoError, setVideoError] = useState(false);
const [showPlayButton, setShowPlayButton] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const hasTriedAutoplay = useRef(false);
// Deteksi iOS dengan lebih akurat // ---- TYPOGRAPHY SCALE (RESPONSIVE) ----
const isIOS = typeof window !== 'undefined' && ( // ukuran dalam px, lh = line-height
/iPad|iPhone|iPod/.test(navigator.userAgent) || const TYPO = {
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) // iPad dengan iPadOS 13+ // utama / hero title
); title: { base: 22, md: 36, lh: 1.08 }, // lebih menonjol di desktop
// subheading / loader / tagline
useEffect(() => { subtitle: { base: 14, md: 16, lh: 1.35 },
// Di iOS, coba autoplay dulu, kalau gagal tampilkan fallback // teks body / deskripsi umum
if (isIOS && videoRef.current) { body: { base: 14, md: 16, lh: 1.6 },
const playPromise = videoRef.current.play(); // caption / small notes
small: { base: 12, md: 13, lh: 1.4 },
if (playPromise !== undefined) { // judul dalam kartu (card title)
playPromise paperTitle: { base: 15, md: 18, lh: 1.25 },
.then(() => { };
// Autoplay berhasil
setShowVideo(true);
setIsVideoLoaded(true);
})
.catch(() => {
// Autoplay gagal, tampilkan fallback
setShowVideo(false);
setVideoError(true);
});
}
}
}, [isIOS]);
// Load data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
setLoading(true);
try { try {
setLoading(true);
await state.findMany.load(); await state.findMany.load();
} finally { } finally {
setLoading(false); setLoading(false);
@@ -57,99 +59,134 @@ function Penghargaan() {
loadData(); loadData();
}, []); }, []);
const handlePlayVideo = () => { // Attempt autoplay setelah video loaded
setShowVideo(true); useEffect(() => {
setVideoError(false); if (isVideoLoaded && videoRef.current && !hasTriedAutoplay.current) {
hasTriedAutoplay.current = true;
// Paksa play video setelah user interaction
setTimeout(() => { const attemptAutoplay = async () => {
if (videoRef.current) { try {
videoRef.current.play().catch(err => { // Pastikan video muted sebelum play
console.error("Video play error:", err); videoRef.current!.muted = true;
setVideoError(true); await videoRef.current!.play();
}); setShowPlayButton(false);
console.log("✅ Autoplay berhasil");
} catch (err) {
console.warn("⚠️ Autoplay diblokir browser:", err);
// Tampilkan tombol play jika autoplay gagal
setShowPlayButton(true);
}
};
// Delay sedikit untuk memastikan video siap
setTimeout(attemptAutoplay, 100);
}
}, [isVideoLoaded]);
// Handle manual play
const handlePlayVideo = async () => {
if (videoRef.current) {
try {
videoRef.current.muted = true;
await videoRef.current.play();
setShowPlayButton(false);
setVideoError(false);
} catch (err) {
console.error("❌ Gagal memutar video:", err);
setVideoError(true);
} }
}, 100); }
}; };
// kalau mobile ambil 1 data aja, kalau desktop ambil 3 // Ambil data terbatas berdasarkan perangkat
const data = state.findMany.data?.slice(0, isMobile ? 1 : 3); const data = state.findMany.data?.slice(0, isMobile ? 1 : 3);
return ( return (
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }} style={{ overflow: 'hidden' }}> <Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }} style={{ overflow: "hidden" }}>
{/* Video Layer */} {/* Video background */}
{showVideo && !videoError && ( {!videoError && (
<video <video
ref={videoRef} ref={videoRef}
autoPlay
muted muted
loop loop
playsInline playsInline
preload="auto" preload="auto"
webkit-playsinline="true"
onLoadedData={() => setIsVideoLoaded(true)} onLoadedData={() => setIsVideoLoaded(true)}
onError={() => { onError={() => {
console.error("Video load error"); console.error("Video gagal dimuat");
setVideoError(true); setVideoError(true);
setShowVideo(false); }}
onCanPlayThrough={() => {
console.log("✅ Video siap diputar");
}} }}
style={{ style={{
position: 'absolute', position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: "100%",
height: '100%', height: "100%",
objectFit: 'cover', objectFit: "cover",
opacity: isVideoLoaded ? 1 : 0, opacity: isVideoLoaded ? 1 : 0,
transition: 'opacity 0.5s ease', transition: "opacity 0.5s ease",
zIndex: 0, zIndex: 0,
}} }}
> >
<source src="/assets/videos/award.mp4" type="video/mp4" /> <source src="/assets/videos/award.mp4" type="video/mp4" />
Browser Anda tidak mendukung video.
</video> </video>
)} )}
{/* Fallback Image + Play Button */} {/* Fallback background image */}
{(!showVideo || videoError) && ( {(videoError || !isVideoLoaded) && (
<Box <Box
onClick={handlePlayVideo}
style={{ style={{
position: 'absolute', position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: "100%",
height: '100%', height: "100%",
backgroundImage: "url('/mangupuraaward.jpeg')", backgroundImage: "url('/mangupuraaward.jpeg')",
backgroundSize: 'cover', backgroundSize: "cover",
backgroundPosition: 'center', backgroundPosition: "center",
backgroundRepeat: 'no-repeat', backgroundRepeat: "no-repeat",
cursor: 'pointer',
zIndex: 0, zIndex: 0,
}} }}
> />
<Center
style={{
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.3)', // overlay gelap agar icon terlihat
}}
>
<ActionIcon
size={80}
radius="xl"
variant="filled"
color="blue"
style={{
backgroundColor: 'rgba(255,255,255,0.9)',
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
}}
>
<IconPlayerPlay size={40} color="var(--mantine-color-blue-6)" />
</ActionIcon>
</Center>
</Box>
)} )}
{/* Overlay Gradient + Content */} {/* Tombol Play (muncul jika autoplay gagal atau video error) */}
{(showPlayButton || videoError) && (
<Center
onClick={handlePlayVideo}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
cursor: "pointer",
zIndex: 2,
pointerEvents: showPlayButton || videoError ? "auto" : "none",
}}
>
<ActionIcon
size={isMobile ? 64 : 80}
radius="xl"
variant="filled"
style={{
backgroundColor: "rgba(255,255,255,0.95)",
boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
animation: "pulse 2s infinite",
}}
aria-label="Play background video"
>
<IconPlayerPlay size={isMobile ? 34 : 40} color="var(--mantine-color-blue-6)" />
</ActionIcon>
</Center>
)}
{/* Overlay konten */}
<Box <Box
style={{ style={{
width: "100%", width: "100%",
@@ -161,22 +198,39 @@ function Penghargaan() {
zIndex: 1, zIndex: 1,
}} }}
> >
<Container w={{ base: "100%", md: "80%" }} mih={{ base: 500, md: 720 }} p="xl"> <Container w={{ base: "100%", md: "80%" }} maw={1100} mih={{ base: 500, md: 720 }} p={{ base: "lg", md: "xl" }}>
<Stack justify="center" align="center" gap="xl" h="100%"> <Stack justify="center" align="center" gap="xl" h="100%">
<Text {/* Hero Title - pakai Title agar semantics lebih jelas */}
fw={900} <Title
fz={{ base: "2rem", md: "2.8rem" }} order={2}
ta="center" style={{
variant="gradient" fontWeight: 800,
gradient={{ from: "cyan", to: "blue", deg: 60 }} lineHeight: TYPO.title.lh,
// Mantine support fz prop but inline style fallback ok:
fontSize: isMobile ? TYPO.title.base : TYPO.title.md,
textAlign: "center",
// gradient via CSS text-fill technique (ke Mantine gradient prop juga bisa)
background: "-webkit-linear-gradient(60deg, #22D3EE 0%, #3B82F6 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}}
aria-label="Penghargaan Desa"
> >
Penghargaan Desa Penghargaan Desa
</Text> </Title>
{/* Content area */}
{loading ? ( {loading ? (
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Loader color="blue" size="lg" /> <Loader color="blue" size={isMobile ? "md" : "lg"} />
<Text c="gray.3" fz="lg">Sedang memuat data penghargaan...</Text> <Text
c="gray.3"
fz={isMobile ? TYPO.subtitle.base : TYPO.subtitle.md}
lh={TYPO.subtitle.lh}
ta="center"
>
Sedang memuat data penghargaan...
</Text>
</Stack> </Stack>
) : data && data.length > 0 ? ( ) : data && data.length > 0 ? (
<Stack gap="md" w="100%" maw={600}> <Stack gap="md" w="100%" maw={600}>
@@ -185,47 +239,98 @@ function Penghargaan() {
key={k} key={k}
withBorder withBorder
radius="xl" radius="xl"
p="lg" p={isMobile ? "md" : "lg"}
shadow="xl" shadow="xl"
style={{ style={{
background: "rgba(255,255,255,0.07)", background: "rgba(255,255,255,0.07)",
backdropFilter: "blur(12px)", backdropFilter: "blur(12px)",
transition: "all 0.3s ease", transition: "all 0.3s ease",
}} }}
aria-label={`Penghargaan ${v.name}`}
> >
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<IconAward size={40} color="var(--mantine-color-blue-4)" /> <IconAward size={isMobile ? 36 : 40} color="var(--mantine-color-blue-4)" />
<Text fz="lg" fw={700} c="white" ta="center"> <Text
// card title: lebih tegas
fz={isMobile ? TYPO.paperTitle.base : TYPO.paperTitle.md}
fw={700}
c="white"
ta="center"
lh={TYPO.paperTitle.lh}
style={{ wordBreak: "break-word" }}
title={v.name}
>
{v.name} {v.name}
</Text> </Text>
{/* Jika ingin menambahkan deskripsi ringkas di card, gunakan body scale */}
{v.description && (
<Text
fz={isMobile ? TYPO.body.base : TYPO.body.md}
c="gray.2"
ta="center"
lh={TYPO.body.lh}
style={{ maxWidth: 520 }}
>
{v.description}
</Text>
)}
</Stack> </Stack>
</Paper> </Paper>
))} ))}
</Stack> </Stack>
) : ( ) : (
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<IconAward size={48} color="var(--mantine-color-gray-5)" /> <IconAward size={isMobile ? 40 : 48} color="var(--mantine-color-gray-5)" />
<Text c="gray.4" fz="lg" ta="center"> <Text
c="gray.4"
fz={isMobile ? TYPO.body.base : TYPO.body.md}
ta="center"
lh={TYPO.body.lh}
>
Belum ada penghargaan yang tercatat Belum ada penghargaan yang tercatat
</Text> </Text>
</Stack> </Stack>
)} )}
<Button <Button
size="lg" size={isMobile ? "md" : "lg"}
radius="xl" radius="xl"
variant="gradient" variant="gradient"
gradient={{ from: "#26667F", to: "#124170", deg: 45 }} gradient={{ from: "#26667F", to: "#124170", deg: 45 }}
rightSection={<IconArrowRight size={20} />} rightSection={<IconArrowRight size={isMobile ? 16 : 20} />}
onClick={() => router.push("/darmasaba/penghargaan")} onClick={() => router.push("/darmasaba/penghargaan")}
aria-label="Lihat semua penghargaan"
> >
Lihat Semua Penghargaan <Text
c="white"
fz={isMobile ? TYPO.body.base : TYPO.body.md}
fw={700}
style={{ lineHeight: TYPO.body.lh }}
>
Lihat Semua Penghargaan
</Text>
</Button> </Button>
</Stack> </Stack>
</Container> </Container>
</Box> </Box>
{/* CSS untuk animasi tombol play */}
<style jsx>{`
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 0.9;
}
50% {
transform: scale(1.05);
opacity: 1;
}
}
`}</style>
</Stack> </Stack>
); );
} }
export default Penghargaan; export default Penghargaan;

View File

@@ -50,31 +50,52 @@ function Potensi() {
return ( return (
<Stack p="sm" gap="xl"> <Stack p="sm" gap="xl">
<Container w={{ base: "100%", md: "80%" }} p={"md"} > {/* HEADER */}
<Text id="news-title" ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}> <Container w={{ base: "100%", md: "80%" }} p="md">
<Text
id="news-title"
ta="center"
fw={800}
c={colors["blue-button"]}
fz={{ base: "2rem", md: "3.2rem" }}
lh={{ base: "2.6rem", md: "3.6rem" }}
style={{ letterSpacing: "-0.5px" }}
>
{textHeading.title} {textHeading.title}
</Text> </Text>
<Text id="news-content" ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
<Text
id="news-content"
ta="center"
c="gray.7"
fz={{ base: "1rem", md: "1.25rem" }}
lh={{ base: "1.5rem", md: "1.9rem" }}
style={{ marginTop: 8, maxWidth: 800, marginInline: "auto" }}
>
{textHeading.des} {textHeading.des}
</Text> </Text>
</Container> </Container>
{/* LOADING STATE */}
{loading ? ( {loading ? (
<Stack align="center" justify="center" h={300}> <Stack align="center" justify="center" h={300}>
<Loader size="lg" color={colors["blue-button"]} /> <Loader size="lg" color={colors["blue-button"]} />
<Text c="gray.4">Sedang memuat potensi desa...</Text> <Text c="gray.4" fz="1rem" lh="1.4rem">
Sedang memuat potensi desa...
</Text>
</Stack> </Stack>
) : data.length === 0 ? ( ) : data.length === 0 ? (
<Stack align="center" justify="center" h={300} gap="xs"> <Stack align="center" justify="center" h={300} gap="xs">
<IconInfoCircle size={48} color={colors["blue-button"]} /> <IconInfoCircle size={48} color={colors["blue-button"]} />
<Text fw={600} c="gray.3"> <Text fw={600} c="gray.3" fz="1.2rem" lh="1.4rem">
Belum ada potensi tersedia Belum ada potensi tersedia
</Text> </Text>
<Text size="sm" c="gray.5"> <Text fz="0.9rem" lh="1.3rem" c="gray.5">
Silakan cek kembali nanti untuk pembaruan terbaru. Silakan cek kembali nanti untuk pembaruan terbaru.
</Text> </Text>
</Stack> </Stack>
) : ( ) : (
/* CARD LIST */
<SimpleGrid cols={{ base: 1, sm: 2 }}> <SimpleGrid cols={{ base: 1, sm: 2 }}>
{_.take(data, 4).map((v, k) => ( {_.take(data, 4).map((v, k) => (
<motion.div <motion.div
@@ -84,7 +105,12 @@ function Potensi() {
onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)} onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<BackgroundImage src={v.image?.link} h={320} radius={20} pos="relative"> <BackgroundImage
src={v.image?.link}
h={320}
radius={20}
pos="relative"
>
<Box <Box
pos="absolute" pos="absolute"
w="100%" w="100%"
@@ -92,6 +118,8 @@ function Potensi() {
bg={colors.trans.dark[2]} bg={colors.trans.dark[2]}
style={{ borderRadius: 20, zIndex: 0 }} style={{ borderRadius: 20, zIndex: 0 }}
/> />
{/* CARD CONTENT */}
<Stack <Stack
justify="end" justify="end"
h="100%" h="100%"
@@ -101,11 +129,24 @@ function Potensi() {
style={{ zIndex: 1 }} style={{ zIndex: 1 }}
> >
<Tooltip label={v.name} position="top-start"> <Tooltip label={v.name} position="top-start">
<Text fw={700} c="white" fz={{ base: "1.2rem", md: "1.4rem" }} truncate> <Text
fw={700}
c="white"
fz={{ base: "1.25rem", md: "1.45rem" }}
lh={{ base: "1.6rem", md: "1.8rem" }}
truncate
>
{v.name} {v.name}
</Text> </Text>
</Tooltip> </Tooltip>
<Text lineClamp={2} c="gray.2" fz={{ base: "0.8rem", md: "1rem" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
<Text
lineClamp={2}
c="gray.2"
fz={{ base: "0.85rem", md: "1rem" }}
lh={{ base: "1.2rem", md: "1.4rem" }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Stack> </Stack>
</BackgroundImage> </BackgroundImage>
</motion.div> </motion.div>
@@ -113,16 +154,18 @@ function Potensi() {
</SimpleGrid> </SimpleGrid>
)} )}
{/* BUTTON */}
<Stack align="center"> <Stack align="center">
<Group> <Group>
<Button <Button
onClick={() => router.push("/darmasaba/desa/potensi")} onClick={() => router.push("/darmasaba/desa/potensi")}
color={colors["blue-button"]} color={colors["blue-button"]}
variant="gradient" variant="gradient"
gradient={{ from: "#26667F", to: "#124170", }} gradient={{ from: "#26667F", to: "#124170" }}
radius="xl" radius="xl"
size="md" size="md"
rightSection={<IconArrowRight size={18} />} rightSection={<IconArrowRight size={18} />}
style={{ fontWeight: 600 }}
> >
Lihat Semua Potensi Lihat Semua Potensi
</Button> </Button>

View File

@@ -2,7 +2,19 @@
'use client' 'use client'
import prestasiState from "@/app/admin/(dashboard)/_state/landing-page/prestasi-desa"; import prestasiState from "@/app/admin/(dashboard)/_state/landing-page/prestasi-desa";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { BackgroundImage, Box, Button, Center, Container, Group, Loader, SimpleGrid, Stack, Text } from "@mantine/core"; import {
BackgroundImage,
Box,
Button,
Center,
Container,
Group,
Loader,
SimpleGrid,
Stack,
Text,
Title,
} from "@mantine/core";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -32,12 +44,31 @@ function Prestasi() {
<Stack p="sm" bg="linear-gradient(180deg, #ffffff 0%, #f8fbff 100%)"> <Stack p="sm" bg="linear-gradient(180deg, #ffffff 0%, #f8fbff 100%)">
<Container w={{ base: "100%", md: "80%" }} p="xl"> <Container w={{ base: "100%", md: "80%" }} p="xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Text c={colors["blue-button"]} ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}>
Prestasi Desa {/* TITLE UTAMA */}
</Text> <Title
<Text fz={{ base: "1rem", md: "1.3rem" }} ta="center" c="dimmed" maw={700}> order={1}
Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama. c={colors["blue-button"]}
ta="center"
fz={{ base: "2rem", sm: "2.6rem", md: "3.2rem" }}
lh={{ base: "2.4rem", md: "3.5rem" }}
>
Prestasi Desa
</Title>
{/* SUBTEXT */}
<Text
fz={{ base: "1rem", md: "1.2rem" }}
lh={{ base: "1.5rem", md: "1.8rem" }}
ta="center"
c="black"
maw={700}
>
Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini
menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan
bersama.
</Text> </Text>
<Button <Button
radius="xl" radius="xl"
size="lg" size="lg"
@@ -59,13 +90,13 @@ function Prestasi() {
) : data.length === 0 ? ( ) : data.length === 0 ? (
<Center mih={200}> <Center mih={200}>
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<Text fz="1.2rem" fw={500} c="dimmed"> <Text fz="1.2rem" fw={500} c="dimmed" ta="center" lh="1.4rem">
Belum ada prestasi yang ditampilkan Belum ada prestasi yang ditampilkan
</Text> </Text>
</Stack> </Stack>
</Center> </Center>
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mb={"xl"}> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mb="xl">
{data.map((v, k) => ( {data.map((v, k) => (
<BackgroundImage <BackgroundImage
key={k} key={k}
@@ -79,26 +110,32 @@ function Prestasi() {
bg="linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.7) 100%)" bg="linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.7) 100%)"
style={{ borderRadius: 27 }} style={{ borderRadius: 27 }}
/> />
<Stack justify="space-between" h="100%" pos="relative" p="lg"> <Stack justify="space-between" h="100%" pos="relative" p="lg">
<Box>
<Text {/* KATEGORI */}
c="white" <Text
fz={{ base: "1rem", md: "1.25rem" }} c="white"
ta="center" fz={{ base: "1rem", md: "1.15rem" }}
fw={500} lh={{ base: "1.4rem", md: "1.6rem" }}
> ta="center"
{v.kategori.name} fw={500}
</Text> >
</Box> {v.kategori.name}
</Text>
{/* DESKRIPSI */}
<Text <Text
fw={700} fw={700}
c="white" c="white"
fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem" }} fz={{ base: "1.4rem", md: "1.8rem", lg: "2.2rem" }}
lh={{ base: "1.8rem", md: "2.2rem", lg: "2.6rem" }}
ta="center" ta="center"
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={5} lineClamp={5}
/> />
<Group justify="center"> <Group justify="center">
<Button <Button
onClick={() => router.push(`/darmasaba/prestasi-desa/${v.id}`)} onClick={() => router.push(`/darmasaba/prestasi-desa/${v.id}`)}

View File

@@ -20,12 +20,11 @@ export default function SDGS() {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const result = await response.json() const result = await response.json()
let data = [] let data = []
if (Array.isArray(result.data)) data = result.data if (Array.isArray(result.data)) data = result.data
else if (Array.isArray(result)) data = result else if (Array.isArray(result)) data = result
else { else return setSdgsDesa([])
setSdgsDesa([])
return
}
const top4Sdgs = [...data].sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah)).slice(0, 4) const top4Sdgs = [...data].sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah)).slice(0, 4)
setSdgsDesa(top4Sdgs) setSdgsDesa(top4Sdgs)
} catch { } catch {
@@ -36,24 +35,38 @@ export default function SDGS() {
}, []) }, [])
return ( return (
<Stack p="sm" my={"xs"}> <Stack p="sm" my="xs">
<Container w={{ base: "100%", md: "80%" }} p="xl"> <Container w={{ base: "100%", md: "80%" }} p="xl">
{/* ========== TITLE SECTION ========== */}
<Center> <Center>
<Title <Title
order={1} order={1}
fz={{ base: "2.4rem", md: "3.6rem" }} fz={{ base: "2.2rem", md: "3.4rem" }}
lh={{ base: 1.1, md: 1.1 }}
fw={900} fw={900}
c={colors["blue-button"]} c={colors["blue-button"]}
ta="center"
> >
SDGs Desa SDGs Desa
</Title> </Title>
</Center> </Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan. <Text
ta="center"
fz={{ base: "1rem", md: "1.2rem" }}
lh={{ base: 1.5, md: 1.6 }}
c="black"
mt="xs"
mb="md"
>
SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
</Text> </Text>
<Box py="lg"> <Box py="lg">
{sdgsDesa && sdgsDesa.length > 0 ? ( {sdgsDesa && sdgsDesa.length > 0 ? (
/* ========== LIST GRID ========== */
<SimpleGrid cols={{ base: 1, sm: 4 }} spacing="xl" verticalSpacing="xl" pb={30}> <SimpleGrid cols={{ base: 1, sm: 4 }} spacing="xl" verticalSpacing="xl" pb={30}>
{sdgsDesa.map((item) => ( {sdgsDesa.map((item) => (
<motion.div <motion.div
@@ -70,7 +83,7 @@ export default function SDGS() {
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)", background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)", border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease", transition: "all 0.3s ease",
height: "100%", // biar tinggi antar card konsisten height: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}} }}
@@ -101,23 +114,26 @@ export default function SDGS() {
</Box> </Box>
</Center> </Center>
{/* Stack isi teks & angka */}
<Stack justify="space-between" align="center" gap="xs" h="100%"> <Stack justify="space-between" align="center" gap="xs" h="100%">
{/* JUDUL ITEM */}
<Text <Text
ta="center" ta="center"
fz={{ base: "lg", md: "xl" }} fz={{ base: "lg", md: "xl" }}
lh={{ base: 1.3, md: 1.3 }}
fw={700} fw={700}
mb="xs" style={{ minHeight: mobile ? 60 : 70 }}
style={{ minHeight: mobile ? 60 : 70 }} // biar judulnya punya tinggi tetap
> >
{item.name} {item.name}
</Text> </Text>
{/* ANGKA */}
<Title <Title
order={2} order={2}
ta="center" ta="center"
style={{ style={{
fontSize: mobile ? "2.4rem" : "3.2rem", fontSize: mobile ? "2.2rem" : "3rem",
lineHeight: 1.1,
fontWeight: 900, fontWeight: 900,
letterSpacing: "-0.5px", letterSpacing: "-0.5px",
color: "#124170", color: "#124170",
@@ -132,14 +148,15 @@ export default function SDGS() {
</SimpleGrid> </SimpleGrid>
) : ( ) : (
/* ========== EMPTY STATE ========== */
<Center mih={200} style={{ flexDirection: "column" }}> <Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} /> <IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
<Text fz="lg" c="dimmed"> <Text fz="lg" lh={1.4} c="dimmed">Data SDGs Desa belum tersedia</Text>
Data SDGs Desa belum tersedia
</Text>
</Center> </Center>
)} )}
{/* BUTTON */}
<Center> <Center>
<Button <Button
component={Link} component={Link}
@@ -152,18 +169,19 @@ export default function SDGS() {
style={{ style={{
boxShadow: "0 6px 14px rgba(18,65,112,0.25)", boxShadow: "0 6px 14px rgba(18,65,112,0.25)",
transition: "all 0.3s ease", transition: "all 0.3s ease",
transform: "translateY(0)",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-4px)"; e.currentTarget.style.transform = "translateY(-4px)"
e.currentTarget.style.boxShadow = "0 10px 20px rgba(18,65,112,0.35)"; e.currentTarget.style.boxShadow = "0 10px 20px rgba(18,65,112,0.35)"
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0)"; e.currentTarget.style.transform = "translateY(0)"
e.currentTarget.style.boxShadow = "0 6px 14px rgba(18,65,112,0.25)"; e.currentTarget.style.boxShadow = "0 6px 14px rgba(18,65,112,0.25)"
}} }}
> >
<Text c="white" fz={{ base: "md", md: "lg" }} fw="bold">Jelajahi Semua Tujuan SDGs Desa</Text> <Text c="white" fz={{ base: "md", md: "lg" }} lh={1.3} fw={600}>
Jelajahi Semua Tujuan SDGs Desa
</Text>
</Button> </Button>
</Center> </Center>
</Box> </Box>

View File

@@ -158,8 +158,6 @@ export default function Page() {
<SDGS /> <SDGS />
<Apbdes /> <Apbdes />
<Prestasi /> <Prestasi />
</Stack>
<ScrollToTopButton /> <ScrollToTopButton />
<NewsReaderLanding /> <NewsReaderLanding />
@@ -170,6 +168,8 @@ export default function Page() {
onSeen={handleSeen} onSeen={handleSeen}
autoShowDelay={2000} autoShowDelay={2000}
/> />
</Stack>
</Box> </Box>
); );
} }

View File

@@ -5,8 +5,8 @@ const navbarListMenu = [
children: [ children: [
{ {
id: "1.1", id: "1.1",
name: "Profile PPID", name: "Profil PPID",
href: "/darmasaba/ppid/profile-ppid" href: "/darmasaba/ppid/profil-ppid"
}, },
{ {
id: "1.2", id: "1.2",
@@ -53,8 +53,8 @@ const navbarListMenu = [
children: [ children: [
{ {
id: "2.1", id: "2.1",
name: "Profile", name: "Profil",
href: "/darmasaba/desa/profile" href: "/darmasaba/desa/profil"
}, },
{ {
id: "2.2", id: "2.2",