diff --git a/frontend/src/containers/map/sidebar/layers-panel/index.tsx b/frontend/src/containers/map/sidebar/layers-panel/index.tsx index 6b1b7057..841c4e6f 100644 --- a/frontend/src/containers/map/sidebar/layers-panel/index.tsx +++ b/frontend/src/containers/map/sidebar/layers-panel/index.tsx @@ -5,26 +5,24 @@ import { useLocale, useTranslations } from 'next-intl'; import TooltipButton from '@/components/tooltip-button'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; -import { - useSyncMapLayers, - useSyncMapLayerSettings, - useSyncMapSettings, -} from '@/containers/map/content/map/sync-settings'; +import { useSyncMapSettings } from '@/containers/map/content/map/sync-settings'; +import { useSyncMapContentSettings } from '@/containers/map/sync-settings'; import { FCWithMessages } from '@/types'; import { useGetDatasets } from '@/types/generated/dataset'; -import { DatasetUpdatedByData, LayerResponseDataObject } from '@/types/generated/strapi.schemas'; +import { DatasetUpdatedByData } from '@/types/generated/strapi.schemas'; -const SWITCH_LABEL_CLASSES = '-mb-px cursor-pointer pt-px font-mono text-xs font-normal'; +import LayersGroup, { SWITCH_LABEL_CLASSES } from './layers-group'; const LayersPanel: FCWithMessages = (): JSX.Element => { const t = useTranslations('containers.map-sidebar-layers-panel'); const locale = useLocale(); - - const [activeLayers, setMapLayers] = useSyncMapLayers(); - const [layerSettings, setLayerSettings] = useSyncMapLayerSettings(); const [{ labels }, setMapSettings] = useSyncMapSettings(); + const [{ tab }] = useSyncMapContentSettings(); - const { data: datasets }: { data: DatasetUpdatedByData[] } = useGetDatasets( + const { + data: datasets, + isFetching: isFetchingDatasets, + }: { data: DatasetUpdatedByData[]; isFetching: boolean } = useGetDatasets( { locale, sort: 'name:asc', @@ -43,37 +41,6 @@ const LayersPanel: FCWithMessages = (): JSX.Element => { } ); - const onToggleLayer = useCallback( - (layerId: LayerResponseDataObject['id'], isActive: boolean) => { - setMapLayers( - isActive - ? [...activeLayers, Number(layerId)] - : activeLayers.filter((_layerId) => _layerId !== Number(layerId)) - ); - - // If we don't have layerSettings entries, the view is in its default state; we wish to - // show all legend accordion items expanded by default. - const initialSettings = (() => { - const layerSettingsKeys = Object.keys(layerSettings); - if (layerSettingsKeys.length) return {}; - return Object.assign( - {}, - ...activeLayers.map((layerId) => ({ [layerId]: { expanded: true } })) - ); - })(); - - setLayerSettings((prev) => ({ - ...initialSettings, - ...prev, - [layerId]: { - ...prev[layerId], - expanded: true, - }, - })); - }, - [activeLayers, layerSettings, setLayerSettings, setMapLayers] - ); - const handleLabelsChange = useCallback( (active: Parameters['onCheckedChange']>[0]) => { setMapSettings((prev) => ({ @@ -84,62 +51,50 @@ const LayersPanel: FCWithMessages = (): JSX.Element => { [setMapSettings] ); - return ( -
-

{t('layers')}

-
- {datasets?.map((dataset) => { - return ( -
-

{dataset?.attributes?.name}

-
    - {dataset.attributes?.layers?.data?.map((layer) => { - const isActive = activeLayers.findIndex((layerId) => layerId === layer.id) !== -1; - const onCheckedChange = onToggleLayer.bind(null, layer.id) as ( - isActive: boolean - ) => void; - const metadata = layer?.attributes?.metadata; - - return ( -
  • - - - - - {metadata?.description && ( - - )} -
  • - ); - })} + // ? NOTE: This is temporary, just to debug display until we connect it with final data format + const terrestrialDatasets = []; + const marineDatasets = datasets?.filter(({ attributes }) => attributes?.name !== 'Basemap'); + const basemapDatasets = datasets?.filter(({ attributes }) => attributes?.name === 'Basemap'); - <> - {dataset.attributes?.slug === 'basemap' && ( -
  • - - - - -
  • - )} - -
-
- ); - })} + return ( +
+
+

{t('layers')}

+ + + + {/* + The labels toggle doesn't come from the basemap dataset and has slightly functionality implemented. + Not ideal, but given it's a one-off, we'll pass the entry as a child to be displayed alongside the + other entries, much like in the previous implementation. + */} +
  • + + + + +
  • +
    ); }; diff --git a/frontend/src/containers/map/sidebar/layers-panel/layers-group/index.tsx b/frontend/src/containers/map/sidebar/layers-panel/layers-group/index.tsx new file mode 100644 index 00000000..6aabb21d --- /dev/null +++ b/frontend/src/containers/map/sidebar/layers-panel/layers-group/index.tsx @@ -0,0 +1,169 @@ +import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'; + +import { useTranslations } from 'next-intl'; +import { LuChevronDown, LuChevronUp } from 'react-icons/lu'; + +import TooltipButton from '@/components/tooltip-button'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { + useSyncMapLayers, + useSyncMapLayerSettings, +} from '@/containers/map/content/map/sync-settings'; +import { useSyncMapContentSettings } from '@/containers/map/sync-settings'; +import { cn } from '@/lib/classnames'; +import { FCWithMessages } from '@/types'; +import { DatasetUpdatedByData, LayerResponseDataObject } from '@/types/generated/strapi.schemas'; + +export const SWITCH_LABEL_CLASSES = '-mb-px cursor-pointer pt-px font-mono text-xs font-normal'; +const COLLAPSIBLE_TRIGGER_ICONS_CLASSES = 'w-5 h-5 hidden'; +const COLLAPSIBLE_TRIGGER_CLASSES = + 'group flex w-full items-center justify-between py-2 text-xs font-bold'; +const COLLAPSIBLE_CONTENT_CLASSES = + 'data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down border-black py-2'; + +type LayersGroupProps = PropsWithChildren<{ + name: string; + datasets: DatasetUpdatedByData[]; + showDatasetsNames?: boolean; + showBottomBorder?: boolean; + isOpen?: boolean; + loading?: boolean; +}>; + +const LayersGroup: FCWithMessages = ({ + name, + datasets, + showDatasetsNames = true, + showBottomBorder = true, + isOpen = true, + loading = true, + children, +}): JSX.Element => { + const [open, setOpen] = useState(isOpen); + const t = useTranslations('containers.map-sidebar-layers-panel'); + + const [activeLayers, setMapLayers] = useSyncMapLayers(); + const [layerSettings, setLayerSettings] = useSyncMapLayerSettings(); + const [{ tab }] = useSyncMapContentSettings(); + + const datasetsLayersIds = useMemo(() => { + return ( + datasets?.map(({ attributes }) => attributes?.layers?.data?.map(({ id }) => id))?.flat() || [] + ); + }, [datasets]); + + const numActiveDatasetsLayers = useMemo(() => { + return datasetsLayersIds?.filter((id) => activeLayers?.includes(id))?.length || 0; + }, [datasetsLayersIds, activeLayers]); + + const onToggleLayer = useCallback( + (layerId: LayerResponseDataObject['id'], isActive: boolean) => { + setMapLayers( + isActive + ? [...activeLayers, Number(layerId)] + : activeLayers.filter((_layerId) => _layerId !== Number(layerId)) + ); + + // If we don't have layerSettings entries, the view is in its default state; we wish to + // show all legend accordion items expanded by default. + const initialSettings = (() => { + const layerSettingsKeys = Object.keys(layerSettings); + if (layerSettingsKeys.length) return {}; + return Object.assign( + {}, + ...activeLayers.map((layerId) => ({ [layerId]: { expanded: true } })) + ); + })(); + + setLayerSettings((prev) => ({ + ...initialSettings, + ...prev, + [layerId]: { + ...prev[layerId], + expanded: true, + }, + })); + }, + [activeLayers, layerSettings, setLayerSettings, setMapLayers] + ); + + useEffect(() => { + setOpen(isOpen); + }, [isOpen, tab]); + + const displayNumActiveLayers = !open && numActiveDatasetsLayers > 0; + const noData = !loading && !datasets?.length; + + return ( + + + + {name} + {displayNumActiveLayers && ( + + {numActiveDatasetsLayers} + + )} + + + + + +
    + {loading && {t('loading')}} + {noData && {t('no-data')}} + {datasets?.map((dataset) => { + return ( +
    + {showDatasetsNames &&

    {dataset?.attributes?.name}

    } +
      + {dataset.attributes?.layers?.data?.map((layer) => { + const isActive = + activeLayers.findIndex((layerId) => layerId === layer.id) !== -1; + const onCheckedChange = onToggleLayer.bind(null, layer.id) as ( + isActive: boolean + ) => void; + const metadata = layer?.attributes?.metadata; + + return ( +
    • + + + + + {metadata?.description && ( + + )} +
    • + ); + })} + <>{children} +
    +
    + ); + })} +
    +
    +
    + ); +}; + +LayersGroup.messages = ['containers.map-sidebar-layers-panel', ...TooltipButton.messages]; + +export default LayersGroup; diff --git a/frontend/translations/en.json b/frontend/translations/en.json index a31ce687..62477651 100644 --- a/frontend/translations/en.json +++ b/frontend/translations/en.json @@ -231,7 +231,12 @@ }, "map-sidebar-layers-panel": { "layers": "Layers", - "labels": "Labels" + "labels": "Labels", + "basemap": "Basemap", + "marine-data": "Marine Data", + "terrestrial-data": "Terrestrial Data", + "loading": "Loading data...", + "no-data": "Data not available" }, "map": { "loading": "Loading...",