Compare commits

...

2 Commits

Author SHA1 Message Date
75bf0652b1 Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct 2025-10-17 10:03:03 +08:00
0b574406e2 Fix QC Kak Inno : tanggal 14 Oktober
Fitur Search bisa digunakan di 6 Menu, sisa 3 Menu Lagi
2025-10-15 17:29:57 +08:00
36 changed files with 2690 additions and 733 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -82,6 +82,7 @@
"react-simple-toasts": "^6.1.0", "react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1", "readdirp": "^4.1.1",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"sharp": "^0.34.3", "sharp": "^0.34.3",

View File

@@ -1606,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan") ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
} }
// ========================================= INOVASI ========================================= // // ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= // // ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital { model DesaDigital {
id String @id @default(cuid()) id String @id @default(cuid())

View File

@@ -75,17 +75,18 @@ const berita = proxy({
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => { load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path const startTime = Date.now();
berita.findMany.loading = true;
berita.findMany.page = page; berita.findMany.page = page;
berita.findMany.search = search; berita.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
if (kategori) query.kategori = kategori; if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query }); const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? []; berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1; berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -98,9 +99,16 @@ const berita = proxy({
berita.findMany.data = []; berita.findMany.data = [];
berita.findMany.totalPages = 1; berita.findMany.totalPages = 1;
} finally { } finally {
berita.findMany.loading = false; // pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
berita.findMany.loading = false;
}, delay);
} }
}, },
}, },
findUnique: { findUnique: {

View File

@@ -55,81 +55,95 @@ const dataPerpustakaan = proxy({
}, },
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.DataPerpustakaanGetPayload<{ | Prisma.DataPerpustakaanGetPayload<{
include: { include: {
image: true; image: true;
kategori: true; kategori: true;
}; };
}>[] }>[]
| null, | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => { load: async (page = 1, limit = 10, search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path const startTime = Date.now();
dataPerpustakaan.findMany.page = page; dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.search = search; dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit }; try {
if (search) query.search = search; const query: any = { page, limit };
if (kategori) query.kategori = kategori; if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
const res =
if (res.status === 200 && res.data?.success) { await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
dataPerpustakaan.findMany.data = res.data.data ?? []; "findMany"
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1; ].get({ query });
} else {
dataPerpustakaan.findMany.data = []; if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.totalPages = 1; dataPerpustakaan.findMany.data = res.data.data ?? [];
} dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} catch (err) { } else {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findMany.data = []; dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1; dataPerpustakaan.findMany.totalPages = 1;
} finally { }
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
dataPerpustakaan.findMany.loading = false; dataPerpustakaan.findMany.loading = false;
} }, delay);
}, }
}, },
findManyAll: { },
data: null as findManyAll: {
| Prisma.DataPerpustakaanGetPayload<{ data: null as
include: { | Prisma.DataPerpustakaanGetPayload<{
image: true; include: {
kategori: true; image: true;
}; kategori: true;
}>[] };
| null, }>[]
loading: false, | null,
search: "", loading: false,
load: async (search = "", kategori = "") => { search: "",
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path load: async (search = "", kategori = "") => {
dataPerpustakaan.findMany.search = search; dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.search = search;
try {
const query: any = {}; try {
if (search) query.search = search; const query: any = {};
if (kategori) query.kategori = kategori; if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findManyAll"].get({ query });
const res =
if (res.status === 200 && res.data?.success) { await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
dataPerpustakaan.findManyAll.data = res.data.data ?? []; "findManyAll"
} else { ].get({ query });
dataPerpustakaan.findManyAll.data = [];
} if (res.status === 200 && res.data?.success) {
} catch (err) { dataPerpustakaan.findManyAll.data = res.data.data ?? [];
console.error("Gagal fetch data perpustakaan paginated:", err); } else {
dataPerpustakaan.findManyAll.data = []; dataPerpustakaan.findManyAll.data = [];
} finally {
dataPerpustakaan.findManyAll.loading = false;
} }
}, } catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findManyAll.data = [];
} finally {
dataPerpustakaan.findManyAll.loading = false;
}
}, },
},
findUnique: { findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{ data: null as Prisma.DataPerpustakaanGetPayload<{
include: { include: {
@@ -356,17 +370,20 @@ const kategoriBuku = proxy({
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "") => { load: async (page = 1, limit = 10, search = "") => {
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBuku.findMany.page = page; kategoriBuku.findMany.page = page;
kategoriBuku.findMany.search = search; kategoriBuku.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query }); const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? []; kategoriBuku.findMany.data = res.data.data ?? [];
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1; kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
@@ -557,7 +574,7 @@ const templatePeminjamanBuku = z.object({
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"), tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
batasKembali: z.string().min(1, "Batas Kembali harus diisi"), batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"), tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
catatan: z.string().min(1, "Catatan harus diisi") catatan: z.string().min(1, "Catatan harus diisi"),
}); });
const defaultPeminjamanBuku = { const defaultPeminjamanBuku = {
@@ -568,7 +585,7 @@ const defaultPeminjamanBuku = {
tanggalPinjam: "", tanggalPinjam: "",
batasKembali: "", batasKembali: "",
tanggalKembali: "", tanggalKembali: "",
catatan: "" catatan: "",
}; };
interface FormEditData { interface FormEditData {
@@ -584,7 +601,7 @@ interface FormEditData {
batasKembali: string; batasKembali: string;
tanggalKembali: string; tanggalKembali: string;
catatan: string; catatan: string;
status: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan'; status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
} }
const editForm: FormEditData = { const editForm: FormEditData = {
@@ -596,8 +613,8 @@ const editForm: FormEditData = {
batasKembali: "", batasKembali: "",
tanggalKembali: "", tanggalKembali: "",
catatan: "", catatan: "",
status: "Dipinjam" status: "Dipinjam",
} };
const peminjamanBuku = proxy({ const peminjamanBuku = proxy({
create: { create: {
@@ -646,13 +663,16 @@ const peminjamanBuku = proxy({
peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
peminjamanBuku.findMany.page = page; peminjamanBuku.findMany.page = page;
peminjamanBuku.findMany.search = search; peminjamanBuku.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku["findMany"].get({ query }); const res =
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
peminjamanBuku.findMany.data = res.data.data ?? []; peminjamanBuku.findMany.data = res.data.data ?? [];
peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1; peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
@@ -720,7 +740,9 @@ const peminjamanBuku = proxy({
); );
await peminjamanBuku.findMany.load(); // refresh list await peminjamanBuku.findMany.load(); // refresh list
} else { } else {
toast.error(result?.message || "Gagal menghapus Data Peminjaman Buku"); toast.error(
result?.message || "Gagal menghapus Data Peminjaman Buku"
);
} }
} catch (error) { } catch (error) {
console.error("Gagal delete:", error); console.error("Gagal delete:", error);
@@ -768,7 +790,7 @@ const peminjamanBuku = proxy({
batasKembali: data.batasKembali, batasKembali: data.batasKembali,
tanggalKembali: data.tanggalKembali, tanggalKembali: data.tanggalKembali,
catatan: data.catatan, catatan: data.catatan,
status: data.status status: data.status,
}; };
return data; // Return the loaded data return data; // Return the loaded data
} else { } else {
@@ -811,7 +833,7 @@ const peminjamanBuku = proxy({
batasKembali: this.form.batasKembali, batasKembali: this.form.batasKembali,
tanggalKembali: this.form.tanggalKembali, tanggalKembali: this.form.tanggalKembali,
catatan: this.form.catatan, catatan: this.form.catatan,
status: this.form.status status: this.form.status,
}), }),
} }
); );
@@ -830,7 +852,9 @@ const peminjamanBuku = proxy({
await peminjamanBuku.findMany.load(); // refresh list await peminjamanBuku.findMany.load(); // refresh list
return true; return true;
} else { } else {
throw new Error(result.message || "Gagal update data peminjaman buku"); throw new Error(
result.message || "Gagal update data peminjaman buku"
);
} }
} catch (error) { } catch (error) {
console.error("Error updating data peminjaman buku:", error); console.error("Error updating data peminjaman buku:", error);
@@ -849,7 +873,7 @@ const peminjamanBuku = proxy({
peminjamanBuku.update.form = { ...editForm }; peminjamanBuku.update.form = { ...editForm };
}, },
}, },
}) });
const perpustakaanDigitalState = proxy({ const perpustakaanDigitalState = proxy({
dataPerpustakaan, dataPerpustakaan,

View File

@@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Profil Desa Profile Desa
</Title> </Title>
<Tabs <Tabs

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import { proxy, subscribe } from 'valtio'; import { proxy } from 'valtio';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
interface SearchResult { interface SearchResult {
type?: string; // optional biar gak error type?: string;
id: string | number; id: string | number;
title?: string; title?: string;
[key: string]: any; [key: string]: any;
@@ -21,36 +21,30 @@ const searchState = proxy({
loading: false, loading: false,
async fetch() { async fetch() {
if (!searchState.query) return; if (!searchState.query) {
searchState.results = [];
return;
}
searchState.loading = true; searchState.loading = true;
try { const res = await ApiFetch.api.search.findMany.get({
const res = await ApiFetch.api.search.findMany.get({ query: {
query: { query: searchState.query,
query: searchState.query, page: searchState.page,
page: searchState.page, limit: searchState.limit,
limit: searchState.limit, type: searchState.type,
type: searchState.type, },
}, });
});
const data = (res.data?.data || []).map((item: any) => ({ if (searchState.page === 1) {
type: item.type ?? 'unknown', // pastikan selalu ada type searchState.results = res.data?.data || [];
...item, } else {
})); searchState.results.push(...(res.data?.data || []));
if (searchState.page === 1) {
searchState.results = data;
} else {
searchState.results.push(...data);
}
searchState.nextPage = res.data?.nextPage || null;
} catch (e) {
console.error('Search fetch error:', e);
} finally {
searchState.loading = false;
} }
searchState.nextPage = res.data?.nextPage || null;
searchState.loading = false;
}, },
async next() { async next() {
@@ -60,15 +54,95 @@ const searchState = proxy({
}, },
}); });
// 🔁 Auto debounce search trigger // 🕒 debounce-nya tetap kita export biar bisa dipanggil manual
const debouncedFetch = debounce(() => { export const debouncedFetch = debounce(() => {
if (!searchState.query) return;
searchState.page = 1; searchState.page = 1;
searchState.fetch(); searchState.fetch();
}, 500); }, 500);
subscribe(searchState, () => {
debouncedFetch();
});
export default searchState; export default searchState;
// 'use client';
// import { proxy, subscribe } from 'valtio';
// import { debounce } from 'lodash';
// import ApiFetch from '@/lib/api-fetch';
// interface SearchResult {
// type?: string;
// id: string | number;
// title?: string;
// [key: string]: any;
// }
// const searchState = proxy({
// query: '',
// page: 1,
// limit: 10,
// type: '', // kosong = global search
// results: [] as SearchResult[],
// nextPage: null as number | null,
// loading: false,
// // --- fetch utama ---
// async fetch() {
// if (!searchState.query.trim()) {
// // 🧹 kalau query kosong, kosongin data dan stop
// searchState.results = [];
// searchState.nextPage = null;
// searchState.loading = false;
// 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,
// },
// });
// const newData = res.data?.data || [];
// // Kalau ini page pertama, replace data
// if (searchState.page === 1) {
// searchState.results = newData;
// } else {
// // Kalau page berikutnya, append data
// searchState.results = [...searchState.results, ...newData];
// }
// searchState.nextPage = res.data?.nextPage || null;
// } catch (err) {
// console.error('Search fetch error:', err);
// } finally {
// searchState.loading = false;
// }
// },
// // --- load next page (infinite scroll) ---
// async next() {
// if (!searchState.nextPage || searchState.loading) return;
// searchState.page = searchState.nextPage;
// await searchState.fetch();
// },
// });
// // --- debounce agar gak fetch tiap ketik ---
// const debouncedFetch = debounce(() => {
// // reset pagination setiap query berubah
// searchState.page = 1;
// searchState.fetch();
// }, 500);
// // --- auto trigger setiap query berubah ---
// subscribe(searchState, () => {
// // kalau query berubah, jalankan debounce fetch
// debouncedFetch();
// });
// export default searchState;

View File

@@ -10,31 +10,36 @@ import ProfilPerbekel from './ui/profilPerbekel';
// import LembagaDesa from './ui/lembagaDesa'; // import LembagaDesa from './ui/lembagaDesa';
import MotoDesa from './ui/motoDesa'; import MotoDesa from './ui/motoDesa';
import SemuaPerbekel from './ui/semuaPerbekel'; import SemuaPerbekel from './ui/semuaPerbekel';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() { function Page() {
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Box>
<Box px={{ base: 'md', md: 100 }}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<BackButton /> <Box px={{ base: 'md', md: 100 }}>
</Box> <BackButton />
<Container w={{ base: "100%", md: "50%" }}> </Box>
<Stack align='center' gap={0}> <Container w={{ base: "100%", md: "50%" }}>
<Text fz={{base: "h1", md: "2.5rem"}} c={colors["blue-button"]} fw={"bold"}> <Stack align='center' gap={0}>
Profile Desa <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
</Text> Profile Desa
</Stack> </Text>
</Container> </Stack>
<Box px={{ base: "md", md: 100 }}> </Container>
<ProfileDesa /> <Box px={{ base: "md", md: 100 }}>
<SejarahDesa /> <ProfileDesa />
<VisimisiDesa /> <SejarahDesa />
<LambangDesa /> <VisimisiDesa />
<MaskotDesa /> <LambangDesa />
<ProfilPerbekel /> <MaskotDesa />
<MotoDesa /> <ProfilPerbekel />
<SemuaPerbekel/> <MotoDesa />
</Box> <SemuaPerbekel />
</Stack> </Box>
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
); );
} }

View File

@@ -28,6 +28,32 @@ function Page() {
) )
} }
// Add this check before the return statement
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Sektor Unggulan Desa Darmasaba
</Text>
<Text c="dimmed" mt="md">
Data sektor unggulan belum tersedia
</Text>
</Box>
</Stack>
);
}
const chartData = data
.filter(item => item?.name && typeof item.value === 'number')
.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}));
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -54,11 +80,7 @@ function Page() {
<BarChart <BarChart
p={10} p={10}
h={300} h={300}
data={data.map((item) => ({ data={chartData}
id: item.id,
sektor: item.name,
Ton: item.value,
}))}
dataKey="sektor" dataKey="sektor"
series={[ series={[
{ name: 'Ton', color: colors['blue-button'] }, { name: 'Ton', color: colors['blue-button'] },

View File

@@ -41,6 +41,37 @@ function Page() {
) )
} }
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Pencegahan Kriminalitas
</Text>
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}>
Keamanan Komunitas & Pencegahan Kriminal
</Text>
</Box>
<SimpleGrid
px={{ base: 20, md: 100 }}
cols={{ base: 1, md: 2 }}
spacing="xl"
>
<Paper p="xl" radius="xl" shadow="lg" >
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
Program Keamanan Berjalan
</Text>
<Stack pt={30} gap="lg">
<Text c="dimmed">
Tidak ada data pencegahan kriminalitas yang cocok
</Text>
</Stack>
</Paper>
</SimpleGrid>
</Stack>
)
}
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}> <Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>

View File

@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BarChart as MantineBarChart } from '@mantine/charts'; import { BarChart as MantineBarChart } from '@mantine/charts';
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -107,20 +107,7 @@ function Page() {
<Box> <Box>
<Paper p={"xl"} bg={colors['white-trans-1']}> <Paper p={"xl"} bg={colors['white-trans-1']}>
<Box pb={30}> <Box pb={30}>
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}> <Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
<ColorSwatch color="#EF3E3E" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
<ColorSwatch color="#3290CA" size={30} />
</Flex>
</Box>
</Flex>
{chartData.length === 0 ? ( {chartData.length === 0 ? (
<Text c="dimmed" ta="center" py="xl"> <Text c="dimmed" ta="center" py="xl">
Belum ada data yang tersedia untuk ditampilkan Belum ada data yang tersedia untuk ditampilkan
@@ -150,6 +137,20 @@ function Page() {
</Center> </Center>
</> </>
)} )}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
<ColorSwatch color="#EF3E3E" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
<ColorSwatch color="#3290CA" size={30} />
</Flex>
</Box>
</Flex>
</Box> </Box>
</Paper> </Paper>
</Box> </Box>
@@ -163,11 +164,11 @@ function Page() {
}} }}
> >
{/* Fasilitas Kesehatan */} {/* Fasilitas Kesehatan */}
<FasilitasKesehatan/> <FasilitasKesehatan />
{/* Jadwal Kegiatan */} {/* Jadwal Kegiatan */}
<JadwalKegiatan/> <JadwalKegiatan />
{/* Artikel Kesehatan */} {/* Artikel Kesehatan */}
<ArtikelKesehatanPage/> <ArtikelKesehatanPage />
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Stack> </Stack>

View File

@@ -101,27 +101,42 @@ function Page() {
}} }}
> >
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Image <Box
src={v.image.link} style={{
alt={v.name} width: '100%',
w={140} aspectRatio: '16/9',
h={140} borderRadius: '12px',
fit="contain" overflow: 'hidden',
radius="md" position: 'relative',
loading="lazy" }}
/> >
<Image
src={v.image.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}> <Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name} {v.name}
</Text> </Text>
<Text fz="sm" c="dimmed" ta="center" lineClamp={3}> <Text fz="sm" c="dimmed" ta="center" lineClamp={3}>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> <span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Text> </Text>
<Button <Button
variant="light" variant="light"
leftSection={<IconBrandWhatsapp size={18} />} leftSection={<IconBrandWhatsapp size={18} />}
component="a" component="a"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`} href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
target="_blank" target="_blank"
aria-label="Hubungi WhatsApp" aria-label="Hubungi WhatsApp"
>WhatsApp</Button> >WhatsApp</Button>
</Stack> </Stack>

View File

@@ -2,7 +2,6 @@
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat' import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
import colors from '@/con/colors' import colors from '@/con/colors'
import { import {
Badge,
Box, Box,
Center, Center,
Grid, Grid,
@@ -106,15 +105,30 @@ function Page() {
> >
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Center> <Center>
<Image <Box
src={v.image.link} style={{
alt={v.name} width: '100%',
h={180} aspectRatio: '16/9',
w="100%" borderRadius: '12px',
radius="md" overflow: 'hidden',
fit="cover" position: 'relative',
style={{ aspectRatio: '4/3' }} }}
/> >
<Image
src={v.image.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
</Center> </Center>
<Stack gap={4} w="100%"> <Stack gap={4} w="100%">
<Text <Text
@@ -136,9 +150,6 @@ function Page() {
/> />
</Box> </Box>
</Stack> </Stack>
<Badge radius="md" color="blue" variant="light" mt="sm">
Darurat
</Badge>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
@@ -160,7 +171,7 @@ function Page() {
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' }, '&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
}, },
}} }}
/> />
</Center> </Center>

View File

@@ -7,16 +7,18 @@ import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/ico
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";
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 { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 6, search); load(page, 6, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (

View File

@@ -78,9 +78,7 @@ function Page() {
<Tooltip label={materi.data?.judul} position="top" withArrow> <Tooltip label={materi.data?.judul} position="top" withArrow>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<IconRecycle size={28} color={colors['blue-button']} /> <IconRecycle size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center"> <Text fz="h3" fw="bold" c={colors['blue-button']} ta="center" dangerouslySetInnerHTML={{ __html: materi.data?.judul || '' }} />
{materi.data?.judul}
</Text>
</Stack> </Stack>
</Tooltip> </Tooltip>
</Box> </Box>

View File

@@ -12,10 +12,9 @@ import {
SimpleGrid, SimpleGrid,
Stack, Stack,
Text, Text,
TextInput, TextInput
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { IconDownload, IconSend2 } from '@tabler/icons-react'; import { IconSend2 } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -150,23 +149,6 @@ function Page() {
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
<Center pb={30}>
<Tooltip label="Unduh dokumen tata cara permohonan" withArrow>
<Button
fz="sm"
size="md"
radius="md"
bg={colors['blue-button']}
leftSection={
<IconDownload size={20} color={colors['white-1']} />
}
>
Unduh Tata Cara
</Button>
</Tooltip>
</Center>
<Group justify="center"> <Group justify="center">
<Paper <Paper
p="xl" p="xl"

View File

@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react'; import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-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 ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() { function Page() {
const allList = useProxy(stateProfilePPID) const allList = useProxy(stateProfilePPID)
@@ -36,99 +37,103 @@ function Page() {
: [allList.profile.data] : [allList.profile.data]
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22"> <Box>
<Box px={{ base: 'md', md: 100 }}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<BackButton /> <Box px={{ base: 'md', md: 100 }}>
</Box> <BackButton />
<Box px={{ base: 'md', md: 100 }}>
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Profil PPID Desa Darmasaba
</Text>
</Box>
{dataArray.map((item) => (
<Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}>
<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
</Text>
</Flex>
</Box>
<Divider my="lg" />
<Box px={{ base: 0, md: 50 }} pb={40}>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
<Box px={{ base: 0, md: 50 }}>
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
<Stack gap="md">
<Center>
<Image
loading='lazy'
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
w={{ base: 220, md: 330 }}
alt="Foto Pimpinan"
radius="md"
/>
</Center>
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
{item.name}
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box>
<Stack gap="xl">
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconUser size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTimeline size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
</Stack>
</Box>
</SimpleGrid>
</Box>
<Box pb={40}>
<Flex align="center" gap="sm" mb="sm">
<IconBuildingCommunity size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
</List>
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTargetArrow size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
</List>
</Box>
</Paper>
</Box> </Box>
))} <Box px={{ base: 'md', md: 100 }}>
</Stack> <Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Profil PPID Desa Darmasaba
</Text>
</Box>
{dataArray.map((item) => (
<Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}>
<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
</Text>
</Flex>
</Box>
<Divider my="lg" />
<Box px={{ base: 0, md: 50 }} pb={40}>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
<Box px={{ base: 0, md: 50 }}>
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
<Stack gap="md">
<Center>
<Image
loading='lazy'
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
w={{ base: 220, md: 330 }}
alt="Foto Pimpinan"
radius="md"
/>
</Center>
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
{item.name}
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box>
<Stack gap="xl">
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconUser size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTimeline size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</Stack>
</Box>
</SimpleGrid>
</Box>
<Box pb={40}>
<Flex align="center" gap="sm" mb="sm">
<IconBuildingCommunity size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTargetArrow size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box>
</Paper>
</Box>
))}
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
) )
} }

View File

@@ -0,0 +1,157 @@
'use client';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import {
Box,
Divider,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function DetailPegawaiUser() {
const statePegawai = useProxy(stateStrukturPPID.pegawai);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateStrukturPPID.posisiOrganisasi.findMany.load();
statePegawai.findUnique.load(params?.id as string);
}, []);
if (!statePegawai.findUnique.data) {
return (
<Stack py="lg">
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = statePegawai.findUnique.data;
return (
<Box px={{ base: 'sm', md: 'lg' }} py="xl">
{/* Back button */}
<Group mb="lg">
<Box
onClick={() => router.back()}
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<IconArrowBack size={22} color={colors['blue-button']} />
<Text c={colors['blue-button']} fw={500}>
Kembali
</Text>
</Box>
</Group>
<Paper
w={{ base: '100%', md: '70%' }}
mx="auto"
p="xl"
radius="lg"
shadow="sm"
bg="white"
style={{
border: '1px solid #eaeaea',
}}
>
<Stack align="center" gap="md">
{/* Foto Profil */}
<Image
src={data.image?.link || '/placeholder-profile.png'}
alt={data.namaLengkap || 'Foto Profil'}
w={160}
h={160}
radius={100}
fit="cover"
style={{ border: `2px solid ${colors['blue-button']}` }}
loading="lazy"
/>
{/* Nama & Jabatan */}
<Stack align="center" gap={2}>
<Title order={3} fw={700} c={colors['blue-button']}>
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Title>
<Text fz="sm" c="dimmed">
{data.posisi?.nama || 'Posisi tidak tersedia'}
</Text>
</Stack>
</Stack>
<Divider my="lg" />
{/* Informasi Detail */}
<Stack gap="md">
<InfoRow label="Email" value={data.email} />
<InfoRow label="Telepon" value={data.telepon} />
<InfoRow label="Alamat" value={data.alamat} multiline />
<InfoRow
label="Tanggal Masuk"
value={
data.tanggalMasuk
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'
}
/>
<InfoRow
label="Status"
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
valueColor={data.isActive ? 'green' : 'red'}
/>
</Stack>
</Paper>
</Box>
);
}
/* Komponen kecil untuk menampilkan baris informasi */
function InfoRow({
label,
value,
valueColor,
multiline = false,
}: {
label: string;
value?: string | null;
valueColor?: string;
multiline?: boolean;
}) {
return (
<Box>
<Text fz="sm" fw={600} c="dark">
{label}
</Text>
<Text
fz="sm"
c={valueColor || 'dimmed'}
style={{
whiteSpace: multiline ? 'normal' : 'nowrap',
wordBreak: 'break-word',
}}
>
{value || '-'}
</Text>
</Box>
);
}
export default DetailPegawaiUser;

View File

@@ -1,129 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// /* eslint-disable react-hooks/exhaustive-deps */
// /* eslint-disable @typescript-eslint/no-explicit-any */
// 'use client'
// import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
// import colors from '@/con/colors';
// import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
// import { OrganizationChart } from 'primereact/organizationchart';
// import { useEffect } from 'react';
// import { useProxy } from 'valtio/utils';
// import BackButton from '../../desa/layanan/_com/BackButto';
// function Page() {
// return (
// <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
// <Box px={{ base: 'md', md: 100 }}>
// <BackButton />
// </Box>
// <Title ta={"center"} fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>Struktur PPID</Title>
// <StrukturOrganisasiPPID />
// </Stack>
// );
// }
// function StrukturOrganisasiPPID() {
// const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
// useEffect(() => {
// stateOrganisasi.findMany.load()
// }, [])
// if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
// return (
// <Stack py={10}>
// <Skeleton h={500} />
// </Stack>
// );
// }
// // Step 1: Group pegawai berdasarkan posisiId
// const posisiMap = new Map<string, any>();
// for (const pegawai of stateOrganisasi.findMany.data) {
// const posisiId = pegawai.posisi.id;
// if (!posisiMap.has(posisiId)) {
// posisiMap.set(posisiId, {
// ...pegawai.posisi,
// pegawaiList: [],
// children: []
// });
// }
// posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
// }
// // Step 2: Buat struktur pohon berdasarkan parentId
// const root: any[] = [];
// posisiMap.forEach((posisi) => {
// if (posisi.parentId) {
// const parent = posisiMap.get(posisi.parentId);
// if (parent) {
// parent.children.push(posisi);
// }
// } else {
// root.push(posisi);
// }
// });
// // Step 3: Ubah struktur ke format OrganizationChart
// function toOrgChartFormat(node: any): any {
// return {
// expanded: true,
// type: 'person',
// styleClass: 'p-person',
// data: {
// name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
// status: node.nama,
// image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
// },
// children: node.children.map(toOrgChartFormat)
// };
// }
// const chartData = root.map(toOrgChartFormat);
// return (
// <Box py={10}>
// <Paper bg={colors.grey} p="md" style={{ overflowX: 'auto' }}>
// <OrganizationChart style={{ color: colors['blue-button'] }} value={chartData} nodeTemplate={nodeTemplate} />
// </Paper>
// </Box>
// );
// }
// function nodeTemplate(node: any) {
// const imageSrc = node?.data?.image || '/img/default.png';
// const name = node?.data?.name || 'Tanpa Nama';
// const status = node?.data?.status || 'Tidak ada deskripsi';
// return (
// <Stack pos={"relative"} py={"xl"} gap={"22"}>
// <Stack align="center" gap={4}>
// <Image
// src={imageSrc}
// alt={name}
// radius="xl"
// w={120}
// h={120}
// fit="cover"
// />
// <Text fw={600} ta="center">{name}</Text>
// <Text size="sm" c="dimmed" ta="center">{status}</Text>
// </Stack>
// </Stack>
// );
// }
// export default Page;
'use client' 'use client'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID' import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
import colors from '@/con/colors'
import { import {
Box, Box,
Button, Button,
@@ -136,16 +16,27 @@ import {
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput,
Title, Title,
Tooltip, Tooltip,
Transition, Transition,
} from '@mantine/core' } from '@mantine/core'
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react' import {
IconRefresh,
IconSearch,
IconUsers,
IconZoomIn,
IconZoomOut,
IconArrowsMaximize,
IconArrowsMinimize,
} from '@tabler/icons-react'
import { OrganizationChart } from 'primereact/organizationchart' import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect } from 'react' import { useEffect, useRef, 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 colors from '@/con/colors' import { useTransitionRouter } from 'next-view-transitions'
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
import { debounce } from 'lodash'
export default function Page() { export default function Page() {
return ( return (
@@ -167,7 +58,6 @@ export default function Page() {
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }} fz={{ base: 28, md: 36, lg: 44 }}
> >
Struktur Organisasi PPID Struktur Organisasi PPID
</Title> </Title>
@@ -180,20 +70,34 @@ export default function Page() {
<StrukturOrganisasiPPID /> <StrukturOrganisasiPPID />
</Box> </Box>
</Container> </Container>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box> </Box>
) )
} }
function StrukturOrganisasiPPID() { function StrukturOrganisasiPPID() {
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai) const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
const router = useTransitionRouter()
const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// debounce untuk pencarian
const debouncedSearch = useRef(
debounce((value: string) => {
setSearchQuery(value)
}, 400)
).current
useEffect(() => { useEffect(() => {
void stateOrganisasi.findMany.load() void stateOrganisasi.findMany.load()
}, []) }, [])
const isLoading = const isLoading =
!stateOrganisasi.findMany.data && !stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
stateOrganisasi.findMany.loading !== false
if (isLoading) { if (isLoading) {
return ( return (
@@ -209,10 +113,7 @@ function StrukturOrganisasiPPID() {
) )
} }
if ( if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
!stateOrganisasi.findMany.data ||
stateOrganisasi.findMany.data.length === 0
) {
return ( return (
<Center py={40}> <Center py={40}>
<Stack align="center" gap="md"> <Stack align="center" gap="md">
@@ -233,8 +134,7 @@ function StrukturOrganisasiPPID() {
Data pegawai belum tersedia Data pegawai belum tersedia
</Title> </Title>
<Text c="dimmed" mt="xs"> <Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk PPID. Silakan coba Belum ada data pegawai yang tercatat untuk PPID.
muat ulang atau periksa sumber data.
</Text> </Text>
<Group justify="center" mt="lg"> <Group justify="center" mt="lg">
<Button <Button
@@ -245,15 +145,6 @@ function StrukturOrganisasiPPID() {
> >
Muat Ulang Muat Ulang
</Button> </Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group> </Group>
</Paper> </Paper>
</Stack> </Stack>
@@ -261,45 +152,42 @@ function StrukturOrganisasiPPID() {
) )
} }
// Buat struktur organisasi
const posisiMap = new Map<string, any>() const posisiMap = new Map<string, any>()
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive)
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
for (const pegawai of aktifPegawai) { for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id; const posisiId = pegawai.posisi.id
if (!posisiMap.has(posisiId)) { if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, { posisiMap.set(posisiId, {
...pegawai.posisi, ...pegawai.posisi,
pegawaiList: [], pegawaiList: [],
children: [], children: [],
}); })
} }
posisiMap.get(posisiId)!.pegawaiList.push(pegawai); posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
} }
const root: any[] = [] const root: any[] = []
posisiMap.forEach((posisi) => { posisiMap.forEach((posisi) => {
if (posisi.parentId) { if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId) const parent = posisiMap.get(posisi.parentId)
if (parent) { if (parent) parent.children.push(posisi)
parent.children.push(posisi) else root.push(posisi)
} else { } else root.push(posisi)
root.push(posisi)
}
} else {
root.push(posisi)
}
}) })
function toOrgChartFormat(node: any): any { function toOrgChartFormat(node: any): any {
const pegawai = node.pegawaiList?.[0]
return { return {
expanded: true, expanded: true,
type: 'person', type: 'person',
styleClass: 'p-person', styleClass: 'p-person',
data: { data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan', id: pegawai?.id || null,
name: pegawai?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan', title: node.nama || 'Tanpa jabatan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png', image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '', description: node.deskripsi || '',
positionId: node.id || null, positionId: node.id || null,
}, },
@@ -307,36 +195,98 @@ function StrukturOrganisasiPPID() {
} }
} }
const chartData = root.map(toOrgChartFormat) let chartData = root.map(toOrgChartFormat)
// 🔍 filter by search
if (searchQuery) {
const filterNodes = (nodes: any[]): any[] =>
nodes
.map((n) => ({
...n,
children: filterNodes(n.children || []),
}))
.filter(
(n) =>
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.children.length > 0
)
chartData = filterNodes(chartData)
}
// 🧭 fungsi fullscreen
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen()
setFullscreen(true)
} else {
document.exitFullscreen()
setFullscreen(false)
}
}
// 🧭 fungsi zoom
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2))
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
const resetZoom = () => setScale(1)
return ( return (
<Box py={16} > <Stack align="center" mt="xl">
<Paper {/* 🔍 Search + Zoom + Fullscreen controls */}
radius="md" <Group mb="md" justify="center" gap="sm">
p="md" <TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
/>
<Button variant="light" size="sm" onClick={handleZoomOut}>
<IconZoomOut size={16} />
</Button>
<Button variant="light" size="sm" onClick={resetZoom}>
100%
</Button>
<Button variant="light" size="sm" onClick={handleZoomIn}>
<IconZoomIn size={16} />
</Button>
<Button
variant="light"
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? <IconArrowsMinimize size={16} /> : <IconArrowsMaximize size={16} />
}
>
{isFullscreen ? 'Keluar' : 'Fullscreen'}
</Button>
</Group>
{/* Chart Container */}
<Box
ref={chartContainerRef}
style={{ style={{
background: 'rgba(28,110,164,0.2)', overflow: 'auto',
border: `1px solid rgba(255,255,255,0.1)`, transform: `scale(${scale})`,
overflowX: 'auto', transformOrigin: 'center top',
transition: 'transform 0.25s ease',
}} }}
> >
<OrganizationChart <OrganizationChart
value={chartData} value={chartData}
nodeTemplate={nodeTemplate} nodeTemplate={(node) => nodeTemplate(node, router)}
/> />
</Paper> </Box>
</Box> </Stack>
) )
} }
function nodeTemplate(node: any) { function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
const imageSrc = node?.data?.image || '/img/default.png' const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama' const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan' const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || '' const description = node?.data?.description || ''
return ( return (
<Transition mounted transition="pop" duration={240}> <Transition mounted transition="pop" duration={240}>
{(styles) => ( {(styles) => (
<Card <Card
radius="lg" radius="lg"
@@ -357,15 +307,15 @@ function nodeTemplate(node: any) {
src={imageSrc} src={imageSrc}
alt={name} alt={name}
radius="md" radius="md"
width={120} width={60}
height={120} height={60}
fit="cover" fit="cover"
style={{ style={{
objectFit: 'cover', objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.2)', border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 12, marginBottom: 12,
}} }}
loading='lazy' loading="lazy"
/> />
<Text fw={700}>{name}</Text> <Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}> <Text size="sm" c="dimmed" mt={4}>
@@ -374,19 +324,17 @@ function nodeTemplate(node: any) {
<Text size="xs" c="dimmed" mt={8} lineClamp={3}> <Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'} {description || 'Belum ada deskripsi.'}
</Text> </Text>
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom"> <Tooltip label="Lihat Detail" withArrow position="bottom">
<Button <Button
variant="light" variant="light"
size="xs" size="xs"
mt="md" mt="md"
onClick={() => { onClick={() => {
const id = node?.data?.positionId const id = node?.data?.id
if (id && (window as any).scrollTo) { router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
}
}} }}
> >
Kembali Lihat Detail
</Button> </Button>
</Tooltip> </Tooltip>
</Card> </Card>
@@ -394,6 +342,3 @@ function nodeTemplate(node: any) {
</Transition> </Transition>
) )
} }

View File

@@ -73,7 +73,7 @@ function Page() {
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
lh={1.7} lh={1.7}
ta="justify" ta="center"
dangerouslySetInnerHTML={{ __html: item.visi }} dangerouslySetInnerHTML={{ __html: item.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{wordBreak: "break-word", whiteSpace: "normal"}}
/> />
@@ -88,7 +88,7 @@ function Page() {
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
lh={1.7} lh={1.7}
ta="justify" ta="center"
dangerouslySetInnerHTML={{ __html: item.misi }} dangerouslySetInnerHTML={{ __html: item.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{wordBreak: "break-word", whiteSpace: "normal"}}
/> />

View File

@@ -1,20 +1,63 @@
import { useRef, useState, useEffect } from 'react';
import stateNav from "@/state/state-nav"; import stateNav from "@/state/state-nav";
import { Container, Stack, Tooltip } from "@mantine/core"; import { Container, Stack, ActionIcon, Box } from "@mantine/core";
import { IconX } from '@tabler/icons-react';
import GlobalSearch from "./globalSearch"; import GlobalSearch from "./globalSearch";
export function NavbarSearch() { export function NavbarSearch() {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Close when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
stateNav.clear();
}
}
// Add event listener
document.addEventListener('mousedown', handleClickOutside);
return () => {
// Clean up
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return ( return (
<Container <Box
w={{ base: "100%", md: "80%" }} ref={containerRef}
fluid style={{ position: 'relative' }}
py="xl"
onMouseLeave={stateNav.clear}
> >
<Stack pt="xl"> <Container
<Tooltip label="Type to search across the site" position="bottom-start" withArrow> w={{ base: "100%", md: "80%" }}
<GlobalSearch /> fluid
</Tooltip> py="xl"
</Stack> >
</Container> <Stack pt="xl">
<Box style={{ position: 'relative' }}>
<GlobalSearch />
{isOpen && (
<ActionIcon
onClick={() => {
setIsOpen(false);
stateNav.clear();
}}
style={{
position: 'absolute',
right: 10,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1000
}}
>
<IconX size={16} />
</ActionIcon>
)}
</Box>
</Stack>
</Container>
</Box>
); );
} }

View File

@@ -86,10 +86,15 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
align="center" align="center"
p="xs" p="xs"
onClick={() => { onClick={() => {
router.push(item.href); if (item.href) {
stateNav.mobileOpen = false; router.push(item.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
opacity: item.href ? 1 : 0.8
}} }}
style={{ cursor: "pointer" }}
> >
<Text c="dark.9" fw={600} fz="md"> <Text c="dark.9" fw={600} fz="md">
{item.name} {item.name}

View File

@@ -2,15 +2,14 @@
import colors from "@/con/colors" import colors from "@/con/colors"
import stateNav from "@/state/state-nav" import stateNav from "@/state/state-nav"
import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core" import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
import { useHover } from "@mantine/hooks"
import { IconSearch, IconUser } from "@tabler/icons-react" import { IconSearch, IconUser } from "@tabler/icons-react"
import { useTransitionRouter } from 'next-view-transitions' import { useTransitionRouter } from 'next-view-transitions'
import { usePathname, useRouter } from "next/navigation"
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio"
import { MenuItem } from "../../../../types/menu-item" import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch" import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu" import { NavbarSubMenu } from "./NavbarSubMenu"
import { useRouter } from "next/navigation"
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu) // contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = { const stateAuth = {
@@ -21,6 +20,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav) const { item, isSearch } = useSnapshot(stateNav)
const router = useTransitionRouter() const router = useTransitionRouter()
const next = useRouter() const next = useRouter()
const pathname = usePathname();
return ( return (
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}> <Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
@@ -47,7 +47,12 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
</Tooltip> </Tooltip>
{listNavbar.map((item, k) => ( {listNavbar.map((item, k) => (
<MenuItemCom key={k} item={item} /> <MenuItemCom
key={k}
item={item}
isActive={pathname === item.href ||
(item.children?.some(child => child.href === pathname))}
/>
))} ))}
<Tooltip label="Search content" position="bottom" withArrow> <Tooltip label="Search content" position="bottom" withArrow>
@@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
) )
} }
function MenuItemCom({ item }: { item: MenuItem }) { function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) {
const { ref, hovered } = useHover()
const router = useTransitionRouter() const router = useTransitionRouter()
return ( return (
<Button <Menu
ref={ref} trigger="hover"
color={hovered ? "gray" : colors["blue-button"]} position="bottom-start"
onMouseEnter={() => { offset={20}
stateNav.item = item.children || null width={300}
stateNav.isSearch = false shadow="md"
withinPortal
onOpen={() => {
stateNav.item = item.children || null;
stateNav.isSearch = false;
}} }}
variant="subtle"
radius="xl"
onClick={() => {
router.push(item.href)
stateNav.clear()
}}
fw={500}
> >
{item.name} <MenuTarget>
</Button> <Button
variant="subtle"
color={isActive ? 'blue' : 'gray'}
onClick={() => {
if (item.href) {
router.push(item.href);
stateNav.clear();
}
}}
styles={{
root: {
fontWeight: isActive ? 600 : 400,
borderBottom: isActive ? `2px solid ${colors['blue-button']}` : 'none',
'&:hover': {
backgroundColor: 'transparent',
}
}
}}
>
{item.name}
</Button>
</MenuTarget>
</Menu>
) )
} }

View File

@@ -7,10 +7,11 @@ import { IconArrowRight } from "@tabler/icons-react";
import { MenuItem } from "../../../../types/menu-item"; import { MenuItem } from "../../../../types/menu-item";
import { useTransitionRouter } from "next-view-transitions"; import { useTransitionRouter } from "next-view-transitions";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { usePathname } from "next/navigation";
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) { export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
const router = useTransitionRouter(); const router = useTransitionRouter();
const pathname = usePathname();
return ( return (
<motion.div <motion.div
key={Math.random().toString(36).slice(2)} key={Math.random().toString(36).slice(2)}
@@ -32,33 +33,34 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
<Stack gap="xs" align="stretch"> <Stack gap="xs" align="stretch">
{item.map((link, index) => ( {item.map((link, index) => (
<Button <Button
key={index} key={index}
variant="subtle" variant="subtle"
justify="space-between" justify="space-between"
size="lg" size="lg"
radius="md" radius="md"
color="gray.0" color={pathname === link.href ? 'blue' : 'gray'}
onClick={() => { onClick={() => {
if (link.href) {
router.push(link.href); router.push(link.href);
stateNav.item = null; stateNav.item = null;
stateNav.isSearch = false; stateNav.isSearch = false;
}} }
rightSection={<IconArrowRight size={18} />} }}
styles={(theme) => ({ rightSection={<IconArrowRight size={18} />}
root: { styles={(theme) => ({
background: "transparent", root: {
color: colors['blue-button'], background: pathname === link.href ? theme.colors.blue[0] : 'transparent',
fontWeight: 500, color: pathname === link.href ? theme.colors.blue[7] : colors['blue-button'],
transition: "all 0.2s ease", fontWeight: pathname === link.href ? 600 : 500,
"&:hover": { transition: "all 0.2s ease",
background: theme.colors.gray[8], "&:hover": {
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`, background: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
}, }
}, },
})} })}
> >
{link.name} {link.name}
</Button> </Button>
))} ))}
</Stack> </Stack>
) : ( ) : (

View File

@@ -1,57 +1,24 @@
'use client'; 'use client';
import { TextInput, Loader, Stack, Box, Text } from '@mantine/core'; import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Modal, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import { useRouter } from 'next/navigation'; import getDetailUrl from './searchUrl';
import { useEffect } from 'react';
import searchState from '@/app/api/[[...slugs]]/_lib/search/searchState';
// Mapping type ke URL
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',
};
return type ? typeUrlMap[type] || '/darmasaba' : '/darmasaba';
};
export default function GlobalSearch() { export default function GlobalSearch() {
const snap = useSnapshot(searchState); const snap = useSnapshot(searchState);
const router = useRouter(); const [isOpen, setIsOpen] = useState(false);
// Infinite scroll listener // Toggle modal when there's a query
useEffect(() => {
setIsOpen(!!snap.query);
}, [snap.query]);
// Infinite scroll
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
const bottom = const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next(); if (bottom && !snap.loading) searchState.next();
}; };
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
@@ -59,39 +26,87 @@ export default function GlobalSearch() {
}, [snap.loading]); }, [snap.loading]);
return ( return (
<Stack> <Box style={{ position: 'relative', width: '100%' }}>
<TextInput <TextInput
placeholder="Cari apapun..." placeholder="Cari apapun..."
value={snap.query} value={snap.query}
onChange={(e) => (searchState.query = e.currentTarget.value)} onChange={(e) => {
searchState.query = e.currentTarget.value;
debouncedFetch();
}}
radius="xl"
rightSection={
snap.query ? (
<IconX
size={16}
style={{ cursor: 'pointer' }}
onClick={() => {
searchState.query = '';
searchState.results = [];
}}
/>
) : undefined
}
/> />
{snap.results.map((item, i) => ( {/* Modal for search results */}
<Box <Modal
key={i} opened={isOpen && !!snap.query}
p="sm" onClose={() => {
style={{ searchState.query = '';
borderBottom: '1px solid #eee', searchState.results = [];
cursor: 'pointer', }}
transition: 'background 0.2s', withCloseButton={false}
}} size="lg"
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')} padding={0}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} radius="md"
onClick={() => { style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 1000 }}
const url = getDetailUrl(item); styles={{
router.push(url); content: { // Changed from 'modal' to 'content'
}} backgroundColor: 'white',
> boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
<Text size="sm" fw={500}> borderRadius: '0.5rem',
{item.judul || item.namaPasar || item.nama || item.name} maxHeight: '400px',
</Text> overflow: 'hidden',
<Text size="xs" c="dimmed"> },
dari modul: {item.type} }}
</Text> >
<Box style={{ maxHeight: '400px', overflowY: 'auto' }}>
{snap.results.map((item, i) => (
<Box
key={i}
p="sm"
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
transition: 'background 0.2s',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%',
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={() => {
const url = getDetailUrl(item);
window.location.href = url;
}}
>
<Text size="sm" fw={500}>
{item.judul || item.namaPasar || item.nama || item.name}
</Text>
<Text size="xs" c="dimmed">
dari modul: {item.type}
</Text>
</Box>
))}
{snap.loading && (
<Center py="md">
<Loader size="sm" />
</Center>
)}
</Box> </Box>
))} </Modal>
</Box>
{snap.loading && <Loader size="sm" />}
</Stack>
); );
} }

View File

@@ -332,7 +332,7 @@ function Kepuasan() {
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type='text'
placeholder="masukkan nama" placeholder="Masukkan nama"
defaultValue={state.create.form.name} defaultValue={state.create.form.name}
onChange={(val) => { onChange={(val) => {
state.create.form.name = val.currentTarget.value; state.create.form.name = val.currentTarget.value;

View File

@@ -6,20 +6,19 @@ import {
Center, Center,
Image, Image,
Paper, Paper,
ScrollArea,
SimpleGrid, SimpleGrid,
Skeleton,
Stack, Stack,
Text, Text,
Tooltip, useMantineColorScheme
Skeleton,
useMantineColorScheme,
ScrollArea,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useTransitionRouter } from "next-view-transitions"; import { useTransitionRouter } from "next-view-transitions";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>; type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
@@ -30,7 +29,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
return ( return (
<motion.div whileHover={{ scale: 1.03 }}> <motion.div whileHover={{ scale: 1.03 }}>
<Tooltip label={`Lihat ${data.name}`} withArrow>
<Paper <Paper
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)} onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="lg" p="lg"
@@ -67,7 +65,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
</Text> </Text>
</Box> </Box>
</Paper> </Paper>
</Tooltip>
</motion.div> </motion.div>
); );
} }

View File

@@ -37,9 +37,13 @@ export default function ProfileView({ data }: ProfileViewProps) {
<Image <Image
src={data.image.link} src={data.image.link}
alt={data.name || 'Foto profil'} alt={data.name || 'Foto profil'}
fit="cover" fit="contain"
radius="lg" radius="lg"
loading="lazy" loading="lazy"
style={{
objectPosition: 'bottom center',
transform: 'translateY(10px)', // sedikit turun biar natural
}}
/> />
) : ( ) : (
<Stack align="center" gap="xs" w="100%" py="xl"> <Stack align="center" gap="xs" w="100%" py="xl">
@@ -49,13 +53,26 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Text> </Text>
</Stack> </Stack>
)} )}
<Box pos="absolute" bottom={0} w="100%" p={{ base: 'xs', md: 'md' }}>
{/* Box nama dan jabatan - sedikit overlap dengan gambar */}
<Box
pos="absolute"
bottom={-20} // bikin naik sedikit ke gambar
right={0}
w="100%"
p={{ base: 'xs', md: 'md' }}
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar
>
<Card <Card
px="lg" px="lg"
radius="2xl" py="sm"
radius="lg"
withBorder withBorder
className="glass3" style={{
style={{ border: '1px solid rgba(255,255,255,0.15)' }} boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)',
pointerEvents: 'auto',
}}
> >
<Tooltip label="Jabatan Resmi" withArrow> <Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">

View File

@@ -68,7 +68,7 @@ function Penghargaan() {
variant="gradient" variant="gradient"
gradient={{ from: "cyan", to: "blue", deg: 60 }} gradient={{ from: "cyan", to: "blue", deg: 60 }}
> >
Penghargaan & Prestasi Desa Penghargaan Desa
</Text> </Text>
{loading ? ( {loading ? (

View File

@@ -51,7 +51,7 @@ export default function SDGS() {
</Title> </Title>
</Center> </Center>
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto"> <Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
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>
<Box py={50}> <Box py={50}>
@@ -67,49 +67,62 @@ export default function SDGS() {
> >
{sdgsDesa && sdgsDesa.length > 0 ? ( {sdgsDesa && sdgsDesa.length > 0 ? (
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl" verticalSpacing="xl"> <SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl" verticalSpacing="xl">
{sdgsDesa.map((item) => ( {sdgsDesa.map((item) => (
<Paper <Paper
key={item.id} key={item.id}
p="lg" p="lg"
radius="xl" radius="xl"
shadow="sm" shadow="sm"
withBorder withBorder
style={{ style={{
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)", background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)", border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease", transition: "all 0.3s ease",
}} height: "100%", // biar tinggi antar card konsisten
> display: "flex",
<Center mb="lg"> flexDirection: "column",
<Box }}
p="md" >
style={{ <Center mb="lg">
background: "rgba(240, 249, 255, 0.8)", <Box
backdropFilter: "blur(6px)", p="md"
width: mobile ? 140 : 160, style={{
height: mobile ? 140 : 160, background: "rgba(240, 249, 255, 0.8)",
display: "flex", backdropFilter: "blur(6px)",
alignItems: "center", width: mobile ? 140 : 160,
justifyContent: "center", height: mobile ? 140 : 160,
borderRadius: "1rem", display: "flex",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)", alignItems: "center",
}} justifyContent: "center",
> borderRadius: "1rem",
<Image boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"} }}
alt={item.name} >
w={mobile ? 90 : 110} <Image
h={mobile ? 90 : 110} src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
fit="contain" alt={item.name}
loading="lazy" w={mobile ? 90 : 110}
/> h={mobile ? 90 : 110}
</Box> fit="contain"
</Center> loading="lazy"
/>
</Box>
</Center>
{/* Stack isi teks & angka */}
<Stack justify="space-between" align="center" gap="xs" h="100%">
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow> <Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow>
<Text ta="center" fz={{ base: "lg", md: "xl" }} fw={700} mb="xs"> <Text
ta="center"
fz={{ base: "lg", md: "xl" }}
fw={700}
mb="xs"
style={{ minHeight: mobile ? 60 : 70 }} // biar judulnya punya tinggi tetap
>
{item.name} {item.name}
</Text> </Text>
</Tooltip> </Tooltip>
<Title <Title
order={2} order={2}
ta="center" ta="center"
@@ -122,9 +135,11 @@ export default function SDGS() {
> >
{item.jumlah} {item.jumlah}
</Title> </Title>
</Paper> </Stack>
))} </Paper>
</SimpleGrid> ))}
</SimpleGrid>
) : ( ) : (
<Center mih={200} style={{ flexDirection: "column" }}> <Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} /> <IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />

View File

@@ -0,0 +1,36 @@
'use client'
import { useWindowScroll } from '@mantine/hooks';
import { ActionIcon, Transition } from '@mantine/core';
import { IconArrowUp } from '@tabler/icons-react';
import colors from '@/con/colors';
function ScrollToTopButton() {
const [scroll, scrollTo] = useWindowScroll();
return (
<Transition
mounted={scroll.y > 300}
transition="slide-up"
duration={300}
timingFunction="ease"
>
{(styles) => (
<ActionIcon
style={styles}
size="xl"
radius="xl"
variant="filled"
color={colors['blue-button']}
onClick={() => scrollTo({ y: 0 })}
pos="fixed"
bottom={24}
right={24}
aria-label="Kembali ke atas"
>
<IconArrowUp size={20} />
</ActionIcon>
)}
</Transition>
);
}
export default ScrollToTopButton

View File

@@ -0,0 +1,91 @@
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/tujuan-edukasi-lingkungan',
materiEdukasiLingkungan: '/darmasaba/lingkungan/materi-edukasi-lingkungan',
contohEdukasiLingkungan: '/darmasaba/lingkungan/contoh-edukasi-lingkungan',
filosofiTriHita: '/darmasaba/lingkungan/filosofi-tri-hita',
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/bentuk-konservasi-berdasarkan-adat',
nilaiKonservasiAdat: '/darmasaba/lingkungan/nilai-konservasi-adat',
jenjangPendidikan: '/darmasaba/inovasi/jenjang-pendidikan',
lembaga: '/darmasaba/inovasi/lembaga',
siswa: '/darmasaba/inovasi/siswa',
pengajar: '/darmasaba/inovasi/pengajar',
keunggulanProgram: '/darmasaba/inovasi/keunggulan-program',
tujuanProgram: '/darmasaba/inovasi/tujuan-program',
programUnggulan: '/darmasaba/inovasi/program-unggulan',
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/inovasi/lokasi-jadwal-bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: '/darmasaba/inovasi/fasilitas-bimbingan-belajar-desa',
tujuanPendidikanNonFormal: '/darmasaba/inovasi/tujuan-pendidikan-non-formal',
tempatKegiatan: '/darmasaba/inovasi/tempat-kegiatan',
jenisProgramYangDiselenggarakan: '/darmasaba/inovasi/jenis-program-yang-diselenggarakan',
dataPerpustakaan: '/darmasaba/inovasi/data-perpustakaan',
dataPendidikan: '/darmasaba/inovasi/data-pendidikan',
};
return typeUrlMap[type || ''] || '/darmasaba';
};
export default getDetailUrl;

View File

@@ -8,23 +8,28 @@ import colors from "@/con/colors";
import SDGS from "./_com/main-page/sdgs"; import SDGS from "./_com/main-page/sdgs";
// import ApiFetch from "@/lib/api-fetch"; // import ApiFetch from "@/lib/api-fetch";
import { Stack } from "@mantine/core"; import { Box, Stack } from "@mantine/core";
import Apbdes from "./_com/main-page/apbdes"; import Apbdes from "./_com/main-page/apbdes";
import Prestasi from "./_com/main-page/prestasi"; import Prestasi from "./_com/main-page/prestasi";
import ScrollToTopButton from "./_com/scrollToTopButton";
export default function Page() { export default function Page() {
return ( return (
<Stack bg={colors.grey[1]} gap={"4rem"}> <Box>
<LandingPage /> <Stack bg={colors.grey[1]} gap={"4rem"}>
<Penghargaan /> <LandingPage />
<Layanan /> <Penghargaan />
<Potensi /> <Layanan />
<DesaAntiKorupsi /> <Potensi />
<Kepuasan /> <DesaAntiKorupsi />
<SDGS /> <Kepuasan />
<Apbdes /> <SDGS />
<Prestasi/> <Apbdes />
</Stack> <Prestasi />
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
); );
} }

View File

@@ -2,7 +2,6 @@ const navbarListMenu = [
{ {
id: "1", id: "1",
name: "PPID", name: "PPID",
href: "/darmasaba/ppid/profile-ppid",
children: [ children: [
{ {
id: "1.1", id: "1.1",
@@ -51,7 +50,6 @@ const navbarListMenu = [
{ {
id: "2", id: "2",
name: "Desa", name: "Desa",
href: "/darmasaba/desa/profile",
children: [ children: [
{ {
id: "2.1", id: "2.1",
@@ -94,7 +92,6 @@ const navbarListMenu = [
{ {
id: "3", id: "3",
name: "Kesehatan", name: "Kesehatan",
href: "/darmasaba/kesehatan/posyandu",
children: [ children: [
{ {
id: "3.1", id: "3.1",
@@ -136,7 +133,6 @@ const navbarListMenu = [
{ {
id: "4", id: "4",
name: "Keamanan", name: "Keamanan",
href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal",
children: [ children: [
{ {
id: "4.1", id: "4.1",
@@ -173,7 +169,6 @@ const navbarListMenu = [
{ {
id: "5", id: "5",
name: "Ekonomi", name: "Ekonomi",
href: "/darmasaba/ekonomi/pasar-desa",
children: [ children: [
{ {
id: "5.1", id: "5.1",
@@ -229,7 +224,6 @@ const navbarListMenu = [
}, { }, {
id: "6", id: "6",
name: "Inovasi", name: "Inovasi",
href: "/darmasaba/inovasi/desa-digital-smart-village",
children: [ children: [
{ {
id: "6.1", id: "6.1",
@@ -266,7 +260,6 @@ const navbarListMenu = [
}, { }, {
id: "7", id: "7",
name: "Lingkungan", name: "Lingkungan",
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah",
children: [ children: [
{ {
id: "7.1", id: "7.1",
@@ -302,7 +295,6 @@ const navbarListMenu = [
}, { }, {
id: "8", id: "8",
name: "Pendidikan", name: "Pendidikan",
href: "/darmasaba/pendidikan/info-sekolah",
children: [ children: [
{ {
id: "8.1", id: "8.1",

View File

@@ -1,6 +1,9 @@
export type MenuItem = { export type MenuItem = {
id: string, id: string;
name: string, name: string;
href: string, href?: string;
children?: MenuItem[] children?: MenuItem[];
} } & (
{ href: string; children?: MenuItem[] } |
{ children: MenuItem[] }
)