@local-logic/maps 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/components/Map/Root/BaseMap/index.tsx +1 -3
- package/src/components/Map/Root/BaseMap/styles.ts +1 -2
- package/src/components/Map/Root/Markers/Cluster/index.tsx +7 -6
- package/src/components/Map/Root/Markers/Cluster/styles.ts +9 -12
- package/src/components/Map/Root/Markers/Google/index.tsx +11 -11
- package/src/components/Map/Root/Markers/Mapbox/index.tsx +20 -9
- package/src/components/Map/Root/Markers/Maptiler/index.tsx +19 -11
- package/src/components/Map/Root/Markers/index.tsx +42 -22
- package/src/components/Map/Root/Markers/styles.ts +16 -8
- package/src/components/Map/Root/Markers/types.ts +9 -1
- package/src/components/Map/Root/Popup/Google/index.tsx +57 -0
- package/src/components/Map/Root/Popup/Google/popup.css +15 -0
- package/src/components/Map/Root/Popup/Mapbox/index.tsx +36 -0
- package/src/components/Map/Root/Popup/Mapbox/popup.css +15 -0
- package/src/components/Map/Root/Popup/Maptiler/index.tsx +37 -0
- package/src/components/Map/Root/Popup/Maptiler/popup.css +15 -0
- package/src/components/Map/Root/Popup/index.tsx +32 -0
- package/src/components/Map/Root/Popup/styles.ts +4 -0
- package/src/components/Map/Root/Popup/types.ts +20 -0
- package/src/components/Map/Root/index.tsx +2 -1
- package/src/components/Map/Root/types.ts +3 -5
- package/src/components/Map/index.stories.tsx +118 -21
- package/src/components/Map/Root/Markers/Google/styles.ts +0 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export const wrapper = `
|
|
2
|
-
export const map = `absolute w-full h-full`;
|
|
1
|
+
export const wrapper = `w-full h-full`;
|
|
@@ -7,9 +7,10 @@ import * as styles from "./styles";
|
|
|
7
7
|
export function Cluster({ cluster }: { cluster: ClusterPoint }) {
|
|
8
8
|
// Taking a somewhat similar calculation to what LocalLogic SDK used to
|
|
9
9
|
// do for cluster diameter (bigger depending on count)
|
|
10
|
-
const diameter = useMemo(
|
|
11
|
-
20 + cluster.properties.point_count * 4
|
|
12
|
-
|
|
10
|
+
const diameter = useMemo(
|
|
11
|
+
() => 20 + cluster.properties.point_count * 4,
|
|
12
|
+
[cluster.properties.point_count]
|
|
13
|
+
);
|
|
13
14
|
|
|
14
15
|
return (
|
|
15
16
|
<div
|
|
@@ -17,10 +18,10 @@ export function Cluster({ cluster }: { cluster: ClusterPoint }) {
|
|
|
17
18
|
style={{
|
|
18
19
|
width: `${diameter}px`,
|
|
19
20
|
height: `${diameter}px`,
|
|
20
|
-
fontSize: `${diameter / 2.5}px
|
|
21
|
+
fontSize: `${diameter / 2.5}px`,
|
|
21
22
|
}}
|
|
22
23
|
>
|
|
23
24
|
{cluster.properties.point_count}
|
|
24
25
|
</div>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
export const container = `
|
|
2
|
-
rounded-full
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
border-
|
|
11
|
-
border-solid
|
|
12
|
-
border-base-white
|
|
13
|
-
shadow-sm
|
|
2
|
+
rounded-full leading-none font-sans border-2
|
|
3
|
+
border-solid shadow-sm
|
|
4
|
+
flex justify-center items-center
|
|
5
|
+
bg-primary-100 text-base-white border-base-white
|
|
6
|
+
transition-all
|
|
7
|
+
|
|
8
|
+
hover:bg-base-white
|
|
9
|
+
hover:text-primary-100
|
|
10
|
+
hover:border-primary-100
|
|
14
11
|
`;
|
|
@@ -8,15 +8,14 @@ import type { MapMarkerProps } from "../types";
|
|
|
8
8
|
import { Cluster } from "../Cluster";
|
|
9
9
|
|
|
10
10
|
import * as globalStyles from "../styles";
|
|
11
|
-
import * as styles from "./styles";
|
|
12
11
|
|
|
13
|
-
export default function GoogleMarkers({ clusters, children }: MapMarkerProps) {
|
|
14
|
-
return clusters
|
|
12
|
+
export default function GoogleMarkers({ clusters, onClick, children }: MapMarkerProps) {
|
|
13
|
+
return clusters?.map((cluster, index) => {
|
|
15
14
|
const longitude = cluster.geometry.coordinates[0];
|
|
16
15
|
const latitude = cluster.geometry.coordinates[1];
|
|
17
16
|
|
|
18
17
|
const handleOnClick = () => {
|
|
19
|
-
|
|
18
|
+
onClick?.(cluster);
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
return (
|
|
@@ -27,18 +26,19 @@ export default function GoogleMarkers({ clusters, children }: MapMarkerProps) {
|
|
|
27
26
|
lng: longitude,
|
|
28
27
|
}}
|
|
29
28
|
onClick={handleOnClick}
|
|
30
|
-
className={styles.marker}
|
|
31
29
|
>
|
|
32
|
-
<div className={globalStyles.container}>
|
|
30
|
+
<div className={globalStyles.container} data-is-cluster={!!cluster.properties.cluster}>
|
|
33
31
|
{cluster.properties.cluster ? (
|
|
34
32
|
<Cluster cluster={cluster} />
|
|
35
33
|
) : (
|
|
36
34
|
<>
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
<div className={globalStyles.iconWrapper}>
|
|
36
|
+
{cluster.properties.icon ? (
|
|
37
|
+
<cluster.properties.icon className={globalStyles.icon} />
|
|
38
|
+
) : (
|
|
39
|
+
<MapPin className={globalStyles.icon} />
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
42
|
{children}
|
|
43
43
|
</>
|
|
44
44
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import { MapPin } from "@phosphor-icons/react";
|
|
4
|
-
import { Marker as MapboxMarker } from "react-map-gl/mapbox";
|
|
4
|
+
import { Marker as MapboxMarker, type MarkerEvent } from "react-map-gl/mapbox";
|
|
5
5
|
|
|
6
6
|
import type { MapMarkerProps } from "../types";
|
|
7
7
|
|
|
@@ -9,28 +9,39 @@ import { Cluster } from "../Cluster";
|
|
|
9
9
|
|
|
10
10
|
import * as styles from "../styles";
|
|
11
11
|
|
|
12
|
-
export default function MapboxMarkers({ clusters, children }: MapMarkerProps) {
|
|
13
|
-
return clusters
|
|
12
|
+
export default function MapboxMarkers({ clusters, onClick, children }: MapMarkerProps) {
|
|
13
|
+
return clusters?.map((cluster, index) => {
|
|
14
14
|
const longitude = cluster.geometry.coordinates[0];
|
|
15
15
|
const latitude = cluster.geometry.coordinates[1];
|
|
16
16
|
|
|
17
|
+
const handleOnClick = (event: MarkerEvent<MouseEvent>) => {
|
|
18
|
+
// If we let the click event propagates to the map, it will immediately close the popup
|
|
19
|
+
// with `closeOnClick: true`
|
|
20
|
+
// https://github.com/visgl/react-map-gl/blob/bdfcfd2d6cfdc641cddb5337d7ed477e5c3c28d5/examples/controls/src/app.tsx#L31-36
|
|
21
|
+
event.originalEvent.stopPropagation();
|
|
22
|
+
onClick?.(cluster);
|
|
23
|
+
};
|
|
24
|
+
|
|
17
25
|
return (
|
|
18
26
|
<MapboxMarker
|
|
19
27
|
key={`map-marker-${longitude}-${latitude}-${index}`}
|
|
20
28
|
anchor="bottom"
|
|
21
29
|
longitude={longitude}
|
|
22
30
|
latitude={latitude}
|
|
31
|
+
onClick={handleOnClick}
|
|
23
32
|
>
|
|
24
|
-
<div className={styles.container}>
|
|
33
|
+
<div className={styles.container} data-is-cluster={!!cluster.properties.cluster}>
|
|
25
34
|
{cluster.properties.cluster ? (
|
|
26
35
|
<Cluster cluster={cluster} />
|
|
27
36
|
) : (
|
|
28
37
|
<>
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
<div className={styles.iconWrapper}>
|
|
39
|
+
{cluster.properties.icon ? (
|
|
40
|
+
<cluster.properties.icon className={styles.icon} />
|
|
41
|
+
) : (
|
|
42
|
+
<MapPin className={styles.icon} />
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
34
45
|
{children}
|
|
35
46
|
</>
|
|
36
47
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import { MapPin } from "@phosphor-icons/react";
|
|
4
|
-
import { Marker as MaplibreMarker } from "react-map-gl/maplibre";
|
|
4
|
+
import { Marker as MaplibreMarker, type MarkerEvent } from "react-map-gl/maplibre";
|
|
5
5
|
|
|
6
6
|
import type { MapMarkerProps } from "../types";
|
|
7
7
|
|
|
@@ -9,31 +9,39 @@ import { Cluster } from "../Cluster";
|
|
|
9
9
|
|
|
10
10
|
import * as styles from "../styles";
|
|
11
11
|
|
|
12
|
-
export default function MaptilerMarkers({
|
|
13
|
-
clusters,
|
|
14
|
-
children,
|
|
15
|
-
}: MapMarkerProps) {
|
|
12
|
+
export default function MaptilerMarkers({ clusters, onClick, children }: MapMarkerProps) {
|
|
16
13
|
return clusters?.map((cluster, index) => {
|
|
17
14
|
const longitude = cluster.geometry.coordinates[0];
|
|
18
15
|
const latitude = cluster.geometry.coordinates[1];
|
|
19
16
|
|
|
17
|
+
const handleOnClick = (event: MarkerEvent<MouseEvent>) => {
|
|
18
|
+
// If we let the click event propagates to the map, it will immediately close the popup
|
|
19
|
+
// with `closeOnClick: true`
|
|
20
|
+
// https://github.com/visgl/react-map-gl/blob/bdfcfd2d6cfdc641cddb5337d7ed477e5c3c28d5/examples/controls/src/app.tsx#L31-36
|
|
21
|
+
event.originalEvent.stopPropagation();
|
|
22
|
+
onClick?.(cluster);
|
|
23
|
+
};
|
|
24
|
+
|
|
20
25
|
return (
|
|
21
26
|
<MaplibreMarker
|
|
22
27
|
key={`map-marker-${longitude}-${latitude}-${index}`}
|
|
23
28
|
anchor="bottom"
|
|
24
29
|
longitude={longitude}
|
|
25
30
|
latitude={latitude}
|
|
31
|
+
onClick={handleOnClick}
|
|
26
32
|
>
|
|
27
|
-
<div className={styles.container}>
|
|
33
|
+
<div className={styles.container} data-is-cluster={!!cluster.properties.cluster}>
|
|
28
34
|
{cluster.properties.cluster ? (
|
|
29
35
|
<Cluster cluster={cluster} />
|
|
30
36
|
) : (
|
|
31
37
|
<>
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
<div className={styles.iconWrapper}>
|
|
39
|
+
{cluster.properties.icon ? (
|
|
40
|
+
<cluster.properties.icon className={styles.icon} />
|
|
41
|
+
) : (
|
|
42
|
+
<MapPin className={styles.icon} />
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
37
45
|
{children}
|
|
38
46
|
</>
|
|
39
47
|
)}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import React, { lazy, useMemo
|
|
1
|
+
import React, { lazy, useMemo } from "react";
|
|
2
2
|
|
|
3
|
-
import type { PointFeature, AnyProps } from "supercluster";
|
|
4
3
|
import { point } from "@turf/turf";
|
|
5
4
|
|
|
6
5
|
import useSupercluser from "use-supercluster";
|
|
7
6
|
|
|
8
7
|
import { useRootElement } from "../context";
|
|
9
8
|
|
|
10
|
-
import type { MarkerProps, ClusterPoint } from "./types";
|
|
9
|
+
import type { MarkerProps, MapMarkerProps, ClusterPoint, MarkerPoint } from "./types";
|
|
11
10
|
|
|
12
11
|
import * as styles from "./styles";
|
|
13
12
|
|
|
@@ -15,17 +14,40 @@ const MaptilerMarkers = lazy(() => import("./Maptiler"));
|
|
|
15
14
|
const GoogleMarkers = lazy(() => import("./Google"));
|
|
16
15
|
const MapboxMarkers = lazy(() => import("./Mapbox"));
|
|
17
16
|
|
|
18
|
-
export const Markers = ({ markers, ...rest }: MarkerProps) => {
|
|
19
|
-
const { mapProvider, bounds
|
|
17
|
+
export const Markers = ({ markers, onClick, ...rest }: MarkerProps) => {
|
|
18
|
+
const { mapProvider, bounds } = useRootElement();
|
|
20
19
|
const { clusters, supercluster } = useSupercluser({
|
|
21
|
-
points: markers.map(marker =>
|
|
22
|
-
icon: marker.icon,
|
|
23
|
-
}))),
|
|
20
|
+
points: markers.map((marker) => point([marker.longitude, marker.latitude], marker)),
|
|
24
21
|
bounds,
|
|
25
22
|
zoom: 16,
|
|
26
23
|
options: { radius: 50, maxZoom: 30 },
|
|
27
24
|
});
|
|
28
25
|
|
|
26
|
+
const handleOnClick: MapMarkerProps["onClick"] = (cluster) => {
|
|
27
|
+
if (!("properties" in cluster)) {
|
|
28
|
+
throw new Error("Cluster does not have properties");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (cluster.properties?.cluster && typeof cluster.properties?.cluster_id === "number") {
|
|
32
|
+
onClick?.({
|
|
33
|
+
id: `${cluster.properties.cluster_id}`,
|
|
34
|
+
latitude: cluster.geometry.coordinates[1],
|
|
35
|
+
longitude: cluster.geometry.coordinates[0],
|
|
36
|
+
markers: (supercluster
|
|
37
|
+
?.getChildren(cluster.properties.cluster_id)
|
|
38
|
+
?.map((child) => child.properties) ?? []) as MarkerPoint[],
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onClick?.({
|
|
44
|
+
id: cluster.properties.id,
|
|
45
|
+
latitude: cluster.properties.latitude,
|
|
46
|
+
longitude: cluster.properties.longitude,
|
|
47
|
+
markers: [cluster.properties],
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
29
51
|
const BaseMarkers = useMemo(() => {
|
|
30
52
|
switch (mapProvider?.name) {
|
|
31
53
|
case "maptiler":
|
|
@@ -44,18 +66,16 @@ export const Markers = ({ markers, ...rest }: MarkerProps) => {
|
|
|
44
66
|
}
|
|
45
67
|
|
|
46
68
|
return (
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
viewBox="0 0 20 28"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
</BaseMarkers>
|
|
60
|
-
)
|
|
69
|
+
<>
|
|
70
|
+
<BaseMarkers clusters={clusters as ClusterPoint[]} onClick={handleOnClick} {...rest}>
|
|
71
|
+
{/* POI Pin Background */}
|
|
72
|
+
<svg viewBox="0 0 20 28" fill="none" className={styles.background}>
|
|
73
|
+
<path
|
|
74
|
+
d="M20 10C20 15.5228 12 27.5 10 27.5C8 27.5 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10Z"
|
|
75
|
+
fill="inherit"
|
|
76
|
+
/>
|
|
77
|
+
</svg>
|
|
78
|
+
</BaseMarkers>
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
61
81
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export const container = `
|
|
2
2
|
cursor-pointer
|
|
3
3
|
relative
|
|
4
|
-
h-
|
|
5
|
-
w-
|
|
4
|
+
h-7
|
|
5
|
+
w-5
|
|
6
6
|
text-base-white
|
|
7
7
|
group
|
|
8
8
|
transition-transform
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
data-[is-cluster=true]:w-auto
|
|
10
11
|
`;
|
|
11
12
|
|
|
12
13
|
export const background = `
|
|
@@ -20,10 +21,17 @@ export const background = `
|
|
|
20
21
|
group-focus:stroke-base-white
|
|
21
22
|
`;
|
|
22
23
|
|
|
24
|
+
export const iconWrapper = `
|
|
25
|
+
absolute top-1 left-1/2 transform -translate-x-1/2
|
|
26
|
+
align-center rounded-full overflow-hidden
|
|
27
|
+
transition-all w-3.5 h-3.5
|
|
28
|
+
|
|
29
|
+
group-hover:bg-base-white
|
|
30
|
+
`;
|
|
31
|
+
|
|
23
32
|
export const icon = `
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
h-4 w-4
|
|
33
|
+
m-auto transition-all w-full h-full
|
|
34
|
+
|
|
35
|
+
group-hover:text-primary-100
|
|
36
|
+
group-hover:p-0.5
|
|
29
37
|
`;
|
|
@@ -4,12 +4,20 @@ import type { Marker } from "../types";
|
|
|
4
4
|
|
|
5
5
|
export type ClusterPoint = PointFeature<ClusterProperties & Marker>;
|
|
6
6
|
|
|
7
|
+
export type MarkerPoint = {
|
|
8
|
+
id: string;
|
|
9
|
+
latitude: number;
|
|
10
|
+
longitude: number;
|
|
11
|
+
markers: Marker[];
|
|
12
|
+
};
|
|
13
|
+
|
|
7
14
|
export type MarkerProps = {
|
|
8
15
|
markers: Marker[];
|
|
9
|
-
onClick?: (
|
|
16
|
+
onClick?: (markers: MarkerPoint) => void;
|
|
10
17
|
children?: ReactNode;
|
|
11
18
|
};
|
|
12
19
|
|
|
13
20
|
export type MapMarkerProps = Omit<MarkerProps, "markers"> & {
|
|
14
21
|
clusters: ClusterPoint[];
|
|
22
|
+
onClick: (cluster: ClusterPoint) => void;
|
|
15
23
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useMemo, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import { useMap, InfoWindow } from "@vis.gl/react-google-maps";
|
|
4
|
+
|
|
5
|
+
import { PopupProps } from "../types";
|
|
6
|
+
|
|
7
|
+
import "./popup.css";
|
|
8
|
+
import * as styles from "../styles";
|
|
9
|
+
|
|
10
|
+
export default function GooglePopup({
|
|
11
|
+
latitude,
|
|
12
|
+
longitude,
|
|
13
|
+
offset = [0, -25],
|
|
14
|
+
onClose,
|
|
15
|
+
children,
|
|
16
|
+
}: PopupProps) {
|
|
17
|
+
const map = useMap();
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!map) return;
|
|
21
|
+
|
|
22
|
+
const listener = map.addListener("click", () => {
|
|
23
|
+
onClose?.();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line consistent-return
|
|
27
|
+
return () => {
|
|
28
|
+
listener.remove();
|
|
29
|
+
};
|
|
30
|
+
}, [map]);
|
|
31
|
+
|
|
32
|
+
const pixelOffset: [number, number] = useMemo(() => {
|
|
33
|
+
if (Array.isArray(offset)) {
|
|
34
|
+
return [offset[0], offset[1]];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [offset, offset];
|
|
38
|
+
}, [offset]);
|
|
39
|
+
|
|
40
|
+
if (typeof latitude === "undefined" || typeof longitude === "undefined") {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<InfoWindow
|
|
46
|
+
shouldFocus
|
|
47
|
+
position={{
|
|
48
|
+
lat: latitude,
|
|
49
|
+
lng: longitude,
|
|
50
|
+
}}
|
|
51
|
+
pixelOffset={pixelOffset}
|
|
52
|
+
onClose={onClose}
|
|
53
|
+
>
|
|
54
|
+
<div className={styles.container}>{children}</div>
|
|
55
|
+
</InfoWindow>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Popup } from "react-map-gl/mapbox";
|
|
4
|
+
|
|
5
|
+
import { PopupProps } from "../types";
|
|
6
|
+
|
|
7
|
+
import "./popup.css";
|
|
8
|
+
import * as styles from "../styles";
|
|
9
|
+
|
|
10
|
+
export default function MapboxPopup({
|
|
11
|
+
latitude,
|
|
12
|
+
longitude,
|
|
13
|
+
anchor,
|
|
14
|
+
offset = [0, -35],
|
|
15
|
+
onClose,
|
|
16
|
+
children,
|
|
17
|
+
}: PopupProps) {
|
|
18
|
+
if (typeof latitude === "undefined" || typeof longitude === "undefined") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Popup
|
|
24
|
+
className="mapbox-popup"
|
|
25
|
+
focusAfterOpen
|
|
26
|
+
latitude={latitude}
|
|
27
|
+
longitude={longitude}
|
|
28
|
+
anchor={anchor}
|
|
29
|
+
offset={offset}
|
|
30
|
+
onClose={onClose}
|
|
31
|
+
closeButton={false}
|
|
32
|
+
>
|
|
33
|
+
<div className={styles.container}>{children}</div>
|
|
34
|
+
</Popup>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.mapbox-popup {
|
|
2
|
+
background: transparent;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.mapbox-popup .mapboxgl-popup-tip {
|
|
6
|
+
display: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.mapbox-popup .mapboxgl-popup-content {
|
|
10
|
+
background: transparent;
|
|
11
|
+
border: none;
|
|
12
|
+
outline: none;
|
|
13
|
+
padding: 0;
|
|
14
|
+
border-radius: 8px;
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Popup } from "react-map-gl/maplibre";
|
|
4
|
+
|
|
5
|
+
import { PopupProps } from "../types";
|
|
6
|
+
|
|
7
|
+
import "./popup.css";
|
|
8
|
+
import * as styles from "../styles";
|
|
9
|
+
|
|
10
|
+
export default function MaptilerPopup({
|
|
11
|
+
latitude,
|
|
12
|
+
longitude,
|
|
13
|
+
anchor,
|
|
14
|
+
offset = [0, -35],
|
|
15
|
+
onClose,
|
|
16
|
+
children,
|
|
17
|
+
}: PopupProps) {
|
|
18
|
+
if (typeof latitude === "undefined" || typeof longitude === "undefined") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Popup
|
|
24
|
+
key="maplibre-popup"
|
|
25
|
+
className="maplibre-popup"
|
|
26
|
+
focusAfterOpen
|
|
27
|
+
latitude={latitude}
|
|
28
|
+
longitude={longitude}
|
|
29
|
+
anchor={anchor}
|
|
30
|
+
offset={offset}
|
|
31
|
+
onClose={onClose}
|
|
32
|
+
closeButton={false}
|
|
33
|
+
>
|
|
34
|
+
<div className={styles.container}>{children}</div>
|
|
35
|
+
</Popup>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.maplibre-popup {
|
|
2
|
+
background: transparent;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.maplibre-popup .maplibregl-popup-tip {
|
|
6
|
+
display: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.maplibre-popup .maplibregl-popup-content {
|
|
10
|
+
background: transparent;
|
|
11
|
+
border: none;
|
|
12
|
+
outline: none;
|
|
13
|
+
padding: 0;
|
|
14
|
+
border-radius: 8px;
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { lazy, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import { PopupProps } from "./types";
|
|
4
|
+
|
|
5
|
+
import { useRootElement } from "../context";
|
|
6
|
+
|
|
7
|
+
const MaptilerPopup = lazy(() => import("./Maptiler"));
|
|
8
|
+
const GooglePopup = lazy(() => import("./Google"));
|
|
9
|
+
const MapboxPopup = lazy(() => import("./Mapbox"));
|
|
10
|
+
|
|
11
|
+
export function Popup({ children, ...rest }: PopupProps) {
|
|
12
|
+
const { mapProvider } = useRootElement();
|
|
13
|
+
|
|
14
|
+
const BasePopup = useMemo(() => {
|
|
15
|
+
switch (mapProvider?.name) {
|
|
16
|
+
case "maptiler":
|
|
17
|
+
return MaptilerPopup;
|
|
18
|
+
case "google":
|
|
19
|
+
return GooglePopup;
|
|
20
|
+
case "mapbox":
|
|
21
|
+
return MapboxPopup;
|
|
22
|
+
default:
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}, [mapProvider]);
|
|
26
|
+
|
|
27
|
+
if (!BasePopup) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return <BasePopup {...rest}>{children}</BasePopup>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type PopupProps = {
|
|
4
|
+
latitude?: number;
|
|
5
|
+
longitude?: number;
|
|
6
|
+
// Anchor only works for Maplibre and Mapbox
|
|
7
|
+
anchor?:
|
|
8
|
+
| "center"
|
|
9
|
+
| "left"
|
|
10
|
+
| "right"
|
|
11
|
+
| "top"
|
|
12
|
+
| "bottom"
|
|
13
|
+
| "top-left"
|
|
14
|
+
| "top-right"
|
|
15
|
+
| "bottom-left"
|
|
16
|
+
| "bottom-right";
|
|
17
|
+
offset?: number | [number, number];
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
};
|
|
@@ -14,4 +14,5 @@ export function Root(props: RootProps) {
|
|
|
14
14
|
|
|
15
15
|
export { BaseMap } from "./BaseMap";
|
|
16
16
|
export { Markers } from "./Markers";
|
|
17
|
-
export
|
|
17
|
+
export { Popup } from "./Popup";
|
|
18
|
+
export type { MarkerProps, MarkerPoint } from "./Markers/types";
|
|
@@ -18,16 +18,14 @@ export type Coordinates = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export type Marker = {
|
|
21
|
+
id: string;
|
|
21
22
|
latitude: Coordinates["latitude"];
|
|
22
23
|
longitude: Coordinates["longitude"];
|
|
24
|
+
name?: string;
|
|
23
25
|
icon?: Icon;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
export type ZoomPosition =
|
|
27
|
-
| "top-left"
|
|
28
|
-
| "top-right"
|
|
29
|
-
| "bottom-left"
|
|
30
|
-
| "bottom-right";
|
|
28
|
+
export type ZoomPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
31
29
|
|
|
32
30
|
export type RootProps = {
|
|
33
31
|
mapProvider: MapProvider;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
2
|
import { StoryFn, Meta } from "@storybook/react";
|
|
3
3
|
import { mapDefaults as storybookMapDefaults } from "~/../.storybook/defaults";
|
|
4
|
+
import {
|
|
5
|
+
Storefront,
|
|
6
|
+
ShoppingCartSimple,
|
|
7
|
+
Brandy,
|
|
8
|
+
ForkKnife,
|
|
9
|
+
Coffee,
|
|
10
|
+
Baby,
|
|
11
|
+
Train,
|
|
12
|
+
Barbell,
|
|
13
|
+
GasPump,
|
|
14
|
+
Heartbeat,
|
|
15
|
+
} from "@phosphor-icons/react";
|
|
4
16
|
import type { RootProps, Marker } from "./Root/types";
|
|
5
17
|
import { defaultMapValues } from "./Root/constants";
|
|
6
18
|
import * as Map from "./Root";
|
|
@@ -20,56 +32,76 @@ const defaultValues = {
|
|
|
20
32
|
zoomPosition: "bottom-right" as RootProps["zoomPosition"],
|
|
21
33
|
};
|
|
22
34
|
|
|
23
|
-
const
|
|
35
|
+
const markerList = [
|
|
24
36
|
{
|
|
37
|
+
id: "marker-1",
|
|
38
|
+
name: "Marker 1",
|
|
25
39
|
latitude: 45.527399,
|
|
26
40
|
longitude: -73.598126,
|
|
27
|
-
|
|
41
|
+
icon: Storefront,
|
|
28
42
|
},
|
|
29
43
|
{
|
|
44
|
+
id: "marker-2",
|
|
45
|
+
name: "Marker 2",
|
|
30
46
|
latitude: 45.527302,
|
|
31
47
|
longitude: -73.597405,
|
|
32
|
-
|
|
48
|
+
icon: ShoppingCartSimple,
|
|
33
49
|
},
|
|
34
50
|
{
|
|
51
|
+
id: "marker-3",
|
|
52
|
+
name: "Marker 3",
|
|
35
53
|
latitude: 45.527302,
|
|
36
54
|
longitude: -73.597405,
|
|
37
|
-
|
|
55
|
+
icon: Brandy,
|
|
38
56
|
},
|
|
39
57
|
{
|
|
58
|
+
id: "marker-4",
|
|
59
|
+
name: "Marker 4",
|
|
40
60
|
latitude: 45.527302,
|
|
41
61
|
longitude: -73.597405,
|
|
42
|
-
|
|
62
|
+
icon: ForkKnife,
|
|
43
63
|
},
|
|
44
64
|
{
|
|
65
|
+
id: "marker-5",
|
|
66
|
+
name: "Marker 5",
|
|
45
67
|
latitude: 45.527302,
|
|
46
68
|
longitude: -73.597405,
|
|
47
|
-
|
|
69
|
+
icon: Coffee,
|
|
48
70
|
},
|
|
49
71
|
{
|
|
72
|
+
id: "marker-6",
|
|
73
|
+
name: "Marker 6",
|
|
50
74
|
latitude: 45.527302,
|
|
51
75
|
longitude: -73.597405,
|
|
52
|
-
|
|
76
|
+
icon: Baby,
|
|
53
77
|
},
|
|
54
78
|
{
|
|
79
|
+
id: "marker-7",
|
|
80
|
+
name: "Marker 7",
|
|
55
81
|
latitude: 45.527256,
|
|
56
82
|
longitude: -73.600229,
|
|
57
|
-
|
|
83
|
+
icon: Train,
|
|
58
84
|
},
|
|
59
85
|
{
|
|
86
|
+
id: "marker-8",
|
|
87
|
+
name: "Marker 8",
|
|
60
88
|
latitude: 45.527256,
|
|
61
89
|
longitude: -73.600229,
|
|
62
|
-
|
|
90
|
+
icon: Barbell,
|
|
63
91
|
},
|
|
64
92
|
{
|
|
93
|
+
id: "marker-9",
|
|
94
|
+
name: "Marker 9",
|
|
65
95
|
latitude: 45.526643,
|
|
66
96
|
longitude: -73.600293,
|
|
67
|
-
|
|
97
|
+
icon: GasPump,
|
|
68
98
|
},
|
|
69
99
|
{
|
|
100
|
+
id: "marker-10",
|
|
101
|
+
name: "Marker 10",
|
|
70
102
|
latitude: 45.527025,
|
|
71
103
|
longitude: -73.600897,
|
|
72
|
-
|
|
104
|
+
icon: Heartbeat,
|
|
73
105
|
},
|
|
74
106
|
] as Marker[];
|
|
75
107
|
|
|
@@ -78,15 +110,80 @@ const maptilerThemes = {
|
|
|
78
110
|
night: "dd191599-2a92-49fc-9a33-e12391753ad5",
|
|
79
111
|
};
|
|
80
112
|
|
|
81
|
-
const Template: StoryFn<RootProps> = (args) =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
113
|
+
const Template: StoryFn<RootProps> = (args) => {
|
|
114
|
+
const [activeMarker, setActiveMarker] = useState<Map.MarkerPoint | undefined>(undefined);
|
|
115
|
+
|
|
116
|
+
const handleMarkerClick = (markers: Map.MarkerPoint) => {
|
|
117
|
+
if (
|
|
118
|
+
activeMarker?.latitude === markers.latitude &&
|
|
119
|
+
activeMarker?.longitude === markers.longitude
|
|
120
|
+
) {
|
|
121
|
+
setActiveMarker(undefined);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setActiveMarker(markers);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const popupContent = useMemo(() => {
|
|
129
|
+
if (typeof activeMarker?.markers === "undefined") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (activeMarker.markers.length === 1) {
|
|
134
|
+
return (
|
|
135
|
+
<>
|
|
136
|
+
{activeMarker.markers[0].icon && React.createElement(activeMarker.markers[0].icon)}
|
|
137
|
+
<strong>{activeMarker.markers[0]?.name}</strong>
|
|
138
|
+
<p>
|
|
139
|
+
{activeMarker?.latitude}, {activeMarker?.longitude}
|
|
140
|
+
</p>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (activeMarker.markers.length > 1) {
|
|
146
|
+
return activeMarker.markers.map((marker) => (
|
|
147
|
+
<div key={marker.id} className="flex flex-row gap-y-3">
|
|
148
|
+
<button
|
|
149
|
+
className="rounded px-3 py-2 border border-primary-100"
|
|
150
|
+
onClick={() =>
|
|
151
|
+
handleMarkerClick({
|
|
152
|
+
id: marker.id,
|
|
153
|
+
latitude: marker.latitude,
|
|
154
|
+
longitude: marker.longitude,
|
|
155
|
+
markers: [marker],
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
>
|
|
159
|
+
{marker.icon && React.createElement(marker.icon, { className: "inline mr-5" })}
|
|
160
|
+
<strong>{marker.name}</strong>
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}, [activeMarker]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="w-full h-[calc(100vh-30px)]">
|
|
171
|
+
<Map.Root {...args}>
|
|
172
|
+
<Map.BaseMap>
|
|
173
|
+
<Map.Markers markers={markerList} onClick={handleMarkerClick} />
|
|
174
|
+
|
|
175
|
+
<Map.Popup
|
|
176
|
+
latitude={activeMarker?.latitude}
|
|
177
|
+
longitude={activeMarker?.longitude}
|
|
178
|
+
onClose={() => setActiveMarker(undefined)}
|
|
179
|
+
>
|
|
180
|
+
{popupContent}
|
|
181
|
+
</Map.Popup>
|
|
182
|
+
</Map.BaseMap>
|
|
183
|
+
</Map.Root>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
90
187
|
|
|
91
188
|
export const Maptiler = Template.bind({});
|
|
92
189
|
Maptiler.args = {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const marker = `-translate-y-1/2`;
|