Mengerjakan QC Kak Inno & Kak Ayu Tanggal 16 Oktober
Fix Search
This commit is contained in:
@@ -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' }}>"Desa Kuat, Warga Sejahtera!"</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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ export default function ProfileView({ data }: ProfileViewProps) {
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectPosition: 'bottom center',
|
||||
transform: 'translateY(10px)', // sedikit turun biar natural
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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',
|
||||
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user