Skip to content

Commit

Permalink
feat(map): Allow dynamic legend_config objects
Browse files Browse the repository at this point in the history
  • Loading branch information
clementprdhomme committed Oct 30, 2024
1 parent c910901 commit d4ecd75
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useCallback, useMemo } from 'react';

import { useMap } from 'react-map-gl';

import { useParams } from 'next/navigation';

import { useAtom } from 'jotai';
Expand All @@ -10,7 +8,7 @@ import { useLocale } from 'next-intl';
import DeckJsonLayer from '@/components/map/layers/deck-json-layer';
import MapboxLayer from '@/components/map/layers/mapbox-layer';
import { layersInteractiveAtom, layersInteractiveIdsAtom } from '@/containers/map/store';
import useConfig from '@/hooks/use-config';
import useResolvedConfig from '@/hooks/use-resolved-config';
import { useGetLayersId } from '@/types/generated/layer';
import { LayerResponseDataObject } from '@/types/generated/strapi.schemas';
import { Config, LayerTyped } from '@/types/layers';
Expand Down Expand Up @@ -48,7 +46,7 @@ const LayerManagerItem = ({ id, beforeId, settings }: LayerManagerItemProps) =>
[config, locationCode, params_config, settings]
);

const parsedConfig = useConfig(configParams);
const parsedConfig = useResolvedConfig(configParams);

const handleAddMapboxLayer = useCallback(
({ styles }: Config) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';

import { useLocale, useTranslations } from 'next-intl';
import { HiEye, HiEyeOff } from 'react-icons/hi';
Expand All @@ -25,7 +25,7 @@ import {
LayerListResponseDataItem,
LayerResponseDataObject,
} from '@/types/generated/strapi.schemas';
import { LayerTyped } from '@/types/layers';
import { LayerTyped, ParamsConfig } from '@/types/layers';

import LegendItem from './item';

Expand All @@ -43,7 +43,7 @@ const Legend: FCWithMessages = () => {
sort: 'title:asc',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
fields: ['title'],
fields: ['title', 'params_config'],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
populate: {
Expand Down Expand Up @@ -147,18 +147,15 @@ const Legend: FCWithMessages = () => {
[activeLayers, setMapLayers]
);

return (
<div className="px-4 py-2">
{!layersQuery.data?.length && (
<p>
{t.rich('open-layers-to-add-to-map', {
b: (chunks) => <span className="text-sm font-black uppercase">{chunks}</span>,
})}
</p>
)}
{layersQuery.data?.length > 0 && (
<div>
{layersQuery.data?.map(({ id, attributes: { title, legend_config } }, index) => {
const legendItems = useMemo(() => {
if (!layersQuery.data?.length) {
return null;
}

return (
<div>
{layersQuery.data?.map(
({ id, attributes: { title, legend_config, params_config } }, index) => {
const isFirst = index === 0;
const isLast = index + 1 === layersQuery.data.length;

Expand Down Expand Up @@ -274,13 +271,39 @@ const Legend: FCWithMessages = () => {
</TooltipProvider>
</div>
<div className="pt-1.5">
<LegendItem config={legend_config as LayerTyped['legend_config']} />
<LegendItem
config={legend_config as LayerTyped['legend_config']}
paramsConfig={params_config as ParamsConfig}
/>
</div>
</div>
);
}
)}
</div>
);
}, [
activeLayers.length,
layerSettings,
layersQuery.data,
onChangeLayerOpacity,
onMoveLayerDown,
onMoveLayerUp,
onRemoveLayer,
onToggleLayerVisibility,
t,
]);

return (
<div className="px-4 py-2">
{!layersQuery.data?.length && (
<p>
{t.rich('open-layers-to-add-to-map', {
b: (chunks) => <span className="text-sm font-black uppercase">{chunks}</span>,
})}
</div>
</p>
)}
{legendItems}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { useEffect, useMemo, useState } from 'react';

import { useMap } from 'react-map-gl';

import { useLocale } from 'next-intl';

import TooltipButton from '@/components/tooltip-button';
import Icon from '@/components/ui/icon';
import BoundariesPopup from '@/containers/map/content/map/popup/boundaries';
import GenericPopup from '@/containers/map/content/map/popup/generic';
import ProtectedAreaPopup from '@/containers/map/content/map/popup/protected-area';
import useResolvedParamsConfig from '@/hooks/use-resolved-params-config';
import { cn } from '@/lib/classnames';
import CircleWithDottedRedStrokeIcon from '@/styles/icons/circle-with-dotted-red-stroke.svg';
import CircleWithFillIcon from '@/styles/icons/circle-with-fill.svg';
Expand All @@ -12,10 +19,11 @@ import EstablishmentImplementedIcon from '@/styles/icons/implemented.svg';
import EstablishmentManagedIcon from '@/styles/icons/managed.svg';
import EstablishmentProposedIcon from '@/styles/icons/proposed.svg';
import { FCWithMessages } from '@/types';
import { LayerTyped } from '@/types/layers';
import { LayerTyped, ParamsConfig } from '@/types/layers';

export interface LegendItemsProps {
config: LayerTyped['legend_config'];
paramsConfig: ParamsConfig;
}

const ICONS_MAPPING = {
Expand All @@ -28,8 +36,44 @@ const ICONS_MAPPING = {
'establishment-implemented': EstablishmentImplementedIcon,
};

const LegendItem: FCWithMessages<LegendItemsProps> = ({ config }) => {
const { type, items } = config || {};
const LegendItem: FCWithMessages<LegendItemsProps> = ({ config, paramsConfig }) => {
const locale = useLocale();
const { current: map } = useMap();

const [zoom, setZoom] = useState(map?.getZoom() ?? 1);

const resolvedParamsConfigParams = useMemo(
() => ({
locale,
zoom,
}),
[locale, zoom]
);

const resolvedParamsConfig = useResolvedParamsConfig(paramsConfig, resolvedParamsConfigParams);
const dynamicLegendConfig = useMemo(
() =>
resolvedParamsConfig?.find(({ key }) => key === 'legend_config')?.default as
| LayerTyped['legend_config']['items']
| undefined,
[resolvedParamsConfig]
);

useEffect(() => {
const onZoom = () => {
setZoom(map.getZoom());
};

map.on('zoomend', onZoom);

return () => {
map.off('zoomend', onZoom);
};
}, [map, setZoom]);

const { type } = config ?? {};
let { items } = config ?? {};
items = dynamicLegendConfig ?? items;

switch (type) {
case 'basic':
Expand Down Expand Up @@ -93,10 +137,14 @@ const LegendItem: FCWithMessages<LegendItemsProps> = ({ config }) => {
</ul>

<ul className="mt-1 flex w-full">
{items.map(({ value }) => (
{items.map(({ color, value }, index) => (
<li
key={`${value}`}
className="flex-shrink-0 text-center text-xs"
key={`${color}`}
className={cn({
'flex flex-shrink-0 justify-center text-xs': true,
'justify-start': index === 0,
'justify-end': index + 1 === items.length,
})}
style={{
width: `${100 / items.length}%`,
}}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/containers/map/content/map/popup/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useLocale } from 'next-intl';
import BoundariesPopup from '@/containers/map/content/map/popup/boundaries';
import GenericPopup from '@/containers/map/content/map/popup/generic';
import ProtectedAreaPopup from '@/containers/map/content/map/popup/protected-area';
import useConfig from '@/hooks/use-config';
import useResolvedConfig from '@/hooks/use-resolved-config';
import { FCWithMessages } from '@/types';
import { useGetLayersId } from '@/types/generated/layer';
import { InteractionConfig, LayerTyped } from '@/types/layers';
Expand Down Expand Up @@ -39,7 +39,7 @@ const PopupItem: FCWithMessages<PopupItemProps> = ({ id }) => {
[id, interaction_config, params_config]
);

const parsedConfig = useConfig<InteractionConfig | ReactElement>(configParams);
const parsedConfig = useResolvedConfig<InteractionConfig | ReactElement>(configParams);

const INTERACTION_COMPONENT = useMemo(() => {
if (!parsedConfig) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { useEffect, useState } from 'react';

import { parseConfig } from '@/lib/json-converter';

export default function useConfig<Config>(params: Parameters<typeof parseConfig<Config>>[0]) {
export default function useResolvedConfig<Config>(
params: Parameters<typeof parseConfig<Config>>[0]
) {
const [config, setConfig] = useState<Config | null>(null);

useEffect(() => {
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/hooks/use-resolved-params-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';

import { resolveParamsConfig } from '@/lib/json-converter';
import { ParamsConfig } from '@/types/layers';

export default function useResolvedParamsConfig(
params_config: ParamsConfig,
settings: Record<string, unknown>
) {
const [paramsConfig, setParamsConfig] = useState<ParamsConfig | null>(null);

useEffect(() => {
const updateParamsConfig = async () => {
setParamsConfig(await resolveParamsConfig(params_config, settings));
};

updateParamsConfig();
}, [params_config, settings, setParamsConfig]);

return paramsConfig;
}
76 changes: 46 additions & 30 deletions frontend/src/lib/json-converter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const getParams = ({ params_config, settings = {} }: GetParamsProps) => {
if (!params_config) {
return {};
}

return params_config.reduce(
(acc, p) => {
return {
Expand Down Expand Up @@ -78,35 +79,17 @@ const createNewParamsConfigParam = async (
};
};

interface ParseConfigurationProps {
config: unknown;
params_config: unknown;
settings: Record<string, unknown>;
}

/**
* *`parseConfig`*
* Parse config with params_config
* @param {Object} config
* @param {Object} params_config
* @returns {Object} config
*
* Resolve the final params_config after creating any new eventual dynamic parameter
* @param params_config Initial params_config
* @param settings Values to replace the defaults of the parameters
*/
export const parseConfig = async <T>({
config,
params_config,
settings,
}: ParseConfigurationProps): Promise<T | null> => {
if (!config || !params_config) {
return null;
}

const JSON_CONVERTER = new JSONConverter({
configuration: JSON_CONFIGURATION,
});

let pc = params_config as ParamsConfig;
const initParameter = pc.find(({ key }) => key === '_INIT_');
export const resolveParamsConfig = async (
params_config: ParamsConfig,
settings: Record<string, unknown>
) => {
let finalParamsConfig = params_config;
const initParameter = finalParamsConfig.find(({ key }) => key === '_INIT_');

// If `initParameter` is defined, then we want to asynchronously create new parameters within `pc`
if (initParameter) {
Expand All @@ -128,7 +111,7 @@ export const parseConfig = async <T>({

return {
...res,
[key]: getParams({ params_config: pc, settings })[
[key]: getParams({ params_config: finalParamsConfig, settings })[
(name as string).replace('@@#params.', '')
],
};
Expand All @@ -137,10 +120,43 @@ export const parseConfig = async <T>({
newParams.push(param);
}

pc = [...pc, ...newParams];
finalParamsConfig = [...finalParamsConfig, ...newParams];
}

const params = getParams({ params_config: pc, settings });
return finalParamsConfig;
};

interface ParseConfigurationProps {
config: unknown;
params_config: ParamsConfig;
settings: Record<string, unknown>;
}

/**
* *`parseConfig`*
* Parse config with params_config
* @param {Object} config
* @param {Object} params_config
* @returns {Object} config
*
*/
export const parseConfig = async <T>({
config,
params_config,
settings,
}: ParseConfigurationProps): Promise<T | null> => {
if (!config || !params_config) {
return null;
}

const JSON_CONVERTER = new JSONConverter({
configuration: JSON_CONFIGURATION,
});

const params = getParams({
params_config: await resolveParamsConfig(params_config, settings),
settings,
});

// Merge enumerations with config
JSON_CONVERTER.mergeConfiguration({
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/lib/utils/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export function formatPercentage(
return v.format(displayPercentageSign ? value / 100 : value);
}

export function formatNumber(locale: string, value: number, options?: Intl.NumberFormatOptions) {
const formatter = Intl.NumberFormat(locale === 'en' ? 'en-US' : locale, options);
return formatter.format(value);
}

export function formatKM(locale: string, value: number, options?: Intl.NumberFormatOptions) {
if (value < 1 && value > 0) return '<1';

Expand All @@ -49,6 +54,7 @@ export function formatKM(locale: string, value: number, options?: Intl.NumberFor

const FORMATS = {
formatPercentage,
formatNumber,
formatKM,
} as const;

Expand Down
Loading

0 comments on commit d4ecd75

Please sign in to comment.