1- import { Box , GlobalStyles , Skeleton } from "@tracktor/design-system" ;
2- import mapboxgl from "mapbox-gl" ;
3- import { memo , ReactElement } from "react" ;
4- import useMarkerMap from "@/components/MarkerMap/useMarkerMap" ;
5- import { MarkerMapProps } from "@/types/MarkerMapProps.ts" ;
1+ import { Box , GlobalStyles , Skeleton , useTheme } from "@tracktor/design-system" ;
2+ import { memo , ReactElement , useMemo , useState } from "react" ;
3+ import MapboxMap , { Marker , Popup } from "react-map-gl" ;
4+ import { MarkerMapProps } from "@/types/MarkerMapProps" ;
5+ import "mapbox-gl/dist/mapbox-gl.css" ;
6+ import { isArray } from "@tracktor/react-utils" ;
7+ import FitBounds from "@/Features/Bounds/FitsBounds.ts" ;
68
7- /**
8- * MarkerMap is a reusable React component that displays an interactive Mapbox map
9- * with customizable markers and behavior.
10- *
11- * It supports features like:
12- * - Auto-fitting bounds to markers
13- * - Custom marker icons and tooltips
14- * - Light/dark theming
15- * - Fly animations and zooming
16- * - Popup display (on click or hover)
17- * - Custom styling for the map container
18- * - Manual or automatic control of map centering and zoom
19- *
20- * @param {object } props - Props used to configure the map rendering.
21- * @param {boolean } [props.fitBounds] - If true, automatically adjusts the viewport to fit all markers.
22- * @param {number } [props.fitBoundsPadding] - Padding in pixels when fitting bounds to markers.
23- * @param {LngLatLike | number[] } [props.center] - Initial center of the map [lng, lat].
24- * @param {string } [props.mapStyle] - Mapbox style URL or identifier (e.g. "mapbox://styles/mapbox/streets-v11").
25- * @param {number } [props.zoom] - Initial zoom level of the map.
26- * @param {string } [props.popupMaxWidth] - Maximum width of popups (e.g., "200px").
27- * @param {number | string } [props.width="100%"] - Width of the map container.
28- * @param {number | string } [props.height=300] - Height of the map container.
29- * @param {boolean } [props.loading] - Optional flag indicating if the map is in loading state.
30- * @param {string } [props.markerImageURL] - URL of a custom image used for default marker icons.
31- * @param {SxProps } [props.containerStyle] - Style object (MUI `sx`) to customize the map container.
32- * @param {number } [props.fitBoundDuration] - Duration of fitBounds animation in milliseconds.
33- * @param {boolean } [props.square] - If true, forces the map container to be a square.
34- * @param {number | string } [props.openPopup] - ID of the marker whose popup should be open by default.
35- * @param {boolean } [props.openPopupOnHover] - If true, opens the popup on marker hover instead of click.
36- * @param {MarkerProps[] } [props.markers] - Array of marker objects to render on the map.
37- * @param {(lng: number, lat: number) => void } [props.onMapClick] - Callback triggered when the map is clicked.
38- * @param {"light" | "dark" } [props.theme] - Optional theme override for map rendering.
39- *
40- * @returns {ReactElement } The rendered map component with optional markers and behavior.
41- *
42- * @example
43- * ```tsx
44- * <MarkerMap
45- * center={[2.3488, 48.8534]}
46- * zoom={13}
47- * fitBounds
48- * openPopupOnHover
49- * popupMaxWidth="250px"
50- * mapStyle="mapbox://styles/mapbox/light-v10"
51- * theme="light"
52- * markers={[
53- * { id: 1, lat: 48.8534, lng: 2.3488, name: "Marker 1" },
54- * { id: 2, lat: 48.8566, lng: 2.3522, name: "Marker 2" },
55- * ]}
56- * />
57- * ```
58- */
59- const MarkerMap = ( { containerStyle, square, loading, height = 300 , width = "100%" , ...props } : MarkerMapProps ) : ReactElement => {
60- const { containerRef, currentTheme } = useMarkerMap ( props ) ;
9+ const MarkerMap = ( {
10+ containerStyle,
11+ square,
12+ loading,
13+ height = 300 ,
14+ width = "100%" ,
15+ center = [ 2.3522 , 48.8566 ] ,
16+ zoom = 5 ,
17+ popupMaxWidth,
18+ openPopup,
19+ openPopupOnHover,
20+ markers = [ ] ,
21+ fitBounds = true ,
22+ fitBoundsPadding,
23+ fitBoundDuration,
24+ onMapClick,
25+ mapStyle = "mapbox://styles/mapbox/streets-v11" ,
26+ } : MarkerMapProps ) : ReactElement => {
27+ const theme = useTheme ( ) ;
28+ const [ selected , setSelected ] = useState < string | number | null > ( openPopup ?? null ) ;
29+
30+ const selectedMarker = useMemo ( ( ) => ( selected ? ( markers . find ( ( m ) => m . id === selected ) ?? null ) : null ) , [ selected , markers ] ) ;
31+
32+ const handleMarkerClick = ( id : string | number ) => {
33+ if ( ! openPopupOnHover ) {
34+ setSelected ( id ) ;
35+ }
36+ } ;
37+
38+ const handleMarkerHover = ( id : string | number | null ) => {
39+ if ( openPopupOnHover ) {
40+ setSelected ( id ) ;
41+ }
42+ } ;
6143
6244 return (
6345 < Box sx = { { height, position : "relative" , width, ...containerStyle } } >
@@ -71,42 +53,51 @@ const MarkerMap = ({ containerStyle, square, loading, height = 300, width = "100
7153 width : "fit-content!important" ,
7254 } ,
7355 ".mapboxgl-popup-tip" : {
74- borderTopColor : currentTheme === "dark" ? "#1e1e1e !important" : '#ffffff !important"' ,
56+ borderTopColor : theme . palette . mode === "dark" ? "#1e1e1e !important" : '#ffffff !important"' ,
7557 } ,
7658 } }
7759 />
78- { mapboxgl . supported ( ) ? (
79- < Box
80- ref = { containerRef }
81- sx = { {
82- alignItems : "center" ,
83- borderRadius : square ? 0 : 1 ,
84- height,
85- justifyContent : "center" ,
86- left : 0 ,
87- overflow : "hidden" ,
88- position : "absolute" ,
89- top : 0 ,
90- visibility : loading ? "hidden !important" : "visible" ,
91- width,
92- zIndex : 1 ,
93- } }
94- />
95- ) : (
96- < Box
97- sx = { {
98- left : "50%" ,
99- position : "absolute" ,
100- textAlign : "center" ,
101- top : "50%" ,
102- transform : "translate(-50%, -50%)" ,
103- } }
104- >
105- WebGL is not enabled in your browser. This technology is required to display the interactive map.
106- </ Box >
107- ) }
10860
109- { /* Loading skeleton */ }
61+ < MapboxMap
62+ initialViewState = { {
63+ latitude : isArray ( center ) ? center [ 1 ] : center . lat ,
64+ longitude : isArray ( center ) ? center [ 0 ] : center . lng ,
65+ zoom,
66+ } }
67+ style = { { height : "100%" , width : "100%" } }
68+ mapStyle = { mapStyle }
69+ mapboxAccessToken = { import . meta. env . VITE_MAPBOX_ACCESS_TOKEN }
70+ onClick = { ( e ) => onMapClick ?.( e . lngLat . lng , e . lngLat . lat ) }
71+ >
72+ { markers . map ( ( m ) => (
73+ < Marker
74+ key = { m . id }
75+ longitude = { m . lng }
76+ latitude = { m . lat }
77+ anchor = "bottom"
78+ onClick = { ( ) => handleMarkerClick ( m . id ) }
79+ onMouseEnter = { ( ) => handleMarkerHover ( m . id ) }
80+ onMouseLeave = { ( ) => handleMarkerHover ( null ) }
81+ >
82+ { m . IconComponent ? < m . IconComponent { ...m . iconProps } /> : < div > 📍</ div > }
83+ </ Marker >
84+ ) ) }
85+
86+ { selectedMarker && (
87+ < Popup
88+ longitude = { selectedMarker . lng }
89+ latitude = { selectedMarker . lat }
90+ anchor = "top"
91+ onClose = { ( ) => setSelected ( null ) }
92+ maxWidth = { popupMaxWidth }
93+ >
94+ { selectedMarker . Tooltip ?? < div > Marker { selectedMarker . id } </ div > }
95+ </ Popup >
96+ ) }
97+
98+ { fitBounds && markers . length > 1 && < FitBounds markers = { markers } padding = { fitBoundsPadding } duration = { fitBoundDuration } /> }
99+ </ MapboxMap >
100+
110101 { loading && (
111102 < Skeleton
112103 width = { width }
0 commit comments