miniperplx/components/interactive-maps.tsx

186 lines
4.6 KiB
TypeScript

import React, { useEffect, useRef, useCallback } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { cn } from "@/lib/utils";
interface Location {
lat: number;
lng: number;
}
interface Photo {
thumbnail: string;
small: string;
medium: string;
large: string;
original: string;
caption?: string;
}
interface Place {
name: string;
location: Location;
place_id: string;
vicinity: string;
rating?: number;
reviews_count?: number;
price_level?: string;
description?: string;
photos?: Photo[];
is_closed?: boolean;
next_open_close?: string;
type?: string;
cuisine?: string;
source?: string;
phone?: string;
website?: string;
hours?: string[];
distance?: string;
bearing?: string;
}
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
interface InteractiveMapProps {
center: Location;
places: Place[];
selectedPlace: Place | null;
onPlaceSelect: (place: Place) => void;
className?: string;
}
const InteractiveMap: React.FC<InteractiveMapProps> = ({
center,
places,
selectedPlace,
onPlaceSelect,
className
}) => {
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<mapboxgl.Map | null>(null);
const markersRef = useRef<{ [key: string]: mapboxgl.Marker }>({});
// Handler for marker clicks
const handleMarkerClick = useCallback((place: Place) => {
onPlaceSelect(place);
}, [onPlaceSelect]);
// Initialize map
useEffect(() => {
if (!mapContainerRef.current) return;
mapRef.current = new mapboxgl.Map({
container: mapContainerRef.current,
style: 'mapbox://styles/mapbox/standard',
center: [center.lng, center.lat],
zoom: 13,
attributionControl: false,
});
const map = mapRef.current;
// Add minimal controls
map.addControl(
new mapboxgl.NavigationControl({ showCompass: false }),
'bottom-right',
);
// Compact attribution
map.addControl(
new mapboxgl.AttributionControl({ compact: true }),
'bottom-right'
);
return () => {
Object.values(markersRef.current).forEach(marker => marker.remove());
map.remove();
};
}, [center.lat, center.lng]);
// Update markers
useEffect(() => {
if (!mapRef.current) return;
// Clear existing markers
Object.values(markersRef.current).forEach(marker => marker.remove());
markersRef.current = {};
// Add new markers
places.forEach((place, index) => {
const isSelected = selectedPlace?.name === place.name;
// Create marker element
const el = document.createElement('div');
el.className = cn(
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-300 cursor-pointer',
isSelected
? 'bg-black text-white scale-110'
: 'bg-white text-black hover:scale-105'
);
el.style.border = '2px solid currentColor';
el.innerHTML = `${index + 1}`;
// Create and add marker
const marker = new mapboxgl.Marker({
element: el,
anchor: 'center',
})
.setLngLat([place.location.lng, place.location.lat])
.addTo(mapRef.current!);
// Add click handler
el.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent map click
handleMarkerClick(place);
});
// Store marker reference
markersRef.current[place.name] = marker;
});
}, [places, selectedPlace, handleMarkerClick]);
// Handle map click to deselect
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const handleMapClick = (e: mapboxgl.MapMouseEvent) => {
// Check if click was on a marker
const clickedMarker = Object.values(markersRef.current).some(marker => {
const markerEl = marker.getElement();
return e.originalEvent.target === markerEl || markerEl.contains(e.originalEvent.target as Node);
});
// If click wasn't on a marker, deselect
if (!clickedMarker) {
onPlaceSelect(null as any); // Type cast to satisfy TS
}
};
map.on('click', handleMapClick);
return () => {
map.off('click', handleMapClick);
};
}, [onPlaceSelect]);
// Fly to selected place
useEffect(() => {
if (!mapRef.current || !selectedPlace) return;
mapRef.current.flyTo({
center: [selectedPlace.location.lng, selectedPlace.location.lat],
zoom: 15,
duration: 1500,
essential: true,
});
}, [selectedPlace]);
return (
<div className={cn("w-full h-full relative z-0", className)}>
<div ref={mapContainerRef} className="w-full h-full" />
</div>
);
};
export default InteractiveMap;