diff --git a/src/components/map/property-map-picker-inner.tsx b/src/components/map/property-map-picker-inner.tsx index e39dc13..88886c4 100644 --- a/src/components/map/property-map-picker-inner.tsx +++ b/src/components/map/property-map-picker-inner.tsx @@ -1,16 +1,22 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useCallback } from "react"; import maplibregl from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; import { Search, Loader2, MapPin, X } from "lucide-react"; import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; const STYLE_URL = "https://tiles.openfreemap.org/styles/bright"; const TURKEY_CENTER: [number, number] = [35.0, 39.0]; const MARKER_COLOR = "#2563eb"; +interface NominatimResult { + place_id: number; + display_name: string; + lat: string; + lon: string; +} + interface Props { initialLat?: number | null; initialLng?: number | null; @@ -30,17 +36,20 @@ export function PropertyMapPickerInner({ const mapRef = useRef(null); const markerRef = useRef(null); const placeRef = useRef<(lat: number, lng: number) => void>(() => {}); + const debounceRef = useRef | null>(null); + const dropdownRef = useRef(null); const [search, setSearch] = useState(initialSearchQuery); - const [geocoding, setGeocoding] = useState(false); - const [notFound, setNotFound] = useState(false); + const [suggestions, setSuggestions] = useState([]); + const [loading, setLoading] = useState(false); + const [showDropdown, setShowDropdown] = useState(false); const [coords, setCoords] = useState<{ lat: number; lng: number } | null>( initialLat != null && initialLng != null ? { lat: initialLat, lng: initialLng } : null, ); - // Keep placeRef current so closures in event handlers always have latest state + // Keep placeRef current so closures in map event handlers see latest callbacks placeRef.current = (lat: number, lng: number) => { setCoords({ lat, lng }); onLocationChange(lat, lng); @@ -61,6 +70,7 @@ export function PropertyMapPickerInner({ } }; + // Initialize map useEffect(() => { if (!containerRef.current) return; @@ -106,75 +116,127 @@ export function PropertyMapPickerInner({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - async function handleSearch(e?: React.FormEvent) { - e?.preventDefault(); - if (!search.trim()) return; - setGeocoding(true); - setNotFound(false); + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setShowDropdown(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + // Debounced Nominatim fetch + const fetchSuggestions = useCallback(async (query: string) => { + if (!query.trim() || query.trim().length < 3) { + setSuggestions([]); + setShowDropdown(false); + return; + } + + setLoading(true); try { - const q = search.includes("Türkiye") ? search : `${search}, Türkiye`; + const q = query.includes("Türkiye") ? query : `${query}, Türkiye`; const res = await fetch( - `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=1`, + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=6&addressdetails=0`, { headers: { "Accept-Language": "tr,en" } }, ); - const data = (await res.json()) as { lat: string; lon: string }[]; - - if (!data.length) { - setNotFound(true); - return; - } - - const lat = parseFloat(data[0].lat); - const lng = parseFloat(data[0].lon); - - placeRef.current(lat, lng); - mapRef.current?.flyTo({ center: [lng, lat], zoom: 15, duration: 1000 }); + const data = (await res.json()) as NominatimResult[]; + setSuggestions(data); + setShowDropdown(data.length > 0); } catch { - setNotFound(true); + setSuggestions([]); + setShowDropdown(false); } finally { - setGeocoding(false); + setLoading(false); } + }, []); + + function handleInputChange(value: string) { + setSearch(value); + + if (debounceRef.current) clearTimeout(debounceRef.current); + + if (value.trim().length < 3) { + setSuggestions([]); + setShowDropdown(false); + setLoading(false); + return; + } + + setLoading(true); + debounceRef.current = setTimeout(() => { + void fetchSuggestions(value); + }, 400); + } + + function handleSelect(result: NominatimResult) { + const lat = parseFloat(result.lat); + const lng = parseFloat(result.lon); + + // Shorten display_name for the search box (first two parts) + const short = result.display_name.split(",").slice(0, 2).join(",").trim(); + setSearch(short); + setSuggestions([]); + setShowDropdown(false); + + placeRef.current(lat, lng); + mapRef.current?.flyTo({ center: [lng, lat], zoom: 15, duration: 900 }); } function handleClear() { markerRef.current?.remove(); markerRef.current = null; setCoords(null); + setSearch(""); + setSuggestions([]); + setShowDropdown(false); onClear(); mapRef.current?.flyTo({ center: TURKEY_CENTER, zoom: 5.5 }); } return (
- {/* Search bar */} -
-
+ {/* Search input with autocomplete dropdown */} +
+
{ - setSearch(e.target.value); - setNotFound(false); - }} + autoComplete="off" + onChange={(e) => handleInputChange(e.target.value)} + onFocus={() => suggestions.length > 0 && setShowDropdown(true)} /> -
- - +
- {notFound && ( -

- Adres bulunamadı — farklı bir arama deneyin. -

- )} + {showDropdown && suggestions.length > 0 && ( +
+
    + {suggestions.map((s) => ( +
  • + +
  • + ))} +
+
+ )} +
{/* Map */}