Files
desa-darmasaba/src/app/darmasaba/_com/globalSearch.tsx

184 lines
5.4 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
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';
import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const [opened, setOpened] = useState(false);
const [isNavigating, setIsNavigating] = useState(false);
// Buka popover saat ada query
useEffect(() => {
setOpened(!!snap.query);
}, [snap.query]);
// Infinite scroll handler
useEffect(() => {
const handleScroll = () => {
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (nearBottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [snap.loading]);
const handleSelect = async (e: React.MouseEvent, item: any) => {
e.preventDefault();
e.stopPropagation();
if (isNavigating) return;
setIsNavigating(true);
try {
// 🔥 pastikan objek udah “dikeluarkan” dari Proxy valtio
const rawItem = JSON.parse(JSON.stringify(item));
// 🔥 pastikan type-nya string murni
const type = String(rawItem.type || '').trim().toLowerCase();
// 🔥 panggil getDetailUrl pakai type yang fix
let url = getDetailUrl({ ...rawItem, type });
// kalau hasil undefined atau default, fallback ke link eksternal
if (!url || url === '/darmasaba') {
if (rawItem.link && rawItem.link.startsWith('http')) {
url = rawItem.link;
}
}
if (!url) {
console.warn('URL tidak ditemukan untuk item:', rawItem);
setIsNavigating(false);
return;
}
console.log('Navigating to:', url);
// tutup popover dulu
setOpened(false);
searchState.query = '';
searchState.results = [];
searchState.loading = false;
// kasih delay biar UI nutup dulu
await new Promise((r) => setTimeout(r, 100));
// navigasi
if (url.startsWith('http')) {
window.location.href = url;
} else {
window.location.href = url;
}
} catch (err) {
console.error('Error saat navigasi:', err);
setIsNavigating(false);
}
};
const clearSearch = () => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
setIsNavigating(false);
};
return (
<Box pos="relative">
<Popover
opened={opened && !!snap.query}
onChange={(isOpen) => {
if (!isOpen) clearSearch();
setOpened(isOpen);
}}
width="target"
position="bottom"
shadow="md"
withinPortal
radius="md"
zIndex={2000}
closeOnClickOutside={true}
closeOnEscape={true}
styles={{
dropdown: {
zIndex: 2000,
borderRadius: 12,
overflow: 'hidden',
},
}}
>
<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={clearSearch}
/>
) : undefined
}
/>
</Popover.Target>
<Popover.Dropdown
p={0}
style={{
maxHeight: 350,
overflowY: 'auto',
backgroundColor: '#fff',
border: '1px solid #eee',
}}
>
{[...snap.results].length > 0 ? (
[...snap.results].map((item: any, i: number) => (
<Box
key={i}
p="sm"
className="search-result-item" // Add class untuk prevent close
style={{
borderBottom: '1px solid #f1f1f1',
cursor: isNavigating ? 'wait' : 'pointer',
background: 'white',
transition: 'background 0.2s',
opacity: isNavigating ? 0.6 : 1,
}}
onMouseEnter={(e) => !isNavigating && (e.currentTarget.style.background = '#f9f9f9')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'white')}
onClick={(e) => handleSelect(e, item)}
>
<Text size="sm" fw={500} lineClamp={1}>
{item.name ?? item.nama ?? item.namaPasar ?? item.judul ?? '(Tanpa nama)'}
</Text>
<Text size="xs" c="dimmed" lineClamp={1}>
dari modul: {item.type || '-'}
</Text>
</Box>
))
) : (
<Center py="md">
{snap.loading ? <Loader size="sm" /> : <Text fz="sm">Tidak ada hasil</Text>}
</Center>
)}
</Popover.Dropdown>
</Popover>
</Box>
);
}