diff --git a/client/package.json b/client/package.json index 64077f1a..29ce627f 100644 --- a/client/package.json +++ b/client/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "1.0.7", "@t3-oss/env-nextjs": "0.6.1", + "@tailwindcss/typography": "^0.5.10", "@tanstack/react-query": "4.35.3", "@typescript-eslint/eslint-plugin": "6.7.3", "@typescript-eslint/parser": "6.7.3", @@ -46,7 +47,7 @@ "lodash-es": "^4.17.21", "mapbox-gl": "2.15.0", "next": "13.5.3", - "next-usequerystate": "^1.8.4", + "next-usequerystate": "^1.9.1", "pino": "8.15.1", "pino-http": "8.5.0", "pino-pretty": "10.2.0", @@ -60,7 +61,6 @@ "tailwind-merge": "1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "1.0.7", - "@tailwindcss/typography": "^0.5.10", "typescript": "5.2.2", "zod": "3.22.2" }, diff --git a/client/public/presentation.js b/client/public/presentation.js deleted file mode 100644 index c6701516..00000000 --- a/client/public/presentation.js +++ /dev/null @@ -1,219 +0,0 @@ -const presentation = { - "name": "Unemployment", - "category": "Test", - "description": ` -# h1 Heading 8-) -## h2 Heading -### h3 Heading -#### h4 Heading -##### h5 Heading -###### h6 Heading - -"Smartypants, double quotes" and 'single quotes' - - -## Emphasis - -**This is bold text** - -__This is bold text__ - -*This is italic text* - -_This is italic text_ - -## Blockquotes - -> Blockquotes can also be nested... ->> ...by using additional greater-than signs right next to each other... -> > > ...or with spaces between arrows. - -## Lists - -Unordered - -+ Create a list by starting a line with `+`, `-`, or `*` -+ Sub-lists are made by indenting 2 spaces: - - Marker character change forces new list start: - * Ac tristique libero volutpat at - + Facilisis in pretium nisl aliquet - - Nulla volutpat aliquam velit -+ Very easy! - -Ordered - -1. Lorem ipsum dolor sit amet -2. Consectetur adipiscing elit -3. Integer molestie lorem at massa - `, - "datum" : [ - { - "iso3": "ABW", - "2022": 3346 - }, - { - "iso3": "AIA", - "2022": 1262 - }, - { - "iso3": "ATG", - "2022": 4013 - }, - { - "iso3": "BES", - "2022": 1860 - }, - { - "iso3": "BHS", - "2022": 4336 - }, - { - "iso3": "BLZ", - "2022": 3844 - }, - { - "iso3": "BMU", - "2022": 4469 - }, - { - "iso3": "BRB", - "2022": 4798 - }, - { - "iso3": "CRI", - "2022": 4865 - }, - { - "iso3": "CUW", - "2022": 3957 - }, - { - "iso3": "CYM", - "2022": 4603 - }, - { - "iso3": "DMA", - "2022": 4381 - }, - { - "iso3": "DOM", - "2022": 4926 - }, - { - "iso3": "GLP", - "2022": 2665 - }, - { - "iso3": "GRD", - "2022": 4063 - }, - { - "iso3": "GUY", - "2022": 1896 - }, - { - "iso3": "HND", - "2022": 2565 - }, - { - "iso3": "HTI", - "2022": 2840 - }, - { - "iso3": "JAM", - "2022": 4020 - }, - { - "iso3": "KNA", - "2022": 4437 - }, - { - "iso3": "LCA", - "2022": 4143 - }, - { - "iso3": "MEX", - "2022": 3941 - }, - { - "iso3": "MSR", - "2022": 3438 - }, - { - "iso3": "PAN", - "2022": 4060 - }, - { - "iso3": "SUR", - "2022": 3030 - }, - { - "iso3": "TCA", - "2022": 3839 - }, - { - "iso3": "TTO", - "2022": 2544 - }, - { - "iso3": "VCT", - "2022": 1864 - }, - { - "iso3": "VGB", - "2022": 1665 - }, - { - "iso3": "VIR", - "2022": 1784 - } - ], - "layers": [ - { - "name": "Unemployment layer", - "type": "countries", - "dataset": "Unemployment", - "config": { - "styles": [ - { - "id": "unemployment-layer", - "type": "fill", - "paint": { - "fill-color": [ - "interpolate", - [ - "linear" - ], - [ - "get", - "2022" - ], - 0, - "#00F", - 5000, - "#0FF" - ], - "fill-opacity": "@@#params.opacity" - }, - "layout": { - "visibility": { - "v": "@@#params.visibility", - "@@function": "setVisibility" - } - } - } - ] - }, - "params_config": [ - { - "key": "opacity", - "default": 1 - }, - { - "key": "visibility", - "default": true - } - ] - } - ] -} \ No newline at end of file diff --git a/client/src/app/url-query-params.ts b/client/src/app/url-query-params.ts index ba4110ff..021aab2c 100644 --- a/client/src/app/url-query-params.ts +++ b/client/src/app/url-query-params.ts @@ -11,11 +11,8 @@ export const useSyncLayersSettings = () => { return useQueryState( "layers-settings", parseAsJson<{ - [key: string]: { - opacity: number; - visibility: boolean; - }; - }>().withDefault({}), + [key: string]: Record; + }>(), ); }; diff --git a/client/src/components/map/legend/item/index.tsx b/client/src/components/map/legend/item/index.tsx index a214e9c9..5033c0ff 100644 --- a/client/src/components/map/legend/item/index.tsx +++ b/client/src/components/map/legend/item/index.tsx @@ -28,10 +28,7 @@ export const LegendItem: React.FC = ({ // events onChangeOpacity, onChangeVisibility, - onChangeExpand, }) => { - const { expand } = settings || {}; - const validChildren = useMemo(() => { const chldn = Children.map(children, (Child) => { return isValidElement(Child); @@ -39,10 +36,8 @@ export const LegendItem: React.FC = ({ return chldn && chldn.some((c) => !!c); }, [children]); - const acordionState = expand ? `${id}` : undefined; - return ( - +
= ({ settingsManager={settingsManager} onChangeOpacity={onChangeOpacity} onChangeVisibility={onChangeVisibility} - onChangeExpand={onChangeExpand} InfoContent={InfoContent} /> diff --git a/client/src/components/map/legend/item/toolbar/index.tsx b/client/src/components/map/legend/item/toolbar/index.tsx index bf0baf37..96e1dcac 100644 --- a/client/src/components/map/legend/item/toolbar/index.tsx +++ b/client/src/components/map/legend/item/toolbar/index.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { PopoverArrow } from "@radix-ui/react-popover"; -import { LuEye, LuEyeOff, LuDroplet, LuChevronDown, LuInfo } from "react-icons/lu"; +import { LuEye, LuEyeOff, LuDroplet, LuInfo } from "react-icons/lu"; import { cn } from "@/lib/classnames"; @@ -19,10 +19,9 @@ export const LegendItemToolbar: React.FC = ({ settingsManager, onChangeOpacity, onChangeVisibility, - onChangeExpand, }: LegendItemToolbarProps) => { const [popoverOpen, setPopoverOpen] = useState(false); - const { opacity = 1, visibility = true, expand = true } = settings || {}; + const { opacity = 1, visibility = true } = settings || {}; return (
@@ -136,41 +135,6 @@ export const LegendItemToolbar: React.FC = ({
)}
- - {settingsManager?.expand && ( -
-
- - {/* */} - { - if (onChangeExpand) onChangeExpand(!expand); - }} - > - - - {/* */} - - -
{expand ? "Collapse layer" : "Expand layer"}
- - -
-
-
-
- )} ); }; diff --git a/client/src/components/map/legend/types.ts b/client/src/components/map/legend/types.ts index 220cc1fb..a7538fa1 100644 --- a/client/src/components/map/legend/types.ts +++ b/client/src/components/map/legend/types.ts @@ -14,26 +14,22 @@ type Sortable = { type OnChangeOrder = (id: string[]) => void; type OnChangeOpacity = (opacity: number) => void; type OnChangeVisibility = (visibility: boolean) => void; -type OnChangeExpand = (expand: boolean) => void; type OnChangeColumn = (column: string) => void; export type Settings = Record & { opacity?: number; visibility?: boolean; - expand?: boolean; }; export type SettingsManager = { opacity?: boolean; visibility?: boolean; - expand?: boolean; info?: boolean; }; export type LegendItemEvents = { onChangeOpacity?: OnChangeOpacity; onChangeVisibility?: OnChangeVisibility; - onChangeExpand?: OnChangeExpand; onChangeColumn?: OnChangeColumn; }; /* diff --git a/client/src/components/ui/loader.tsx b/client/src/components/ui/loader.tsx new file mode 100644 index 00000000..b4b0b473 --- /dev/null +++ b/client/src/components/ui/loader.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { PropsWithChildren } from "react"; + +import { cn } from "@/lib/classnames"; + +import { Skeleton } from "@/components/ui/skeleton"; + +export interface ContentLoaderProps extends PropsWithChildren { + skeletonClassName?: string; + data: unknown | undefined; + isPlaceholderData: boolean; + isFetching: boolean; + isFetched: boolean; + isError: boolean; +} + +const ContentLoader = ({ + skeletonClassName, + children, + data, + isPlaceholderData, + isFetching, + isFetched, + isError, +}: ContentLoaderProps) => { + return ( +
+ {isFetching && !isFetched && } + + {/* */} + + {isError && isFetched && !isFetching && "Error"} + + {!isPlaceholderData && !isError && isFetched && !!data && children} + + {!isPlaceholderData && !isError && isFetched && !data && "No data"} +
+ ); +}; + +export default ContentLoader; diff --git a/client/src/components/ui/skeleton.tsx b/client/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..af10ffd6 --- /dev/null +++ b/client/src/components/ui/skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from "@/lib/classnames"; + +function Skeleton({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export { Skeleton }; diff --git a/client/src/containers/map/index.tsx b/client/src/containers/map/index.tsx index efd20ac5..9b0888ee 100644 --- a/client/src/containers/map/index.tsx +++ b/client/src/containers/map/index.tsx @@ -5,6 +5,7 @@ import { LngLatBoundsLike, useMap } from "react-map-gl"; import { useSyncBbox } from "@/app/url-query-params"; import LayerManager from "@/containers/map/layer-manager"; +import Legend from "@/containers/map/legend"; import MapSettingsManagerPanel from "@/containers/map-settings"; import MapSettingsManager from "@/containers/map-settings/manager"; @@ -44,9 +45,12 @@ export default function MapContainer({ id = "default" }: { id?: string }) { - - + + + + + ); diff --git a/client/src/containers/map/layer-manager/index.tsx b/client/src/containers/map/layer-manager/index.tsx index d12f54aa..1f317fa5 100644 --- a/client/src/containers/map/layer-manager/index.tsx +++ b/client/src/containers/map/layer-manager/index.tsx @@ -1,5 +1,7 @@ "use client"; +import { useMemo } from "react"; + import { Layer } from "react-map-gl"; import { useSyncLayers, useSyncLayersSettings } from "@/app/url-query-params"; @@ -10,7 +12,29 @@ import { DeckMapboxOverlayProvider } from "@/components/map/provider"; const LayerManager = () => { const [layers] = useSyncLayers(); - const [layersSettings] = useSyncLayersSettings(); + const [layersSettings, setLayersSettings] = useSyncLayersSettings(); + + // Sync layers settings with layers + useMemo(() => { + if (!layers?.length && !layersSettings) return; + + if (!layers?.length && layersSettings) { + setLayersSettings(null); + return; + } + + const lSettingsKeys = Object.keys(layersSettings || {}); + + lSettingsKeys.forEach((key) => { + if (layers.includes(Number(key))) return; + + setLayersSettings((prev) => { + const current = { ...prev }; + delete current[key]; + return current; + }); + }); + }, [layers, layersSettings, setLayersSettings]); return ( @@ -43,7 +67,7 @@ const LayerManager = () => { key={l} id={l} beforeId={beforeId} - settings={layersSettings[l] ?? { opacity: 1, visibility: true, expand: true }} + settings={(layersSettings && layersSettings[l]) ?? { opacity: 1, visibility: true }} /> ); })} diff --git a/client/src/containers/map/legend/index.tsx b/client/src/containers/map/legend/index.tsx new file mode 100644 index 00000000..6890c314 --- /dev/null +++ b/client/src/containers/map/legend/index.tsx @@ -0,0 +1,99 @@ +import { useCallback, useMemo } from "react"; + +import { cn } from "@/lib/classnames"; + +import { useSyncLayers, useSyncLayersSettings } from "@/app/url-query-params"; + +import MapLegendItem from "@/containers/map/legend/item"; + +import Legend from "@/components/map/legend"; + +const MapLegend = ({ className = "" }) => { + const [layers, setLayers] = useSyncLayers(); + const [layersSettings, setLayersSettings] = useSyncLayersSettings(); + + const handleChangeOrder = useCallback( + (order: string[]) => { + const newLayers: number[] = order.reduce((prev: number[], curr) => { + const id = layers.find((layer) => layer === Number(curr)); + return !!id ? [...prev, id] : prev; + }, []); + + setLayers(newLayers); + }, + [layers, setLayers], + ); + + const handleChangeOpacity = useCallback( + (id: number, opacity: number) => + setLayersSettings((prev) => ({ + ...prev, + [id]: { + ...(prev && prev[id]), + opacity, + }, + })), + [setLayersSettings], + ); + + const handleChangeVisibility = useCallback( + (id: number, visibility: boolean) => + setLayersSettings((prev) => ({ + ...prev, + [id]: { + ...(prev && prev[id]), + visibility, + }, + })), + [setLayersSettings], + ); + + const sortable = layers?.length > 1; + + const ITEMS = useMemo(() => { + return layers.map((layer) => { + const settings = (layersSettings && layersSettings[layer]) ?? { + opacity: 1, + visibility: true, + }; + + return ( + { + handleChangeOpacity(layer, opacity); + }} + onChangeVisibility={(visibility: boolean) => { + handleChangeVisibility(layer, visibility); + }} + sortable={{ + enabled: sortable, + handle: layers.length > 1, + }} + /> + ); + }); + }, [layers, layersSettings, sortable, handleChangeOpacity, handleChangeVisibility]); + + return ( +
+ + {ITEMS} + +
+ ); +}; + +export default MapLegend; diff --git a/client/src/containers/map/legend/item.tsx b/client/src/containers/map/legend/item.tsx new file mode 100644 index 00000000..68c07c08 --- /dev/null +++ b/client/src/containers/map/legend/item.tsx @@ -0,0 +1,103 @@ +"use-client"; +import { ReactElement, createElement, isValidElement, useMemo } from "react"; + +import { parseConfig } from "@/lib/json-converter"; + +import { useGetLayersId } from "@/types/generated/layer"; +import { LayerTyped, LegendConfig } from "@/types/layers"; +import { LegendType } from "@/types/legend"; + +import { useSyncLayersSettings } from "@/app/url-query-params"; + +import LegendItem from "@/components/map/legend/item"; +import { + LegendTypeBasic, + LegendTypeChoropleth, + LegendTypeGradient, +} from "@/components/map/legend/item-types"; +import { LegendItemProps, LegendTypeProps, SettingsManager } from "@/components/map/legend/types"; +import ContentLoader from "@/components/ui/loader"; + +const LEGEND_TYPES: Record> = { + basic: LegendTypeBasic, + choropleth: LegendTypeChoropleth, + gradient: LegendTypeGradient, +}; + +type MapLegendItemProps = LegendItemProps; + +const getSettingsManager = (data: LayerTyped = {} as LayerTyped): SettingsManager => { + const { params_config, metadata } = data; + + if (!params_config?.length) return {}; + const p = params_config.reduce((acc: Record, { key }) => { + if (!key) return acc; + return { + ...acc, + [`${key}`]: true, + }; + }, {}); + + return { + ...p, + info: !!metadata, + }; +}; + +const MapLegendItem = ({ id, ...props }: MapLegendItemProps) => { + const [layersSettings] = useSyncLayersSettings(); + + const { data, isError, isFetched, isFetching, isPlaceholderData } = useGetLayersId(id, { + populate: "metadata", + }); + + const attributes = data?.data?.attributes as LayerTyped; + const legend_config = attributes?.legend_config; + const params_config = attributes?.params_config; + + const settingsManager = getSettingsManager(attributes); + + const LEGEND_COMPONENT = useMemo(() => { + const l = parseConfig({ + config: legend_config, + params_config, + settings: (layersSettings && layersSettings[`${id}`]) ?? {}, + }); + + if (!l) return null; + + if (isValidElement(l)) { + return l; + } + + if (!isValidElement(l) && "items" in l) { + const { type, ...props } = l; + return createElement(LEGEND_TYPES[type], props); + } + + return null; + }, [id, legend_config, params_config, layersSettings]); + + return ( + + } + > + {LEGEND_COMPONENT} + + + ); +}; + +export default MapLegendItem; diff --git a/client/src/types/legend.ts b/client/src/types/legend.ts new file mode 100644 index 00000000..3453a12b --- /dev/null +++ b/client/src/types/legend.ts @@ -0,0 +1,12 @@ +export const LEGEND_TYPE = ["basic", "choropleth", "gradient"] as const; + +export type LegendType = (typeof LEGEND_TYPE)[number]; + +export type Legend = { + type: LegendType; + title: string; + info?: string; + description?: string; + items?: { color: string; value: string }[]; + intersections?: { id: number; color: string }[]; +}; diff --git a/client/yarn.lock b/client/yarn.lock index 12292d26..27af7d19 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -3903,7 +3903,7 @@ __metadata: lodash-es: ^4.17.21 mapbox-gl: 2.15.0 next: 13.5.3 - next-usequerystate: ^1.8.4 + next-usequerystate: ^1.9.1 orval: ^6.19.0 pino: 8.15.1 pino-http: 8.5.0 @@ -7953,14 +7953,14 @@ __metadata: languageName: node linkType: hard -"next-usequerystate@npm:^1.8.4": - version: 1.8.4 - resolution: "next-usequerystate@npm:1.8.4" +"next-usequerystate@npm:^1.9.1": + version: 1.9.1 + resolution: "next-usequerystate@npm:1.9.1" dependencies: mitt: ^3.0.1 peerDependencies: - next: ^13.4 - checksum: 4ab19aa11fd32058246375bdcd7c76fdff357e15a460e80a30835b972b7458ab99e59e8ae38be26b32d27727b156b655c2a0105c200b994408fb26366133b496 + next: ^13.4 || ^14 + checksum: bb82e85ba2e1085b91d402878ce5b1d4eba37838c2e5f7ffba3a4e5b07f28fc1c78a077e130a1ec2d8e240e72b953ded85311020231e8e7660440baa53a54b40 languageName: node linkType: hard