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-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",

View File

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

View File

@@ -75,17 +75,18 @@ const berita = proxy({
loading: false,
search: "",
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.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -98,9 +99,16 @@ const berita = proxy({
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} 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: {

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { proxy, subscribe } from 'valtio';
import { proxy } from 'valtio';
import { debounce } from 'lodash';
import ApiFetch from '@/lib/api-fetch';
interface SearchResult {
type?: string; // optional biar gak error
type?: string;
id: string | number;
title?: string;
[key: string]: any;
@@ -21,36 +21,30 @@ const searchState = proxy({
loading: false,
async fetch() {
if (!searchState.query) return;
if (!searchState.query) {
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,
},
});
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
const data = (res.data?.data || []).map((item: any) => ({
type: item.type ?? 'unknown', // pastikan selalu ada type
...item,
}));
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;
if (searchState.page === 1) {
searchState.results = res.data?.data || [];
} else {
searchState.results.push(...(res.data?.data || []));
}
searchState.nextPage = res.data?.nextPage || null;
searchState.loading = false;
},
async next() {
@@ -60,15 +54,95 @@ const searchState = proxy({
},
});
// 🔁 Auto debounce search trigger
const debouncedFetch = debounce(() => {
if (!searchState.query) return;
// 🕒 debounce-nya tetap kita export biar bisa dipanggil manual
export const debouncedFetch = debounce(() => {
searchState.page = 1;
searchState.fetch();
}, 500);
subscribe(searchState, () => {
debouncedFetch();
});
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 MotoDesa from './ui/motoDesa';
import SemuaPerbekel from './ui/semuaPerbekel';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Container w={{ base: "100%", md: "50%" }}>
<Stack align='center' gap={0}>
<Text fz={{base: "h1", md: "2.5rem"}} c={colors["blue-button"]} fw={"bold"}>
Profile Desa
</Text>
</Stack>
</Container>
<Box px={{ base: "md", md: 100 }}>
<ProfileDesa />
<SejarahDesa />
<VisimisiDesa />
<LambangDesa />
<MaskotDesa />
<ProfilPerbekel />
<MotoDesa />
<SemuaPerbekel/>
</Box>
</Stack>
<Box>
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Container w={{ base: "100%", md: "50%" }}>
<Stack align='center' gap={0}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Profile Desa
</Text>
</Stack>
</Container>
<Box px={{ base: "md", md: 100 }}>
<ProfileDesa />
<SejarahDesa />
<VisimisiDesa />
<LambangDesa />
<MaskotDesa />
<ProfilPerbekel />
<MotoDesa />
<SemuaPerbekel />
</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 (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
@@ -54,11 +80,7 @@ function Page() {
<BarChart
p={10}
h={300}
data={data.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}))}
data={chartData}
dataKey="sektor"
series={[
{ 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 (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>

View File

@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors';
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 BackButton from '../../desa/layanan/_com/BackButto';
@@ -107,20 +107,7 @@ function Page() {
<Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Box pb={30}>
<Flex pb={30} justify={'flex-end'} 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>
<Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
{chartData.length === 0 ? (
<Text c="dimmed" ta="center" py="xl">
Belum ada data yang tersedia untuk ditampilkan
@@ -150,6 +137,20 @@ function Page() {
</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>
</Paper>
</Box>
@@ -163,11 +164,11 @@ function Page() {
}}
>
{/* Fasilitas Kesehatan */}
<FasilitasKesehatan/>
<FasilitasKesehatan />
{/* Jadwal Kegiatan */}
<JadwalKegiatan/>
<JadwalKegiatan />
{/* Artikel Kesehatan */}
<ArtikelKesehatanPage/>
<ArtikelKesehatanPage />
</SimpleGrid>
</Box>
</Stack>

View File

@@ -101,27 +101,42 @@ function Page() {
}}
>
<Stack align="center" gap="sm">
<Image
src={v.image.link}
alt={v.name}
w={140}
h={140}
fit="contain"
radius="md"
loading="lazy"
/>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<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']}>
{v.name}
</Text>
<Text fz="sm" c="dimmed" ta="center" lineClamp={3}>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Text>
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
target="_blank"
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
target="_blank"
aria-label="Hubungi WhatsApp"
>WhatsApp</Button>
</Stack>

View File

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

View File

@@ -7,16 +7,18 @@ import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/ico
import { useState } from "react";
import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto";
import { useDebouncedValue } from "@mantine/hooks";
export default function Page() {
const state = useProxy(posyandustate);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 6, search);
}, [page, search]);
load(page, 6, debouncedSearch);
}, [page, debouncedSearch]);
if (loading || !data) {
return (

View File

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

View File

@@ -12,10 +12,9 @@ import {
SimpleGrid,
Stack,
Text,
TextInput,
Tooltip,
TextInput
} from '@mantine/core';
import { IconDownload, IconSend2 } from '@tabler/icons-react';
import { IconSend2 } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -150,23 +149,6 @@ function Page() {
</Paper>
))}
</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">
<Paper
p="xl"

View File

@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() {
const allList = useProxy(stateProfilePPID)
@@ -36,99 +37,103 @@ function Page() {
: [allList.profile.data]
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Profil PPID Desa Darmasaba
</Text>
</Box>
{dataArray.map((item) => (
<Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}>
<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 pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
))}
</Stack>
<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>
))}
</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 @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'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
import colors from '@/con/colors'
import {
Box,
Button,
@@ -136,16 +16,27 @@ import {
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Transition,
} 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 { useEffect } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
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() {
return (
@@ -167,7 +58,6 @@ export default function Page() {
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }}
>
Struktur Organisasi PPID
</Title>
@@ -180,20 +70,34 @@ export default function Page() {
<StrukturOrganisasiPPID />
</Box>
</Container>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
)
}
function StrukturOrganisasiPPID() {
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(() => {
void stateOrganisasi.findMany.load()
}, [])
const isLoading =
!stateOrganisasi.findMany.data &&
stateOrganisasi.findMany.loading !== false
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
if (isLoading) {
return (
@@ -209,10 +113,7 @@ function StrukturOrganisasiPPID() {
)
}
if (
!stateOrganisasi.findMany.data ||
stateOrganisasi.findMany.data.length === 0
) {
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
return (
<Center py={40}>
<Stack align="center" gap="md">
@@ -233,8 +134,7 @@ function StrukturOrganisasiPPID() {
Data pegawai belum tersedia
</Title>
<Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk PPID. Silakan coba
muat ulang atau periksa sumber data.
Belum ada data pegawai yang tercatat untuk PPID.
</Text>
<Group justify="center" mt="lg">
<Button
@@ -245,15 +145,6 @@ function StrukturOrganisasiPPID() {
>
Muat Ulang
</Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group>
</Paper>
</Stack>
@@ -261,45 +152,42 @@ function StrukturOrganisasiPPID() {
)
}
// Buat struktur organisasi
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) {
const posisiId = pegawai.posisi.id;
const posisiId = pegawai.posisi.id
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: [],
});
})
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
}
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)
}
} else {
root.push(posisi)
}
if (parent) parent.children.push(posisi)
else root.push(posisi)
} else root.push(posisi)
})
function toOrgChartFormat(node: any): any {
const pegawai = node.pegawaiList?.[0]
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
id: pegawai?.id || null,
name: pegawai?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '',
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 (
<Box py={16} >
<Paper
radius="md"
p="md"
<Stack align="center" mt="xl">
{/* 🔍 Search + Zoom + Fullscreen controls */}
<Group mb="md" justify="center" gap="sm">
<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={{
background: 'rgba(28,110,164,0.2)',
border: `1px solid rgba(255,255,255,0.1)`,
overflowX: 'auto',
overflow: 'auto',
transform: `scale(${scale})`,
transformOrigin: 'center top',
transition: 'transform 0.25s ease',
}}
>
<OrganizationChart
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 name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || ''
return (
<Transition mounted transition="pop" duration={240}>
<Transition mounted transition="pop" duration={240}>
{(styles) => (
<Card
radius="lg"
@@ -357,15 +307,15 @@ function nodeTemplate(node: any) {
src={imageSrc}
alt={name}
radius="md"
width={120}
height={120}
width={60}
height={60}
fit="cover"
style={{
objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 12,
}}
loading='lazy'
loading="lazy"
/>
<Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}>
@@ -374,19 +324,17 @@ function nodeTemplate(node: any) {
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
<Tooltip label="Lihat Detail" withArrow position="bottom">
<Button
variant="light"
size="xs"
mt="md"
onClick={() => {
const id = node?.data?.positionId
if (id && (window as any).scrollTo) {
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
}
const id = node?.data?.id
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
}}
>
Kembali
Lihat Detail
</Button>
</Tooltip>
</Card>
@@ -394,6 +342,3 @@ function nodeTemplate(node: any) {
</Transition>
)
}

View File

@@ -73,7 +73,7 @@ function Page() {
<Text
fz={{ base: 'md', md: 'lg' }}
lh={1.7}
ta="justify"
ta="center"
dangerouslySetInnerHTML={{ __html: item.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
@@ -88,7 +88,7 @@ function Page() {
<Text
fz={{ base: 'md', md: 'lg' }}
lh={1.7}
ta="justify"
ta="center"
dangerouslySetInnerHTML={{ __html: item.misi }}
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 { Container, Stack, Tooltip } from "@mantine/core";
import { Container, Stack, ActionIcon, Box } from "@mantine/core";
import { IconX } from '@tabler/icons-react';
import GlobalSearch from "./globalSearch";
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 (
<Container
w={{ base: "100%", md: "80%" }}
fluid
py="xl"
onMouseLeave={stateNav.clear}
<Box
ref={containerRef}
style={{ position: 'relative' }}
>
<Stack pt="xl">
<Tooltip label="Type to search across the site" position="bottom-start" withArrow>
<GlobalSearch />
</Tooltip>
</Stack>
</Container>
<Container
w={{ base: "100%", md: "80%" }}
fluid
py="xl"
>
<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"
p="xs"
onClick={() => {
router.push(item.href);
stateNav.mobileOpen = false;
if (item.href) {
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">
{item.name}

View File

@@ -2,15 +2,14 @@
import colors from "@/con/colors"
import stateNav from "@/state/state-nav"
import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core"
import { useHover } from "@mantine/hooks"
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
import { IconSearch, IconUser } from "@tabler/icons-react"
import { useTransitionRouter } from 'next-view-transitions'
import { usePathname, useRouter } from "next/navigation"
import { useSnapshot } from "valtio"
import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu"
import { useRouter } from "next/navigation"
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = {
@@ -21,6 +20,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav)
const router = useTransitionRouter()
const next = useRouter()
const pathname = usePathname();
return (
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
@@ -47,7 +47,12 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
</Tooltip>
{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>
@@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
)
}
function MenuItemCom({ item }: { item: MenuItem }) {
const { ref, hovered } = useHover()
function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) {
const router = useTransitionRouter()
return (
<Button
ref={ref}
color={hovered ? "gray" : colors["blue-button"]}
onMouseEnter={() => {
stateNav.item = item.children || null
stateNav.isSearch = false
<Menu
trigger="hover"
position="bottom-start"
offset={20}
width={300}
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}
</Button>
<MenuTarget>
<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 { useTransitionRouter } from "next-view-transitions";
import colors from "@/con/colors";
import { usePathname } from "next/navigation";
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
const router = useTransitionRouter();
const pathname = usePathname();
return (
<motion.div
key={Math.random().toString(36).slice(2)}
@@ -32,33 +33,34 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
<Stack gap="xs" align="stretch">
{item.map((link, index) => (
<Button
key={index}
variant="subtle"
justify="space-between"
size="lg"
radius="md"
color="gray.0"
onClick={() => {
key={index}
variant="subtle"
justify="space-between"
size="lg"
radius="md"
color={pathname === link.href ? 'blue' : 'gray'}
onClick={() => {
if (link.href) {
router.push(link.href);
stateNav.item = null;
stateNav.isSearch = false;
}}
rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({
root: {
background: "transparent",
color: colors['blue-button'],
fontWeight: 500,
transition: "all 0.2s ease",
"&:hover": {
background: theme.colors.gray[8],
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`,
},
},
})}
>
{link.name}
</Button>
}
}}
rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({
root: {
background: pathname === link.href ? theme.colors.blue[0] : 'transparent',
color: pathname === link.href ? theme.colors.blue[7] : colors['blue-button'],
fontWeight: pathname === link.href ? 600 : 500,
transition: "all 0.2s ease",
"&:hover": {
background: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
}
},
})}
>
{link.name}
</Button>
))}
</Stack>
) : (

View File

@@ -1,57 +1,24 @@
'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 { useRouter } from 'next/navigation';
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';
};
import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
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(() => {
const handleScroll = () => {
const bottom =
window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
@@ -59,39 +26,87 @@ export default function GlobalSearch() {
}, [snap.loading]);
return (
<Stack>
<Box style={{ position: 'relative', width: '100%' }}>
<TextInput
placeholder="Cari apapun..."
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) => (
<Box
key={i}
p="sm"
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
transition: 'background 0.2s',
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={() => {
const url = getDetailUrl(item);
router.push(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>
{/* Modal for search results */}
<Modal
opened={isOpen && !!snap.query}
onClose={() => {
searchState.query = '';
searchState.results = [];
}}
withCloseButton={false}
size="lg"
padding={0}
radius="md"
style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 1000 }}
styles={{
content: { // Changed from 'modal' to 'content'
backgroundColor: 'white',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
borderRadius: '0.5rem',
maxHeight: '400px',
overflow: 'hidden',
},
}}
>
<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>
))}
{snap.loading && <Loader size="sm" />}
</Stack>
</Modal>
</Box>
);
}
}

View File

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

View File

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

View File

@@ -37,9 +37,13 @@ export default function ProfileView({ data }: ProfileViewProps) {
<Image
src={data.image.link}
alt={data.name || 'Foto profil'}
fit="cover"
fit="contain"
radius="lg"
loading="lazy"
style={{
objectPosition: 'bottom center',
transform: 'translateY(10px)', // sedikit turun biar natural
}}
/>
) : (
<Stack align="center" gap="xs" w="100%" py="xl">
@@ -49,13 +53,26 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Text>
</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
px="lg"
radius="2xl"
py="sm"
radius="lg"
withBorder
className="glass3"
style={{ border: '1px solid rgba(255,255,255,0.15)' }}
style={{
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)',
pointerEvents: 'auto',
}}
>
<Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed">

View File

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

View File

@@ -51,7 +51,7 @@ export default function SDGS() {
</Title>
</Center>
<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>
<Box py={50}>
@@ -67,49 +67,62 @@ export default function SDGS() {
>
{sdgsDesa && sdgsDesa.length > 0 ? (
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl" verticalSpacing="xl">
{sdgsDesa.map((item) => (
<Paper
key={item.id}
p="lg"
radius="xl"
shadow="sm"
withBorder
style={{
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
}}
>
<Center mb="lg">
<Box
p="md"
style={{
background: "rgba(240, 249, 255, 0.8)",
backdropFilter: "blur(6px)",
width: mobile ? 140 : 160,
height: mobile ? 140 : 160,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "1rem",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
}}
>
<Image
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
alt={item.name}
w={mobile ? 90 : 110}
h={mobile ? 90 : 110}
fit="contain"
loading="lazy"
/>
</Box>
</Center>
{sdgsDesa.map((item) => (
<Paper
key={item.id}
p="lg"
radius="xl"
shadow="sm"
withBorder
style={{
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
height: "100%", // biar tinggi antar card konsisten
display: "flex",
flexDirection: "column",
}}
>
<Center mb="lg">
<Box
p="md"
style={{
background: "rgba(240, 249, 255, 0.8)",
backdropFilter: "blur(6px)",
width: mobile ? 140 : 160,
height: mobile ? 140 : 160,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "1rem",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
}}
>
<Image
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
alt={item.name}
w={mobile ? 90 : 110}
h={mobile ? 90 : 110}
fit="contain"
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>
<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}
</Text>
</Tooltip>
<Title
order={2}
ta="center"
@@ -122,9 +135,11 @@ export default function SDGS() {
>
{item.jumlah}
</Title>
</Paper>
))}
</SimpleGrid>
</Stack>
</Paper>
))}
</SimpleGrid>
) : (
<Center mih={200} style={{ flexDirection: "column" }}>
<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 ApiFetch from "@/lib/api-fetch";
import { Stack } from "@mantine/core";
import { Box, Stack } from "@mantine/core";
import Apbdes from "./_com/main-page/apbdes";
import Prestasi from "./_com/main-page/prestasi";
import ScrollToTopButton from "./_com/scrollToTopButton";
export default function Page() {
return (
<Stack bg={colors.grey[1]} gap={"4rem"}>
<LandingPage />
<Penghargaan />
<Layanan />
<Potensi />
<DesaAntiKorupsi />
<Kepuasan />
<SDGS />
<Apbdes />
<Prestasi/>
</Stack>
<Box>
<Stack bg={colors.grey[1]} gap={"4rem"}>
<LandingPage />
<Penghargaan />
<Layanan />
<Potensi />
<DesaAntiKorupsi />
<Kepuasan />
<SDGS />
<Apbdes />
<Prestasi />
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
);
}

View File

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

View File

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