'use client'; import mapboxgl from 'mapbox-gl'; import * as React from 'react'; import 'mapbox-gl/dist/mapbox-gl.css'; import type { ListingDetail } from '@/lib/listings-api'; function formatPrice(priceVND: string): string { const num = Number(priceVND); if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`; if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} tr`; return num.toLocaleString('vi-VN'); } interface ListingMapProps { listings: ListingDetail[]; onMarkerClick?: (listing: ListingDetail) => void; selectedListingId?: string; className?: string; } interface MapMarker { listing: ListingDetail; lat: number; lng: number; } const CITY_COORDS: Record = { 'Hồ Chí Minh': [10.8231, 106.6297], 'Hà Nội': [21.0285, 105.8542], 'Đà Nẵng': [16.0544, 108.2022], 'Nha Trang': [12.2388, 109.1967], 'Cần Thơ': [10.0452, 105.7469], }; const DEFAULT_CENTER: [number, number] = [106.6297, 10.8231]; // HCMC [lng, lat] const DEFAULT_ZOOM = 12; function getMarkerCoords(listing: ListingDetail, index: number): { lat: number; lng: number } { const base = CITY_COORDS[listing.property.city] || [10.8231, 106.6297]; const seed = listing.id.charCodeAt(0) + index; return { lat: base[0] + ((seed % 100) - 50) * 0.001, lng: base[1] + ((seed % 73) - 36) * 0.001, }; } export function ListingMap({ listings, onMarkerClick, selectedListingId, className }: ListingMapProps) { const mapContainerRef = React.useRef(null); const mapRef = React.useRef(null); const markersRef = React.useRef([]); const popupRef = React.useRef(null); const markers: MapMarker[] = React.useMemo( () => listings.map((listing, index) => ({ listing, ...getMarkerCoords(listing, index) })), [listings], ); // Initialize map React.useEffect(() => { if (!mapContainerRef.current) return; const token = process.env['NEXT_PUBLIC_MAPBOX_TOKEN']; if (!token) { console.warn('NEXT_PUBLIC_MAPBOX_TOKEN is not set. Map will not render.'); return; } mapboxgl.accessToken = token; const map = new mapboxgl.Map({ container: mapContainerRef.current, style: 'mapbox://styles/mapbox/streets-v12', center: DEFAULT_CENTER, zoom: DEFAULT_ZOOM, attributionControl: false, }); map.addControl(new mapboxgl.NavigationControl(), 'top-right'); map.addControl(new mapboxgl.AttributionControl({ compact: true }), 'bottom-right'); mapRef.current = map; return () => { map.remove(); mapRef.current = null; }; }, []); // Update markers when listings change React.useEffect(() => { const map = mapRef.current; if (!map) return; // Clear existing markers markersRef.current.forEach((m) => m.remove()); markersRef.current = []; if (markers.length === 0) return; const bounds = new mapboxgl.LngLatBounds(); markers.forEach((marker) => { const el = document.createElement('button'); el.className = 'mapbox-price-marker'; const isSelected = selectedListingId === marker.listing.id; el.innerHTML = `${formatPrice(marker.listing.priceVND)}`; el.style.cssText = 'border:none;cursor:pointer;background:none;padding:0;'; el.addEventListener('click', (e) => { e.stopPropagation(); onMarkerClick?.(marker.listing); showPopup(map, marker); }); const mbMarker = new mapboxgl.Marker({ element: el, anchor: 'bottom' }) .setLngLat([marker.lng, marker.lat]) .addTo(map); markersRef.current.push(mbMarker); bounds.extend([marker.lng, marker.lat]); }); // Fit bounds with padding if (markers.length > 1) { map.fitBounds(bounds, { padding: 60, maxZoom: 15 }); } else { map.flyTo({ center: [markers[0]!.lng, markers[0]!.lat], zoom: 14 }); } }, [markers, selectedListingId, onMarkerClick]); function showPopup(map: mapboxgl.Map, marker: MapMarker) { popupRef.current?.remove(); const { listing } = marker; const imgHtml = listing.property.media.length > 0 ? `${listing.property.title}` : ''; const popup = new mapboxgl.Popup({ offset: 25, maxWidth: '260px', closeButton: true }) .setLngLat([marker.lng, marker.lat]) .setHTML(`
${imgHtml}

${formatPrice(listing.priceVND)} VND

${listing.property.title}

${listing.property.district}, ${listing.property.city}

${listing.property.areaM2} m\u00B2 ${listing.property.bedrooms != null ? `${listing.property.bedrooms} PN` : ''} ${listing.property.bathrooms != null ? `${listing.property.bathrooms} WC` : ''}
Xem chi tiet →
`) .addTo(map); popupRef.current = popup; } const hasToken = typeof process !== 'undefined' && process.env['NEXT_PUBLIC_MAPBOX_TOKEN']; return (
{/* Fallback when no Mapbox token */} {!hasToken && (

Thiết lập NEXT_PUBLIC_MAPBOX_TOKEN để hiển thị bản đồ

)} {/* Listing count overlay */}
{markers.length} bất động sản trên bản đồ
{/* Empty state */} {markers.length === 0 && hasToken && (

Không có bất động sản để hiển thị trên bản đồ

)}
); }