diff --git a/client/package.json b/client/package.json
index 15d5285..0c1b31d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -25,10 +25,12 @@
"@radix-ui/react-label": "2.1.0",
"@radix-ui/react-popover": "1.1.2",
"@radix-ui/react-radio-group": "1.2.1",
+ "@radix-ui/react-select": "2.1.2",
"@radix-ui/react-separator": "1.1.0",
"@radix-ui/react-slider": "1.2.1",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-switch": "1.1.1",
+ "@radix-ui/react-tabs": "1.1.1",
"@radix-ui/react-tooltip": "1.1.3",
"@t3-oss/env-nextjs": "0.11.1",
"@tanstack/react-query": "5.59.16",
@@ -46,6 +48,7 @@
"react-map-gl": "7.1.7",
"tailwind-merge": "2.5.4",
"tailwindcss-animate": "1.0.7",
+ "tailwindcss-border-image": "1.1.2",
"typescript-eslint": "8.9.0",
"zod": "3.23.8"
},
diff --git a/client/public/assets/images/border-image.svg b/client/public/assets/images/border-image.svg
new file mode 100644
index 0000000..d7a7791
--- /dev/null
+++ b/client/public/assets/images/border-image.svg
@@ -0,0 +1,11 @@
+
diff --git a/client/src/app/globals.css b/client/src/app/globals.css
index c961aca..acf900f 100644
--- a/client/src/app/globals.css
+++ b/client/src/app/globals.css
@@ -13,10 +13,10 @@ body {
@layer base {
:root {
--radius: 0.5rem;
- --sidebar-background: 0 0% 98%;
+ --sidebar-background: 0 0% 100%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
- --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
diff --git a/client/src/components/dataset-card/index.tsx b/client/src/components/dataset-card/index.tsx
new file mode 100644
index 0000000..168811a
--- /dev/null
+++ b/client/src/components/dataset-card/index.tsx
@@ -0,0 +1,156 @@
+"use client";
+
+import { useCallback, useMemo, useState } from "react";
+
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import useMapLayers from "@/hooks/use-map-layers";
+import { DatasetLayersDataItem } from "@/types/generated/strapi.schemas";
+
+import { getDefaultReturnPeriod, getDefaultSelectedLayerId, getReturnPeriods } from "./utils";
+
+interface DatasetCardProps {
+ id: number;
+ name: string;
+ defaultLayerId: number | undefined;
+ layers: DatasetLayersDataItem[];
+}
+
+const DatasetCard = ({ id, name, defaultLayerId, layers }: DatasetCardProps) => {
+ const [layersConfiguration, { addLayer, updateLayer, removeLayer }] = useMapLayers();
+
+ const defaultSelectedLayerId = useMemo(
+ () => getDefaultSelectedLayerId(defaultLayerId, layers, layersConfiguration),
+ [layers, defaultLayerId, layersConfiguration],
+ );
+
+ const defaultSelectedReturnPeriod = useMemo(
+ () => getDefaultReturnPeriod(defaultSelectedLayerId, layers, layersConfiguration),
+ [layers, layersConfiguration, defaultSelectedLayerId],
+ );
+
+ const [selectedLayerId, setSelectedLayerId] = useState(defaultSelectedLayerId);
+ const [selectedReturnPeriod, setSelectedReturnPeriod] = useState(defaultSelectedReturnPeriod);
+
+ const isDatasetActive = useMemo(() => {
+ if (selectedLayerId === undefined) {
+ return false;
+ }
+
+ return layersConfiguration.findIndex(({ id }) => id === selectedLayerId) !== -1;
+ }, [selectedLayerId, layersConfiguration]);
+
+ const layerReturnPeriods = useMemo(
+ () => getReturnPeriods(selectedLayerId, layers),
+ [layers, selectedLayerId],
+ );
+
+ const onToggleDataset = useCallback(
+ (active: boolean) => {
+ if (selectedLayerId === undefined) {
+ return;
+ }
+
+ if (!active) {
+ removeLayer(selectedLayerId);
+ } else {
+ addLayer(selectedLayerId, { ["return-period"]: selectedReturnPeriod });
+ }
+ },
+ [selectedLayerId, addLayer, removeLayer, selectedReturnPeriod],
+ );
+
+ const onChangeSelectedLayer = useCallback(
+ (stringId: string) => {
+ const id = Number.parseInt(stringId);
+ const previousId = selectedLayerId;
+ const returnPeriod = getDefaultReturnPeriod(id, layers, layersConfiguration);
+
+ setSelectedLayerId(id);
+ setSelectedReturnPeriod(returnPeriod);
+
+ // If the dataset was active and the layer is changed, we replace the current layer by the new
+ // one keeping all the same settings (visibility, opacity, etc.)
+ if (isDatasetActive && previousId !== undefined) {
+ updateLayer(previousId, { id, ["return-period"]: returnPeriod });
+ }
+ },
+ [
+ selectedLayerId,
+ setSelectedLayerId,
+ isDatasetActive,
+ updateLayer,
+ layers,
+ layersConfiguration,
+ ],
+ );
+
+ const onChangeSelectedReturnPeriod = useCallback(
+ (stringReturnPeriod: string) => {
+ const returnPeriod = Number.parseInt(stringReturnPeriod);
+
+ setSelectedReturnPeriod(returnPeriod);
+
+ if (isDatasetActive && selectedLayerId !== undefined) {
+ updateLayer(selectedLayerId, { ["return-period"]: returnPeriod });
+ }
+ },
+ [selectedLayerId, setSelectedReturnPeriod, isDatasetActive, updateLayer],
+ );
+
+ return (
+
+
+
+
+ {!!layerReturnPeriods && (
+
+ )}
+
+
+ );
+};
+
+export default DatasetCard;
diff --git a/client/src/components/dataset-card/utils.ts b/client/src/components/dataset-card/utils.ts
new file mode 100644
index 0000000..2d4e0b3
--- /dev/null
+++ b/client/src/components/dataset-card/utils.ts
@@ -0,0 +1,76 @@
+import useMapLayers from "@/hooks/use-map-layers";
+import { DatasetLayersDataItem } from "@/types/generated/strapi.schemas";
+import { LayerParamsConfig } from "@/types/layer";
+
+export const getDefaultSelectedLayerId = (
+ defaultLayerId: number | undefined,
+ layers: DatasetLayersDataItem[],
+ layersConfiguration: ReturnType[0],
+) => {
+ // The ids of the layers that belong to the dataset
+ const datasetLayerIds = layers.map(({ id }) => id!);
+ // The ids of the layers active on the map, probably not from this dataset
+ const activeLayerIds = layersConfiguration.map(({ id }) => id!);
+ // The id of the layer that belongs to the dataset and is active, if any
+ const activeDatasetLayerId = datasetLayerIds.find((id) => activeLayerIds.includes(id));
+
+ if (activeDatasetLayerId) {
+ return activeDatasetLayerId;
+ }
+
+ return defaultLayerId;
+};
+
+export const getDefaultReturnPeriod = (
+ layerId: number | undefined,
+ layers: DatasetLayersDataItem[],
+ layersConfiguration: ReturnType[0],
+) => {
+ const layerConfiguration = layersConfiguration.find(({ id }) => id === layerId);
+
+ // If the layer is active and already has a selected return period, we return it
+ if (layerConfiguration?.["return-period"] !== undefined) {
+ return layerConfiguration["return-period"];
+ }
+
+ // Else we look for the default return period stored in `params_config`
+ const layer = layers.find(({ id }) => id === layerId);
+ const defaultReturnPeriod = (
+ layer?.attributes!.params_config as LayerParamsConfig | undefined
+ )?.find(({ key }) => key === "return-period");
+
+ if (
+ !defaultReturnPeriod ||
+ defaultReturnPeriod.default === undefined ||
+ defaultReturnPeriod.default === null
+ ) {
+ return undefined;
+ }
+
+ return defaultReturnPeriod.default as number;
+};
+
+export const getReturnPeriods = (layerId: number | undefined, layers: DatasetLayersDataItem[]) => {
+ const layer = layers.find(({ id }) => id === layerId);
+ if (!layer) {
+ return undefined;
+ }
+
+ const returnPeriod = (layer.attributes!.params_config as LayerParamsConfig | undefined)?.find(
+ ({ key }) => key === "return-period",
+ );
+ if (
+ !returnPeriod ||
+ returnPeriod.default === undefined ||
+ returnPeriod.default === null ||
+ returnPeriod.options === undefined ||
+ returnPeriod.options === null
+ ) {
+ return undefined;
+ }
+
+ return {
+ defaultOption: returnPeriod.default as number,
+ options: [...(returnPeriod.options as number[])].sort((a, b) => a - b),
+ };
+};
diff --git a/client/src/components/map/layer-manager/item.tsx b/client/src/components/map/layer-manager/item.tsx
index 73f78e5..89daceb 100644
--- a/client/src/components/map/layer-manager/item.tsx
+++ b/client/src/components/map/layer-manager/item.tsx
@@ -12,12 +12,17 @@ interface LayerManagerItemProps {
const LayerManagerItem = ({ id, beforeId, settings }: LayerManagerItemProps) => {
const config = useLayerConfig(id, settings);
- if (!config) {
+ if (!config?.styles) {
return null;
}
return (
-