import React, { useEffect, useMemo, useRef, useState } from "react"; import { recoSearch } from "../../services/recoSearch"; import "../../styles/smartSearchBar.css"; function defaultZoomForKind(kind) { const k = (kind || "").toLowerCase(); // Specific types from suggestions endpoint if (k === "post") return 16; // Close zoom for posts if (k === "profile") return 14; // Medium zoom for profiles if (k === "city") return 11; // City-level zoom if (k === "region") return 5; // Wide zoom for regions/provinces if (k === "place" || k === "poi") return 13; // Places zoom // Legacy/fallback if (k.includes("post")) return 15; if (k.includes("city")) return 11; if (k.includes("region") || k.includes("area") || k.includes("province") || k.includes("state")) return 5; if (k.includes("place")) return 13; return 12; // Default } function IconPin(props) { return ( ); } function IconPlus(props) { return ( ); } function truncateText(value, maxLen) { const s = (value || "").toString(); if (!maxLen || s.length <= maxLen) return s; return s.slice(0, Math.max(0, maxLen - 1)) + "…"; } /** * Props: * - placeholder * - onPick(item, { zoom, reason }) // call when user selects suggestion * - onSearch(query) // call when user presses Enter to filter/search * - onMySpot() // keep same function (center on user) * - onPlaceTourWire() // keep same function (place/tour/wire) */ export default function SmartSearchBar({ placeholder = "Search places, regions, posts…", onPick, onSearch, onMySpot, onPlaceTourWire, }) { const [q, setQ] = useState(""); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [active, setActive] = useState(-1); const [err, setErr] = useState(""); const [hasSearched, setHasSearched] = useState(false); const abortRef = useRef(null); const skipSearchRef = useRef(false); const boxRef = useRef(null); useEffect(() => { function onDocDown(e) { if (!boxRef.current) return; if (!boxRef.current.contains(e.target)) setOpen(false); } document.addEventListener("mousedown", onDocDown); return () => document.removeEventListener("mousedown", onDocDown); }, []); // Fonction pour déclencher une recherche const triggerSearch = async (searchQuery) => { if (abortRef.current) abortRef.current.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setErr(""); try { console.log('[SmartSearchBar] Searching for:', searchQuery); const res = await recoSearch(searchQuery, { signal: ctrl.signal, limit: 12 }); console.log('[SmartSearchBar] Got results:', res?.length || 0, res); setItems(res); setOpen(true); setActive(res.length ? 0 : -1); setHasSearched(true); } catch (e) { if (ctrl.signal.aborted) return; console.error('[SmartSearchBar] Search error:', e); setErr((e && e.message) ? e.message : "Search error"); setItems([]); } finally { if (!ctrl.signal.aborted) setLoading(false); } }; useEffect(() => { const query = q.trim(); if (skipSearchRef.current) { skipSearchRef.current = false; return; } if (abortRef.current) abortRef.current.abort(); const t = setTimeout(() => { triggerSearch(query); }, 150); // Debounce 150ms return () => { clearTimeout(t); if (abortRef.current) abortRef.current.abort(); }; }, [q]); const showList = open && (items.length > 0 || loading || err); const pick = (it, reason = "click") => { if (!it) return; const zoom = defaultZoomForKind(it.kind); skipSearchRef.current = true; setOpen(false); setQ(it.title || q); onPick && onPick(it, { zoom, reason }); }; const onKeyDown = (e) => { if (!showList && (e.key === "ArrowDown" || e.key === "Enter")) setOpen(true); if (e.key === "ArrowDown") { e.preventDefault(); setActive((i) => Math.min(i + 1, items.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); if (items.length && active >= 0) { // Pick the selected suggestion pick(items[active], "enter"); } else if (q.trim()) { // No item selected, trigger search/filter skipSearchRef.current = true; setOpen(false); onSearch && onSearch(q.trim()); } } else if (e.key === "Escape") { setOpen(false); } }; const rightTitle = useMemo(() => { // visuel flexible, même fonction: tu peux renommer plus tard return "Place / Tour / Wire"; }, []); const handleClear = () => { setQ(""); setItems([]); setOpen(false); onSearch && onSearch(""); // Clear filters }; return (
{ const newValue = e.target.value; setQ(newValue); // Si on efface tout le texte, clear les filtres if (!newValue.trim() && q.trim()) { onSearch && onSearch(""); } }} onFocus={() => { setOpen(true); // Si pas encore cherché et query vide, charger suggestions populaires if (!hasSearched && !q.trim()) { triggerSearch(""); } }} onKeyDown={onKeyDown} placeholder={placeholder} aria-label="Search" /> {q && ( )}
{showList && (
{loading &&
Searching…
} {err &&
{err}
} {!loading && !err && items.map((it, idx) => ( ))} {!loading && !err && items.length === 0 && (
No results
)}
)}
); }