'use client'; /* eslint-disable import-x/no-named-as-default-member */ import mapboxgl from 'mapbox-gl'; import * as React from 'react'; import 'mapbox-gl/dist/mapbox-gl.css'; import { formatPrice } from '@/lib/currency'; import { PROJECT_STATUS_LABELS, type ProjectSummary, } from '@/lib/du-an-api'; import { useMapboxStyle } from '@/lib/mapbox-style'; interface ProjectMapProps { projects: ProjectSummary[]; className?: string; } const DEFAULT_CENTER: [number, number] = [106.6297, 10.8231]; // HCMC const DEFAULT_ZOOM = 12; export function ProjectMap({ projects, className }: ProjectMapProps) { const mapContainerRef = React.useRef(null); const mapRef = React.useRef(null); const markersRef = React.useRef([]); const mapStyle = useMapboxStyle(); const geoProjects = React.useMemo( () => projects.filter((p) => p.latitude != null && p.longitude != null), [projects], ); React.useEffect(() => { if (!mapContainerRef.current) return; const token = process.env['NEXT_PUBLIC_MAPBOX_TOKEN']; if (!token) return; mapboxgl.accessToken = token; const map = new mapboxgl.Map({ container: mapContainerRef.current, style: mapStyle, 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; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { const map = mapRef.current; if (!map) return; // setStyle wipes user-added sources/layers — but these components re-add // markers/sources in another effect that fires after geo data changes. // We only need to preserve markers. Since markers are DOM-attached, they // survive the style swap automatically. map.setStyle(mapStyle); }, [mapStyle]); React.useEffect(() => { const map = mapRef.current; if (!map) return; markersRef.current.forEach((m) => m.remove()); markersRef.current = []; if (geoProjects.length === 0) return; const bounds = new mapboxgl.LngLatBounds(); geoProjects.forEach((project) => { // Mapbox owns `transform: translate(...)` on the marker element. // Apply hover scale to an inner wrapper so we don't clobber it. const el = document.createElement('div'); el.className = 'project-map-marker'; const inner = document.createElement('div'); inner.style.cssText = ` background: hsl(var(--card)); color: hsl(var(--card-foreground)); border-radius: 8px; padding: 4px 8px; font-size: 11px; font-weight: 600; box-shadow: 0 2px 6px rgba(0,0,0,0.3); white-space: nowrap; cursor: pointer; border-left: 3px solid hsl(var(--primary)); transition: transform 0.15s; transform: scale(1); max-width: 160px; overflow: hidden; text-overflow: ellipsis; `; inner.textContent = project.name; el.appendChild(inner); el.addEventListener('mouseenter', () => { inner.style.transform = 'scale(1.05)'; }); el.addEventListener('mouseleave', () => { inner.style.transform = 'scale(1)'; }); const statusLabel = PROJECT_STATUS_LABELS[project.status]; const priceText = project.minPrice ? formatPrice(project.minPrice) : 'Liên hệ'; const popup = new mapboxgl.Popup({ offset: 15, maxWidth: '240px', closeButton: false }) .setHTML( `

${project.name}

${project.district}, ${project.city}

${statusLabel} ${priceText}

Xem chi tiết →
`, ); const marker = new mapboxgl.Marker({ element: el, anchor: 'left' }) .setLngLat([project.longitude!, project.latitude!]) .setPopup(popup) .addTo(map); markersRef.current.push(marker); bounds.extend([project.longitude!, project.latitude!]); }); if (geoProjects.length > 1) { map.fitBounds(bounds, { padding: 60, maxZoom: 15 }); } else { map.flyTo({ center: [geoProjects[0]!.longitude!, geoProjects[0]!.latitude!], zoom: 14 }); } }, [geoProjects]); const hasToken = typeof process !== 'undefined' && process.env['NEXT_PUBLIC_MAPBOX_TOKEN']; return (
{!hasToken && (

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

)}
{geoProjects.length} dự án trên bản đồ
); }