@local-logic/maps 0.0.1 → 0.0.3

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/src/components/Map/Root/BaseMap/index.tsx +1 -3
  4. package/src/components/Map/Root/BaseMap/styles.ts +1 -2
  5. package/src/components/Map/Root/Layers/Google/index.tsx +97 -0
  6. package/src/components/Map/Root/Layers/Mapbox/index.tsx +74 -0
  7. package/src/components/Map/Root/Layers/Maptiler/index.tsx +74 -0
  8. package/src/components/Map/Root/Layers/index.tsx +32 -0
  9. package/src/components/Map/Root/Layers/types.ts +15 -0
  10. package/src/components/Map/Root/Layers/utils.ts +64 -0
  11. package/src/components/Map/Root/Markers/Cluster/index.tsx +7 -6
  12. package/src/components/Map/Root/Markers/Cluster/styles.ts +9 -12
  13. package/src/components/Map/Root/Markers/Google/index.tsx +11 -11
  14. package/src/components/Map/Root/Markers/Mapbox/index.tsx +20 -9
  15. package/src/components/Map/Root/Markers/Maptiler/index.tsx +19 -11
  16. package/src/components/Map/Root/Markers/index.tsx +42 -22
  17. package/src/components/Map/Root/Markers/styles.ts +16 -8
  18. package/src/components/Map/Root/Markers/types.ts +19 -2
  19. package/src/components/Map/Root/Popup/Google/index.tsx +57 -0
  20. package/src/components/Map/Root/Popup/Google/popup.css +15 -0
  21. package/src/components/Map/Root/Popup/Mapbox/index.tsx +36 -0
  22. package/src/components/Map/Root/Popup/Mapbox/popup.css +15 -0
  23. package/src/components/Map/Root/Popup/Maptiler/index.tsx +37 -0
  24. package/src/components/Map/Root/Popup/Maptiler/popup.css +15 -0
  25. package/src/components/Map/Root/Popup/index.tsx +32 -0
  26. package/src/components/Map/Root/Popup/styles.ts +4 -0
  27. package/src/components/Map/Root/Popup/types.ts +20 -0
  28. package/src/components/Map/Root/index.tsx +14 -1
  29. package/src/components/Map/Root/types.ts +1 -13
  30. package/src/components/Map/index.stories.tsx +86 -70
  31. package/src/components/Map/storybook-data.ts +255 -0
  32. package/src/components/Map/Root/Markers/Google/styles.ts +0 -1
@@ -1,13 +1,12 @@
1
- import React, { lazy, useMemo, useEffect } from "react";
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, setBounds } = useRootElement();
17
+ export const Markers = ({ markers, onClick, ...rest }: MarkerProps) => {
18
+ const { mapProvider, bounds } = useRootElement();
20
19
  const { clusters, supercluster } = useSupercluser({
21
- points: markers.map(marker => (point([marker.longitude, marker.latitude], {
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
- <BaseMarkers clusters={clusters as ClusterPoint[]} {...rest}>
48
- {/* POI Pin Background */}
49
- <svg
50
- viewBox="0 0 20 28"
51
- fill="none"
52
- className={styles.background}
53
- >
54
- <path
55
- 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"
56
- fill="inherit"
57
- />
58
- </svg>
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-8
5
- w-6
4
+ h-7
5
+ w-5
6
6
  text-base-white
7
7
  group
8
8
  transition-transform
9
- hover:scale-125
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
- absolute
25
- top-0 left-0 right-0 bottom-1
26
- transition-transform
27
- m-auto
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
  `;
@@ -1,15 +1,32 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { PointFeature, ClusterProperties } from "supercluster";
3
- import type { Marker } from "../types";
3
+ import type { Icon } from "@phosphor-icons/react";
4
+ import type { Coordinates } from "../types";
4
5
 
5
6
  export type ClusterPoint = PointFeature<ClusterProperties & Marker>;
6
7
 
8
+ export type Marker = {
9
+ id: string;
10
+ latitude: Coordinates["latitude"];
11
+ longitude: Coordinates["longitude"];
12
+ name?: string;
13
+ icon?: Icon;
14
+ };
15
+
16
+ export type MarkerPoint = {
17
+ id: string;
18
+ latitude: number;
19
+ longitude: number;
20
+ markers: Marker[];
21
+ };
22
+
7
23
  export type MarkerProps = {
8
24
  markers: Marker[];
9
- onClick?: (marker: Marker) => void;
25
+ onClick?: (markers: MarkerPoint) => void;
10
26
  children?: ReactNode;
11
27
  };
12
28
 
13
29
  export type MapMarkerProps = Omit<MarkerProps, "markers"> & {
14
30
  clusters: ClusterPoint[];
31
+ onClick: (cluster: ClusterPoint) => void;
15
32
  };
@@ -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,15 @@
1
+ .gm-style .gm-style-iw-chr,
2
+ .gm-style .gm-style-iw-tc {
3
+ display: none;
4
+ }
5
+
6
+ .gm-style .gm-style-iw-d {
7
+ overflow: hidden;
8
+ }
9
+
10
+ .gm-style .gm-style-iw-c {
11
+ border: none;
12
+ outline: none;
13
+ padding: 0;
14
+ border-radius: 8px;
15
+ }
@@ -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,4 @@
1
+ export const container = `
2
+ bg-base-white border border-primary-100 w-auto overflow-hidden
3
+ rounded-lg w-44 px-2.5 py-3 shadow-sm
4
+ `;
@@ -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
+ };
@@ -12,6 +12,19 @@ export function Root(props: RootProps) {
12
12
  );
13
13
  }
14
14
 
15
+ // BASE MAP
15
16
  export { BaseMap } from "./BaseMap";
17
+
18
+ // MARKERS
16
19
  export { Markers } from "./Markers";
17
- export type { MarkerProps } from "./Markers/types";
20
+ export type { MarkerProps, Marker, MarkerPoint } from "./Markers/types";
21
+
22
+ // POPUP
23
+ export { Popup } from "./Popup";
24
+
25
+ // LAYERS
26
+ export { Layers } from "./Layers";
27
+ export type { Source, Layer } from "./Layers/types";
28
+
29
+ // OTHER
30
+ export type { RootProps };
@@ -1,5 +1,4 @@
1
1
  import type { ReactNode } from "react";
2
- import { Icon } from "@phosphor-icons/react";
3
2
 
4
3
  /**
5
4
  * We should try to keep the types "agnostic" but if we have to pick a side,
@@ -17,17 +16,7 @@ export type Coordinates = {
17
16
  longitude: number;
18
17
  };
19
18
 
20
- export type Marker = {
21
- latitude: Coordinates["latitude"];
22
- longitude: Coordinates["longitude"];
23
- icon?: Icon;
24
- };
25
-
26
- export type ZoomPosition =
27
- | "top-left"
28
- | "top-right"
29
- | "bottom-left"
30
- | "bottom-right";
19
+ export type ZoomPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
31
20
 
32
21
  export type RootProps = {
33
22
  mapProvider: MapProvider;
@@ -37,6 +26,5 @@ export type RootProps = {
37
26
  pitch?: number;
38
27
  bearing?: number;
39
28
  cooperativeGestures?: boolean;
40
- markers?: Marker[];
41
29
  children?: ReactNode;
42
30
  };
@@ -1,13 +1,21 @@
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 type { RootProps, Marker } from "./Root/types";
5
4
  import { defaultMapValues } from "./Root/constants";
6
- import * as Map from "./Root";
5
+ import {
6
+ Root as MapRoot,
7
+ BaseMap,
8
+ Markers,
9
+ Popup,
10
+ Layers,
11
+ type RootProps,
12
+ type MarkerPoint,
13
+ } from "./Root";
14
+ import { themes, markerList, layerSources } from "./storybook-data";
7
15
 
8
16
  export default {
9
17
  title: "Map",
10
- component: Map.Root,
18
+ component: MapRoot,
11
19
  argTypes: storybookMapDefaults,
12
20
  } as Meta<RootProps>;
13
21
 
@@ -20,80 +28,88 @@ const defaultValues = {
20
28
  zoomPosition: "bottom-right" as RootProps["zoomPosition"],
21
29
  };
22
30
 
23
- const markers = [
24
- {
25
- latitude: 45.527399,
26
- longitude: -73.598126,
27
- // icon: "Storefront",
28
- },
29
- {
30
- latitude: 45.527302,
31
- longitude: -73.597405,
32
- // icon: "CartPlusInside",
33
- },
34
- {
35
- latitude: 45.527302,
36
- longitude: -73.597405,
37
- // icon: "Brandy",
38
- },
39
- {
40
- latitude: 45.527302,
41
- longitude: -73.597405,
42
- // icon: "SpoonAndFork",
43
- },
44
- {
45
- latitude: 45.527302,
46
- longitude: -73.597405,
47
- // icon: "Coffee",
48
- },
49
- {
50
- latitude: 45.527302,
51
- longitude: -73.597405,
52
- // icon: "Baby",
53
- },
54
- {
55
- latitude: 45.527256,
56
- longitude: -73.600229,
57
- // icon: "Train",
58
- },
59
- {
60
- latitude: 45.527256,
61
- longitude: -73.600229,
62
- // icon: "Barbell",
63
- },
64
- {
65
- latitude: 45.526643,
66
- longitude: -73.600293,
67
- // icon: "GasPump",
68
- },
69
- {
70
- latitude: 45.527025,
71
- longitude: -73.600897,
72
- // icon: "Heartbeat",
73
- },
74
- ] as Marker[];
31
+ const Template: StoryFn<RootProps> = (args) => {
32
+ const [activeMarker, setActiveMarker] = useState<MarkerPoint | undefined>(undefined);
75
33
 
76
- const maptilerThemes = {
77
- day: "600d69cb-288d-445e-9839-3dfe4d76b31a",
78
- night: "dd191599-2a92-49fc-9a33-e12391753ad5",
79
- };
34
+ const handleMarkerClick = (markers: MarkerPoint) => {
35
+ if (
36
+ activeMarker?.latitude === markers.latitude &&
37
+ activeMarker?.longitude === markers.longitude
38
+ ) {
39
+ setActiveMarker(undefined);
40
+ return;
41
+ }
42
+
43
+ setActiveMarker(markers);
44
+ };
45
+
46
+ const popupContent = useMemo(() => {
47
+ if (typeof activeMarker?.markers === "undefined") {
48
+ return null;
49
+ }
50
+
51
+ if (activeMarker.markers.length === 1) {
52
+ return (
53
+ <>
54
+ {activeMarker.markers[0].icon && React.createElement(activeMarker.markers[0].icon)}
55
+ <strong>{activeMarker.markers[0]?.name}</strong>
56
+ <p>
57
+ {activeMarker?.latitude}, {activeMarker?.longitude}
58
+ </p>
59
+ </>
60
+ );
61
+ }
80
62
 
81
- const Template: StoryFn<RootProps> = (args) => (
82
- <div className="w-full h-[calc(100vh-30px)]">
83
- <Map.Root {...args}>
84
- <Map.BaseMap>
85
- <Map.Markers markers={markers} />
86
- </Map.BaseMap>
87
- </Map.Root>
88
- </div>
89
- );
63
+ if (activeMarker.markers.length > 1) {
64
+ return activeMarker.markers.map((marker) => (
65
+ <div key={marker.id} className="flex flex-row gap-y-3">
66
+ <button
67
+ className="rounded px-3 py-2 border border-primary-100"
68
+ onClick={() =>
69
+ handleMarkerClick({
70
+ id: marker.id,
71
+ latitude: marker.latitude,
72
+ longitude: marker.longitude,
73
+ markers: [marker],
74
+ })
75
+ }
76
+ >
77
+ {marker.icon && React.createElement(marker.icon, { className: "inline mr-5" })}
78
+ <strong>{marker.name}</strong>
79
+ </button>
80
+ </div>
81
+ ));
82
+ }
83
+
84
+ return null;
85
+ }, [activeMarker]);
86
+
87
+ return (
88
+ <div className="w-full h-[calc(100vh-30px)]">
89
+ <MapRoot {...args}>
90
+ <BaseMap>
91
+ <Layers sources={layerSources} />
92
+ <Markers markers={markerList} onClick={handleMarkerClick} />
93
+
94
+ <Popup
95
+ latitude={activeMarker?.latitude}
96
+ longitude={activeMarker?.longitude}
97
+ onClose={() => setActiveMarker(undefined)}
98
+ >
99
+ {popupContent}
100
+ </Popup>
101
+ </BaseMap>
102
+ </MapRoot>
103
+ </div>
104
+ );
105
+ };
90
106
 
91
107
  export const Maptiler = Template.bind({});
92
108
  Maptiler.args = {
93
109
  mapProvider: {
94
110
  name: "maptiler",
95
111
  apiKey: import.meta.env.VITE_MAPTILER_KEY,
96
- maptilerTheme: maptilerThemes.day,
112
+ maptilerTheme: themes.maptiler.day,
97
113
  },
98
114
  ...defaultValues,
99
115
  };