Mengerjakan QC Kak Inno & Kak Ayu Tanggal 16 Oktober

Fix Search
This commit is contained in:
2025-10-17 17:45:56 +08:00
parent 75bf0652b1
commit bbf13c1cf7
19 changed files with 731 additions and 527 deletions

View File

@@ -1,6 +1,63 @@
'use client'
import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTiktok, IconBrandYoutube } from '@tabler/icons-react';
const sosialMedia = [
{
title: "Facebook",
link: "https://www.facebook.com/DarmasabaDesaku",
icon: IconBrandFacebook,
},
{
title: "Instagram",
link: "https://www.instagram.com/ddarmasaba/",
icon: IconBrandInstagram,
},
{
title: "Youtube",
link: "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
icon: IconBrandYoutube,
},
{
title: "Tiktok",
link: "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
icon: IconBrandTiktok,
},
]
const layanandesa = [
{
title: "Administrasi Kependudukan",
link: "/darmasaba/desa/layanan/",
},
{
title: "Layanan Sosial",
link: "/darmasaba/ekonomi/program-kemiskinan",
},
{
title: "Pengaduan Masyarakat",
link: "/darmasaba/keamanan/laporan-publik",
},
{
title: "Informasi Publik",
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
},
]
const tautanPenting = [
{
title: "Portal Badung",
link: "/darmasaba/desa/berita/semua",
},
{
title: "E-Government",
link: "/darmasaba/inovasi/desa-digital-smart-village",
},
{
title: "Transparansi",
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
}
]
function Footer() {
return (
@@ -46,7 +103,7 @@ function Footer() {
<Group justify="apart" align="center" mt="lg">
<Text c="#F3F2EC" ta="center" fz="md" fw={700} style={{ fontStyle: 'italic' }}>&quot;Desa Kuat, Warga Sejahtera!&quot;</Text>
<ActionIcon size={80} radius="xl" variant="transparent">
<Image src="/chatbot-removebg-preview.png" alt="Logo Desa" width={80} height={80} loading="lazy"/>
<Image src="/chatbot-removebg-preview.png" alt="Logo Desa" width={80} height={80} loading="lazy" />
</ActionIcon>
</Group>
</Stack>
@@ -64,31 +121,39 @@ function Footer() {
Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
</Text>
<Flex gap="md" mt="sm" c="#F3F2EC">
<ActionIcon variant="subtle" color="white"><IconBrandFacebook size={22} /></ActionIcon>
<ActionIcon variant="subtle" color="white"><IconBrandInstagram size={22} /></ActionIcon>
<ActionIcon variant="subtle" color="white"><IconBrandTwitter size={22} /></ActionIcon>
<ActionIcon variant="subtle" color="white"><IconBrandWhatsapp size={22} /></ActionIcon>
{sosialMedia.map((item) => (
<ActionIcon
key={item.title}
component="a"
href={item.link}
target="_blank"
rel="noopener noreferrer"
variant="subtle"
color="white"
>
<item.icon size={22} />
</ActionIcon>
))}
</Flex>
</Stack>
</Box>
<Box>
<Stack gap="xs">
<Text c="white" fz="md" fw={700}>Layanan Desa</Text>
<Anchor c="#F3F2EC" fz="xs">Administrasi Kependudukan</Anchor>
<Anchor c="#F3F2EC" fz="xs">Layanan Sosial</Anchor>
<Anchor c="#F3F2EC" fz="xs">Pengaduan Masyarakat</Anchor>
<Anchor c="#F3F2EC" fz="xs">Informasi Publik</Anchor>
{layanandesa.map((item) => (
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
))}
</Stack>
</Box>
<Box>
<Stack gap="xs">
<Text c="white" fz="md" fw={700}>Tautan Penting</Text>
<Anchor c="#F3F2EC" fz="xs">Portal Badung</Anchor>
<Anchor c="#F3F2EC" fz="xs">E-Government</Anchor>
<Anchor c="#F3F2EC" fz="xs">Transparansi</Anchor>
<Anchor c="#F3F2EC" fz="xs">Unduhan</Anchor>
{tautanPenting.map((item) => (
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
))}
</Stack>
</Box>

View File

@@ -11,16 +11,20 @@ export function NavbarSearch() {
// Close when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
const target = event.target as HTMLElement;
// Only close if clicking outside both the search input and results
if (
containerRef.current &&
!containerRef.current.contains(target) &&
!target.closest('.search-result-item') // Add a class to your search result items
) {
setIsOpen(false);
stateNav.clear();
}
}
// Add event listener
document.addEventListener('mousedown', handleClickOutside);
return () => {
// Clean up
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Modal, Text, TextInput } from '@mantine/core';
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
@@ -8,14 +9,14 @@ import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const [isOpen, setIsOpen] = useState(false);
const [opened, setOpened] = useState(false);
// Toggle modal when there's a query
// buka popover saat ada query
useEffect(() => {
setIsOpen(!!snap.query);
setOpened(!!snap.query);
}, [snap.query]);
// Infinite scroll
// infinite scroll
useEffect(() => {
const handleScroll = () => {
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
@@ -25,88 +26,119 @@ export default function GlobalSearch() {
return () => window.removeEventListener('scroll', handleScroll);
}, [snap.loading]);
return (
<Box style={{ position: 'relative', width: '100%' }}>
<TextInput
placeholder="Cari apapun..."
value={snap.query}
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
}
/>
const handleSelect = async (e: React.MouseEvent, item: any) => {
e.stopPropagation();
e.preventDefault();
{/* Modal for search results */}
<Modal
opened={isOpen && !!snap.query}
onClose={() => {
searchState.query = '';
searchState.results = [];
const url = getDetailUrl(item);
if (!url) return;
// Immediately close the search dropdown
setOpened(false);
searchState.results = []; // Clear results immediately
searchState.loading = false;
// Use window.location for navigation to ensure full page reload
window.location.href = url;
};
return (
<Box pos="relative">
<Popover
opened={opened && !!snap.query}
onChange={(isOpen) => {
if (!isOpen) {
// Clear search state when popover is closed
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
}
setOpened(isOpen);
}}
withCloseButton={false}
size="lg"
padding={0}
width="target"
position="bottom"
shadow="md"
withinPortal
radius="md"
style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 1000 }}
zIndex={1000} // Add this line to ensure it appears above other elements
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',
dropdown: {
zIndex: 1000, // Add this to ensure the dropdown appears above other elements
},
}}
>
<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 && (
<Popover.Target>
<TextInput
placeholder="Cari apapun..."
value={snap.query}
onChange={(e) => {
searchState.query = e.currentTarget.value;
debouncedFetch();
}}
radius="xl"
size="md"
rightSection={
snap.query ? (
<IconX
size={16}
style={{ cursor: 'pointer' }}
onClick={() => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
}}
/>
) : undefined
}
/>
</Popover.Target>
<Popover.Dropdown
p={0}
style={{
maxHeight: 350,
overflowY: 'auto',
borderRadius: 12,
zIndex: 1000, // Add this line to ensure dropdown stays above other elements
position: 'relative', // Add this to contain child elements
}}
>
{snap.results.length > 0 ? (
snap.results.map((item, i) => (
<Box
key={i}
p="sm"
className="search-result-item" // Add this class
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
transition: 'background 0.2s',
position: 'relative', // Add this
zIndex: 1, // Add this to ensure proper stacking context
backgroundColor: 'white', // Ensure background is set
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f7f7f7')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={(e) => handleSelect(e, item)} // Pass the event here
>
<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>
))
) : (
<Center py="md">
<Loader size="sm" />
{snap.loading ? <Loader size="sm" /> : <Text fz="sm">Tidak ada hasil</Text>}
</Center>
)}
</Box>
</Modal>
</Popover.Dropdown>
</Popover>
</Box>
);
}
}

View File

@@ -42,7 +42,6 @@ export default function ProfileView({ data }: ProfileViewProps) {
loading="lazy"
style={{
objectPosition: 'bottom center',
transform: 'translateY(10px)', // sedikit turun biar natural
}}
/>
) : (

View File

@@ -62,26 +62,26 @@ const getDetailUrl = (item: { type?: string; id: string | number; [key: string]:
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',
tujuanEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
materiEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
contohEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
filosofiTriHita: '/darmasaba/lingkungan/konservasi-adat-bali',
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
nilaiKonservasiAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
jenjangPendidikan: '/darmasaba/pendidikan/info-sekolah/semua',
lembaga: '/darmasaba/pendidikan/info-sekolah/semua/lembaga',
siswa: '/darmasaba/pendidikan/info-sekolah/semua/siswa',
pengajar: '/darmasaba/pendidikan/info-sekolah/semua/pengajar',
keunggulanProgram: '/darmasaba/pendidikan/beasiswa-desa',
tujuanProgram: '/darmasaba/pendidikan/program-pendidikan-anak',
programUnggulan: '/darmasaba/pendidikan/program-pendidikan-anak',
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
tujuanPendidikanNonFormal: '/darmasaba/pendidikan/pendidikan-non-formal',
tempatKegiatan: '/darmasaba/pendidikan/pendidikan-non-formal',
jenisProgramYangDiselenggarakan: '/darmasaba/pendidikan/pendidikan-non-formal',
dataPerpustakaan: '/darmasaba/pendidikan/perpustakaan-digital/semua',
dataPendidikan: '/darmasaba/pendidikan/data-pendidikan',
};