Penambahan fungsi search disetiap menu & submenu,

Menu Landing Page
Menu PPID
Menu Desa
This commit is contained in:
2025-10-15 10:13:02 +08:00
parent 3c21f7742c
commit ccf39bc778
13 changed files with 774 additions and 34 deletions

View File

@@ -143,7 +143,7 @@ model MediaSosial {
isActive Boolean @default(true)
}
//========================================= PROFILE ========================================= //
//========================================= DESA ANTI KORUPSI ========================================= //
model DesaAntiKorupsi {
id String @id @default(cuid())
name String @unique

View File

@@ -89,26 +89,26 @@ function ListArtikelKesehatan({ search }: { search: string }) {
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Konten</TableTh>
<TableTh>Aksi</TableTh>
<TableTh style={{ minWidth: 200 }}>Judul</TableTh>
<TableTh style={{ minWidth: 200 }}>Konten</TableTh>
<TableTh style={{ minWidth: 200 }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.title}
</Text>
</TableTd>
<TableTd>
<TableTd style={{ minWidth: 200 }} >
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.content}
</Text>
</TableTd>
<TableTd>
<TableTd style={{ minWidth: 200 }}>
<Button
variant="light"
color="blue"

View File

@@ -223,7 +223,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{/* Chart */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper withBorder bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 ? (
<Center>

View File

@@ -111,9 +111,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
</TableTd>
<TableTd>
<Box w={200}>
<Text truncate fz="sm" c="dimmed">
{item.deskripsiSingkat}
</Text>
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</Box>
</TableTd>
<TableTd>

View File

@@ -0,0 +1,538 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export default async function searchFindMany(context: Context) {
const { query, page = 1, limit = 10, type } = context.query as any;
// Convert to numbers
const pageNum = parseInt(String(page), 10) || 1;
const limitNum = parseInt(String(limit), 10) || 10;
const skip = (pageNum - 1) * limitNum;
if (!query || query.trim() === "") {
return { data: [], nextPage: null };
}
// 🔍 kalau type dikirim → cari spesifik modul
//========================================= MENU LANDING PAGE ========================================= //
//========================================= PROFILE ========================================= //
if (type === "pejabatdesa") {
const data = await prisma.pejabatDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return {
data: data.map((b) => ({ type: "pejabatdesa", ...b })),
nextPage: data.length < limitNum ? null : pageNum + 1,
};
}
if (type === "programinovasi") {
const data = await prisma.programInovasi.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "mediasosial") {
const data = await prisma.mediaSosial.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//========================================= DESA ANTI KORUPSI ========================================= //
if (type === "desaantikorupsi") {
const data = await prisma.desaAntiKorupsi.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//========================================= SDGS Desa ========================================= //
if (type === "sdgsdesa") {
const data = await prisma.sdgsDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//========================================= APBDes ========================================= //
if (type === "apbdes") {
const data = await prisma.aPBDes.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//========================================= PRESTASI DESA ========================================= //
if (type === "prestasidesa") {
const data = await prisma.prestasiDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//========================================= INDEKS KEPUASAAN MASYARAKAT ========================================= //
if (type === "responden") {
const data = await prisma.responden.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//========================================= MENU PPID ========================================= //
//========================================= STRUKTUR PPID ========================================= //
if (type === "strukturppid") {
const data = await prisma.strukturPPID.findMany({
where: {
PegawaiPPID: { namaLengkap: { contains: query, mode: "insensitive" } },
},
include: {
PosisiOrganisasiPPID: true,
PegawaiPPID: true,
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= VISI MISI PPID ========================================= //
if (type === "visimisippid") {
const data = await prisma.visiMisiPPID.findMany({
where: {
visi: { contains: query, mode: "insensitive" },
misi: { contains: query, mode: "insensitive" },
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//
// ========================================= DASAR HUKUM PPID ========================================= //
if (type === "dasarhukumppid") {
const data = await prisma.dasarHukumPPID.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= PROFILE PPID ========================================= //
if (type === "profileppid") {
const data = await prisma.profilePPID.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= DAFTAR INFORMASI PUBLIK ========================================= //
if (type === "daftarinformasipublik") {
const data = await prisma.daftarInformasiPublik.findMany({
where: { jenisInformasi: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//=========================================PERMOHONAN INFORMASI PUBLIK========================= //
if (type === "permohonaninformasipublik") {
const data = await prisma.permohonanInformasiPublik.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
//=========================================PERMOHONAN INFORMASI KEBERATAN PUBLIK========================= //
if (type === "permohonaninformasikeberatanpublik") {
const data = await prisma.formulirPermohonanKeberatan.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= IKM ========================================= //
if (type === "ikm") {
const data = await prisma.indeksKepuasanMasyarakat.findMany({
where: { label: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= MENU DESA ========================================= //
// ========================================= PROFILE DESA ========================================= //
if (type === "sejarahdesa") {
const data = await prisma.sejarahDesa.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "visimisidesa") {
const data = await prisma.visiMisiDesa.findMany({
where: { visi: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "lambangdesa") {
const data = await prisma.lambangDesa.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "maskotdesa") {
const data = await prisma.maskotDesa.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "profilperbekel") {
const data = await prisma.profilPerbekel.findMany({
where: { biodata: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "perbekeldarmasaba") {
const data = await prisma.perbekelDariMasaKeMasa.findMany({
where: { nama: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= BERITA ========================================= //
if (type === "berita") {
const data = await prisma.berita.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "kategoriBerita") {
const data = await prisma.kategoriBerita.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= POTENSI DESA ========================================= //
if (type === "potensi") {
const data = await prisma.potensiDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= PENGUMUMAN ========================================= //
if (type === "pengumuman") {
const data = await prisma.pengumuman.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= GALLERY ========================================= //
if (type === "galleryFoto") {
const data = await prisma.galleryFoto.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "galleryVideo") {
const data = await prisma.galleryVideo.findMany({
where: { name: { contains: query, mode: "insensitive" } },
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// 🌍 GLOBAL SEARCH — cari di beberapa modul sekaligus
const [
pejabatdesa,
programinovasi,
mediasosial,
desaantikorupsi,
sdgsdesa,
apbdes,
prestasidesa,
responden,
strukturppid,
visimisippid,
dasarhukumppid,
profileppid,
daftarinformasipublik,
permohonaninformasipublik,
permohonaninformasikeberatanpublik,
ikm,
sejarahdesa,
visimisidesa,
lambangdesa,
maskotdesa,
profilperbekel,
perbekeldarmasaba,
berita,
kategoriBerita,
potensi,
pengumuman,
galleryFoto,
galleryVideo,
pelayananSuratKeterangan,
pelayananPerizinanBerusaha,
pelayananTelunjukSaktiDesa,
pelayananPendudukNonPermanent,
penghargaan
] = await Promise.all([
prisma.pejabatDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.programInovasi.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.mediaSosial.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.desaAntiKorupsi.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.sdgsDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.aPBDes.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.prestasiDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.responden.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
// ✅ FIXED
prisma.strukturPPID.findMany({
where: {
PegawaiPPID: { namaLengkap: { contains: query, mode: "insensitive" } },
},
include: { PegawaiPPID: true },
take: limitNum,
}),
prisma.visiMisiPPID.findMany({
where: {
OR: [
{ visi: { contains: query, mode: "insensitive" } },
{ misi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.dasarHukumPPID.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.profilePPID.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.daftarInformasiPublik.findMany({
where: { jenisInformasi: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.permohonanInformasiPublik.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.formulirPermohonanKeberatan.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.indeksKepuasanMasyarakat.findMany({
where: { label: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.sejarahDesa.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.visiMisiDesa.findMany({
where: {
OR: [
{ visi: { contains: query, mode: "insensitive" } },
{ misi: { contains: query, mode: "insensitive" } },
],
},
take: limitNum,
}),
prisma.lambangDesa.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.maskotDesa.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.profilPerbekel.findMany({
where: { biodata: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.perbekelDariMasaKeMasa.findMany({
where: { nama: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.berita.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.kategoriBerita.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.potensiDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.pengumuman.findMany({
where: { judul: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.galleryFoto.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.galleryVideo.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.pelayananSuratKeterangan.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.pelayananPerizinanBerusaha.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.pelayananTelunjukSaktiDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.pelayananPendudukNonPermanen.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
prisma.penghargaan.findMany({
where: { name: { contains: query, mode: "insensitive" } },
take: limitNum,
}),
]);
return {
data: [
...pejabatdesa.map((b) => ({ type: "pejabatdesa", ...b })),
...programinovasi.map((b) => ({ type: "programinovasi", ...b })),
...mediasosial.map((b) => ({ type: "mediaSosial", ...b })),
...desaantikorupsi.map((b) => ({ type: "desaantikorupsi", ...b })),
...sdgsdesa.map((b) => ({ type: "sdgsdesa", ...b })),
...apbdes.map((b) => ({ type: "apbdes", ...b })),
...prestasidesa.map((b) => ({ type: "prestasidesa", ...b })),
...responden.map((b) => ({ type: "responden", ...b })),
...strukturppid.map((b) => ({ type: "strukturppid", ...b })),
...visimisippid.map((b) => ({ type: "visimisippid", ...b })),
...dasarhukumppid.map((b) => ({ type: "dasarhukumppid", ...b })),
...profileppid.map((b) => ({ type: "profileppid", ...b })),
...daftarinformasipublik.map((b) => ({
type: "daftarinformasipublik",
...b,
})),
...permohonaninformasipublik.map((b) => ({
type: "permohonaninformasipublik",
...b,
})),
...permohonaninformasikeberatanpublik.map((b) => ({
type: "permohonaninformasikeberatanpublik",
...b,
})),
...ikm.map((b) => ({ type: "ikm", ...b })),
...sejarahdesa.map((b) => ({ type: "sejarahdesa", ...b })),
...visimisidesa.map((b) => ({ type: "visimisidesa", ...b })),
...lambangdesa.map((b) => ({ type: "lambangdesa", ...b })),
...maskotdesa.map((b) => ({ type: "maskotdesa", ...b })),
...profilperbekel.map((b) => ({ type: "profilperbekel", ...b })),
...perbekeldarmasaba.map((b) => ({ type: "perbekeldarmasaba", ...b })),
...berita.map((b) => ({ type: "berita", ...b })),
...kategoriBerita.map((b) => ({ type: "kategoriBerita", ...b })),
...potensi.map((b) => ({ type: "potensi", ...b })),
...pengumuman.map((b) => ({ type: "pengumuman", ...b })),
...galleryFoto.map((b) => ({ type: "galleryFoto", ...b })),
...galleryVideo.map((b) => ({ type: "galleryVideo", ...b })),
...pelayananSuratKeterangan.map((b) => ({ type: "pelayananSuratKeterangan", ...b })),
...pelayananPerizinanBerusaha.map((b) => ({ type: "pelayananPerizinanBerusaha", ...b })),
...pelayananTelunjukSaktiDesa.map((b) => ({ type: "pelayananTelunjukSaktiDesa", ...b })),
...pelayananPendudukNonPermanent.map((b) => ({ type: "pelayananPendudukNonPermanent", ...b })),
...penghargaan.map((b) => ({ type: "penghargaan", ...b })),
],
nextPage: null, // bisa dibuat lebih kompleks kalau perlu
};
}

View File

@@ -0,0 +1,10 @@
import Elysia from "elysia";
import searchFindMany from "./findMany";
const Search = new Elysia({
prefix: "/api/search",
tags: ["Search"],
})
.get("/findMany", searchFindMany);
export default Search;

View File

@@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { proxy, subscribe } from 'valtio';
import { debounce } from 'lodash';
import ApiFetch from '@/lib/api-fetch';
interface SearchResult {
type?: string; // optional biar gak error
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,
async fetch() {
if (!searchState.query) 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 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;
}
},
async next() {
if (!searchState.nextPage || searchState.loading) return;
searchState.page = searchState.nextPage;
await searchState.fetch();
},
});
// 🔁 Auto debounce search trigger
const debouncedFetch = debounce(() => {
if (!searchState.query) return;
searchState.page = 1;
searchState.fetch();
}, 500);
subscribe(searchState, () => {
debouncedFetch();
});
export default searchState;

View File

@@ -25,6 +25,7 @@ import LandingPage from "./_lib/landing_page";
import Pendidikan from "./_lib/pendidikan";
import User from "./_lib/user";
import Role from "./_lib/user/role";
import Search from "./_lib/search";
const ROOT = process.cwd();
@@ -95,6 +96,7 @@ const ApiServer = new Elysia()
.use(Pendidikan)
.use(User)
.use(Role)
.use(Search)
.onError(({ code }) => {
if (code === "NOT_FOUND") {

View File

@@ -121,13 +121,13 @@ function Page() {
</Badge>
</Group>
<Text fz="sm" c="dimmed">
Diposting: 12 Februari 2025 · Dinas Kesehatan
Diposting: {v.createdAt.toLocaleDateString()}
</Text>
<Divider />
<Text fz="sm" lh={1.5}>
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
{v.deskripsiSingkat}
</Text>
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${v.id}`)}>
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
Selengkapnya
</Button>
</Stack>

View File

@@ -97,18 +97,23 @@ function Page() {
shadow="sm"
withBorder
bg={colors['white-trans-1']}
style={{ transition: 'all 0.3s ease' }}
style={{
transition: 'all 0.3s ease',
transform: 'translateY(0)',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-5px)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
>
<Stack align="center" gap="md">
<Center>
<Image
src={v.image.link}
alt={v.name}
w={160}
h={160}
fit="contain"
h={180}
w="100%"
radius="md"
loading="lazy"
fit="cover"
style={{ aspectRatio: '4/3' }}
/>
</Center>
<Stack gap={4} w="100%">
@@ -151,8 +156,11 @@ function Page() {
styles={{
control: {
border: `1px solid ${colors['blue-button']}`,
transition: 'all 0.3s ease',
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
},
}}
/>
</Center>

View File

@@ -28,11 +28,31 @@ export default function Page() {
if (data.length === 0) {
return (
<Box py="xl" px={{ base: "md", md: 100 }}>
<Text fz="lg" fw="bold" c={colors["blue-button"]}>
Tidak ada posyandu yang ditemukan
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Box px={{ base: "md", md: 100 }}>
<BackButton />
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
<Text
ta="left"
fz={{ base: "1.8rem", md: "2.5rem" }}
c={colors["blue-button"]}
fw="bold"
>
Posyandu Desa Darmasaba
</Text>
<TextInput
placeholder="Cari posyandu berdasarkan nama..."
aria-label="Pencarian Posyandu"
radius="xl"
size="md"
leftSection={<IconSearch size={20} />}
w={{ base: "100%", md: "35%" }}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Flex>
</Box>
</Stack>
);
}

View File

@@ -1,6 +1,6 @@
import stateNav from "@/state/state-nav";
import { Container, Stack, TextInput, Tooltip } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { Container, Stack, Tooltip } from "@mantine/core";
import GlobalSearch from "./globalSearch";
export function NavbarSearch() {
return (
@@ -12,14 +12,7 @@ export function NavbarSearch() {
>
<Stack pt="xl">
<Tooltip label="Type to search across the site" position="bottom-start" withArrow>
<TextInput
autoFocus
size="lg"
variant="filled"
radius="xl"
placeholder="Search anything..."
leftSection={<IconSearch size={20} />}
/>
<GlobalSearch />
</Tooltip>
</Stack>
</Container>

View File

@@ -0,0 +1,97 @@
'use client';
import { TextInput, Loader, Stack, Box, Text } from '@mantine/core';
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';
};
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const router = useRouter();
// Infinite scroll listener
useEffect(() => {
const handleScroll = () => {
const bottom =
window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [snap.loading]);
return (
<Stack>
<TextInput
placeholder="Cari apapun..."
value={snap.query}
onChange={(e) => (searchState.query = e.currentTarget.value)}
/>
{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>
</Box>
))}
{snap.loading && <Loader size="sm" />}
</Stack>
);
}