Skip to content

Commit

Permalink
Merge pull request #306 from Vizzuality/SKY30-444-fe-update-layers-pa…
Browse files Browse the repository at this point in the history
…nel-for-the-three-modes

[SKY30-444] Rework layers panel to support the three modes
  • Loading branch information
SARodrigues authored Oct 4, 2024
2 parents 2c8c56a + 39d958a commit 1b7d4e7
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 97 deletions.
147 changes: 51 additions & 96 deletions frontend/src/containers/map/sidebar/layers-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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<ComponentProps<typeof Switch>['onCheckedChange']>[0]) => {
setMapSettings((prev) => ({
Expand All @@ -84,62 +51,50 @@ const LayersPanel: FCWithMessages = (): JSX.Element => {
[setMapSettings]
);

return (
<div className="h-full space-y-3 overflow-auto p-4 text-xs">
<h3 className="text-xl font-bold">{t('layers')}</h3>
<div className="space-y-3 divide-y divide-dashed divide-black">
{datasets?.map((dataset) => {
return (
<div key={dataset.id} className="[&:not(:first-child)]:pt-3">
<h4 className="font-bold">{dataset?.attributes?.name}</h4>
<ul className="my-3 flex flex-col space-y-4">
{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 (
<li key={layer.id} className="flex items-center justify-between">
<span className="flex gap-2">
<Switch
id={`${layer.id}-switch`}
checked={isActive}
onCheckedChange={onCheckedChange}
/>
<Label htmlFor={`${layer.id}-switch`} className={SWITCH_LABEL_CLASSES}>
{layer.attributes.title}
</Label>
</span>
{metadata?.description && (
<TooltipButton className="-my-1" text={metadata?.description} />
)}
</li>
);
})}
// ? 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' && (
<li className="flex items-center justify-between">
<span className="flex gap-2">
<Switch
id="labels-switch"
checked={labels}
onCheckedChange={handleLabelsChange}
/>
<Label htmlFor="labels-switch" className={SWITCH_LABEL_CLASSES}>
{t('labels')}
</Label>
</span>
</li>
)}
</>
</ul>
</div>
);
})}
return (
<div className="h-full overflow-auto px-4 text-xs">
<div className="py-1">
<h3 className="text-xl font-extrabold">{t('layers')}</h3>
</div>
<LayersGroup
name={t('terrestrial-data')}
datasets={terrestrialDatasets}
isOpen={['terrestrial'].includes(tab)}
loading={isFetchingDatasets}
/>
<LayersGroup
name={t('marine-data')}
datasets={marineDatasets}
isOpen={['marine'].includes(tab)}
loading={isFetchingDatasets}
/>
<LayersGroup
name={t('basemap')}
datasets={basemapDatasets}
isOpen={['summary'].includes(tab)}
loading={isFetchingDatasets}
showDatasetsNames={false}
showBottomBorder={false}
>
{/*
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.
*/}
<li className="flex items-center justify-between">
<span className="flex gap-2">
<Switch id="labels-switch" checked={labels} onCheckedChange={handleLabelsChange} />
<Label htmlFor="labels-switch" className={SWITCH_LABEL_CLASSES}>
{t('labels')}
</Label>
</span>
</li>
</LayersGroup>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LayersGroupProps> = ({
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 (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger
className={cn(COLLAPSIBLE_TRIGGER_CLASSES, { 'border-b border-black': !open })}
>
<span>
{name}
{displayNumActiveLayers && (
<span className="ml-2 border border-black px-1 font-normal">
{numActiveDatasetsLayers}
</span>
)}
</span>
<LuChevronDown
className={`group-data-[state=closed]:block ${COLLAPSIBLE_TRIGGER_ICONS_CLASSES}`}
/>
<LuChevronUp
className={`group-data-[state=open]:block ${COLLAPSIBLE_TRIGGER_ICONS_CLASSES}`}
/>
</CollapsibleTrigger>
<CollapsibleContent
className={cn(COLLAPSIBLE_CONTENT_CLASSES, { 'border-b': showBottomBorder })}
>
<div className="space-y-4 divide-y divide-dashed divide-black">
{loading && <span>{t('loading')}</span>}
{noData && <span>{t('no-data')}</span>}
{datasets?.map((dataset) => {
return (
<div key={dataset.id} className="[&:not(:first-child)]:pt-3">
{showDatasetsNames && <h4 className="font-mono">{dataset?.attributes?.name}</h4>}
<ul className={cn('my-3 flex flex-col space-y-3', { '-my-0': !showDatasetsNames })}>
{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 (
<li key={layer.id} className="flex items-center justify-between">
<span className="flex gap-2">
<Switch
id={`${layer.id}-switch`}
checked={isActive}
onCheckedChange={onCheckedChange}
/>
<Label htmlFor={`${layer.id}-switch`} className={SWITCH_LABEL_CLASSES}>
{layer.attributes.title}
</Label>
</span>
{metadata?.description && (
<TooltipButton className="-my-1" text={metadata?.description} />
)}
</li>
);
})}
<>{children}</>
</ul>
</div>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
};

LayersGroup.messages = ['containers.map-sidebar-layers-panel', ...TooltipButton.messages];

export default LayersGroup;
7 changes: 6 additions & 1 deletion frontend/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
Expand Down

0 comments on commit 1b7d4e7

Please sign in to comment.