From 8a2e3bb4f285c968f0c7d2a88b0c65a8a9fc9d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Mon, 28 Oct 2024 15:41:09 +0100 Subject: [PATCH 01/12] feat(client): Add the Switch component --- client/package.json | 1 + client/src/components/ui/switch.tsx | 29 +++++++++++++++++++++++++++++ client/yarn.lock | 26 ++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 client/src/components/ui/switch.tsx diff --git a/client/package.json b/client/package.json index ea940d4..bbd693b 100644 --- a/client/package.json +++ b/client/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-radio-group": "1.2.1", "@radix-ui/react-separator": "1.1.0", "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-switch": "1.1.1", "@radix-ui/react-tooltip": "1.1.3", "@t3-oss/env-nextjs": "0.11.1", "@tanstack/react-query": "5.59.16", diff --git a/client/src/components/ui/switch.tsx b/client/src/components/ui/switch.tsx new file mode 100644 index 0000000..5edccf3 --- /dev/null +++ b/client/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as SwitchPrimitives from "@radix-ui/react-switch"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/client/yarn.lock b/client/yarn.lock index ecc7d67..40dab0f 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2487,6 +2487,31 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-switch@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-switch@npm:1.1.1" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-controllable-state": "npm:1.1.0" + "@radix-ui/react-use-previous": "npm:1.1.0" + "@radix-ui/react-use-size": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/8b61aa3bf80d3a2037d67495cf5de9e1ffc0d0843edc0cde5adc1ff1a9b99b0a6b63a85951c79769ab5a44d484611d90dc85933a86d71f28028caa53d8db177b + languageName: node + linkType: hard + "@radix-ui/react-tooltip@npm:1.1.3": version: 1.1.3 resolution: "@radix-ui/react-tooltip@npm:1.1.3" @@ -4245,6 +4270,7 @@ __metadata: "@radix-ui/react-radio-group": "npm:1.2.1" "@radix-ui/react-separator": "npm:1.1.0" "@radix-ui/react-slot": "npm:1.1.0" + "@radix-ui/react-switch": "npm:1.1.1" "@radix-ui/react-tooltip": "npm:1.1.3" "@svgr/webpack": "npm:8.1.0" "@t3-oss/env-nextjs": "npm:0.11.1" From 8408b74a0f85f608b8ab7d2a6fd53482bb38de06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Mon, 28 Oct 2024 15:41:53 +0100 Subject: [PATCH 02/12] feat(client): Display toggles for contextual layers --- .../src/components/panels/contextual-layers/item.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/src/components/panels/contextual-layers/item.tsx b/client/src/components/panels/contextual-layers/item.tsx index ee1468f..8965e74 100644 --- a/client/src/components/panels/contextual-layers/item.tsx +++ b/client/src/components/panels/contextual-layers/item.tsx @@ -1,3 +1,5 @@ +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; import { Dataset, Layer } from "@/types/generated/strapi.schemas"; interface ItemProps { @@ -11,8 +13,13 @@ const Item = ({ name, layers }: ItemProps) => {
{name}
    {layers.map((layer) => ( -
  • - {layer.name} +
  • + +
    + +
  • ))}
From 91f3f384d5817bf725d55af1f307fd8e383832f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Mon, 28 Oct 2024 16:04:47 +0100 Subject: [PATCH 03/12] feat(client): Connect toggles to URL --- .../panels/contextual-layers/item.tsx | 11 +++- client/src/hooks/use-map-layers.ts | 66 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 client/src/hooks/use-map-layers.ts diff --git a/client/src/components/panels/contextual-layers/item.tsx b/client/src/components/panels/contextual-layers/item.tsx index 8965e74..94090a7 100644 --- a/client/src/components/panels/contextual-layers/item.tsx +++ b/client/src/components/panels/contextual-layers/item.tsx @@ -1,5 +1,6 @@ import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; +import useMapLayers from "@/hooks/use-map-layers"; import { Dataset, Layer } from "@/types/generated/strapi.schemas"; interface ItemProps { @@ -8,6 +9,8 @@ interface ItemProps { } const Item = ({ name, layers }: ItemProps) => { + const [layersConfiguration, { addLayer, removeLayer }] = useMapLayers(); + return (
{name}
@@ -18,7 +21,13 @@ const Item = ({ name, layers }: ItemProps) => { {layer.name}
- + id === layer.id) !== -1} + onCheckedChange={(checked) => + checked ? addLayer(layer.id) : removeLayer(layer.id) + } + />
))} diff --git a/client/src/hooks/use-map-layers.ts b/client/src/hooks/use-map-layers.ts new file mode 100644 index 0000000..8ddbde1 --- /dev/null +++ b/client/src/hooks/use-map-layers.ts @@ -0,0 +1,66 @@ +import { parseAsArrayOf, parseAsJson, useQueryState } from "nuqs"; +import { useCallback } from "react"; +import { z } from "zod"; + +// order, visibility, opacity +const schema = z.object({ + id: z.number(), + visibility: z.boolean(), + opacity: z.number().int().min(0).max(100), +}); + +export default function useMapLayers() { + const [layers, setLayers] = useQueryState( + "layers", + parseAsArrayOf(parseAsJson(schema.parse)).withDefault([]), + ); + + const addLayer = useCallback( + (id: number) => { + setLayers((layers) => [ + { + id, + visibility: true, + opacity: 100, + }, + ...layers, + ]); + }, + [setLayers], + ); + + const removeLayer = useCallback( + (id: number) => { + setLayers((layers) => layers.filter((layer) => layer.id !== id)); + }, + [setLayers], + ); + + const updateLayer = useCallback( + (id: number, attributes: Partial>) => { + setLayers((layers) => + layers.map((layer) => (layer.id === id ? { ...layer, ...attributes } : layer)), + ); + }, + [setLayers], + ); + + const updateLayerOrder = useCallback( + (id: number, position: number) => { + setLayers((layers) => { + const newLayers = [...layers]; + const previousLayerPosition = layers.findIndex((layer) => layer.id === id); + + if (previousLayerPosition !== -1) { + const [layer] = newLayers.splice(previousLayerPosition, 1); + newLayers.splice(position, 0, layer); + } + + return newLayers; + }); + }, + [setLayers], + ); + + return [layers, { addLayer, removeLayer, updateLayer, updateLayerOrder }] as const; +} From 39d9cb142495c65dbf504c1a6758f369c597f316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Mon, 28 Oct 2024 17:05:08 +0100 Subject: [PATCH 04/12] feat(client): Add Collapsible component --- client/package.json | 1 + client/src/components/ui/collapsible.tsx | 27 ++++++++++++++++++++++++ client/tailwind.config.ts | 15 +++++++++++++ client/yarn.lock | 27 ++++++++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 client/src/components/ui/collapsible.tsx diff --git a/client/package.json b/client/package.json index bbd693b..c8395bd 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "dependencies": { "@artsy/fresnel": "7.1.4", "@radix-ui/react-checkbox": "1.1.2", + "@radix-ui/react-collapsible": "1.1.1", "@radix-ui/react-dialog": "1.1.2", "@radix-ui/react-label": "2.1.0", "@radix-ui/react-popover": "1.1.2", diff --git a/client/src/components/ui/collapsible.tsx b/client/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..a9bcf1e --- /dev/null +++ b/client/src/components/ui/collapsible.tsx @@ -0,0 +1,27 @@ +"use client"; + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; +import React from "react"; + +import { cn } from "@/lib/utils"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index b8f5340..97244aa 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -54,6 +54,21 @@ const config: Config = { ring: "hsl(var(--sidebar-ring))", }, }, + // From https://github.com/shadcn-ui/ui/issues/2053#issuecomment-1902542088 + keyframes: { + "collapsible-down": { + from: { height: "0" }, + to: { height: "var(--radix-collapsible-content-height)" }, + }, + "collapsible-up": { + from: { height: "var(--radix-collapsible-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "collapsible-down": "collapsible-down 0.2s ease-out", + "collapsible-up": "collapsible-up 0.2s ease-out", + }, }, }, extend: {}, diff --git a/client/yarn.lock b/client/yarn.lock index 40dab0f..af3269a 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2081,6 +2081,32 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-collapsible@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-collapsible@npm:1.1.1" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.1" + "@radix-ui/react-id": "npm:1.1.0" + "@radix-ui/react-presence": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-controllable-state": "npm:1.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/e3a510c8f3a31709add35c31e3e108a2bc4db2df06e9e50cb5f25144b1cf9596b8118ad2618f851fa7c1498e057938f641a842a6770b5b7b6cd068cd2b4914f1 + languageName: node + linkType: hard + "@radix-ui/react-collection@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-collection@npm:1.1.0" @@ -4264,6 +4290,7 @@ __metadata: dependencies: "@artsy/fresnel": "npm:7.1.4" "@radix-ui/react-checkbox": "npm:1.1.2" + "@radix-ui/react-collapsible": "npm:1.1.1" "@radix-ui/react-dialog": "npm:1.1.2" "@radix-ui/react-label": "npm:2.1.0" "@radix-ui/react-popover": "npm:1.1.2" From b7e1830f3d5356755a41e1c0085c572a2789ab89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Mon, 28 Oct 2024 17:07:03 +0100 Subject: [PATCH 05/12] feat(client): Add legend controls on map --- client/src/components/map/controls/index.tsx | 10 ++-- client/src/components/map/controls/legend.tsx | 46 +++++++++++++++++++ client/src/components/map/legend/index.tsx | 5 ++ client/src/components/ui/button.tsx | 2 + client/src/components/ui/collapsible.tsx | 2 +- client/src/components/ui/popover.tsx | 2 +- client/src/svgs/list-bullet.svg | 6 +++ 7 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 client/src/components/map/controls/legend.tsx create mode 100644 client/src/components/map/legend/index.tsx create mode 100644 client/src/svgs/list-bullet.svg diff --git a/client/src/components/map/controls/index.tsx b/client/src/components/map/controls/index.tsx index fa77efe..611b1b1 100644 --- a/client/src/components/map/controls/index.tsx +++ b/client/src/components/map/controls/index.tsx @@ -1,16 +1,20 @@ import ContextualLayersControls from "./contextual-layers"; +import LegendControls from "./legend"; import MapSettingsControls from "./map-settings"; import ZoomControls from "./zoom"; const Controls = () => { return ( <> -
+
-
+
- +
+ + +
); diff --git a/client/src/components/map/controls/legend.tsx b/client/src/components/map/controls/legend.tsx new file mode 100644 index 0000000..a14cf76 --- /dev/null +++ b/client/src/components/map/controls/legend.tsx @@ -0,0 +1,46 @@ +import Legend from "@/components/map/legend"; +import { Button } from "@/components/ui/button"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Media, MediaContextProvider } from "@/media"; +import ChevronDownIcon from "@/svgs/chevron-down.svg"; +import ListBulletIcon from "@/svgs/list-bullet.svg"; + +const LegendControls = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default LegendControls; diff --git a/client/src/components/map/legend/index.tsx b/client/src/components/map/legend/index.tsx new file mode 100644 index 0000000..211f39d --- /dev/null +++ b/client/src/components/map/legend/index.tsx @@ -0,0 +1,5 @@ +const Legend = () => { + return
Coming soon!
; +}; + +export default Legend; diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index fe2446f..42c6f2b 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -13,6 +13,8 @@ const buttonVariants = cva( "bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 ring-offset-white focus-visible:ring-casper-blue-400", yellow: "bg-supernova-yellow-400 hover:bg-supernova-yellow-300 text-casper-blue-950 focus-visible:ring-casper-blue-400 data-[state=open]:bg-rhino-blue-900 data-[state=open]:hover:bg-rhino-blue-950 data-[state=open]:text-supernova-yellow-400", + "yellow-alt": + "bg-supernova-yellow-400 hover:bg-supernova-yellow-300 text-casper-blue-950 focus-visible:ring-casper-blue-400 data-[state=open]:bg-white data-[state=open]:hover:bg-casper-blue-200", ghost: "bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 ring-offset-white focus-visible:ring-casper-blue-400", }, diff --git a/client/src/components/ui/collapsible.tsx b/client/src/components/ui/collapsible.tsx index a9bcf1e..17213df 100644 --- a/client/src/components/ui/collapsible.tsx +++ b/client/src/components/ui/collapsible.tsx @@ -15,7 +15,7 @@ const CollapsibleContent = React.forwardRef< >(({ className, ...props }, ref) => ( , React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 26, ...props }, ref) => ( +>(({ className, align = "center", sideOffset = 24, ...props }, ref) => ( + + + + From af7164761855dcbd1d4c7a7d36edc2d3bf38471a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Thu, 31 Oct 2024 13:27:02 +0100 Subject: [PATCH 06/12] fix(client): Style of the Switch --- client/src/components/ui/switch.tsx | 2 +- .../documentation/documentation/1.0.0/full_documentation.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/ui/switch.tsx b/client/src/components/ui/switch.tsx index 5edccf3..6edbc0f 100644 --- a/client/src/components/ui/switch.tsx +++ b/client/src/components/ui/switch.tsx @@ -19,7 +19,7 @@ const Switch = React.forwardRef< > diff --git a/cms/src/extensions/documentation/documentation/1.0.0/full_documentation.json b/cms/src/extensions/documentation/documentation/1.0.0/full_documentation.json index 997cbba..5dbcbd7 100644 --- a/cms/src/extensions/documentation/documentation/1.0.0/full_documentation.json +++ b/cms/src/extensions/documentation/documentation/1.0.0/full_documentation.json @@ -14,7 +14,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-generation-date": "2024-10-31T09:42:41.784Z" + "x-generation-date": "2024-10-31T12:25:30.950Z" }, "x-strapi-config": { "path": "/documentation", From 4732df1e87e5ca04cb80631449d7f3d642361fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Thu, 31 Oct 2024 17:14:41 +0100 Subject: [PATCH 07/12] feat(client): Implement logic to render layers on the map --- client/package.json | 3 + client/src/components/map/index.tsx | 2 + .../components/map/layer-manager/index.tsx | 48 +++ .../src/components/map/layer-manager/item.tsx | 28 ++ client/src/hooks/use-layer-config.ts | 92 ++++++ client/src/hooks/use-map-layers.ts | 12 +- client/src/types/layer.ts | 22 ++ client/yarn.lock | 296 +++++++++++++++++- 8 files changed, 496 insertions(+), 7 deletions(-) create mode 100644 client/src/components/map/layer-manager/index.tsx create mode 100644 client/src/components/map/layer-manager/item.tsx create mode 100644 client/src/hooks/use-layer-config.ts create mode 100644 client/src/types/layer.ts diff --git a/client/package.json b/client/package.json index c8395bd..d9b4ccd 100644 --- a/client/package.json +++ b/client/package.json @@ -13,6 +13,9 @@ }, "dependencies": { "@artsy/fresnel": "7.1.4", + "@deck.gl/core": "9.0.34", + "@deck.gl/json": "9.0.34", + "@deck.gl/layers": "9.0.34", "@radix-ui/react-checkbox": "1.1.2", "@radix-ui/react-collapsible": "1.1.1", "@radix-ui/react-dialog": "1.1.2", diff --git a/client/src/components/map/index.tsx b/client/src/components/map/index.tsx index e852e00..55e5faa 100644 --- a/client/src/components/map/index.tsx +++ b/client/src/components/map/index.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactMapGL from "react-map-gl"; +import LayerManager from "@/components/map/layer-manager"; import { SIDEBAR_WIDTH } from "@/components/ui/sidebar"; import { env } from "@/env"; import useApplyMapSettings from "@/hooks/use-apply-map-settings"; @@ -72,6 +73,7 @@ const Map = () => { logoPosition="bottom-right" onLoad={() => setMap(mapRef.current)} > + ); diff --git a/client/src/components/map/layer-manager/index.tsx b/client/src/components/map/layer-manager/index.tsx new file mode 100644 index 0000000..789ebf1 --- /dev/null +++ b/client/src/components/map/layer-manager/index.tsx @@ -0,0 +1,48 @@ +import { useMemo } from "react"; +import { Layer } from "react-map-gl"; + +import useMapLayers from "@/hooks/use-map-layers"; + +import LayerManagerItem from "./item"; + +const LayerManager = () => { + const [layers] = useMapLayers(); + + /** + * These layers are here to aid with positioning the real data layers between themselves. + * See more: https://github.com/visgl/react-map-gl/issues/939#issuecomment-625290200 + */ + const positioningLayers = useMemo(() => { + return layers.map((layer, index) => { + const beforeId = index === 0 ? "data-layers" : `layer-position-${layers[index - 1].id}`; + return ( + + ); + }); + }, [layers]); + + const layerManagerItems = useMemo(() => { + return layers.map((layer, index) => { + const beforeId = index === 0 ? "data-layers" : `layer-position-${layers[index - 1].id}`; + const { id, ...settings } = layer; + return ( + + ); + }); + }, [layers]); + + return ( + <> + {positioningLayers} + {layerManagerItems} + + ); +}; + +export default LayerManager; diff --git a/client/src/components/map/layer-manager/item.tsx b/client/src/components/map/layer-manager/item.tsx new file mode 100644 index 0000000..73f78e5 --- /dev/null +++ b/client/src/components/map/layer-manager/item.tsx @@ -0,0 +1,28 @@ +import { Layer, Source } from "react-map-gl"; + +import useLayerConfig from "@/hooks/use-layer-config"; +import { LayerSettings } from "@/types/layer"; + +interface LayerManagerItemProps { + id: number; + beforeId: string; + settings: LayerSettings; +} + +const LayerManagerItem = ({ id, beforeId, settings }: LayerManagerItemProps) => { + const config = useLayerConfig(id, settings); + + if (!config) { + return null; + } + + return ( + + {config.styles.map((style) => ( + + ))} + + ); +}; + +export default LayerManagerItem; diff --git a/client/src/hooks/use-layer-config.ts b/client/src/hooks/use-layer-config.ts new file mode 100644 index 0000000..27090cc --- /dev/null +++ b/client/src/hooks/use-layer-config.ts @@ -0,0 +1,92 @@ +import { JSONConverter } from "@deck.gl/json"; +import { useMemo } from "react"; + +import { useGetLayersId } from "@/types/generated/layer"; +import { + LayerConfig, + LayerParamsConfig, + LayerResolvedParamsConfig, + LayerSettings, +} from "@/types/layer"; + +const resolveLayerParamsConfig = ( + paramsConfig: LayerParamsConfig, + settings: LayerSettings, +): LayerResolvedParamsConfig => { + return paramsConfig.reduce((res, param) => { + const hasSettings = (key: string): key is keyof LayerSettings => { + return key in settings; + }; + + // No setting for this param, we just keep the default value + if (!hasSettings(param.key)) { + return { ...res, [param.key]: param.default }; + } + + // We replace the default by the setting's value + return { + ...res, + [param.key]: settings[param.key] ?? param.default, + }; + }, {}); +}; + +const resolveLayerConfig = ( + config: LayerConfig, + resolvedParamsConfig: LayerResolvedParamsConfig, +): LayerConfig => { + const converter = new JSONConverter({ + configuration: { + functions: { + setOpacity({ o = 1, base = 1 }: { o: number; base: number }) { + return o * base; + }, + setVisibility({ v }: { v: boolean }) { + return v ? "visible" : "none"; + }, + }, + enumerations: { + params: resolvedParamsConfig, + }, + }, + }); + + return converter.convertJson(config); +}; + +export default function useLayerConfig(layerId: number, settings: LayerSettings) { + const { data, isLoading } = useGetLayersId(layerId, { + query: { + select: (data) => { + if (!data?.data) { + return undefined; + } + + const { params_config: paramsConfig, mapbox_config: config } = data.data.attributes!; + + return { + paramsConfig, + config, + } as { paramsConfig: LayerParamsConfig; config: LayerConfig }; + }, + }, + }); + + const resolvedParamsConfig = useMemo(() => { + if (isLoading || !data) { + return undefined; + } + + return resolveLayerParamsConfig(data.paramsConfig, settings); + }, [data, isLoading, settings]); + + const resolvedConfig = useMemo(() => { + if (isLoading || !data || !resolvedParamsConfig) { + return undefined; + } + + return resolveLayerConfig(data.config, resolvedParamsConfig); + }, [data, isLoading, resolvedParamsConfig]); + + return resolvedConfig; +} diff --git a/client/src/hooks/use-map-layers.ts b/client/src/hooks/use-map-layers.ts index 8ddbde1..c55cbb9 100644 --- a/client/src/hooks/use-map-layers.ts +++ b/client/src/hooks/use-map-layers.ts @@ -2,11 +2,12 @@ import { parseAsArrayOf, parseAsJson, useQueryState } from "nuqs"; import { useCallback } from "react"; import { z } from "zod"; -// order, visibility, opacity +import { LayerSettings } from "@/types/layer"; + const schema = z.object({ id: z.number(), visibility: z.boolean(), - opacity: z.number().int().min(0).max(100), + opacity: z.number().min(0).max(1), }); export default function useMapLayers() { @@ -21,7 +22,7 @@ export default function useMapLayers() { { id, visibility: true, - opacity: 100, + opacity: 1, }, ...layers, ]); @@ -62,5 +63,8 @@ export default function useMapLayers() { [setLayers], ); - return [layers, { addLayer, removeLayer, updateLayer, updateLayerOrder }] as const; + return [ + layers as (LayerSettings & { id: number })[], + { addLayer, removeLayer, updateLayer, updateLayerOrder }, + ] as const; } diff --git a/client/src/types/layer.ts b/client/src/types/layer.ts new file mode 100644 index 0000000..69ab1b5 --- /dev/null +++ b/client/src/types/layer.ts @@ -0,0 +1,22 @@ +import { AnyLayer, AnySource, SkyLayer } from "react-map-gl"; + +export interface LayerSettings { + visibility: boolean; + opacity: number; +} + +export interface LayerConfig { + source: AnySource; + styles: Exclude[]; +} + +export interface LayerParamsConfigValue { + key: string; + default: unknown; +} + +export type LayerParamsConfig = LayerParamsConfigValue[]; + +export interface LayerResolvedParamsConfig { + [key: string]: unknown; +} diff --git a/client/yarn.lock b/client/yarn.lock index af3269a..90a66a1 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1316,6 +1316,61 @@ __metadata: languageName: node linkType: hard +"@deck.gl/core@npm:9.0.34": + version: 9.0.34 + resolution: "@deck.gl/core@npm:9.0.34" + dependencies: + "@loaders.gl/core": "npm:^4.2.0" + "@loaders.gl/images": "npm:^4.2.0" + "@luma.gl/constants": "npm:~9.0.27" + "@luma.gl/core": "npm:~9.0.27" + "@luma.gl/engine": "npm:~9.0.27" + "@luma.gl/shadertools": "npm:~9.0.27" + "@luma.gl/webgl": "npm:~9.0.27" + "@math.gl/core": "npm:^4.0.0" + "@math.gl/sun": "npm:^4.0.0" + "@math.gl/web-mercator": "npm:^4.0.0" + "@probe.gl/env": "npm:^4.0.9" + "@probe.gl/log": "npm:^4.0.9" + "@probe.gl/stats": "npm:^4.0.9" + "@types/offscreencanvas": "npm:^2019.6.4" + gl-matrix: "npm:^3.0.0" + mjolnir.js: "npm:^2.7.0" + checksum: 10c0/13983e5e742ac2e38969ec941b179f193d384d93f0b51ab724e292653af36c3f3953b027554276aae86a4048d36ad8aaaa33cf5cb9d1b0cf384e0a3b6b95f4b7 + languageName: node + linkType: hard + +"@deck.gl/json@npm:9.0.34": + version: 9.0.34 + resolution: "@deck.gl/json@npm:9.0.34" + dependencies: + jsep: "npm:^0.3.0" + peerDependencies: + "@deck.gl/core": ^9.0.0 + checksum: 10c0/bafda5c8edcd96f768753fb316f6b4416ec5f0300216b6856f81fcc06818580bded25fd55b111b1f8e14f842edf75fb394848334014e72cdf9ff81f3cfb97730 + languageName: node + linkType: hard + +"@deck.gl/layers@npm:9.0.34": + version: 9.0.34 + resolution: "@deck.gl/layers@npm:9.0.34" + dependencies: + "@loaders.gl/images": "npm:^4.2.0" + "@loaders.gl/schema": "npm:^4.2.0" + "@mapbox/tiny-sdf": "npm:^2.0.5" + "@math.gl/core": "npm:^4.0.0" + "@math.gl/polygon": "npm:^4.0.0" + "@math.gl/web-mercator": "npm:^4.0.0" + earcut: "npm:^2.2.4" + peerDependencies: + "@deck.gl/core": ^9.0.0 + "@loaders.gl/core": ^4.2.0 + "@luma.gl/core": ~9.0.0 + "@luma.gl/engine": ~9.0.0 + checksum: 10c0/16373b0674989ac24936b7fda9874cfa41e72ad868867cdd0585e3d9fd1aed0a9f6feefd57eeeb3bd834ec81c3f5ebd7310252411bb10b7936d09e264e169d74 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/aix-ppc64@npm:0.24.0" @@ -1702,6 +1757,122 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/core@npm:^4.2.0": + version: 4.3.2 + resolution: "@loaders.gl/core@npm:4.3.2" + dependencies: + "@loaders.gl/loader-utils": "npm:4.3.2" + "@loaders.gl/schema": "npm:4.3.2" + "@loaders.gl/worker-utils": "npm:4.3.2" + "@probe.gl/log": "npm:^4.0.2" + checksum: 10c0/3dbc564707996f376221e49f6ceaa53c8ce1b8026af591cbdce5c6eb87ffaeddc5e4174c0f4c4f2b589c5c586a4fa0045b6891d196ea3a6929d9a8ce1a6ae9e5 + languageName: node + linkType: hard + +"@loaders.gl/images@npm:^4.2.0": + version: 4.3.2 + resolution: "@loaders.gl/images@npm:4.3.2" + dependencies: + "@loaders.gl/loader-utils": "npm:4.3.2" + peerDependencies: + "@loaders.gl/core": ^4.3.0 + checksum: 10c0/2fc2f32880436dbf28d14b642fe2007b41372951b65248e5c5c4dd1bc631236af387b0561b4f5fb2ad85fe72852be79b26c91fa206980aa8d57465d21ba332b3 + languageName: node + linkType: hard + +"@loaders.gl/loader-utils@npm:4.3.2": + version: 4.3.2 + resolution: "@loaders.gl/loader-utils@npm:4.3.2" + dependencies: + "@loaders.gl/schema": "npm:4.3.2" + "@loaders.gl/worker-utils": "npm:4.3.2" + "@probe.gl/log": "npm:^4.0.2" + "@probe.gl/stats": "npm:^4.0.2" + peerDependencies: + "@loaders.gl/core": ^4.3.0 + checksum: 10c0/bbaeb362c63b760569323c50ad3a695737fdc93f5ef68c594aa35668ebb35a8332f53cd0cb74c49cc5cf026b9fad4f889c2efde0dc294a269c10c6b304ec5f0c + languageName: node + linkType: hard + +"@loaders.gl/schema@npm:4.3.2, @loaders.gl/schema@npm:^4.2.0": + version: 4.3.2 + resolution: "@loaders.gl/schema@npm:4.3.2" + dependencies: + "@types/geojson": "npm:^7946.0.7" + peerDependencies: + "@loaders.gl/core": ^4.3.0 + checksum: 10c0/6ba7f1b60b1c746f71a62e0dc627a13e1f7ebedb338630ce8b00972ef8c67dae614bc698be35618f4bc02c7d2536b2e686329af6076ed22ddddaf2269fc5efd1 + languageName: node + linkType: hard + +"@loaders.gl/worker-utils@npm:4.3.2": + version: 4.3.2 + resolution: "@loaders.gl/worker-utils@npm:4.3.2" + peerDependencies: + "@loaders.gl/core": ^4.3.0 + checksum: 10c0/53a2b595f6e85786fb7233e28239dc49b52fb08278d93c85a3f01d9315dcd9404aa0d3458ec586768a43894d7ba1a32e14f10e11a8308dc1c6baed63a06bd8da + languageName: node + linkType: hard + +"@luma.gl/constants@npm:9.0.27, @luma.gl/constants@npm:~9.0.27": + version: 9.0.27 + resolution: "@luma.gl/constants@npm:9.0.27" + checksum: 10c0/3c7c1c56373c4c2bccf2c12cdd5cb3ae9ade2b6040d66ebef2dd45c7cb564ecdd9555f6f03690d736dbe337f25500863baa9468226f96618b3e930e3ecf8adf0 + languageName: node + linkType: hard + +"@luma.gl/core@npm:~9.0.27": + version: 9.0.27 + resolution: "@luma.gl/core@npm:9.0.27" + dependencies: + "@math.gl/types": "npm:^4.0.0" + "@probe.gl/env": "npm:^4.0.2" + "@probe.gl/log": "npm:^4.0.2" + "@probe.gl/stats": "npm:^4.0.2" + "@types/offscreencanvas": "npm:^2019.6.4" + checksum: 10c0/e61f52042cc368fcbb80eaa43b6bfe62c7a9c57c2bc05d42b4c8c33b9e957a710566c3f2237112150754f7ee1de346a04123bb19cfa5477aa892c109d5cf9cf4 + languageName: node + linkType: hard + +"@luma.gl/engine@npm:~9.0.27": + version: 9.0.27 + resolution: "@luma.gl/engine@npm:9.0.27" + dependencies: + "@luma.gl/shadertools": "npm:9.0.27" + "@math.gl/core": "npm:^4.0.0" + "@probe.gl/log": "npm:^4.0.2" + "@probe.gl/stats": "npm:^4.0.2" + peerDependencies: + "@luma.gl/core": ^9.0.0 + checksum: 10c0/810499cb07ec33c7443b5d318a23d8c792ba45cd74267bac104114247af8b945c4e28e2cc3927b1d2ae3f64c1f9b92be554be34c1057c64ed7bcafdb7798845d + languageName: node + linkType: hard + +"@luma.gl/shadertools@npm:9.0.27, @luma.gl/shadertools@npm:~9.0.27": + version: 9.0.27 + resolution: "@luma.gl/shadertools@npm:9.0.27" + dependencies: + "@math.gl/core": "npm:^4.0.0" + "@math.gl/types": "npm:^4.0.0" + wgsl_reflect: "npm:^1.0.1" + peerDependencies: + "@luma.gl/core": ^9.0.0 + checksum: 10c0/36b4206c2c23e3aa3918f8504ec5f606bd20656b936086a39bdbf7cf4ba1e1a6fa59fa39decc21c16be00fd7d8ff5037c27163ec600c7d3fe986c7a8204ddd9c + languageName: node + linkType: hard + +"@luma.gl/webgl@npm:~9.0.27": + version: 9.0.27 + resolution: "@luma.gl/webgl@npm:9.0.27" + dependencies: + "@luma.gl/constants": "npm:9.0.27" + "@probe.gl/env": "npm:^4.0.2" + peerDependencies: + "@luma.gl/core": ^9.0.0 + checksum: 10c0/01ee92d421932c95c71db485b73988c1bc5daf7bb444e428acda37d71c59835b132ae0d360b7c22732fe3d82bf8db02a133c208befa6a4346f8526bae227dc7d + languageName: node + linkType: hard + "@mapbox/jsonlint-lines-primitives@npm:^2.0.2, @mapbox/jsonlint-lines-primitives@npm:~2.0.2": version: 2.0.2 resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.2" @@ -1723,7 +1894,7 @@ __metadata: languageName: node linkType: hard -"@mapbox/tiny-sdf@npm:^2.0.6": +"@mapbox/tiny-sdf@npm:^2.0.5, @mapbox/tiny-sdf@npm:^2.0.6": version: 2.0.6 resolution: "@mapbox/tiny-sdf@npm:2.0.6" checksum: 10c0/cb272578a30c88d6694937af9b084106aa251e92c71089e7d57b0df8152fd0ce0598d5816182a4cd478dc40b188ea680cb6d53f4385107719424beabe7ed4e13 @@ -1771,6 +1942,47 @@ __metadata: languageName: node linkType: hard +"@math.gl/core@npm:4.1.0, @math.gl/core@npm:^4.0.0": + version: 4.1.0 + resolution: "@math.gl/core@npm:4.1.0" + dependencies: + "@math.gl/types": "npm:4.1.0" + checksum: 10c0/495934dc2be0b60cd6ff2cc16a0215608c9254919db741a0074b6b41cef9a0543c7f790eda7d529afa102d2937490608ef75fcc64c789ef2876ae750fd0ed3d6 + languageName: node + linkType: hard + +"@math.gl/polygon@npm:^4.0.0": + version: 4.1.0 + resolution: "@math.gl/polygon@npm:4.1.0" + dependencies: + "@math.gl/core": "npm:4.1.0" + checksum: 10c0/0fcfb489c5613ddff6dd0cbea65084e10fa9a3523fb87a36a4fdf10057d12d2a99f1ebd93da6e72b45db0783fb8cd9cff704765473372a8db580faaf50c85ab5 + languageName: node + linkType: hard + +"@math.gl/sun@npm:^4.0.0": + version: 4.1.0 + resolution: "@math.gl/sun@npm:4.1.0" + checksum: 10c0/baf813f3134124a1701c5f64cc8725fdb1d8d4c9e47c7fbd0d2e960d823f776d35312f82c62a09dcb140ea31d42b3945aa781066495291df190566ae17b5857b + languageName: node + linkType: hard + +"@math.gl/types@npm:4.1.0, @math.gl/types@npm:^4.0.0": + version: 4.1.0 + resolution: "@math.gl/types@npm:4.1.0" + checksum: 10c0/3c4dfa5ac5c9e2cef24d31f56b89c1dde785a5d70fd1a7030386346cb7dd4fa2cce5ba983b89842c1971492e30870dd22a078d64893f9c66887e38367bf992fa + languageName: node + linkType: hard + +"@math.gl/web-mercator@npm:^4.0.0": + version: 4.1.0 + resolution: "@math.gl/web-mercator@npm:4.1.0" + dependencies: + "@math.gl/core": "npm:4.1.0" + checksum: 10c0/7aa4921b9442da75664ef517f41de65b1eae9970d7eee61d14d2eb0b332e1af6203785e8d198376a8ab5924d62b24856d648ff6478cca95b14d3a8d82822ef93 + languageName: node + linkType: hard + "@next/env@npm:14.2.15": version: 14.2.15 resolution: "@next/env@npm:14.2.15" @@ -2029,6 +2241,29 @@ __metadata: languageName: node linkType: hard +"@probe.gl/env@npm:4.0.9, @probe.gl/env@npm:^4.0.2, @probe.gl/env@npm:^4.0.9": + version: 4.0.9 + resolution: "@probe.gl/env@npm:4.0.9" + checksum: 10c0/c6fcd1742aea014d15fe36a6cf0724d7faf3eeda27856978d87c1658b26ceaefc86254b011de65de35c1dfc0e3074fdbeaef5fd4362a05541a5e46b880e4024f + languageName: node + linkType: hard + +"@probe.gl/log@npm:^4.0.2, @probe.gl/log@npm:^4.0.9": + version: 4.0.9 + resolution: "@probe.gl/log@npm:4.0.9" + dependencies: + "@probe.gl/env": "npm:4.0.9" + checksum: 10c0/23521b46fdda80470d8b38d70c62d77f3b50257a63b3e7660655936593cf5f54ec7f1e2b0a36e8ecb7635c7fd45280bb66bd379a3e58afbde99f23d46f43c112 + languageName: node + linkType: hard + +"@probe.gl/stats@npm:^4.0.2, @probe.gl/stats@npm:^4.0.9": + version: 4.0.9 + resolution: "@probe.gl/stats@npm:4.0.9" + checksum: 10c0/23c205232a45941b13d1e87efe060d81f3a09339b0294991fc9fdf6dd9d090a9e6c79cccf23ce8f09e5c14aa44ef128836639490df22729a13214b0ff3c7cd80 + languageName: node + linkType: hard + "@radix-ui/primitive@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/primitive@npm:1.1.0" @@ -3212,13 +3447,20 @@ __metadata: languageName: node linkType: hard -"@types/geojson@npm:*, @types/geojson@npm:^7946.0.14": +"@types/geojson@npm:*, @types/geojson@npm:^7946.0.14, @types/geojson@npm:^7946.0.7": version: 7946.0.14 resolution: "@types/geojson@npm:7946.0.14" checksum: 10c0/54f3997708fa2970c03eeb31f7e4540a0eb6387b15e9f8a60513a1409c23cafec8d618525404573468b59c6fecbfd053724b3327f7fca416729c26271d799f55 languageName: node linkType: hard +"@types/hammerjs@npm:^2.0.41": + version: 2.0.46 + resolution: "@types/hammerjs@npm:2.0.46" + checksum: 10c0/f3c1cb20dc2f0523f7b8c76065078544d50d8ae9b0edc1f62fed657210ed814266ff2dfa835d2c157a075991001eec3b64c88bf92e3e6e895c0db78d05711d06 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.7": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -3278,6 +3520,13 @@ __metadata: languageName: node linkType: hard +"@types/offscreencanvas@npm:^2019.6.4": + version: 2019.7.3 + resolution: "@types/offscreencanvas@npm:2019.7.3" + checksum: 10c0/6d1dfae721d321cd2b5435f347a0e53b09f33b2f9e9333396480f592823bc323847b8169f7d251d2285cb93dbc1ba2e30741ac5cf4b1c003d660fd4c24526963 + languageName: node + linkType: hard + "@types/pbf@npm:*, @types/pbf@npm:^3.0.5": version: 3.0.5 resolution: "@types/pbf@npm:3.0.5" @@ -4289,6 +4538,9 @@ __metadata: resolution: "client@workspace:." dependencies: "@artsy/fresnel": "npm:7.1.4" + "@deck.gl/core": "npm:9.0.34" + "@deck.gl/json": "npm:9.0.34" + "@deck.gl/layers": "npm:9.0.34" "@radix-ui/react-checkbox": "npm:1.1.2" "@radix-ui/react-collapsible": "npm:1.1.1" "@radix-ui/react-dialog": "npm:1.1.2" @@ -4850,6 +5102,13 @@ __metadata: languageName: node linkType: hard +"earcut@npm:^2.2.4": + version: 2.2.4 + resolution: "earcut@npm:2.2.4" + checksum: 10c0/01ca51830edd2787819f904ae580087d37351f6048b4565e7add4b3da8a86b7bc19262ab2aa7fdc64129ab03af2d9cec8cccee4d230c82275f97ef285c79aafb + languageName: node + linkType: hard + "earcut@npm:^3.0.0": version: 3.0.0 resolution: "earcut@npm:3.0.0" @@ -6079,7 +6338,7 @@ __metadata: languageName: node linkType: hard -"gl-matrix@npm:^3.4.3": +"gl-matrix@npm:^3.0.0, gl-matrix@npm:^3.4.3": version: 3.4.3 resolution: "gl-matrix@npm:3.4.3" checksum: 10c0/c8ee6e2ce2d089b4ba4ae13ec9d4cb99bf2abe5f68f0cb08d94bbd8bafbec13aacc7230b86539ce5ca01b79226ea8c3194f971f5ca0c81838bc5e4e619dc398e @@ -6219,6 +6478,13 @@ __metadata: languageName: node linkType: hard +"hammerjs@npm:^2.0.8": + version: 2.0.8 + resolution: "hammerjs@npm:2.0.8" + checksum: 10c0/5c95e5774b5ea49492cb3fa8f1949aea67048a0b84af33acb555e7139abfcf3c83aca2b83e0c5008755bc230166df7b5e469d1e3eb6746c48f215f3672609fed + languageName: node + linkType: hard + "has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": version: 1.0.2 resolution: "has-bigints@npm:1.0.2" @@ -6893,6 +7159,13 @@ __metadata: languageName: node linkType: hard +"jsep@npm:^0.3.0": + version: 0.3.5 + resolution: "jsep@npm:0.3.5" + checksum: 10c0/fb5def7a4ba1cee41d144ebdd0d477785dc84b6bc1fed6cf5169f106de980dbe363bf99cb36a450435d7fd952d22b1d76e1609aeb5c7e7cbbbdb6d15fad03614 + languageName: node + linkType: hard + "jsep@npm:^1.1.2, jsep@npm:^1.2.0": version: 1.3.9 resolution: "jsep@npm:1.3.9" @@ -7502,6 +7775,16 @@ __metadata: languageName: node linkType: hard +"mjolnir.js@npm:^2.7.0": + version: 2.7.3 + resolution: "mjolnir.js@npm:2.7.3" + dependencies: + "@types/hammerjs": "npm:^2.0.41" + hammerjs: "npm:^2.0.8" + checksum: 10c0/82f86dedfd410a640e5d0ef6f04e1d72ab292e61d42434bc56f103670ada45b2aacf130527f434d7d4bd0b8d2d5bc82bd36850e7586f1b70ebb72da2fe2c72b2 + languageName: node + linkType: hard + "mkdirp@npm:^1.0.3": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -10182,6 +10465,13 @@ __metadata: languageName: node linkType: hard +"wgsl_reflect@npm:^1.0.1": + version: 1.0.14 + resolution: "wgsl_reflect@npm:1.0.14" + checksum: 10c0/5e292b2fa4bae694b46d93a0642d9bdace2a27a8f0e6f1faa16a9fbab9613a58c7947bf79ec6112479ff5cf01bd9752039f5ac48d915781b4f5c393d9d709622 + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" From b389a772df4be0950f9396e5b8b8bc3e18a3b978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Fri, 1 Nov 2024 09:13:34 +0100 Subject: [PATCH 08/12] feat(client): Only show the legend when necessary --- client/src/components/map/controls/index.tsx | 13 ++++++++++++- client/src/components/map/controls/legend.tsx | 11 +++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/client/src/components/map/controls/index.tsx b/client/src/components/map/controls/index.tsx index 611b1b1..b9c9228 100644 --- a/client/src/components/map/controls/index.tsx +++ b/client/src/components/map/controls/index.tsx @@ -1,12 +1,23 @@ +import useMapLayers from "@/hooks/use-map-layers"; +import { cn } from "@/lib/utils"; + import ContextualLayersControls from "./contextual-layers"; import LegendControls from "./legend"; import MapSettingsControls from "./map-settings"; import ZoomControls from "./zoom"; const Controls = () => { + const [layers] = useMapLayers(); + return ( <> -
+
0, + "bottom-[80px]": layers.length === 0, + })} + >
diff --git a/client/src/components/map/controls/legend.tsx b/client/src/components/map/controls/legend.tsx index a14cf76..90b4c70 100644 --- a/client/src/components/map/controls/legend.tsx +++ b/client/src/components/map/controls/legend.tsx @@ -2,14 +2,21 @@ import Legend from "@/components/map/legend"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import useMapLayers from "@/hooks/use-map-layers"; import { Media, MediaContextProvider } from "@/media"; import ChevronDownIcon from "@/svgs/chevron-down.svg"; import ListBulletIcon from "@/svgs/list-bullet.svg"; const LegendControls = () => { + const [layers] = useMapLayers(); + + if (layers.length === 0) { + return null; + } + return ( - + - + @@ -35,13 +39,13 @@ const LegendControls = () => { - + diff --git a/client/src/components/map/legend/index.tsx b/client/src/components/map/legend/index.tsx index 211f39d..b3ab3ef 100644 --- a/client/src/components/map/legend/index.tsx +++ b/client/src/components/map/legend/index.tsx @@ -1,5 +1,17 @@ +import { useMemo } from "react"; + +import useMapLayers from "@/hooks/use-map-layers"; + +import LegendItem from "./item"; + const Legend = () => { - return
Coming soon!
; + const [layers] = useMapLayers(); + + const legendItems = useMemo(() => { + return layers.map(({ id, ...settings }) => ); + }, [layers]); + + return legendItems; }; export default Legend; diff --git a/client/src/components/map/legend/item/basic-legend.tsx b/client/src/components/map/legend/item/basic-legend.tsx new file mode 100644 index 0000000..8e76f97 --- /dev/null +++ b/client/src/components/map/legend/item/basic-legend.tsx @@ -0,0 +1,27 @@ +import Square from "@/components/map/legend/item/square"; +import useLayerLegend from "@/hooks/use-layer-legend"; + +type BasicLegendProps = ReturnType["data"]; + +const BasicLegend = (data: BasicLegendProps) => { + if (!data.items || data.items.length === 0) { + return null; + } + + if (data.items.length === 1) { + return ; + } + + return ( +
+ {data.items.map((item) => ( +
+ +
{item.value}
+
+ ))} +
+ ); +}; + +export default BasicLegend; diff --git a/client/src/components/map/legend/item/choropleth-legend.tsx b/client/src/components/map/legend/item/choropleth-legend.tsx new file mode 100644 index 0000000..c40106c --- /dev/null +++ b/client/src/components/map/legend/item/choropleth-legend.tsx @@ -0,0 +1,26 @@ +import Square from "@/components/map/legend/item/square"; +import Unit from "@/components/map/legend/item/unit"; +import Values from "@/components/map/legend/item/values"; +import useLayerLegend from "@/hooks/use-layer-legend"; + +type ChoroplethLegendProps = ReturnType["data"]; + +const ChoroplethLegend = (data: ChoroplethLegendProps) => { + if (!data.items || data.items.length === 0) { + return null; + } + + return ( + <> + +
+ {data.items.map((item) => ( + + ))} +
+ + + ); +}; + +export default ChoroplethLegend; diff --git a/client/src/components/map/legend/item/gradient-legend.tsx b/client/src/components/map/legend/item/gradient-legend.tsx new file mode 100644 index 0000000..14288a6 --- /dev/null +++ b/client/src/components/map/legend/item/gradient-legend.tsx @@ -0,0 +1,31 @@ +import BasicLegend from "@/components/map/legend/item/basic-legend"; +import Unit from "@/components/map/legend/item/unit"; +import Values from "@/components/map/legend/item/values"; +import useLayerLegend from "@/hooks/use-layer-legend"; + +type GradientLegendProps = ReturnType["data"]; + +const GradientLegend = (data: GradientLegendProps) => { + if (!data.items || data.items.length === 0) { + return null; + } + + if (data.items.length === 1) { + return ; + } + + return ( + <> + +
item.color).join(", ")})`, + }} + /> + + + ); +}; + +export default GradientLegend; diff --git a/client/src/components/map/legend/item/index.tsx b/client/src/components/map/legend/item/index.tsx new file mode 100644 index 0000000..6a8ce0a --- /dev/null +++ b/client/src/components/map/legend/item/index.tsx @@ -0,0 +1,129 @@ +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Slider } from "@/components/ui/slider"; +import useLayerLegend from "@/hooks/use-layer-legend"; +import useMapLayers from "@/hooks/use-map-layers"; +import EyeSlashedIcon from "@/svgs/eye-slashed.svg"; +import EyeIcon from "@/svgs/eye.svg"; +import HandleIcon from "@/svgs/handle.svg"; +import OpacityIcon from "@/svgs/opacity.svg"; +import { LayerSettings } from "@/types/layer"; + +import BasicLegend from "./basic-legend"; +import ChoroplethLegend from "./choropleth-legend"; +import GradientLegend from "./gradient-legend"; + +interface LegendItemProps { + id: number; + settings: LayerSettings; +} + +const LegendItem = ({ id, settings }: LegendItemProps) => { + const { data, isLoading } = useLayerLegend(id); + const [, { updateLayer }] = useMapLayers(); + + if (!isLoading && !data) { + return null; + } + + return ( +
+ +
+ {isLoading && ( +
+ + +
+ )} + {!isLoading && ( +
+
+ {data.topicSlug === "contextual" ? data.name : data.dataset} +
+
+ + + + + + + updateLayer(id, { opacity: value })} + /> + + + + +
+
+ )} + {!isLoading && data.topicSlug !== "contextual" && ( +
{data.name}
+ )} +
+ {isLoading && ( + <> + + + + + )} + {!isLoading && !!data.items?.length && ( + <> + {data.type === "basic" && } + {data.type === "choropleth" && } + {data.type === "gradient" && } + + )} +
+
+
+ ); +}; + +export default LegendItem; diff --git a/client/src/components/map/legend/item/square.tsx b/client/src/components/map/legend/item/square.tsx new file mode 100644 index 0000000..effb848 --- /dev/null +++ b/client/src/components/map/legend/item/square.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils"; +import { LegendLegendConfigComponentItemsItem } from "@/types/generated/strapi.schemas"; + +interface SquareProps { + item: LegendLegendConfigComponentItemsItem; + className?: string; +} + +const Square = ({ item, className }: SquareProps) => { + if (item.pattern === "stripes") { + return ( +
+ ); + } + + return ( +
+ ); +}; + +export default Square; diff --git a/client/src/components/map/legend/item/unit.tsx b/client/src/components/map/legend/item/unit.tsx new file mode 100644 index 0000000..4def2ed --- /dev/null +++ b/client/src/components/map/legend/item/unit.tsx @@ -0,0 +1,13 @@ +import useLayerLegend from "@/hooks/use-layer-legend"; + +type UnitProps = ReturnType["data"]; + +const Unit = (data: UnitProps) => { + if (!data.unit) { + return null; + } + + return
({data.unit})
; +}; + +export default Unit; diff --git a/client/src/components/map/legend/item/values.tsx b/client/src/components/map/legend/item/values.tsx new file mode 100644 index 0000000..0037229 --- /dev/null +++ b/client/src/components/map/legend/item/values.tsx @@ -0,0 +1,31 @@ +import useLayerLegend from "@/hooks/use-layer-legend"; +import { cn } from "@/lib/utils"; + +type ValuesProps = ReturnType["data"]; + +const Values = (data: ValuesProps) => { + if (!data.items || data.items.length === 0) { + return null; + } + + return ( +
+ {data.items.map((item, index) => ( +
0 && index + 1 < data.items!.length, + "text-right": index + 1 === data.items!.length, + })} + > + {item.value} +
+ ))} +
+ ); +}; + +export default Values; diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index 42c6f2b..d5b133d 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -15,12 +15,12 @@ const buttonVariants = cva( "bg-supernova-yellow-400 hover:bg-supernova-yellow-300 text-casper-blue-950 focus-visible:ring-casper-blue-400 data-[state=open]:bg-rhino-blue-900 data-[state=open]:hover:bg-rhino-blue-950 data-[state=open]:text-supernova-yellow-400", "yellow-alt": "bg-supernova-yellow-400 hover:bg-supernova-yellow-300 text-casper-blue-950 focus-visible:ring-casper-blue-400 data-[state=open]:bg-white data-[state=open]:hover:bg-casper-blue-200", - ghost: - "bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 ring-offset-white focus-visible:ring-casper-blue-400", + ghost: "focus-visible:ring-casper-blue-400", }, size: { default: "h-8 w-auto xl:h-10 px-4 xl:py-2", icon: "h-8 w-8 xl:h-10 xl:w-10", + "icon-sm": "h-4 w-4", auto: "", }, }, diff --git a/client/src/components/ui/popover.tsx b/client/src/components/ui/popover.tsx index e3d62c1..93d7f0f 100644 --- a/client/src/components/ui/popover.tsx +++ b/client/src/components/ui/popover.tsx @@ -1,6 +1,7 @@ "use client"; import * as PopoverPrimitive from "@radix-ui/react-popover"; +import { cva, VariantProps } from "class-variance-authority"; import * as React from "react"; import { cn } from "@/lib/utils"; @@ -9,23 +10,61 @@ const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; +const popoverContentVariants = cva( + "z-10 outline-none duration-500 ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + { + variants: { + variant: { + default: "bg-white", + dark: "bg-rhino-blue-900 text-white", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + const PopoverContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 24, ...props }, ref) => ( + React.ComponentPropsWithoutRef & + VariantProps +>(({ variant, className, align = "center", sideOffset = 24, ...props }, ref) => ( )); PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverTrigger, PopoverContent }; +const popoverArrowVariants = cva("", { + variants: { + variant: { + default: "fill-white", + dark: "fill-rhino-blue-900", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +const PopoverArrow = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ variant, className, ...props }, ref) => ( + +)); +PopoverArrow.displayName = PopoverPrimitive.Arrow.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverArrow }; diff --git a/client/src/components/ui/slider.tsx b/client/src/components/ui/slider.tsx new file mode 100644 index 0000000..9a2f429 --- /dev/null +++ b/client/src/components/ui/slider.tsx @@ -0,0 +1,54 @@ +"use client"; + +import * as SliderPrimitive from "@radix-ui/react-slider"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { showPercentage?: boolean } +>(({ className, min = 0, max = 1, step = 0.1, showPercentage = true, ...props }, ref) => { + const value = props.value?.[0] ?? props.defaultValue?.[0] ?? 0; + + return ( + + + +
+
+ + + {showPercentage && ( +
min && value < max, + "right-0": value === max, + })} + aria-hidden + > + {value * 100}% +
+ )} +
+ + ); +}); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/client/src/hooks/use-layer-legend.ts b/client/src/hooks/use-layer-legend.ts new file mode 100644 index 0000000..1740dae --- /dev/null +++ b/client/src/hooks/use-layer-legend.ts @@ -0,0 +1,63 @@ +import { useGetLayers } from "@/types/generated/layer"; +import { LegendLegendConfigComponent } from "@/types/generated/strapi.schemas"; + +export default function useLayerLegend(id: number) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return useGetLayers<{ + name: string; + dataset: string; + topicSlug: string; + type: LegendLegendConfigComponent["type"]; + unit: string; + items: LegendLegendConfigComponent["items"]; + }>( + { + fields: ["name"], + populate: { + legend_config: { + populate: { + items: true, + }, + }, + dataset: { + fields: ["name"], + populate: { + topic: { + fields: ["slug"], + }, + }, + }, + }, + filters: { + id: { + $eq: id, + }, + }, + "pagination[limit]": 1, + }, + { + query: { + select: (data) => { + if (!data?.data?.length) { + return undefined; + } + + const { name, dataset, legend_config } = data.data[0].attributes!; + const { name: datasetName, topic } = dataset!.data!.attributes!; + const { slug: topicSlug } = topic!.data!.attributes!; + const { type, unit, items } = legend_config!; + + return { + name, + dataset: datasetName, + topicSlug, + type, + unit, + items, + }; + }, + }, + }, + ); +} diff --git a/client/src/svgs/eye-slashed.svg b/client/src/svgs/eye-slashed.svg new file mode 100644 index 0000000..f89d896 --- /dev/null +++ b/client/src/svgs/eye-slashed.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/client/src/svgs/eye.svg b/client/src/svgs/eye.svg new file mode 100644 index 0000000..862f1d6 --- /dev/null +++ b/client/src/svgs/eye.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/client/src/svgs/handle.svg b/client/src/svgs/handle.svg new file mode 100644 index 0000000..0ae445f --- /dev/null +++ b/client/src/svgs/handle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/client/src/svgs/opacity.svg b/client/src/svgs/opacity.svg new file mode 100644 index 0000000..9acb947 --- /dev/null +++ b/client/src/svgs/opacity.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 97244aa..0607e31 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -13,6 +13,9 @@ const config: Config = { }, colors: { white: "#ffffff", + gray: { + "500": "#60626A", + }, "casper-blue": { "50": "#f4f8fa", "200": "#d4e0e9", @@ -37,6 +40,9 @@ const config: Config = { }, }, extend: { + fontSize: { + "2xs": ["10px", "16px"], + }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", diff --git a/client/yarn.lock b/client/yarn.lock index 90a66a1..33fd08b 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2264,6 +2264,13 @@ __metadata: languageName: node linkType: hard +"@radix-ui/number@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/number@npm:1.1.0" + checksum: 10c0/a48e34d5ff1484de1b7cf5d7317fefc831d49e96a2229f300fd37b657bd8cfb59c922830c00ec02838ab21de3b299a523474592e4f30882153412ed47edce6a4 + languageName: node + linkType: hard + "@radix-ui/primitive@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/primitive@npm:1.1.0" @@ -2733,6 +2740,35 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-slider@npm:1.2.1": + version: 1.2.1 + resolution: "@radix-ui/react-slider@npm:1.2.1" + dependencies: + "@radix-ui/number": "npm:1.1.0" + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-collection": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.1" + "@radix-ui/react-direction": "npm:1.1.0" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-controllable-state": "npm:1.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + "@radix-ui/react-use-previous": "npm:1.1.0" + "@radix-ui/react-use-size": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/f39b67f019accc2fddebddb0d7f5bca7fc6ce8b370d294779e5139f7f36135ead9dff8672a9e4d5c771103280df9a05185ff17656d11e9eb88170e8f30e5efb4 + languageName: node + linkType: hard + "@radix-ui/react-slot@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-slot@npm:1.1.0" @@ -4548,6 +4584,7 @@ __metadata: "@radix-ui/react-popover": "npm:1.1.2" "@radix-ui/react-radio-group": "npm:1.2.1" "@radix-ui/react-separator": "npm:1.1.0" + "@radix-ui/react-slider": "npm:1.2.1" "@radix-ui/react-slot": "npm:1.1.0" "@radix-ui/react-switch": "npm:1.1.1" "@radix-ui/react-tooltip": "npm:1.1.3" From 89aa0b1aa0a4fe05ec7a4586fc045bf8c6ccb3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Prod=27homme?= Date: Mon, 4 Nov 2024 14:02:41 +0100 Subject: [PATCH 12/12] feat(client): Layer sorting --- client/package.json | 3 + client/src/components/map/legend/index.tsx | 116 +++++++++++++++++- .../src/components/map/legend/item/index.tsx | 18 ++- .../components/map/legend/sortable-item.tsx | 39 ++++++ client/yarn.lock | 65 ++++++++++ 5 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 client/src/components/map/legend/sortable-item.tsx diff --git a/client/package.json b/client/package.json index aee8239..15d5285 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,9 @@ "@deck.gl/core": "9.0.34", "@deck.gl/json": "9.0.34", "@deck.gl/layers": "9.0.34", + "@dnd-kit/core": "6.1.0", + "@dnd-kit/modifiers": "7.0.0", + "@dnd-kit/sortable": "8.0.0", "@radix-ui/react-checkbox": "1.1.2", "@radix-ui/react-collapsible": "1.1.1", "@radix-ui/react-dialog": "1.1.2", diff --git a/client/src/components/map/legend/index.tsx b/client/src/components/map/legend/index.tsx index b3ab3ef..6028891 100644 --- a/client/src/components/map/legend/index.tsx +++ b/client/src/components/map/legend/index.tsx @@ -1,17 +1,125 @@ -import { useMemo } from "react"; +"use client"; +import { + Announcements, + closestCenter, + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + ScreenReaderInstructions, +} from "@dnd-kit/core"; +import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; +import { useCallback, useMemo, useState } from "react"; + +import SortableItem from "@/components/map/legend/sortable-item"; import useMapLayers from "@/hooks/use-map-layers"; import LegendItem from "./item"; const Legend = () => { - const [layers] = useMapLayers(); + const [layers, { updateLayerOrder }] = useMapLayers(); + const [draggedLayerId, setDraggedLayerId] = useState(null); const legendItems = useMemo(() => { - return layers.map(({ id, ...settings }) => ); + return layers.map(({ id, ...settings }) => ( + + )); }, [layers]); - return legendItems; + const draggedLegendItem = useMemo(() => { + if (draggedLayerId === null) { + return null; + } + + const layer = layers.find(({ id }) => id === draggedLayerId); + if (!layer) { + return null; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, ...settings } = layer; + + return ( +
+ +
+ ); + }, [layers, draggedLayerId]); + + const layerIds = useMemo(() => layers.map(({ id }) => id), [layers]); + + const accessibility: { + announcements: Announcements; + screenReaderInstructions: ScreenReaderInstructions; + } = useMemo( + () => ({ + announcements: { + onDragStart({ active }) { + const position = layers.findIndex(({ id }) => id === active.id); + return `Picked up layer in position ${position} of ${layers.length}`; + }, + onDragOver({ over }) { + if (over) { + const position = layers.findIndex(({ id }) => id === over.id); + return `Layer was moved into position ${position} of ${layers.length}`; + } + }, + onDragEnd({ over }) { + if (over) { + const position = layers.findIndex(({ id }) => id === over.id); + return `Layer was dropped at position ${position} of ${layers.length}`; + } + }, + onDragCancel() { + return `Dragging was cancelled.`; + }, + }, + screenReaderInstructions: { + draggable: + "Press space or enter to grab the layer. Use the arrow keys to move the layer up or down. Press space or enter again to drop the layer. Press escape to cancel.", + }, + }), + [layers], + ); + + const onDragStart = useCallback( + ({ active }: DragStartEvent) => { + setDraggedLayerId(active.id as number); + }, + [setDraggedLayerId], + ); + + const onDragEnd = useCallback( + ({ active, over }: DragEndEvent) => { + if (over === null || active.id === over.id) { + return; + } + + const id = active.id as number; + const index = layerIds.indexOf(over.id as number); + + updateLayerOrder(id, index); + setDraggedLayerId(null); + }, + [layerIds, updateLayerOrder, setDraggedLayerId], + ); + + return ( + + + {legendItems} + + {draggedLegendItem} + + ); }; export default Legend; diff --git a/client/src/components/map/legend/item/index.tsx b/client/src/components/map/legend/item/index.tsx index 6a8ce0a..e5c5ba9 100644 --- a/client/src/components/map/legend/item/index.tsx +++ b/client/src/components/map/legend/item/index.tsx @@ -1,3 +1,6 @@ +import { DraggableAttributes } from "@dnd-kit/core"; +import { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"; + import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -18,9 +21,11 @@ import GradientLegend from "./gradient-legend"; interface LegendItemProps { id: number; settings: LayerSettings; + sortableAttributes?: DraggableAttributes; + sortableListeners?: SyntheticListenerMap; } -const LegendItem = ({ id, settings }: LegendItemProps) => { +const LegendItem = ({ id, settings, sortableAttributes, sortableListeners }: LegendItemProps) => { const { data, isLoading } = useLayerLegend(id); const [, { updateLayer }] = useMapLayers(); @@ -29,8 +34,15 @@ const LegendItem = ({ id, settings }: LegendItemProps) => { } return ( -
- diff --git a/client/src/components/map/legend/sortable-item.tsx b/client/src/components/map/legend/sortable-item.tsx new file mode 100644 index 0000000..5aba79b --- /dev/null +++ b/client/src/components/map/legend/sortable-item.tsx @@ -0,0 +1,39 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { useMemo } from "react"; + +import LegendItem from "@/components/map/legend/item"; +import { cn } from "@/lib/utils"; +import { LayerSettings } from "@/types/layer"; + +interface SortableItemProps { + id: number; + settings: LayerSettings; +} + +const SortableItem = ({ id, settings }: SortableItemProps) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + }); + + const style = useMemo( + () => ({ + transform: CSS.Transform.toString(transform), + transition, + }), + [transform, transition], + ); + + return ( +
+ +
+ ); +}; + +export default SortableItem; diff --git a/client/yarn.lock b/client/yarn.lock index 33fd08b..d6b918f 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1371,6 +1371,68 @@ __metadata: languageName: node linkType: hard +"@dnd-kit/accessibility@npm:^3.1.0": + version: 3.1.0 + resolution: "@dnd-kit/accessibility@npm:3.1.0" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/4f9d24e801d66d4fbb551ec389ed90424dd4c5bbdf527000a618e9abb9833cbd84d9a79e362f470ccbccfbd6d00217a9212c92f3cef66e01c951c7f79625b9d7 + languageName: node + linkType: hard + +"@dnd-kit/core@npm:6.1.0": + version: 6.1.0 + resolution: "@dnd-kit/core@npm:6.1.0" + dependencies: + "@dnd-kit/accessibility": "npm:^3.1.0" + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10c0/c793eb97cb59285ca8937ebcdfcd27cff09d750ae06722e36ca5ed07925e41abc36a38cff98f9f6056f7a07810878d76909826142a2968330e7e22060e6be584 + languageName: node + linkType: hard + +"@dnd-kit/modifiers@npm:7.0.0": + version: 7.0.0 + resolution: "@dnd-kit/modifiers@npm:7.0.0" + dependencies: + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@dnd-kit/core": ^6.1.0 + react: ">=16.8.0" + checksum: 10c0/542e1d2b6102a5c826118c36158aab23c5437d24008cab4848b0866d3d850b4410c4f465690767dd1f31fde33a1fa9d238675be70f174c179485ce376f0c8aa6 + languageName: node + linkType: hard + +"@dnd-kit/sortable@npm:8.0.0": + version: 8.0.0 + resolution: "@dnd-kit/sortable@npm:8.0.0" + dependencies: + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@dnd-kit/core": ^6.1.0 + react: ">=16.8.0" + checksum: 10c0/a6066c652b892c6a11320c7d8f5c18fdf723e721e8eea37f4ab657dee1ac5e7ca710ac32ce0712a57fe968bc07c13bcea5d5599d90dfdd95619e162befd4d2fb + languageName: node + linkType: hard + +"@dnd-kit/utilities@npm:^3.2.2": + version: 3.2.2 + resolution: "@dnd-kit/utilities@npm:3.2.2" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/aix-ppc64@npm:0.24.0" @@ -4577,6 +4639,9 @@ __metadata: "@deck.gl/core": "npm:9.0.34" "@deck.gl/json": "npm:9.0.34" "@deck.gl/layers": "npm:9.0.34" + "@dnd-kit/core": "npm:6.1.0" + "@dnd-kit/modifiers": "npm:7.0.0" + "@dnd-kit/sortable": "npm:8.0.0" "@radix-ui/react-checkbox": "npm:1.1.2" "@radix-ui/react-collapsible": "npm:1.1.1" "@radix-ui/react-dialog": "npm:1.1.2"