app-s01qyw/frontend/src/components/MapView.tsx
2026-09-29 20:17:53 +05:30

164 lines
3.9 KiB
TypeScript

import { useEffect, useRef } from 'react';
import L from 'leaflet';
export interface MapMarker {
id?: string;
lat: number;
lng: number;
label?: string;
type?: 'pickup' | 'destination' | 'driver' | 'default';
}
export interface MapViewProps {
center: [number, number];
zoom?: number;
markers?: MapMarker[];
polyline?: [number, number][];
className?: string;
onMapClick?: (lat: number, lng: number) => void;
}
const createCustomIcon = (type: MapMarker['type'] = 'default') => {
let bgColor = '#0f172a';
let ringColor = 'rgba(15, 23, 42, 0.2)';
let symbol = '•';
if (type === 'pickup') {
bgColor = '#15803d';
ringColor = 'rgba(21, 128, 61, 0.3)';
symbol = 'A';
} else if (type === 'destination') {
bgColor = '#dc2626';
ringColor = 'rgba(220, 38, 38, 0.3)';
symbol = 'B';
} else if (type === 'driver') {
bgColor = '#0284c7';
ringColor = 'rgba(2, 132, 199, 0.35)';
symbol = '🚕';
}
const html = `
<div style="
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background-color: ${bgColor};
color: #ffffff;
font-weight: 700;
font-size: 13px;
border-radius: 50%;
border: 2px solid #ffffff;
box-shadow: 0 0 0 4px ${ringColor}, 0 4px 10px rgba(0,0,0,0.2);
">
${symbol}
</div>
`;
return L.divIcon({
html,
className: 'custom-leaflet-marker-icon',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16],
});
};
export default function MapView({
center,
zoom = 13,
markers = [],
polyline = [],
className,
onMapClick,
}: MapViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<L.Map | null>(null);
const markerLayerRef = useRef<L.LayerGroup | null>(null);
const polylineLayerRef = useRef<L.Polyline | null>(null);
useEffect(() => {
if (!containerRef.current || mapRef.current) return;
const map = L.map(containerRef.current, {
zoomControl: true,
attributionControl: false,
}).setView(center, zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap contributors',
}).addTo(map);
markerLayerRef.current = L.layerGroup().addTo(map);
mapRef.current = map;
if (onMapClick) {
map.on('click', (e: L.LeafletMouseEvent) => {
onMapClick(e.latlng.lat, e.latlng.lng);
});
}
const timer = setTimeout(() => {
map.invalidateSize();
}, 150);
return () => {
clearTimeout(timer);
map.remove();
mapRef.current = null;
};
}, []);
useEffect(() => {
if (mapRef.current) {
mapRef.current.setView(center, zoom);
}
}, [center[0], center[1], zoom]);
useEffect(() => {
const layer = markerLayerRef.current;
if (!layer) return;
layer.clearLayers();
markers.forEach((m) => {
const icon = createCustomIcon(m.type);
const marker = L.marker([m.lat, m.lng], { icon });
if (m.label) {
marker.bindPopup(
`<div style="font-family: sans-serif; font-weight: 600; font-size: 13px; color: #0f172a;">${m.label}</div>`
);
}
layer.addLayer(marker);
});
}, [markers]);
useEffect(() => {
if (!mapRef.current) return;
if (polylineLayerRef.current) {
polylineLayerRef.current.remove();
polylineLayerRef.current = null;
}
if (polyline && polyline.length > 0) {
polylineLayerRef.current = L.polyline(polyline, {
color: '#0284c7',
weight: 5,
opacity: 0.85,
lineCap: 'round',
lineJoin: 'round',
}).addTo(mapRef.current);
}
}, [polyline]);
return (
<div
ref={containerRef}
className={className || 'h-[450px] w-full rounded-xl border border-slate-300 shadow-sm bg-slate-100 overflow-hidden'}
/>
);
}