Compare commits

...

2 Commits

27 changed files with 729 additions and 363 deletions

View File

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

View File

@@ -25,26 +25,40 @@ const searchState = proxy({
searchState.results = [];
return;
}
searchState.loading = true;
try {
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
console.log("Search API Response:", res);
const rawItems = res.data?.data || [];
const parsedItems = structuredClone(rawItems); // ✅ penting!
console.log("✅ Parsed items:", parsedItems);
if (searchState.page === 1) {
searchState.results = parsedItems;
} else {
searchState.results.push(...parsedItems);
}
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
console.log("Search results render:", searchState.results);
if (searchState.page === 1) {
searchState.results = res.data?.data || [];
} else {
searchState.results.push(...(res.data?.data || []));
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() {

View File

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

View File

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

View File

@@ -43,15 +43,48 @@ export default function Page() {
const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading;
// Load featured data once on component mount
useEffect(() => {
if (!featured.data && !loadingFeatured) {
gotongRoyongState.kegiatanDesa.findFirst.load();
let mounted = true;
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(() => {
const limit = 3;
state.findMany.load(page, limit, search);
let mounted = true;
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]);
const handlePageChange = (newPage: number) => {
@@ -59,7 +92,9 @@ export default function Page() {
if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString());
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;

View File

@@ -1,9 +1,9 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
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 { 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 { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -122,20 +122,28 @@ function Page() {
<Stack gap="md">
{data2?.map((v, k) => (
<Paper key={k} p="md" withBorder radius="md">
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
{v.lat && v.lng ? (
<a
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">📌 Buka di Google Maps</Text>
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
<Group justify='space-between'>
<Box>
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
</Box>
<Box>
<IconRoute color={colors['blue-button']} size={30} />
<Text fw={"bold"} fz="sm" c={colors['blue-button']}>Rute</Text>
</Box>
</Group>
{v.lat && v.lng ? (
<a
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>
))}
</Stack>

View File

@@ -92,7 +92,7 @@ function Page() {
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
/>
<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>
</ResponsiveContainer>
</Paper>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,12 +51,12 @@ function Page() {
<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 }}>
<Flex align="center" gap={40} justify="center">
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi Publik
<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>
</Flex>
</Box>
<Divider my="lg" />

View File

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

View File

@@ -5,7 +5,7 @@ import stateNav from "@/state/state-nav";
import { ActionIcon, Box, Burger, Group, Image, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
import { IconSquareArrowRight } from "@tabler/icons-react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useSnapshot } from "valtio";
import { MenuItem } from "../../../../types/menu-item";
import { NavbarMainMenu } from "./NavbarMainMenu";
@@ -19,14 +19,18 @@ export function Navbar() {
<Paper
radius="0"
className="glass2"
w="100%"
w="100vw"
pos="fixed"
top={0}
style={{ zIndex: 100 }}
>
<NavbarMainMenu listNavbar={navbarListMenu} />
{/* Desktop navbar (muncul mulai 992px ke atas) */}
<Box visibleFrom="md">
<NavbarMainMenu listNavbar={navbarListMenu} />
</Box>
<Box hiddenFrom="sm" bg={colors.grey[2]} px="md" py="sm">
{/* Mobile navbar (muncul di bawah 992px, termasuk iPad Mini) */}
<Box hiddenFrom="md" bg={colors.grey[2]} px="md" py="sm">
<Group justify="space-between" wrap="nowrap">
<ActionIcon
variant="transparent"
@@ -38,10 +42,21 @@ export function Navbar() {
}}
>
<Tooltip label="Go to homepage" position="bottom" withArrow>
<Image src="/darmasaba-icon.png" alt="Village Logo" width={48} height={48} loading="lazy"/>
<Image
src="/darmasaba-icon.png"
alt="Village Logo"
width={48}
height={48}
loading="lazy"
/>
</Tooltip>
</ActionIcon>
<Tooltip label={mobileOpen ? "Close menu" : "Open menu"} position="bottom" withArrow>
<Tooltip
label={mobileOpen ? "Close menu" : "Open menu"}
position="bottom"
withArrow
>
<Burger
opened={mobileOpen}
color={colors["blue-button"]}
@@ -50,12 +65,14 @@ export function Navbar() {
/>
</Tooltip>
</Group>
{mobileOpen && (
<Paper
component={motion.div}
initial={{ x: '100%' }}
bg="white"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
exit={{ x: "100%" }}
transition={{ duration: 0.2 }}
pos="absolute"
left={0}
@@ -63,12 +80,14 @@ export function Navbar() {
top="100%"
m={0}
radius={0}
shadow="md"
>
<NavbarMobile listNavbar={navbarListMenu} />
</Paper>
)}
</Box>
</Paper>
{(item || isSearch) && <Box className="glass" />}
</Box>
);
@@ -76,40 +95,105 @@ export function Navbar() {
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
const router = useRouter();
const pathname = usePathname(); // 👈 untuk cek path aktif
// fungsi bantu: cek apakah path sekarang sama dengan menu / sub-menu
const isActive = (href?: string) => href && pathname.startsWith(href);
return (
<ScrollArea.Autosize mah="calc(100vh - 80px)" offsetScrollbars>
<Stack p="md" gap="xs">
{listNavbar.map((item, k) => (
<Box key={k}>
<Group
justify="space-between"
align="center"
p="xs"
onClick={() => {
if (item.href) {
router.push(item.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
opacity: item.href ? 1 : 0.8
}}
>
<Text c="dark.9" fw={600} fz="md">
{item.name}
</Text>
<IconSquareArrowRight size={18} />
</Group>
{item.children && (
<Box pl="md">
<NavbarMobile listNavbar={item.children} />
</Box>
)}
</Box>
))}
<ScrollArea.Autosize
mah="calc(100dvh - 80px)"
type="auto"
offsetScrollbars
>
<Stack p="sm" gap="xs">
{listNavbar.map((item, k) => {
const active = isActive(item.href);
return (
<Box key={k}>
<Paper
shadow={active ? "sm" : "xs"}
radius="md"
p="sm"
withBorder
bg={active ? "blue.0" : "gray.0"}
onClick={() => {
if (item.href) {
router.push(item.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
transition: "background 0.15s ease",
borderLeft: active ? "4px solid #1e66f5" : "4px solid transparent",
}}
>
<Group justify="space-between" align="center" wrap="nowrap">
<Text
fw={active ? 700 : 600}
fz="md"
c={active ? "blue.7" : "dark.9"}
>
{item.name}
</Text>
{item.href && (
<IconSquareArrowRight
size={18}
color={active ? "#1e66f5" : "inherit"}
/>
)}
</Group>
</Paper>
{/* Submenu */}
{item.children && (
<Box pl="md" mt={4}>
{item.children.map((child, j) => {
const childActive = isActive(child.href);
return (
<Group
key={j}
justify="space-between"
align="center"
p="xs"
onClick={() => {
if (child.href) {
router.push(child.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: child.href ? "pointer" : "default",
opacity: child.href ? 1 : 0.8,
borderRadius: "0.5rem",
backgroundColor: childActive ? "#e7f0ff" : "transparent",
borderLeft: childActive ? "3px solid #1e66f5" : "3px solid transparent",
transition: "background 0.15s ease",
}}
>
<Text
fz="sm"
fw={childActive ? 600 : 400}
c={childActive ? "blue.7" : "dark.8"}
>
{child.name}
</Text>
<IconSquareArrowRight
size={14}
color={childActive ? "#1e66f5" : "inherit"}
/>
</Group>
);
})}
</Box>
)}
</Box>
);
})}
</Stack>
</ScrollArea.Autosize>
);
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
@@ -10,36 +11,85 @@ import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const [opened, setOpened] = useState(false);
const [isNavigating, setIsNavigating] = useState(false);
// buka popover saat ada query
// Buka popover saat ada query
useEffect(() => {
setOpened(!!snap.query);
}, [snap.query]);
// infinite scroll
// Infinite scroll handler
useEffect(() => {
const handleScroll = () => {
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next();
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (nearBottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [snap.loading]);
const handleSelect = async (e: React.MouseEvent, item: any) => {
e.stopPropagation();
e.preventDefault();
e.stopPropagation();
if (isNavigating) return;
setIsNavigating(true);
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 url = getDetailUrl(item);
if (!url) return;
// Immediately close the search dropdown
const clearSearch = () => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
searchState.results = []; // Clear results immediately
searchState.loading = false;
// Use window.location for navigation to ensure full page reload
window.location.href = url;
setIsNavigating(false);
};
return (
@@ -47,13 +97,7 @@ export default function GlobalSearch() {
<Popover
opened={opened && !!snap.query}
onChange={(isOpen) => {
if (!isOpen) {
// Clear search state when popover is closed
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
}
if (!isOpen) clearSearch();
setOpened(isOpen);
}}
width="target"
@@ -61,10 +105,14 @@ export default function GlobalSearch() {
shadow="md"
withinPortal
radius="md"
zIndex={1000} // Add this line to ensure it appears above other elements
zIndex={2000}
closeOnClickOutside={true}
closeOnEscape={true}
styles={{
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
size={16}
style={{ cursor: 'pointer' }}
onClick={() => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
}}
onClick={clearSearch}
/>
) : undefined
}
@@ -101,34 +143,32 @@ export default function GlobalSearch() {
style={{
maxHeight: 350,
overflowY: 'auto',
borderRadius: 12,
zIndex: 1000, // Add this line to ensure dropdown stays above other elements
position: 'relative', // Add this to contain child elements
backgroundColor: '#fff',
border: '1px solid #eee',
}}
>
{snap.results.length > 0 ? (
snap.results.map((item, i) => (
{[...snap.results].length > 0 ? (
[...snap.results].map((item: any, i: number) => (
<Box
key={i}
p="sm"
className="search-result-item" // Add this class
className="search-result-item" // Add class untuk prevent close
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
borderBottom: '1px solid #f1f1f1',
cursor: isNavigating ? 'wait' : 'pointer',
background: 'white',
transition: 'background 0.2s',
position: 'relative', // Add this
zIndex: 1, // Add this to ensure proper stacking context
backgroundColor: 'white', // Ensure background is set
opacity: isNavigating ? 0.6 : 1,
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f7f7f7')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={(e) => handleSelect(e, item)} // Pass the event here
onMouseEnter={(e) => !isNavigating && (e.currentTarget.style.background = '#f9f9f9')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'white')}
onClick={(e) => handleSelect(e, item)}
>
<Text size="sm" fw={500}>
{item.judul || item.namaPasar || item.nama || item.name}
<Text size="sm" fw={500} lineClamp={1}>
{item.name ?? item.nama ?? item.namaPasar ?? item.judul ?? '(Tanpa nama)'}
</Text>
<Text size="xs" c="dimmed">
dari modul: {item.type}
<Text size="xs" c="dimmed" lineClamp={1}>
dari modul: {item.type || '-'}
</Text>
</Box>
))
@@ -141,4 +181,4 @@ export default function GlobalSearch() {
</Popover>
</Box>
);
}
}

View File

@@ -37,10 +37,10 @@ function Apbdes() {
<Stack p="lg" gap="4rem" bg={colors.Bg}>
<Box>
<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}
</Text>
<Text ta={"center"} fz={{ base: '1rem', sm: '1.3rem' }} c="dimmed">
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des}
</Text>
</Stack>

View File

@@ -156,9 +156,9 @@ function Kepuasan() {
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<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>
<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}>
<Button
radius={"lg"}

View File

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

View File

@@ -30,20 +30,41 @@ export default function ProfileView({ data }: ProfileViewProps) {
justify="end"
align="end"
pos="relative"
w={{ base: '100%', md: '40%' }}
px="xl"
w={{
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 ? (
<Image
src={data.image.link}
alt={data.name || 'Foto profil'}
fit="contain"
radius="lg"
loading="lazy"
<Box
pos="relative"
w="100%"
h="100%"
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">
<IconUserCircle size={96} stroke={1.5} />
@@ -53,36 +74,47 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Stack>
)}
{/* Box nama dan jabatan - sedikit overlap dengan gambar */}
{/* Box nama dan jabatan - responsive positioning */}
<Box
pos="absolute"
bottom={-20} // bikin naik sedikit ke gambar
bottom={{ base: -30, sm: -25, md: -20 }}
right={0}
w="100%"
p={{ base: 'xs', md: 'md' }}
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar
w={{ base: '95%', sm: '100%' }}
px={{ base: 'xs', sm: 'sm', md: 'md' }}
style={{ pointerEvents: 'none' }}
>
<Card
px="lg"
py="sm"
px={{ base: 'md', sm: 'lg' }}
py={{ base: 'xs', sm: 'sm' }}
radius="lg"
withBorder
style={{
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)',
pointerEvents: 'auto',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
}}
>
<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'}
</Text>
</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}
</Text>
</Card>
</Box>
</Stack>
);
}
}

View File

@@ -1,26 +1,27 @@
"use client";
import colors from "@/con/colors";
import { Prisma } from "@prisma/client";
import {
Badge,
Box,
Card,
Skeleton,
Center,
Flex,
Grid,
GridCol,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Center,
Tooltip,
Badge,
} from "@mantine/core";
import { Prisma } from "@prisma/client";
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import ModuleView from "./ModuleView";
import SosmedView from "./SosmedView";
import ProfileView from "./ProfileView";
import SosmedView from "./SosmedView";
const getDayOfWeek = () => {
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">
<Stack gap="xl">
<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.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}>
<Paper
bg={colors["blue-button"]}
@@ -199,7 +198,7 @@ function LandingPage() {
)}
<Text ta="center" c={colors.trans.dark[2]}>
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.
</Text>
</Stack>

View File

@@ -31,16 +31,12 @@ function Layanan() {
return (
<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"}>
<Text fz={"3.4rem"} fw={"bold"}>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title}
</Text>
<Text
style={{
textAlign: "center",
}}
>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des}
</Text>
<Box p={"md"}>

View File

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

View File

@@ -50,7 +50,7 @@ export default function SDGS() {
SDGs Desa
</Title>
</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.
</Text>

View File

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