Fix QC Kak Inno 17 Okt 25, Fix QC Kak Ayu 17 Okt 25, & Fix Qc Pak Jun 17 Okt 25

This commit is contained in:
2025-10-21 12:17:30 +08:00
parent 9055b40769
commit fb596f9033
26 changed files with 606 additions and 324 deletions

View File

@@ -132,7 +132,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<Tooltip label="Keluar" position="bottom" withArrow> <Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
router.push("/login"); router.push("/darmasaba");
}} }}
color={colors["blue-button"]} color={colors["blue-button"]}
radius="xl" radius="xl"

View File

@@ -28,23 +28,37 @@ const searchState = proxy({
searchState.loading = true; searchState.loading = true;
const res = await ApiFetch.api.search.findMany.get({ try {
query: { const res = await ApiFetch.api.search.findMany.get({
query: searchState.query, query: {
page: searchState.page, query: searchState.query,
limit: searchState.limit, page: searchState.page,
type: searchState.type, limit: searchState.limit,
}, type: searchState.type,
}); },
});
if (searchState.page === 1) { console.log("Search API Response:", res);
searchState.results = res.data?.data || []; const rawItems = res.data?.data || [];
} else { const parsedItems = structuredClone(rawItems); // ✅ penting!
searchState.results.push(...(res.data?.data || []));
console.log("✅ Parsed items:", parsedItems);
if (searchState.page === 1) {
searchState.results = parsedItems;
} else {
searchState.results.push(...parsedItems);
}
console.log("Search results render:", searchState.results);
searchState.nextPage = res.data?.nextPage || null;
} catch (error) {
console.error("Search fetch error:", error);
} finally {
searchState.loading = false;
} }
searchState.nextPage = res.data?.nextPage || null;
searchState.loading = false;
}, },
async next() { async next() {

View File

@@ -48,7 +48,7 @@ function Page() {
p={10} p={10}
mb={50} mb={50}
h={400} h={400}
w={150} w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
data={data.map((item) => ({ data={data.map((item) => ({
id: item.id, id: item.id,
Pekerjaan: item.pekerjaan, Pekerjaan: item.pekerjaan,

View File

@@ -121,7 +121,12 @@ function Page() {
</Badge> </Badge>
</Group> </Group>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
Diposting: {v.createdAt.toLocaleDateString()} Diposting: {new Date(v.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Text> </Text>
<Divider /> <Divider />
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end"> <Text fz="sm" lh={1.5} lineClamp={3} truncate="end">

View File

@@ -0,0 +1,120 @@
'use client';
import colors from '@/con/colors';
import { Button, Center, Flex, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import posyanduState from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
export default function DetailPosyanduUser() {
const statePosyandu = useProxy(posyanduState);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePosyandu.findUnique.load(params?.id as string);
}, []);
if (!statePosyandu.findUnique.data) {
return (
<Stack py="xl" px={{ base: 'md', md: 100 }}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = statePosyandu.findUnique.data;
return (
<Stack pos="relative" bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} gap="xl">
{/* Tombol Kembali */}
<Group>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
mb="sm"
c={colors['blue-button']}
>
Kembali
</Button>
</Group>
<Paper
withBorder
p="xl"
radius="lg"
shadow="md"
bg={colors['white-trans-1']}
maw={800}
mx="auto"
>
<Stack gap="md">
{/* Header */}
<Text
ta="center"
fz={{ base: '1.8rem', md: '2.2rem' }}
fw={700}
c={colors['blue-button']}
>
{data.name || 'Posyandu Desa'}
</Text>
{/* Gambar */}
{data.image?.link ? (
<Center>
<Image
src={data.image.link}
alt={`Gambar ${data.name}`}
w="100%"
h={300}
radius="md"
fit="cover"
loading="lazy"
/>
</Center>
) : (
<Center>
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
</Center>
)}
{/* Info utama */}
<Stack gap="sm" mt="md">
<Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text fz="sm" c="dimmed">
{data.nomor || 'Nomor tidak tersedia'}
</Text>
</Flex>
<Flex align="flex-start" gap="xs">
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
<Flex align="flex-start" gap="xs">
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
c="dimmed"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
</Stack>
</Stack>
</Paper>
</Stack>
);
}

View File

@@ -1,18 +1,19 @@
'use client' 'use client'
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu"; import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Badge, Box, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core"; import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react"; import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto"; import BackButton from "../../desa/layanan/_com/BackButto";
import { useDebouncedValue } from "@mantine/hooks"; import { useTransitionRouter } from "next-view-transitions";
export default function Page() { export default function Page() {
const state = useProxy(posyandustate); const state = useProxy(posyandustate);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useTransitionRouter()
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
@@ -133,33 +134,41 @@ export default function Page() {
loading="lazy" loading="lazy"
/> />
</Center> </Center>
<Flex align="center" gap="xs"> <Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} /> <IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text fz="sm" c="dimmed"> <Box>
{v.nomor || "Tidak tersedia"} <Text fz="sm" c="dimmed" lh={1.4}>
</Text> {v.nomor || "Tidak tersedia"}
</Text>
</Box>
</Flex> </Flex>
<Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} /> <Flex align="flex-start" gap="xs">
<Text fz="sm" c="dimmed"> <IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
Jadwal:{" "} <Box>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} /> <Text fz="sm" c="dimmed" lh={1.4}>
</Text> <strong>Jadwal:</strong>{" "}
<span
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
/>
</Text>
</Box>
</Flex> </Flex>
<Spoiler
key={`spoiler-${v.id}`} <Flex align="flex-start" gap="xs">
maxHeight={70} <IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
showLabel="Lihat selengkapnya"
hideLabel="Sembunyikan"
transitionDuration={300}
>
<Text <Text
fz="sm" fz="sm"
lh={1.5} lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={3}
truncate="end"
/> />
</Spoiler> </Flex>
<Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>Detail</Button>
</Stack> </Stack>
</Paper> </Paper>
))} ))}

View File

@@ -43,15 +43,48 @@ export default function Page() {
const loadingGrid = state.findMany.loading; const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading; const loadingFeatured = featured.loading;
// Load featured data once on component mount
useEffect(() => { useEffect(() => {
if (!featured.data && !loadingFeatured) { let mounted = true;
gotongRoyongState.kegiatanDesa.findFirst.load();
const loadFeatured = async () => {
try {
if (!featured.data && !loadingFeatured) {
await gotongRoyongState.kegiatanDesa.findFirst.load();
}
} catch (error) {
console.error('Error loading featured data:', error);
}
};
if (mounted) {
loadFeatured();
} }
}, [featured.data, loadingFeatured]);
return () => {
mounted = false;
};
}, []); // Empty dependency array to run only once on mount
useEffect(() => { useEffect(() => {
const limit = 3; let mounted = true;
state.findMany.load(page, limit, search);
const loadData = async () => {
try {
const limit = 3;
await state.findMany.load(page, limit, search);
} catch (error) {
console.error('Error loading data:', error);
}
};
if (mounted) {
loadData();
}
return () => {
mounted = false;
};
}, [page, search]); }, [page, search]);
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
@@ -59,7 +92,9 @@ export default function Page() {
if (search) url.set('search', search); if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString()); if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); else url.delete('page');
router.replace(`?${url.toString()}`);
// Use push instead of replace to keep browser history
router.push(`?${url.toString()}`, { scroll: false });
}; };
const featuredData = featured.data; const featuredData = featured.data;

View File

@@ -1,9 +1,9 @@
'use client' 'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react'; import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconRoute, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -122,20 +122,28 @@ function Page() {
<Stack gap="md"> <Stack gap="md">
{data2?.map((v, k) => ( {data2?.map((v, k) => (
<Paper key={k} p="md" withBorder radius="md"> <Paper key={k} p="md" withBorder radius="md">
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text> <Group justify='space-between'>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text> <Box>
{v.lat && v.lng ? ( <Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<a <Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`} </Box>
target="_blank" <Box>
rel="noopener noreferrer" <IconRoute color={colors['blue-button']} size={30} />
style={{ color: colors['blue-button'], textDecoration: 'none' }} <Text fw={"bold"} fz="sm" c={colors['blue-button']}>Rute</Text>
> </Box>
<Text fz="sm">📌 Buka di Google Maps</Text> </Group>
</a> {v.lat && v.lng ? (
) : ( <a
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
)} target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'none' }}
>
<Text fz="sm">📌 Lihat Peta Lebih Besar</Text>
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Paper> </Paper>
))} ))}
</Stack> </Stack>

View File

@@ -92,7 +92,7 @@ function Page() {
cursor={{ fill: 'var(--mantine-color-gray-1)' }} cursor={{ fill: 'var(--mantine-color-gray-1)' }}
/> />
<Legend /> <Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[8, 8, 0, 0]} /> <Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Penduduk dengan Pendidikan" radius={[8, 8, 0, 0]} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</Paper> </Paper>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal'; import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; import { Box, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react'; import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
@@ -59,13 +59,17 @@ function Page() {
withBorder withBorder
> >
<Stack> <Stack>
<Tooltip label="Fokus utama program" withArrow> <Group align="center" gap={8} wrap="nowrap">
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center"> <Tooltip label="Fokus utama program" withArrow>
<IconTarget size={28} style={{ marginRight: 8 }} /> <Box display="flex" style={{ alignItems: "center" }}>
<IconTarget color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateTujuanPendidikanNonFormal.findById.data?.judul} {stateTujuanPendidikanNonFormal.findById.data?.judul}
</Title> </Text>
</Tooltip> </Group>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} /> <Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
<Paper <Paper
@@ -76,13 +80,17 @@ function Page() {
withBorder withBorder
> >
<Stack> <Stack>
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow> <Group align="center" gap={8} wrap="nowrap">
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center"> <Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<IconMapPin size={28} style={{ marginRight: 8 }} /> <Box display="flex" style={{ alignItems: "center" }}>
<IconMapPin color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateTempatKegiatan.findById.data?.judul} {stateTempatKegiatan.findById.data?.judul}
</Title> </Text>
</Tooltip> </Group>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} /> <Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
@@ -95,13 +103,17 @@ function Page() {
withBorder withBorder
> >
<Stack> <Stack>
<Tooltip label="Ragam jenis program yang tersedia" withArrow> <Group align="center" gap={8} wrap="nowrap">
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center"> <Tooltip label="Ragam jenis program yang tersedia" withArrow>
<IconBook2 size={28} style={{ marginRight: 8 }} /> <Box display="flex" style={{ alignItems: "center" }}>
<IconBook2 color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateJenisProgram.findById.data?.judul} {stateJenisProgram.findById.data?.judul}
</Title> </Text>
</Tooltip> </Group>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} /> <Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -32,7 +32,7 @@ export default function JenisInformasiSelector({ onChange }: {
return ( return (
<Group> <Group>
<Select <Select
placeholder='pilih jenis informasi' placeholder='Pilih jenis informasi'
label='Jenis Informasi' label='Jenis Informasi'
data={data.map((item) => ({ data={data.map((item) => ({
value: item.id, value: item.id,

View File

@@ -28,7 +28,7 @@ function MemperolehInformasi({ onChange }: {
return ( return (
<Group> <Group>
<Select <Select
placeholder='pilih cara memperoleh informasi' placeholder='Pilih cara memperoleh informasi'
label={"Cara Memperoleh Informasi"} label={"Cara Memperoleh Informasi"}
data={data.map((item) => ({ data={data.map((item) => ({
value: item.id, value: item.id,

View File

@@ -26,7 +26,7 @@ function MemperolehSalinan({ onChange }: {
return ( return (
<Group> <Group>
<Select <Select
placeholder='pilih cara memperoleh salinan informasi' placeholder='Pilih cara memperoleh salinan informasi'
label={'Cara Memperoleh Salinan Informasi'} label={'Cara Memperoleh Salinan Informasi'}
data={data.map((item) => ({ data={data.map((item) => ({
value: item.id, value: item.id,

View File

@@ -178,7 +178,7 @@ function Page() {
<TextInput <TextInput
label="Alamat Email" label="Alamat Email"
placeholder="contoh: nama@email.com" placeholder="Contoh: nama@email.com"
radius="md" radius="md"
size="md" size="md"
type="email" type="email"
@@ -190,7 +190,7 @@ function Page() {
<TextInput <TextInput
label="Nomor Telepon" label="Nomor Telepon"
placeholder="contoh: 0812-3456-7890" placeholder="Contoh: 0812-3456-7890"
radius="md" radius="md"
size="md" size="md"
withAsterisk withAsterisk

View File

@@ -51,12 +51,12 @@ function Page() {
<Box key={item.id} px={{ base: "md", md: 100 }}> <Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl"> <Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Flex align="center" gap={40} justify="center"> <Center>
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" /> <Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} w={{ base: 70, md: 120 }} alt="Logo Desa" />
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold"> </Center>
Pejabat Pengelola Informasi Publik <Text ta="center" fz={{ base: "1.2rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi dan Dokumentasi
</Text> </Text>
</Flex>
</Box> </Box>
<Divider my="lg" /> <Divider my="lg" />

View File

@@ -7,17 +7,20 @@ import GlobalSearch from "./globalSearch";
export function NavbarSearch() { export function NavbarSearch() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const isNavigatingRef = useRef(false);
// Close when clicking outside // Close when clicking outside
useEffect(() => { useEffect(() => {
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
// Only close if clicking outside both the search input and results
if ( // Jangan close jika klik di search result item (biar handleSelect yang urus)
containerRef.current && if (target.closest('.search-result-item')) {
!containerRef.current.contains(target) && return;
!target.closest('.search-result-item') // Add a class to your search result items }
) {
// Close jika klik di luar container
if (containerRef.current && !containerRef.current.contains(target)) {
setIsOpen(false); setIsOpen(false);
stateNav.clear(); stateNav.clear();
} }
@@ -29,6 +32,13 @@ export function NavbarSearch() {
}; };
}, []); }, []);
// Reset navigation flag saat component unmount atau route change
useEffect(() => {
return () => {
isNavigatingRef.current = false;
};
}, []);
return ( return (
<Box <Box
ref={containerRef} ref={containerRef}

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState'; import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core'; import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react'; import { IconX } from '@tabler/icons-react';
@@ -10,36 +11,85 @@ import getDetailUrl from './searchUrl';
export default function GlobalSearch() { export default function GlobalSearch() {
const snap = useSnapshot(searchState); const snap = useSnapshot(searchState);
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [isNavigating, setIsNavigating] = useState(false);
// buka popover saat ada query // Buka popover saat ada query
useEffect(() => { useEffect(() => {
setOpened(!!snap.query); setOpened(!!snap.query);
}, [snap.query]); }, [snap.query]);
// infinite scroll // Infinite scroll handler
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200; const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next(); if (nearBottom && !snap.loading) searchState.next();
}; };
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll);
}, [snap.loading]); }, [snap.loading]);
const handleSelect = async (e: React.MouseEvent, item: any) => { const handleSelect = async (e: React.MouseEvent, item: any) => {
e.stopPropagation();
e.preventDefault(); e.preventDefault();
e.stopPropagation();
const url = getDetailUrl(item); if (isNavigating) return;
if (!url) return; setIsNavigating(true);
// Immediately close the search dropdown try {
// 🔥 pastikan objek udah “dikeluarkan” dari Proxy valtio
const rawItem = JSON.parse(JSON.stringify(item));
// 🔥 pastikan type-nya string murni
const type = String(rawItem.type || '').trim().toLowerCase();
// 🔥 panggil getDetailUrl pakai type yang fix
let url = getDetailUrl({ ...rawItem, type });
// kalau hasil undefined atau default, fallback ke link eksternal
if (!url || url === '/darmasaba') {
if (rawItem.link && rawItem.link.startsWith('http')) {
url = rawItem.link;
}
}
if (!url) {
console.warn('URL tidak ditemukan untuk item:', rawItem);
setIsNavigating(false);
return;
}
console.log('Navigating to:', url);
// tutup popover dulu
setOpened(false);
searchState.query = '';
searchState.results = [];
searchState.loading = false;
// kasih delay biar UI nutup dulu
await new Promise((r) => setTimeout(r, 100));
// navigasi
if (url.startsWith('http')) {
window.location.href = url;
} else {
window.location.href = url;
}
} catch (err) {
console.error('Error saat navigasi:', err);
setIsNavigating(false);
}
};
const clearSearch = () => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false); setOpened(false);
searchState.results = []; // Clear results immediately setIsNavigating(false);
searchState.loading = false;
// Use window.location for navigation to ensure full page reload
window.location.href = url;
}; };
return ( return (
@@ -47,13 +97,7 @@ export default function GlobalSearch() {
<Popover <Popover
opened={opened && !!snap.query} opened={opened && !!snap.query}
onChange={(isOpen) => { onChange={(isOpen) => {
if (!isOpen) { if (!isOpen) clearSearch();
// Clear search state when popover is closed
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
}
setOpened(isOpen); setOpened(isOpen);
}} }}
width="target" width="target"
@@ -61,10 +105,14 @@ export default function GlobalSearch() {
shadow="md" shadow="md"
withinPortal withinPortal
radius="md" radius="md"
zIndex={1000} // Add this line to ensure it appears above other elements zIndex={2000}
closeOnClickOutside={true}
closeOnEscape={true}
styles={{ styles={{
dropdown: { dropdown: {
zIndex: 1000, // Add this to ensure the dropdown appears above other elements zIndex: 2000,
borderRadius: 12,
overflow: 'hidden',
}, },
}} }}
> >
@@ -83,13 +131,7 @@ export default function GlobalSearch() {
<IconX <IconX
size={16} size={16}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={clearSearch}
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
}}
/> />
) : undefined ) : undefined
} }
@@ -101,34 +143,32 @@ export default function GlobalSearch() {
style={{ style={{
maxHeight: 350, maxHeight: 350,
overflowY: 'auto', overflowY: 'auto',
borderRadius: 12, backgroundColor: '#fff',
zIndex: 1000, // Add this line to ensure dropdown stays above other elements border: '1px solid #eee',
position: 'relative', // Add this to contain child elements
}} }}
> >
{snap.results.length > 0 ? ( {[...snap.results].length > 0 ? (
snap.results.map((item, i) => ( [...snap.results].map((item: any, i: number) => (
<Box <Box
key={i} key={i}
p="sm" p="sm"
className="search-result-item" // Add this class className="search-result-item" // Add class untuk prevent close
style={{ style={{
borderBottom: '1px solid #eee', borderBottom: '1px solid #f1f1f1',
cursor: 'pointer', cursor: isNavigating ? 'wait' : 'pointer',
background: 'white',
transition: 'background 0.2s', transition: 'background 0.2s',
position: 'relative', // Add this opacity: isNavigating ? 0.6 : 1,
zIndex: 1, // Add this to ensure proper stacking context
backgroundColor: 'white', // Ensure background is set
}} }}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f7f7f7')} onMouseEnter={(e) => !isNavigating && (e.currentTarget.style.background = '#f9f9f9')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} onMouseLeave={(e) => (e.currentTarget.style.background = 'white')}
onClick={(e) => handleSelect(e, item)} // Pass the event here onClick={(e) => handleSelect(e, item)}
> >
<Text size="sm" fw={500}> <Text size="sm" fw={500} lineClamp={1}>
{item.judul || item.namaPasar || item.nama || item.name} {item.name ?? item.nama ?? item.namaPasar ?? item.judul ?? '(Tanpa nama)'}
</Text> </Text>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed" lineClamp={1}>
dari modul: {item.type} dari modul: {item.type || '-'}
</Text> </Text>
</Box> </Box>
)) ))

View File

@@ -37,10 +37,10 @@ function Apbdes() {
<Stack p="lg" gap="4rem" bg={colors.Bg}> <Stack p="lg" gap="4rem" bg={colors.Bg}>
<Box> <Box>
<Stack gap="sm"> <Stack gap="sm">
<Text ta={"center"} fz={{ base: '2.4rem', sm: '4rem' }} fw="bold" lh={1.2}> <Text ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title} {textHeading.title}
</Text> </Text>
<Text ta={"center"} fz={{ base: '1rem', sm: '1.3rem' }} c="dimmed"> <Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des} {textHeading.des}
</Text> </Text>
</Stack> </Stack>

View File

@@ -156,9 +156,9 @@ function Kepuasan() {
<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 ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text> <Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</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 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}> <Center mt={10}>
<Button <Button
radius={"lg"} radius={"lg"}

View File

@@ -29,42 +29,42 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
return ( return (
<motion.div whileHover={{ scale: 1.03 }}> <motion.div whileHover={{ scale: 1.03 }}>
<Paper <Paper
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)} onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="lg" p="lg"
radius="xl" radius="xl"
shadow="sm" shadow="sm"
role="button" role="button"
tabIndex={0} tabIndex={0}
className="cursor-pointer transition-all" className="cursor-pointer transition-all"
bg={isDark ? "dark.6" : "white"} bg={isDark ? "dark.6" : "white"}
> >
<Center h={160}> <Center h={160}>
{data.image?.link ? ( {data.image?.link ? (
<Image <Image
src={data.image.link} src={data.image.link}
alt={data.name} alt={data.name}
radius="md" radius="md"
fit="cover" fit="contain"
h={140} h={140}
w="100%" w="100%"
loading="lazy" style={{ objectPosition: "center" }}
/> />
) : ( ) : (
<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"> <Text size="sm" 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"> <Text fw={600} ta="center" size="md">
{data.name} {data.name}
</Text> </Text>
</Box> </Box>
</Paper> </Paper>
</motion.div> </motion.div>
); );
} }
@@ -110,11 +110,11 @@ function ModuleView() {
viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet
}} }}
> >
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">
{listImageState.findMany.data?.map((item) => ( {listImageState.findMany.data?.map((item) => (
<ModuleItem key={item.id} data={item} /> <ModuleItem key={item.id} data={item} />
))} ))}
</SimpleGrid> </SimpleGrid>
</ScrollArea> </ScrollArea>
); );
} }

View File

@@ -30,20 +30,41 @@ export default function ProfileView({ data }: ProfileViewProps) {
justify="end" justify="end"
align="end" align="end"
pos="relative" pos="relative"
w={{ base: '100%', md: '40%' }} w={{
px="xl" base: '100%', // mobile: full width
xs: '100%', // small mobile
sm: '85%', // tablet: 85%
md: '60%', // laptop: 60%
lg: '55%', // laptop large: 55%
xl: '50%' // extra large (4K): 50%
}}
px={{ base: 'md', sm: 'lg', md: 'xl', xl: '2xl' }}
h={{ base: 'auto', sm: '500px', md: '600px', lg: '650px', xl: '700px' }}
> >
{data.image?.link ? ( {data.image?.link ? (
<Image <Box
src={data.image.link} pos="relative"
alt={data.name || 'Foto profil'} w="100%"
fit="contain" h="100%"
radius="lg"
loading="lazy"
style={{ style={{
objectPosition: 'bottom center', display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
}} }}
/> >
<Image
src={data.image.link}
alt={data.name || 'Foto profil'}
fit="contain"
radius="lg"
loading="lazy"
w="100%"
h="100%"
style={{
objectPosition: 'center bottom',
}}
/>
</Box>
) : ( ) : (
<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} />
@@ -53,32 +74,43 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Stack> </Stack>
)} )}
{/* Box nama dan jabatan - sedikit overlap dengan gambar */} {/* Box nama dan jabatan - responsive positioning */}
<Box <Box
pos="absolute" pos="absolute"
bottom={-20} // bikin naik sedikit ke gambar bottom={{ base: -30, sm: -25, md: -20 }}
right={0} right={0}
w="100%" w={{ base: '95%', sm: '100%' }}
p={{ base: 'xs', md: 'md' }} px={{ base: 'xs', sm: 'sm', md: 'md' }}
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar style={{ pointerEvents: 'none' }}
> >
<Card <Card
px="lg" px={{ base: 'md', sm: 'lg' }}
py="sm" py={{ base: 'xs', sm: 'sm' }}
radius="lg" radius="lg"
withBorder withBorder
style={{ style={{
boxShadow: '0 4px 12px rgba(0,0,0,0.1)', boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)', backdropFilter: 'blur(6px)',
pointerEvents: 'auto', pointerEvents: 'auto',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
}} }}
> >
<Tooltip label="Jabatan Resmi" withArrow> <Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed"> <Text
fz={{ base: 'xs', sm: 'sm' }}
c="dimmed"
lineClamp={1}
>
{data.position || 'Tidak ada jabatan'} {data.position || 'Tidak ada jabatan'}
</Text> </Text>
</Tooltip> </Tooltip>
<Text c={colors['blue-button']} fw={700} fz="xl" mt={4}> <Text
c={colors['blue-button']}
fw={700}
fz={{ base: 'lg', sm: 'xl' }}
mt={4}
lineClamp={2}
>
{data.name} {data.name}
</Text> </Text>
</Card> </Card>

View File

@@ -1,26 +1,27 @@
"use client"; "use client";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Prisma } from "@prisma/client";
import { import {
Badge,
Box, Box,
Card, Card,
Skeleton, Center,
Flex, Flex,
Grid, Grid,
GridCol, GridCol,
Group,
Image, Image,
Paper, Paper,
Skeleton,
Stack, Stack,
Text, Text,
Center,
Tooltip, Tooltip,
Badge,
} from "@mantine/core"; } from "@mantine/core";
import { Prisma } from "@prisma/client";
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react"; import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ModuleView from "./ModuleView"; import ModuleView from "./ModuleView";
import SosmedView from "./SosmedView";
import ProfileView from "./ProfileView"; import ProfileView from "./ProfileView";
import SosmedView from "./SosmedView";
const getDayOfWeek = () => { const getDayOfWeek = () => {
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"]; const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
@@ -126,17 +127,15 @@ function LandingPage() {
<Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl"> <Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl">
<Stack gap="xl"> <Stack gap="xl">
<Flex gap="md" wrap="wrap"> <Flex gap="md" wrap="wrap">
<Group>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
</Box>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
</Box>
</Group>
<Grid w="100%"> <Grid w="100%">
<Grid.Col span={{ base: 3, sm: 2 }}>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
</Box>
</Grid.Col>
<Grid.Col span={{ base: 9, sm: 10 }}>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
</Box>
</Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
<Paper <Paper
bg={colors["blue-button"]} bg={colors["blue-button"]}

View File

@@ -31,16 +31,12 @@ function Layanan() {
return ( return (
<Stack pos={"relative"} bg={colors.grey[1]} gap={"42"} py={"xl"}> <Stack pos={"relative"} bg={colors.grey[1]} gap={"42"} py={"xl"}>
<Container w={{ base: "100%", md: "50%" }} p={"xl"}> <Container w={{ base: "100%", md: "80%" }} p={"xl"} >
<Stack align="center" gap={"0"}> <Stack align="center" gap={"0"}>
<Text fz={"3.4rem"} fw={"bold"}> <Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title} {textHeading.title}
</Text> </Text>
<Text <Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
style={{
textAlign: "center",
}}
>
{textHeading.des} {textHeading.des}
</Text> </Text>
<Box p={"md"}> <Box p={"md"}>

View File

@@ -6,6 +6,7 @@ import {
BackgroundImage, BackgroundImage,
Box, Box,
Button, Button,
Container,
Divider, Divider,
Group, Group,
Loader, Loader,
@@ -49,14 +50,14 @@ function Potensi() {
return ( return (
<Stack p="sm" gap="4rem"> <Stack p="sm" gap="4rem">
<Box> <Container w={{ base: "100%", md: "80%" }} p={"xl"} >
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }} fw={700} c={colors["blue-button"]}> <Text ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title} {textHeading.title}
</Text> </Text>
<Text ta={"center"} fz={{ base: "1.4rem", md: "1.6rem" }} c="black"> <Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des} {textHeading.des}
</Text> </Text>
</Box> </Container>
{loading ? ( {loading ? (
<Stack align="center" justify="center" h={300}> <Stack align="center" justify="center" h={300}>

View File

@@ -50,7 +50,7 @@ export default function SDGS() {
SDGs Desa SDGs Desa
</Title> </Title>
</Center> </Center>
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto"> <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. 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> </Text>

View File

@@ -1,91 +1,92 @@
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => { const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
const { type, id, kategori } = item; const { type, id, kategori } = item;
const typeUrlMap: Record<string, string> = { const map: Record<string, (id: string | number, kategori?: string) => string> = {
programinovasi: `/darmasaba/program-inovasi/${id}`, programinovasi: (id) => `/darmasaba/program-inovasi/${id}`,
desaantikorupsi: '/darmasaba/desa-anti-korupsi', desaantikorupsi: () => '/darmasaba/desa-anti-korupsi',
sdgsdesa: '/darmasaba/sdgs-desa', sdgsdesa: () => '/darmasaba/sdgs-desa',
apbdes: '/darmasaba/apbdes', apbdes: () => '/darmasaba/apbdes',
prestasidesa: '/darmasaba/prestasi-desa', prestasidesa: () => '/darmasaba/prestasi-desa',
pejabatdesa: '/darmasaba/profile/pejabat-desa', pejabatdesa: () => '/darmasaba/ppid/profile-ppid',
strukturppid: '/darmasaba/ppid/struktur-ppid', strukturppid: () => '/darmasaba/ppid/struktur-ppid',
visimisippid: '/darmasaba/ppid/visi-misi', visimisippid: () => '/darmasaba/ppid/visi-misi',
dasarhukumppid: '/darmasaba/ppid/dasar-hukum', dasarhukumppid: () => '/darmasaba/ppid/dasar-hukum',
profileppid: '/darmasaba/ppid/profile', profileppid: () => '/darmasaba/ppid/profile',
daftarinformasipublik: '/darmasaba/ppid/daftar-informasi-publik', daftarinformasipublik: () => '/darmasaba/ppid/daftar-informasi-publik',
perbekeldarmasaba: '/darmasaba/desa/profile', perbekeldarmasaba: () => '/darmasaba/desa/profile',
berita: `/darmasaba/desa/berita/${kategori}/${id}`, berita: (id, kategori) => `/darmasaba/desa/berita/${kategori}/${id}`,
pengumuman: `/darmasaba/desa/pengumuman/${kategori}/${id}`, pengumuman: (id, kategori) => `/darmasaba/desa/pengumuman/${kategori}/${id}`,
sejarahdesa: '/darmasaba/desa/profile', sejarahdesa: () => '/darmasaba/desa/profile',
visimisidesa: '/darmasaba/desa/profile', visimisidesa: () => '/darmasaba/desa/profile',
lambangdesa: '/darmasaba/desa/profile', lambangdesa: () => '/darmasaba/desa/profile',
maskotdesa: '/darmasaba/desa/profile', maskotdesa: () => '/darmasaba/desa/profile',
profilperbekel: '/darmasaba/desa/profile', profilperbekel: () => '/darmasaba/desa/profile',
potensi: '/darmasaba/desa/potensi-desa', potensi: () => '/darmasaba/desa/potensi-desa',
galleryFoto: '/darmasaba/desa/gallery/foto', galleryFoto: () => '/darmasaba/desa/gallery/foto',
galleryVideo: '/darmasaba/desa/gallery/video', galleryVideo: () => '/darmasaba/desa/gallery/video',
pelayananSuratKeterangan: '/darmasaba/desa/layanan', pelayananSuratKeterangan: () => '/darmasaba/desa/layanan',
pelayananPerizinanBerusaha: '/darmasaba/desa/layanan', pelayananPerizinanBerusaha: () => '/darmasaba/desa/layanan',
pelayananTelunjukSaktiDesa: '/darmasaba/desa/layanan', pelayananTelunjukSaktiDesa: () => '/darmasaba/desa/layanan',
pelayananPendudukNonPermanent: '/darmasaba/desa/layanan', pelayananPendudukNonPermanent: () => '/darmasaba/desa/layanan',
penghargaan: '/darmasaba/desa/penghargaan', penghargaan: () => '/darmasaba/desa/penghargaan',
posyandu: '/darmasaba/kesehatan/posyandu', posyandu: (id) => `/darmasaba/kesehatan/posyandu/${id}`,
fasilitasKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga', fasilitasKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
jadwalKegiatan: '/darmasaba/kesehatan/data-kesehatan-warga', jadwalKegiatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
artikelKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga', artikelKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
puskesmas: '/darmasaba/kesehatan/puskesmas', puskesmas: () => '/darmasaba/kesehatan/puskesmas',
programKesehatan: '/darmasaba/kesehatan/program-kesehatan', programKesehatan: () => '/darmasaba/kesehatan/program-kesehatan',
penangananDarurat: '/darmasaba/kesehatan/penanganan-darurat', penangananDarurat: () => '/darmasaba/kesehatan/penanganan-darurat',
kontakDarurat: '/darmasaba/kesehatan/kontak-darurat', kontakDarurat: () => '/darmasaba/kesehatan/kontak-darurat',
infoWabahPenyakit: '/darmasaba/kesehatan/info-wabah-penyakit', infoWabahPenyakit: () => '/darmasaba/kesehatan/info-wabah-penyakit',
keamananLingkungan: '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal', keamananLingkungan: () => '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
polsekTerdekat: '/darmasaba/keamanan/polsek-terdekat', polsekTerdekat: () => '/darmasaba/keamanan/polsek-terdekat',
kontakDaruratKeamanan: '/darmasaba/keamanan/kontak-darurat', kontakDaruratKeamanan: () => '/darmasaba/keamanan/kontak-darurat',
pencegahanKriminalitas: '/darmasaba/keamanan/pencegahan-kriminalitas', pencegahanKriminalitas: () => '/darmasaba/keamanan/pencegahan-kriminalitas',
laporanPublik: '/darmasaba/keamanan/laporan-publik', laporanPublik: () => '/darmasaba/keamanan/laporan-publik',
tipsKeamanan: '/darmasaba/keamanan/tips-keamanan', tipsKeamanan: () => '/darmasaba/keamanan/tips-keamanan',
pasarDesa: '/darmasaba/ekonomi/pasar-desa', pasarDesa: () => '/darmasaba/ekonomi/pasar-desa',
lowonganKerjaLokal: '/darmasaba/ekonomi/lowongan-kerja-lokal', lowonganKerjaLokal: () => '/darmasaba/ekonomi/lowongan-kerja-lokal',
strukturOrganisasi: '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa', strukturOrganisasi: () => '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
jumlahPendudukUsiaKerjaYangMenganggurUsia: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur', jumlahPendudukUsiaKerjaYangMenganggurUsia: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur', jumlahPendudukUsiaKerjaYangMenganggurPendidikan: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukMiskin: '/darmasaba/ekonomi/jumlah-penduduk-miskin', jumlahPendudukMiskin: () => '/darmasaba/ekonomi/jumlah-penduduk-miskin',
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan', programKemiskinan: () => '/darmasaba/ekonomi/program-kemiskinan',
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa', sektorUnggulanDesa: () => '/darmasaba/ekonomi/sektor-unggulan-desa',
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan', demografiPekerjaan: () => '/darmasaba/ekonomi/demografi-pekerjaan',
desaDigital: '/darmasaba/inovasi/desa-digital-smart-village', desaDigital: () => '/darmasaba/inovasi/desa-digital-smart-village',
programKreatif: '/darmasaba/inovasi/program-kreatif-desa', programKreatif: () => '/darmasaba/inovasi/program-kreatif-desa',
kolaborasiInovasi: '/darmasaba/inovasi/kolaborasi-inovasi', kolaborasiInovasi: () => '/darmasaba/inovasi/kolaborasi-inovasi',
mitraKolaborasi: '/darmasaba/inovasi/kolaborasi-inovasi', mitraKolaborasi: () => '/darmasaba/inovasi/kolaborasi-inovasi',
infoTekno: '/darmasaba/inovasi/info-teknologi-tepat-guna', infoTekno: () => '/darmasaba/inovasi/info-teknologi-tepat-guna',
pengelolaanSampah: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah', pengelolaanSampah: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
keteranganBankSampahTerdekat: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah', keteranganBankSampahTerdekat: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
programPenghijauan: '/darmasaba/lingkungan/program-penghijauan', programPenghijauan: () => '/darmasaba/lingkungan/program-penghijauan',
dataLingkunganDesa: '/darmasaba/lingkungan/data-lingkungan-desa', dataLingkunganDesa: () => '/darmasaba/lingkungan/data-lingkungan-desa',
gotongRoyong: '/darmasaba/lingkungan/gotong-royong', gotongRoyong: (id, kategori) => `/darmasaba/lingkungan/gotong-royong/${kategori}/${id}`,
tujuanEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan', tujuanEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
materiEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan', materiEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
contohEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan', contohEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
filosofiTriHita: '/darmasaba/lingkungan/konservasi-adat-bali', filosofiTriHita: () => '/darmasaba/lingkungan/konservasi-adat-bali',
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/konservasi-adat-bali', bentukKonservasiBerdasarkanAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali',
nilaiKonservasiAdat: '/darmasaba/lingkungan/konservasi-adat-bali', nilaiKonservasiAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali',
jenjangPendidikan: '/darmasaba/pendidikan/info-sekolah/semua', jenjangPendidikan: () => '/darmasaba/pendidikan/info-sekolah/semua',
lembaga: '/darmasaba/pendidikan/info-sekolah/semua/lembaga', lembaga: () => '/darmasaba/pendidikan/info-sekolah/semua/lembaga',
siswa: '/darmasaba/pendidikan/info-sekolah/semua/siswa', siswa: () => '/darmasaba/pendidikan/info-sekolah/semua/siswa',
pengajar: '/darmasaba/pendidikan/info-sekolah/semua/pengajar', pengajar: () => '/darmasaba/pendidikan/info-sekolah/semua/pengajar',
keunggulanProgram: '/darmasaba/pendidikan/beasiswa-desa', keunggulanProgram: () => '/darmasaba/pendidikan/beasiswa-desa',
tujuanProgram: '/darmasaba/pendidikan/program-pendidikan-anak', tujuanProgram: () => '/darmasaba/pendidikan/program-pendidikan-anak',
programUnggulan: '/darmasaba/pendidikan/program-pendidikan-anak', programUnggulan: () => '/darmasaba/pendidikan/program-pendidikan-anak',
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa', lokasiJadwalBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa', fasilitasBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa',
tujuanPendidikanNonFormal: '/darmasaba/pendidikan/pendidikan-non-formal', tujuanPendidikanNonFormal: () => '/darmasaba/pendidikan/pendidikan-non-formal',
tempatKegiatan: '/darmasaba/pendidikan/pendidikan-non-formal', tempatKegiatan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
jenisProgramYangDiselenggarakan: '/darmasaba/pendidikan/pendidikan-non-formal', jenisProgramYangDiselenggarakan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
dataPerpustakaan: '/darmasaba/pendidikan/perpustakaan-digital/semua', dataPerpustakaan: () => '/darmasaba/pendidikan/perpustakaan-digital/semua',
dataPendidikan: '/darmasaba/pendidikan/data-pendidikan', dataPendidikan: () => '/darmasaba/pendidikan/data-pendidikan',
}; };
return typeUrlMap[type || ''] || '/darmasaba'; if (type && map[type]) return map[type](id, kategori as string | undefined);
return '/darmasaba';
}; };
export default getDetailUrl; export default getDetailUrl;