turash/bugulma/frontend/components/ui/MapPicker.tsx
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- Remove nested git repository from bugulma/frontend/.git
- Add all frontend files to main repository tracking
- Convert from separate frontend/backend repos to unified monorepo
- Preserve all frontend code and development history as tracked files
- Eliminate nested repository complexity for simpler development workflow

This creates a proper monorepo structure with frontend and backend
coexisting in the same repository for easier development and deployment.
2025-11-25 06:02:57 +01:00

114 lines
3.7 KiB
TypeScript

import L, { LatLngTuple } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { useEffect, useRef } from 'react';
import { MapContainer, Marker, TileLayer, useMap } from 'react-leaflet';
// Fix for default marker icon issue in Leaflet with webpack/vite
delete (L.Icon.Default.prototype as unknown as { _getIconUrl?: unknown })._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
});
interface MapPickerProps {
value?: { lat: number; lng: number };
onChange?: (value: { lat: number; lng: number }) => void;
location?: { lat: number; lng: number };
onLocationChange?: (value: { lat: number; lng: number }) => void;
}
/**
* Component to sync map center with value prop and zoom in when location changes
*/
const MapCenterSync = ({ value }: { value: { lat: number; lng: number } }) => {
const map = useMap();
const lastValueRef = useRef<{ lat: number; lng: number } | null>(null);
useEffect(() => {
// Check if this is a new location (not just initial render)
const isNewLocation =
!lastValueRef.current ||
Math.abs(lastValueRef.current.lat - value.lat) > 0.0001 ||
Math.abs(lastValueRef.current.lng - value.lng) > 0.0001;
if (isNewLocation) {
// Zoom in to a closer level when a place is selected
const targetZoom = 16; // Closer zoom level for selected location
map.setView([value.lat, value.lng], targetZoom, { animate: true });
lastValueRef.current = { lat: value.lat, lng: value.lng };
}
}, [map, value.lat, value.lng]);
return null;
};
const MapPicker = ({ value, onChange, location, onLocationChange }: MapPickerProps) => {
// Support both prop naming conventions for backward compatibility
const actualValue = value || location || { lat: 54.5384152, lng: 52.7955953 };
const actualOnChange = onChange || onLocationChange || (() => {});
const position: LatLngTuple = [actualValue.lat, actualValue.lng];
return (
<div className="h-64 w-full rounded-lg border overflow-hidden relative">
<MapContainer
center={position}
zoom={13}
scrollWheelZoom={true}
attributionControl={false}
className="rounded-lg h-full w-full"
>
<MapCenterSync value={actualValue} />
{/* Optional: Add a tile layer for reference (can be removed if you want blank map) */}
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
opacity={0.3}
/>
<Marker
position={position}
draggable={true}
eventHandlers={{
dragend: (e) => {
const marker = e.target;
const newPosition = marker.getLatLng();
actualOnChange({ lat: newPosition.lat, lng: newPosition.lng });
},
}}
/>
{/* Click handler for map */}
<MapClickHandler onChange={actualOnChange} />
</MapContainer>
</div>
);
};
/**
* Component to handle map clicks
*/
const MapClickHandler = ({
onChange,
}: {
onChange: (value: { lat: number; lng: number }) => void;
}) => {
const map = useMap();
useEffect(() => {
const handleClick = (e: L.LeafletMouseEvent) => {
onChange({ lat: e.latlng.lat, lng: e.latlng.lng });
};
map.on('click', handleClick);
return () => {
map.off('click', handleClick);
};
}, [map, onChange]);
return null;
};
export default MapPicker;