diff --git a/apps/web/app/[lng]/map/[projectId]/page.tsx b/apps/web/app/[lng]/map/[projectId]/page.tsx index a105b129..49b0f16e 100644 --- a/apps/web/app/[lng]/map/[projectId]/page.tsx +++ b/apps/web/app/[lng]/map/[projectId]/page.tsx @@ -1,123 +1,138 @@ "use client"; -import type { XYZ_Layer } from "@/types/map/layer"; import { MAPBOX_TOKEN } from "@/lib/constants"; -import { Box, useTheme } from "@mui/material"; +import { Box, debounce, useTheme } from "@mui/material"; import "mapbox-gl/dist/mapbox-gl.css"; -import React, { useCallback, useState } from "react"; -import Map, { MapProvider, Layer, Source } from "react-map-gl"; +import React, { useMemo } from "react"; +import type { ViewStateChangeEvent } from "react-map-gl"; +import Map, { MapProvider } from "react-map-gl"; import Layers from "@/components/map/Layers"; +import Markers from "@/components/map/Markers"; import MobileDrawer from "@/components/map/panels/MobileDrawer"; -import { useSelector, useDispatch } from "react-redux"; +import { useSelector } from "react-redux"; import type { IStore } from "@/types/store"; -import { selectMapLayer } from "@/lib/store/styling/selectors"; import ProjectNavigation from "@/components/map/panels/ProjectNavigation"; import Header from "@/components/header/Header"; -import { useProject } from "@/lib/api/projects"; -import { useFilterExpressions } from "@/hooks/map/FilteringHooks"; -import { setMapLoading } from "@/lib/store/map/slice"; +import { + updateProjectInitialViewState, + useProject, + useProjectInitialViewState, + useProjectLayers, +} from "@/lib/api/projects"; +import { LoadingPage } from "@/components/common/LoadingPage"; -const sidebarWidth = 48; +const sidebarWidth = 52; const toolbarHeight = 52; export default function MapPage({ params: { projectId } }) { - const { basemaps, activeBasemapIndex, initialViewState } = useSelector( + const theme = useTheme(); + const { basemaps, activeBasemapIndex } = useSelector( (state: IStore) => state.styling, ); - const { project } = useProject(projectId); - const mapLayer = useSelector(selectMapLayer); - const dispatch = useDispatch(); - const { layerToBeFiltered } = useSelector( - (state: IStore) => state.mapFilters, - ); - - const { getFilterQueries } = useFilterExpressions(projectId); - - const { data: filters } = getFilterQueries(layerToBeFiltered); + const { + project, + isLoading: isProjectLoading, + isError: projectError, + } = useProject(projectId); + const { + initialView, + isLoading: isInitialViewLoading, + isError: projectInitialViewError, + } = useProjectInitialViewState(projectId); - const [layers, setLayers] = useState([ - { - id: "layer1", - sourceUrl: - "https://geoapi.goat.dev.plan4better.de/collections/user_data.84ca9acb3f30491d82ce938334164496/tiles/{z}/{x}/{y}", - color: "#FF0000", - }, - ]); + const { isLoading: areProjectLayersLoading, isError: projectLayersError } = + useProjectLayers(projectId); - const theme = useTheme(); + const isLoading = useMemo( + () => isProjectLoading || isInitialViewLoading || areProjectLayersLoading, + [isProjectLoading, isInitialViewLoading, areProjectLayersLoading], + ); - const addLayer = useCallback( - (newLayer: XYZ_Layer[]) => { - dispatch(setMapLoading(false)); - setLayers(newLayer); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [filters], + const hasError = useMemo( + () => projectError || projectInitialViewError || projectLayersError, + [projectError, projectInitialViewError, projectLayersError], + ); + const updateViewState = useMemo( + () => + debounce((e: ViewStateChangeEvent) => { + updateProjectInitialViewState(projectId, { + zoom: e.viewState.zoom, + latitude: e.viewState.latitude, + longitude: e.viewState.longitude, + pitch: e.viewState.pitch, + bearing: e.viewState.bearing, + min_zoom: initialView?.min_zoom ?? 0, + max_zoom: initialView?.max_zoom ?? 24, + }); + }, 2000), + [initialView?.max_zoom, initialView?.min_zoom, projectId], ); return ( - -
- - - - - - + {isLoading && } + {!isLoading && !hasError && ( + +
+ - {mapLayer ? ( - + + + + - - - ) : null} - {/* todo check */} - - - - - - - - + + + + + + + + + + )} + ); } diff --git a/apps/web/components/common/LoadingPage.tsx b/apps/web/components/common/LoadingPage.tsx new file mode 100644 index 00000000..41c9b5c2 --- /dev/null +++ b/apps/web/components/common/LoadingPage.tsx @@ -0,0 +1,20 @@ +import { Box, useTheme } from "@mui/material"; +import { Loading } from "@p4b/ui/components/Loading"; + +export function LoadingPage() { + const theme = useTheme(); + return ( + + + + ); +} diff --git a/apps/web/components/jobs/JobsPopper.tsx b/apps/web/components/jobs/JobsPopper.tsx index 50aba5af..28dbde20 100644 --- a/apps/web/components/jobs/JobsPopper.tsx +++ b/apps/web/components/jobs/JobsPopper.tsx @@ -20,7 +20,6 @@ export default function JobsPopper() { }); const [intervalId, setIntervalId] = useState(null); - console.log("jobs", jobs); useEffect(() => { console.log("jobs", jobs); if (!jobs?.items) return; diff --git a/apps/web/components/map/Layers.tsx b/apps/web/components/map/Layers.tsx index dbf2132b..8f6ba53b 100644 --- a/apps/web/components/map/Layers.tsx +++ b/apps/web/components/map/Layers.tsx @@ -1,105 +1,71 @@ -import React, { useEffect, useCallback } from "react"; +import React, { useMemo } from "react"; import { Source, Layer as MapLayer } from "react-map-gl"; -import type { XYZ_Layer } from "@/types/map/layer"; -import { useSelector } from "react-redux"; -import type { IStore } from "@/types/store"; -import { and_operator, or_operator } from "@/lib/utils/filtering/filtering_cql"; -import type { LayerProps } from "react-map-gl"; -import { v4 } from "uuid"; +import type { Layer as ProjectLayer } from "@/lib/validations/layer"; +import { GEOAPI_BASE_URL } from "@/lib/constants"; +import { useProject, useProjectLayers } from "@/lib/api/projects"; interface LayersProps { - layers: XYZ_Layer[]; - addLayer: (newLayer) => void; projectId: string; - filters: string[]; } const Layers = (props: LayersProps) => { - const sampleLayerID = "user_data.84ca9acb3f30491d82ce938334164496"; - const { layers, addLayer, filters } = props; - - const layerUrl = `${process.env.NEXT_PUBLIC_GEOAPI_URL}/collections/${sampleLayerID}/tiles/{z}/{x}/{y}`; - - const availableFilters = filters.filter( - (filterQuery) => filterQuery !== "{}", - ); - - const { logicalOperator } = useSelector((state: IStore) => state.mapFilters); - - const getQuery = useCallback(() => { - if (availableFilters.length) { - if (availableFilters.length === 1) { - return availableFilters[0]; - } else { - if (logicalOperator === "match_all_expressions") { - return and_operator(availableFilters); - } else { - return or_operator(availableFilters); - } - } - } - }, [availableFilters, logicalOperator]); - - const filterJson = getQuery(); - - function modifyLayer() { - const filterJson = getQuery(); - if (filterJson) { - const filteredLayerSource = `${layerUrl}?filter=${encodeURIComponent( - filterJson, - )}`; - addLayer([ - { - id: "layer1", - sourceUrl: filteredLayerSource, - color: "#FF0000", - }, - ]); - } else if (filterJson === "") { - addLayer([ - { - id: "layer1", - sourceUrl: layerUrl, - color: "#FF0000", - }, - ]); - } - - if (!availableFilters.length) { - addLayer([ - { - id: "layer1", - sourceUrl: layerUrl, - color: "#FF0000", - }, - ]); - } - } - - useEffect(() => { - modifyLayer(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filterJson]); - - const clusterLayer: LayerProps = { - id: "clusters", - type: "circle", - source: "composite", - "source-layer": "default", - paint: { - "circle-color": "#51bbd6", - "circle-radius": 5, - }, - }; + const { layers: projectLayers } = useProjectLayers(props.projectId); + const { project } = useProject(props.projectId); + const sortedLayers = useMemo(() => { + if (!projectLayers || !project) return []; + return projectLayers.sort( + (a, b) => + project?.layer_order.indexOf(a.id) - project.layer_order.indexOf(b.id), + ); + }, [projectLayers, project]); return ( <> - {layers.length - ? layers.map((layer: XYZ_Layer) => ( - - - - )) + {sortedLayers?.length + ? sortedLayers.map((layer: ProjectLayer) => + (() => { + if ( + ["feature", "external_vector_tile"].includes(layer.type) && + layer.properties && + ["circle", "fill", "line", "symbol"].includes( + layer.properties.type, + ) + ) { + return ( + + + + ); + } else if (layer.type === "external_imagery") { + return ( + + + + ); + } else { + return null; + } + })(), + ) : null} ); diff --git a/apps/web/components/map/MapboxOverlay.tsx b/apps/web/components/map/MapboxOverlay.tsx deleted file mode 100644 index 038222f8..00000000 --- a/apps/web/components/map/MapboxOverlay.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { DataType } from "@/types/map/common"; -import type { - GroupedLayer, - SourceProps, -} from "@/types/map/layers"; -import { Layer, Source } from "react-map-gl"; -import type { Layer as ZodLayer } from "@/lib/validations/layer"; - -export function groupBySource( - layers: (SourceProps & ZodLayer)[] -): GroupedLayer[] { - const groupedLayers = layers.reduce((acc, layer) => { - const { url, data_type, data_source_name, data_reference_year, layers } = - layer; - const existingGroup = acc.find( - (group) => group.url === url && group.data_type === data_type - ); - if (existingGroup) { - existingGroup.layers.push(layers); - } else { - acc.push({ - url, - data_type, - data_source_name, - data_reference_year, - layers: [layers], - }); - } - return acc; - }, [] as GroupedLayer[]); - return groupedLayers; -} - -export default function MapboxOverlay(layers: (SourceProps & ZodLayer)[]) { - const groupedLayers = groupBySource(layers); - const overlays = groupedLayers.map((group) => { - const { - url, - data_type, - data_source_name: _data_source_name, - data_reference_year: _data_reference_year, - layers, - } = group; - if (data_type === DataType.mvt) { - return ( - - {layers.map((layer) => ( - - ))} - - ); - } else if (data_type === DataType.geojson) { - return ( - - {layers.map((layer) => ( - - ))} - - ); - } else { - return null; - } - }); - return <>{overlays}; -} diff --git a/apps/web/components/map/Markers.tsx b/apps/web/components/map/Markers.tsx new file mode 100644 index 00000000..ee19bb2c --- /dev/null +++ b/apps/web/components/map/Markers.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { Marker } from "react-map-gl"; +import { useSelector } from "react-redux/es/hooks/useSelector"; +import { Icon, ICON_NAME } from "@p4b/ui/components/Icon"; +import { Box, useTheme } from "@mui/material"; + +const Markers = () => { + const { markers } = useSelector((state) => state.styling); + + const theme = useTheme(); + + return ( + <> + {markers.map((marker) => ( + + + + + + + + + ))} + + ); +}; + +export default Markers; diff --git a/apps/web/components/map/Sidebar.tsx b/apps/web/components/map/Sidebar.tsx index 164a2602..50273b5e 100644 --- a/apps/web/components/map/Sidebar.tsx +++ b/apps/web/components/map/Sidebar.tsx @@ -27,18 +27,13 @@ export type MapSidebarProps = { }; const MapSidebarList = (props: MapSidebarListProps) => { - const { items, justifyContent, sidebarPosition, active, sidebarWidth } = props; + const { items, justifyContent, sidebarPosition, active, sidebarWidth } = + props; const theme = useTheme(); - const htmlColor = (name: string) => { - if (name === active?.name) { - return theme.palette.primary.main; - } - return theme.palette.text.secondary - }; - return ( { { if (props.onClick) { @@ -81,7 +72,11 @@ const MapSidebarList = (props: MapSidebarListProps) => { > @@ -103,8 +98,7 @@ export type MapSidebarListProps = { }; export default function MapSidebar(props: MapSidebarProps) { - - const {width} = props; + const { width } = props; return ( + > @@ -72,11 +72,6 @@ export default function Container(props: ContainerProps) { }} > {title} - {direction === "left" ? ( close(undefined)}> @@ -100,6 +95,7 @@ export default function Container(props: ContainerProps) { paddingLeft: theme.spacing(3), paddingRight: theme.spacing(3), overflowY: "auto", + height: "100%", scrollbarGutter: "stable both-edges", "&::-webkit-scrollbar": { width: "6px", diff --git a/apps/web/components/map/panels/ProjectNavigation.tsx b/apps/web/components/map/panels/ProjectNavigation.tsx index 4de57958..437ca936 100644 --- a/apps/web/components/map/panels/ProjectNavigation.tsx +++ b/apps/web/components/map/panels/ProjectNavigation.tsx @@ -8,7 +8,6 @@ import Charts from "@/components/map/panels/Charts"; import Toolbox from "@/components/map/panels/toolbox/Toolbox"; import Filter from "@/components/map/panels/filter/Filter"; import Scenario from "@/components/map/panels/Scenario"; -import Isochrone from "@/components/map/panels/isochrone/Isochrone"; import MapStyle from "@/components/map/panels/mapStyle/MapStyle"; import MapSidebar from "@/components/map/Sidebar"; import { Zoom } from "@/components/map/controls/Zoom"; @@ -33,7 +32,7 @@ import type { MapSidebarItem } from "@/types/map/sidebar"; import type { MapSidebarProps } from "../Sidebar"; import type { IStore } from "@/types/store"; -const sidebarWidth = 48; +const sidebarWidth = 52; const toolbarHeight = 52; const ProjectNavigation = ({ projectId }) => { @@ -116,15 +115,6 @@ const ProjectNavigation = ({ projectId }) => { /> ), }, - { - icon: ICON_NAME.BULLSEYE, - name: t("panels.isochrone.isochrone"), - component: ( - - ), - }, { icon: ICON_NAME.SCENARIO, name: t("panels.scenario.scenario"), @@ -201,7 +191,6 @@ const ProjectNavigation = ({ projectId }) => { height: `calc(100% - ${toolbarHeight}px)`, marginTop: `${toolbarHeight}px`, width: 300, - borderLeft: `1px solid ${theme.palette.secondary.dark}}`, pointerEvents: "all", }} > @@ -247,8 +236,6 @@ const ProjectNavigation = ({ projectId }) => { padding: theme.spacing(4), }} > - {/* - */} { diff --git a/apps/web/components/map/panels/filter/Filter.tsx b/apps/web/components/map/panels/filter/Filter.tsx index 3c559f3b..886c1665 100644 --- a/apps/web/components/map/panels/filter/Filter.tsx +++ b/apps/web/components/map/panels/filter/Filter.tsx @@ -27,8 +27,6 @@ import type { MapSidebarItem } from "@/types/map/sidebar"; import HistogramSlider from "./histogramSlider/HistogramSlider"; import { histogramData } from "@/public/assets/data/histogramSample"; -import { useLayerHook } from "@/hooks/map/LayerHooks"; -import { useFilterExpressions } from "@/hooks/map/FilteringHooks"; import { useTranslation } from "@/i18n/client"; import { setMapLoading } from "@/lib/store/map/slice"; diff --git a/apps/web/components/map/panels/filter/FilterOptionField.tsx b/apps/web/components/map/panels/filter/FilterOptionField.tsx index fcd3013d..2e782c30 100644 --- a/apps/web/components/map/panels/filter/FilterOptionField.tsx +++ b/apps/web/components/map/panels/filter/FilterOptionField.tsx @@ -1,7 +1,6 @@ import type { ComparerMode } from "@/types/map/filtering"; import React, { useState, useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { useFilterExpressions } from "@/hooks/map/FilteringHooks"; import { usePathname } from "next/navigation"; import { is, diff --git a/apps/web/components/map/panels/isochrone/Isochrone.tsx b/apps/web/components/map/panels/isochrone/Isochrone.tsx deleted file mode 100644 index 9b1822f0..00000000 --- a/apps/web/components/map/panels/isochrone/Isochrone.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState } from "react"; -import Container from "@/components/map/panels/Container"; -import { Box, Button, useTheme } from "@mui/material"; -import IsochroneSettings from "@/components/map/panels/isochrone/IsochroneSettings"; -import StartingPoint from "@/components/map/panels/isochrone/StartingPoint"; -import { useTranslation } from "@/i18n/client"; -import SaveResult from "@/components/map/panels/toolbox/tools/SaveResult"; - -import type { MapSidebarItem } from "@/types/map/sidebar"; -import type { StartingPointType } from "@/types/map/isochrone"; - -type IsochroneProps = { - setActiveRight: (item: MapSidebarItem | undefined) => void; -}; - -type RoutingTypes = - | "bus" - | "tram" - | "rail" - | "subway" - | "ferry" - | "cable_car" - | "gondola" - | "funicular" - | "walk" - | "bicycle" - | "car"; - -const Isochrone = (props: IsochroneProps) => { - // Isochrone Settings states - const [routing, setRouting] = useState(undefined); - const [speed, setSpeed] = useState(undefined); - const [distance, setDistance] = useState(undefined); - const [travelTime, setTravelTime] = useState(undefined); - const [steps, setSteps] = useState(undefined); - const [outputName, setOutputName] = useState(undefined); - const [folderSaveID, setFolderSaveID] = useState( - undefined, - ); - - // Sarting point states - const [startingType, setStartingType] = useState< - StartingPointType | undefined - >(undefined); - const [startingPoint, setStartingPoint] = useState( - undefined, - ); - - const theme = useTheme(); - const { t } = useTranslation("maps"); - const { setActiveRight } = props; - - return ( - - - - - - - - - - - - } - /> - ); -}; - -export default Isochrone; diff --git a/apps/web/components/map/panels/isochrone/StartingPoint.tsx b/apps/web/components/map/panels/isochrone/StartingPoint.tsx deleted file mode 100644 index 34a135cb..00000000 --- a/apps/web/components/map/panels/isochrone/StartingPoint.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import React, { useEffect, useState, useMemo } from "react"; -import { - Box, - Typography, - FormControl, - InputLabel, - Select, - MenuItem, - debounce, - useTheme, - TextField, - Button, - Autocomplete, -} from "@mui/material"; -import { useTranslation } from "@/i18n/client"; -import { v4 } from "uuid"; -import { useProjectLayers } from "@/hooks/map/layersHooks"; -import { useMap } from "react-map-gl"; -import { Icon, ICON_NAME } from "@p4b/ui/components/Icon"; -import search from "@/lib/services/geocoder"; -import { testForCoordinates } from "@/components/map/controls/Geocoder"; -import { MAPBOX_TOKEN } from "@/lib/constants"; - -import type { StartingPointType } from "@/types/map/isochrone"; -import type { SelectChangeEvent } from "@mui/material"; -import type { Result } from "@/types/map/controllers"; -import type { FeatureCollection } from "geojson"; - -interface PickLayerProps { - startingType: StartingPointType | undefined; - setStartingType: (value: StartingPointType) => void; - startingPoint: string | undefined; - setStartingPoint: (value: string) => void; -} - -const StartingPoint = (props: PickLayerProps) => { - const { startingType, setStartingPoint, startingPoint, setStartingType } = - props; - const { projectLayers } = useProjectLayers(); - const [getCoordinates, setGetCoordinates] = useState(false); - const [value, setValue] = useState(null); - const [options, setOptions] = useState([]); - const [inputValue, setInputValue] = useState(""); - // const [focused, setFocused] = useState(false); - // const [collapsed, setCollapsed] = useState(true); - - const theme = useTheme(); - const { map } = useMap(); - const { t } = useTranslation("maps"); - - const fetch = useMemo( - () => - debounce( - ( - request: { value: string }, - onresult: (_error: Error, fc: FeatureCollection) => void, - ) => { - search( - "https://api.mapbox.com", //endpoint, - "mapbox.places", //source, - MAPBOX_TOKEN, - request.value, - onresult, - // proximity, - // country, - // bbox, - // types, - // limit, - // autocomplete, - // language, - ); - }, - 400, - ), - [], - ); - - useEffect(() => { - if (getCoordinates) { - map.on("click", (event) => { - setStartingPoint(`${event.lngLat.lat},${event.lngLat.lng}`); - setGetCoordinates(false); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getCoordinates]); - - useEffect(() => { - let active = true; - if (inputValue === "") { - setOptions(value ? [value] : []); - return undefined; - } - const resultCoordinates = testForCoordinates(inputValue); - if (resultCoordinates[0]) { - const [_, latitude, longitude] = resultCoordinates; - setOptions([ - { - feature: { - id: "", - type: "Feature", - place_type: ["coordinate"], - relevance: 1, - properties: { - accuracy: "point", - }, - text: "", - place_name: "", - center: [longitude, latitude], - geometry: { - type: "Point", - coordinates: [longitude, latitude], - interpolated: false, - }, - address: "", - context: [], - }, - label: `${latitude}, ${longitude}`, - }, - ]); - return undefined; - } - fetch({ value: inputValue }, (error: Error, fc: FeatureCollection) => { - if (active) { - if (!error && fc && fc.features) { - setOptions( - fc.features - .map((feature) => ({ - feature: feature, - label: feature.place_name, - })) - .filter((feature) => feature.label), - ); - } - } - }); - - return () => { - active = false; - }; - }, [value, inputValue, fetch]); - - return ( - - - {t("panels.isochrone.starting.starting")} - - - - - {t("panels.isochrone.starting.type")} - - - - - {/* Pick layer in browse_layer */} - - - {t("panels.isochrone.starting.pick_layer")} - - - - - {t("panels.isochrone.starting.layer")} - - - - - - {/* select point on map in place_on_map */} - - - {t("panels.isochrone.starting.pick_on_map")} - - - ) => { - setStartingPoint(event.target.value as string); - }} - sx={{ - margin: `${theme.spacing(1)} 0`, - width: "70%", - }} - /> - - - - - - {t("panels.isochrone.starting.search_location")} - - x} - options={options} - fullWidth - sx={{ - margin: `${theme.spacing(1)} 0`, - }} - onChange={(_event: unknown, newValue: Result | null) => { - setOptions(newValue ? [newValue, ...options] : options); - setValue(newValue); - }} - onInputChange={(_event, newInputValue) => { - setInputValue(newInputValue); - }} - renderInput={(params) => ( - - )} - /> - - - ); -}; - -export default StartingPoint; diff --git a/apps/web/components/map/panels/layer/FilterList.tsx b/apps/web/components/map/panels/layer/FilterList.tsx deleted file mode 100644 index 1679b69e..00000000 --- a/apps/web/components/map/panels/layer/FilterList.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import { Chip, useTheme } from "@mui/material"; -import { Swiper, SwiperSlide } from "swiper/react"; -import { v4 } from "uuid"; - -import "swiper/css"; -import "swiper/css/pagination"; - -interface FilterListProps { - chips: string[]; - setActiveFilter: (value: string) => void; - activeFilter: string; -} - -const FilterList = (props: FilterListProps) => { - const { chips, setActiveFilter, activeFilter } = props; - - const theme = useTheme(); - - function handleDelete() { - setActiveFilter("none"); - } - - return ( - <> - - {chips.map((filter) => ( - - setActiveFilter(filter)} - variant={activeFilter === filter ? undefined : "outlined"} - sx={{ paddingX: theme.spacing(2) }} - onDelete={activeFilter === filter ? handleDelete : undefined} - /> - - ))} - - - ); -}; - -export default FilterList; diff --git a/apps/web/components/map/panels/layer/Layer.tsx b/apps/web/components/map/panels/layer/Layer.tsx index 0a5d6697..a1a2263a 100644 --- a/apps/web/components/map/panels/layer/Layer.tsx +++ b/apps/web/components/map/panels/layer/Layer.tsx @@ -1,51 +1,50 @@ import Container from "@/components/map/panels/Container"; -import { useState, useEffect } from "react"; +import { useMemo, useState } from "react"; import { ICON_NAME, Icon } from "@p4b/ui/components/Icon"; import { - Card, - Checkbox, - Stack, - Typography, useTheme, - Skeleton, Box, FormControl, InputLabel, OutlinedInput, InputAdornment, + Card, + Grid, + IconButton, + styled, + Typography, + Stack, + Tooltip, } from "@mui/material"; -import { getFrequentValuesOnProperty, filterSearch } from "@/lib/utils/helpers"; -import { useAppDispatch } from "@/hooks/useAppDispatch"; -import { activateLayer, deactivateLayer } from "@/lib/store/layer/slice"; -import AddLayer from "../../modals/AddLayer"; -import FilterList from "./FilterList"; import { useTranslation } from "@/i18n/client"; -import { setLayers } from "@/lib/store/layer/slice"; -import { getProjectLayers } from "@/lib/api/projects"; +import type { PopperMenuItem } from "@/components/common/PopperMenu"; +import MoreMenu from "@/components/common/PopperMenu"; import type { ChangeEvent } from "react"; import type { MapSidebarItem } from "@/types/map/sidebar"; -import type { Layer } from "@/lib/validations/layer"; -interface LoaderCheckerProps { - children: React.ReactNode; - projectLayers: Layer[] | null; -} - -const LoaderChecker = (props: LoaderCheckerProps) => { - const { children, projectLayers } = props; - const theme = useTheme(); - - return !projectLayers ? ( - - - - - - ) : ( - <>{children} - ); -}; +import { + addProjectLayers, + deleteProjectLayer, + updateProject, + updateProjectLayer, + useProject, + useProjectLayers, +} from "@/lib/api/projects"; +import React from "react"; +import { DragIndicator } from "@mui/icons-material"; +import type { Layer as MapLayer } from "@/lib/validations/layer"; +import { DndContext, closestCenter } from "@dnd-kit/core"; +import { + SortableContext, + arrayMove, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { useLayerSettingsMoreMenu } from "@/hooks/map/LayerPanelHooks"; +import { toast } from "react-toastify"; interface PanelProps { onCollapse?: () => void; @@ -53,59 +52,183 @@ interface PanelProps { projectId: string; } -const LayerPanel = ({ - setActiveLeft, - projectId -}: PanelProps) => { - const theme = useTheme(); - const dispatch = useAppDispatch(); +const StyledDragHandle = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + color: theme.palette.text.secondary, + opacity: 0, + ":hover": { + cursor: "move", + color: theme.palette.text.primary, + }, +})); - const [activeFilter, setActiveFilter] = useState("All"); - const [searchString, setSearchString] = useState(""); +export const DragHandle: React.FC<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listeners?: any; + children?: React.ReactNode; +}> = ({ listeners, children }) => ( + + {children} + +); + +type SortableLayerTileProps = { + id: number; + layer: MapLayer; + actions?: React.ReactNode; +}; + +export function SortableLayerTile(props: SortableLayerTileProps) { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: props.id }); const { t } = useTranslation("maps"); - const [resultProjectLayers, setResultProjectLayers] = useState< - Layer[] - >([]); + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; - function filterConditionals(layer: Layer) { - if (["none", "All"].includes(activeFilter)) { - return true; - } else if (activeFilter === layer.type) { - return true; - } - return false; - } + return ( + + + + + + + + + + + {t(`maps:${props.layer.type}`)} + + + {props.layer.name} + + + + + {props.actions} + + + + ); +} +const LayerPanel = ({ setActiveLeft, projectId }: PanelProps) => { + const theme = useTheme(); + const { t } = useTranslation("maps"); + const { layers: projectLayers, mutate: mutateProjectLayers } = + useProjectLayers(projectId); + + const { project, mutate: mutateProject } = useProject(projectId); + const sortedLayers = useMemo(() => { + if (!projectLayers || !project) return []; + return projectLayers.sort( + (a, b) => + project?.layer_order.indexOf(a.id) - project.layer_order.indexOf(b.id), + ); + }, [projectLayers, project]); + + const [searchString, setSearchString] = useState(""); function changeSearch(text: string) { setSearchString(text); - setResultProjectLayers( - filterSearch(resultProjectLayers ? resultProjectLayers : [], "name", text), - ); } - function toggleLayerView(checked: boolean, layer: Layer) { - const updatedLayers = resultProjectLayers?.map((projLayer) => - projLayer.name === layer.name - ? { ...projLayer, active: checked } - : projLayer, - ); + const { + layerMoreMenuOptions, + // activeLayer, + // moreMenuState, + // closeMoreMenu, + openMoreMenu, + } = useLayerSettingsMoreMenu(); - if (updatedLayers) { - setResultProjectLayers(updatedLayers); - dispatch(checked ? activateLayer(layer) : deactivateLayer(layer)); + async function toggleLayerVisibility(layer: MapLayer) { + const layers = JSON.parse(JSON.stringify(projectLayers)); + const index = layers.findIndex((l) => l.id === layer.id); + const layerToUpdate = layers[index]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let properties = layerToUpdate.properties as any; + if (!properties) { + properties = {}; + } + if (!properties?.layout) { + properties.layout = {}; } + properties.layout.visibility = + !properties.layout.visibility || + properties.layout.visibility === "visible" + ? "none" + : "visible"; + + layerToUpdate.properties = properties; + await mutateProjectLayers(layers, false); + await updateProjectLayer(projectId, layer.id, layerToUpdate); } + async function handleDragEnd(event) { + if (!project) return; + const { active, over } = event; + const projectToUpdate = JSON.parse(JSON.stringify(project)); + const layerOrder = projectToUpdate.layer_order; + const oldIndex = layerOrder.indexOf(active.id); - useEffect(() => { - getProjectLayers(projectId).then((data) => { - console.log(data) - const layers = data.map((layer: Layer) => ({ ...layer, active: false })); - setResultProjectLayers(layers); - dispatch(setLayers(layers)); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const newIndex = layerOrder.indexOf(over.id); + const newOrderArray = arrayMove(layerOrder, oldIndex, newIndex); + const updatedProject = { + ...projectToUpdate, + layer_order: newOrderArray, + }; + if (projectLayers) { + const oldLayer = projectLayers.find((l) => l.id === active.id); + const newLayer = projectLayers.find((l) => l.id === over.id); + if (oldLayer && newLayer) { + // force a re-render of the map layers (can find a better way to do this later) + oldLayer.updated_at = new Date().toISOString(); + newLayer.updated_at = new Date().toISOString() + "1"; + } + } + try { + mutateProject(updatedProject, false); + await updateProject(projectId, updatedProject); + } catch (error) { + toast.error("Error updating project layer order"); + } + } + + async function deleteLayer(layer: MapLayer) { + try { + await deleteProjectLayer(projectId, layer.id); + mutateProjectLayers(projectLayers?.filter((l) => l.id !== layer.id)); + } catch (error) { + toast.error("Error removing layer from project"); + } + } + + async function duplicateLayer(layer: MapLayer) { + try { + await addProjectLayers(projectId, [layer.layer_id]); + mutateProjectLayers(); + } catch (error) { + toast.error("Error duplicating layer"); + } + } return ( - - - + + {t("search")} + + + + + } + value={searchString} + onChange={( + e: ChangeEvent, + ) => changeSearch(e.target.value)} + label={t("search")} + /> + + + + {sortedLayers && sortedLayers?.length > 0 && ( + - - {t("search")} - - - - - } - value={searchString} - onChange={( - e: ChangeEvent, - ) => changeSearch(e.target.value)} - label={t("search")} - /> - - - - - - - {resultProjectLayers?.map((layer) => - filterConditionals(layer) ? ( - - - - - } - icon={ - - } - /> - - - {layer.name} - - - - , - checked: boolean, - ) => toggleLayerView(checked, layer)} - checkedIcon={ - - } - icon={ - - } - /> - - - - ) : null, - )} - + layer.id)} + strategy={verticalListSortingStrategy} + > + {sortedLayers?.map((layer) => ( + + + toggleLayerVisibility(layer)} + > + + + + + + + + + } + onSelect={async (menuItem: PopperMenuItem) => { + if (menuItem.id === "delete") { + await deleteLayer(layer); + } else if (menuItem.id === "duplicate") { + await duplicateLayer(layer); + } else { + console.log("Selected menu item", menuItem); + openMoreMenu(menuItem, layer); + } + }} + /> + + } + /> + ))} + + + )} } diff --git a/apps/web/components/map/panels/mapStyle/MapStyle.tsx b/apps/web/components/map/panels/mapStyle/MapStyle.tsx index d9f3c60a..773569ce 100644 --- a/apps/web/components/map/panels/mapStyle/MapStyle.tsx +++ b/apps/web/components/map/panels/mapStyle/MapStyle.tsx @@ -1,9 +1,5 @@ import Container from "@/components/map/panels/Container"; import { - // Checkbox, - // IconButton, - // CardContent, - // CardMedia, Button, Card, Divider, @@ -30,7 +26,6 @@ import ColorOptionFill from "@/components/map/panels/mapStyle/ColorOptionFill"; import ColorOptionLine from "@/components/map/panels/mapStyle/ColorOptionLine"; import StrokeOptionLine from "@/components/map/panels/mapStyle/StrokeOptionLine"; import MarkerOptionSymbol from "@/components/map/panels/mapStyle/MarkerOptionSymbol"; -import { fetchLayerData } from "@/lib/store/styling/actions"; import { useAppDispatch } from "@/hooks/useAppDispatch"; import ColorOptionSymbol from "@/components/map/panels/mapStyle/ColorOptionSymbol"; import StrokeOptionSymbol from "@/components/map/panels/mapStyle/StrokeOptionSymbol"; @@ -69,19 +64,13 @@ const MapStylePanel = ({ setActiveRight, projectId }: MapStyleProps) => { dispatch(setTabValue(newValue)); }; - const resetStylesHandler = () => { - if (projectId) { - dispatch(fetchLayerData(projectId)); - } - }; - const saveStylesHandler = () => { dispatch(saveStyles()); }; return ( { }} color="secondary" variant="outlined" - onClick={resetStylesHandler} > {t("panels.layer_design.reset")} diff --git a/apps/web/components/map/panels/toolbox/tools/InputLayer.tsx b/apps/web/components/map/panels/toolbox/tools/InputLayer.tsx index e5a25070..0f6d5159 100644 --- a/apps/web/components/map/panels/toolbox/tools/InputLayer.tsx +++ b/apps/web/components/map/panels/toolbox/tools/InputLayer.tsx @@ -8,9 +8,10 @@ import { InputLabel, MenuItem, } from "@mui/material"; -import { useProjectLayers } from "@/hooks/map/layersHooks"; import { v4 } from "uuid"; import { useTranslation } from "@/i18n/client"; +import { useProjectLayers } from "@/lib/api/projects"; +import { useParams } from "next/navigation"; import type { SelectChangeEvent } from "@mui/material"; @@ -27,7 +28,11 @@ const InputLayer = (props: PickLayerProps) => { const theme = useTheme(); const { t } = useTranslation("maps"); - const { projectLayers } = useProjectLayers(); + const { projectId } = useParams(); + + const { layers: projectLayers } = useProjectLayers( + typeof projectId === "string" ? projectId : "", + ); const handleSingleChange = (event: SelectChangeEvent) => { setInputValues(event.target.value as string); @@ -68,11 +73,13 @@ const InputLayer = (props: PickLayerProps) => { handleMultipleChange(event, 0) } > - {projectLayers.map((layer) => ( - - {layer.name} - - ))} + {projectLayers + ? projectLayers.map((layer) => ( + + {layer.name} + + )) + : null} @@ -102,11 +109,13 @@ const InputLayer = (props: PickLayerProps) => { handleMultipleChange(event, 1) } > - {projectLayers.map((layer) => ( - - {layer.name} - - ))} + {projectLayers + ? projectLayers.map((layer) => ( + + {layer.name} + + )) + : null} @@ -134,13 +143,13 @@ const InputLayer = (props: PickLayerProps) => { value={inputValues} onChange={handleSingleChange} > - {projectLayers.map((layer) => + {projectLayers ? projectLayers.map((layer) => layerTypes.includes(layer.feature_layer_geometry_type) ? ( - + {layer.name} ) : null, - )} + ) : null} diff --git a/apps/web/components/map/panels/toolbox/tools/ToolTabs.tsx b/apps/web/components/map/panels/toolbox/tools/ToolTabs.tsx index ef174440..b2da305f 100644 --- a/apps/web/components/map/panels/toolbox/tools/ToolTabs.tsx +++ b/apps/web/components/map/panels/toolbox/tools/ToolTabs.tsx @@ -1,12 +1,14 @@ import React, { useState } from "react"; import { Box, useTheme, Typography } from "@mui/material"; import { v4 } from "uuid"; -import Join from "@/components/map/panels/toolbox/tools/join/Join"; import { Icon, ICON_NAME } from "@p4b/ui/components/Icon"; -import Aggregate from "@/components/map/panels/toolbox/tools/aggregate/Aggregate"; import { useParams } from "next/navigation"; import { useTranslation } from "@/i18n/client"; +import Join from "@/components/map/panels/toolbox/tools/join/Join"; +import Aggregate from "@/components/map/panels/toolbox/tools/aggregate/Aggregate"; +import IsochroneTabs from "@/components/map/panels/toolbox/tools/accessibility_indicators/IsochroneTabs"; + interface ToolTabType { name: string; tooltip: string; @@ -37,6 +39,13 @@ const ToolTabs = () => { value: "aggregate_features", element: , }, + { + name: t("panels.tools.accessibility_indicators.accessibility_indicators"), + tooltip: + "Utilize join tool to merge columns from different layers. Apply functions like count, sum, min, etc., for desired results.", + value: "aggregate_features", + element: , + }, { name: "Summarize features", tooltip: @@ -52,7 +61,7 @@ const ToolTabs = () => { element:

origin

, }, ]; - + const handleChange = (newValue: ToolTabType | undefined) => { setValue(newValue); }; diff --git a/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/IsochroneTabs.tsx b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/IsochroneTabs.tsx new file mode 100644 index 00000000..3b908a0b --- /dev/null +++ b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/IsochroneTabs.tsx @@ -0,0 +1,79 @@ +import React, { useState } from "react"; +import { Box, useTheme, Typography } from "@mui/material"; +import { v4 } from "uuid"; +import { Icon, ICON_NAME } from "@p4b/ui/components/Icon"; +// import { useParams } from "next/navigation"; +import { useTranslation } from "@/i18n/client"; + +import Isochrone from "@/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/Isochrone"; + +interface ToolTabType { + name: string; + tooltip: string; + value: string; + element: React.ReactNode; +} + +const IsochroneTabs = () => { + const [value, setValue] = useState(undefined); + + const {t} = useTranslation("maps"); + + const theme = useTheme(); + // const params = useParams(); + + const tabs: ToolTabType[] = [ + { + name: t("panels.tools.accessibility_indicators.isochrone"), + tooltip: + "Utilize join tool to merge columns from different layers. Apply functions like count, sum, min, etc., for desired results.", + value: "aggregate_features", + element: , + }, + ]; + + const handleChange = (newValue: ToolTabType | undefined) => { + setValue(newValue); + }; + + return ( + + {!value && + tabs.map((tab) => ( + handleChange(tab)} + > + + + {tab.name} + + + + ))} + {value ? ( + <> + {/* handleChange(undefined)}>Back */} + {value.element} + + ) : null} + + ); +}; + +export default IsochroneTabs; diff --git a/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/Isochrone.tsx b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/Isochrone.tsx new file mode 100644 index 00000000..9765f99f --- /dev/null +++ b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/Isochrone.tsx @@ -0,0 +1,220 @@ +import React, { useState } from "react"; +import { Box, Button, useTheme, Typography } from "@mui/material"; +import IsochroneSettings from "@/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/IsochroneSettings"; +import StartingPoint from "@/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/StartingPoint"; +import { useTranslation } from "@/i18n/client"; +import SaveResult from "@/components/map/panels/toolbox/tools/SaveResult"; +import { + SendIsochroneRequest, + SendPTIsochroneRequest, + SendCarIsochroneRequest, +} from "@/lib/api/isochrone"; +import { v4 } from "uuid"; +import { useDispatch } from "react-redux"; +import { removeMarker } from "@/lib/store/styling/slice"; + +import type { StartingPointType } from "@/types/map/isochrone"; +import type { RoutingTypes, PTModeTypes } from "@/types/map/isochrone"; + +const Isochrone = () => { + // Isochrone Settings states + const [routing, setRouting] = useState(undefined); + const [ptModes, setPtModes] = useState([ + "bus", + "tram", + "rail", + "subway", + "ferry", + "cable_car", + "gondola", + "funicular", + ]); + const [speed, setSpeed] = useState(undefined); + const [distance, setDistance] = useState(undefined); + const [travelTime, setTravelTime] = useState(undefined); + const [steps, setSteps] = useState(undefined); + + // Sarting point states + const [startingType, setStartingType] = useState< + StartingPointType | undefined + >(undefined); + const [startingPoint, setStartingPoint] = useState( + undefined, + ); + + // Save Result states + const [outputName, setOutputName] = useState(`isochrone-${v4()}`); + const [folderSaveID, setFolderSaveID] = useState( + undefined, + ); + + const theme = useTheme(); + const { t } = useTranslation("maps"); + const dispatch = useDispatch(); + + const handleReset = () => { + setRouting(undefined); + setSpeed(undefined); + setDistance(undefined); + setTravelTime(undefined); + setSteps(undefined); + setOutputName(`isochrone-${v4()}`); + setFolderSaveID(undefined); + setStartingType(undefined); + setStartingPoint(undefined); + dispatch(removeMarker()); + }; + + const getStartingPoint = () => { + if (startingType && startingPoint) { + switch (startingType) { + case "place_on_map": + console.log(startingPoint) + return { + latitude: [ + ...startingPoint.map((startPoint) => + parseFloat(startPoint.split(",")[0]), + ), + ], + longitude: [ + ...startingPoint.map((startPoint) => + parseFloat(startPoint.split(",")[1]), + ), + ], + }; + case "address_input": + return { + latitude: [parseFloat(startingPoint[1])], + longitude: [parseFloat(startingPoint[0])], + }; + case "browse_layers": + return { + layer_id: startingPoint, + }; + } + } + }; + + const handleRun = () => { + if ( + routing && + startingPoint && + startingType && + steps && + outputName && + folderSaveID + ) { + const isochroneBody = { + starting_points: getStartingPoint(), + routing_type: routing, + travel_cost: {}, + result_target: { + layer_name: outputName, + folder_id: folderSaveID, + }, + }; + + if (!distance) { + isochroneBody.travel_cost = { + max_traveltime: travelTime, + traveltime_step: steps, + speed: speed ? speed : undefined, + }; + } + + if (!speed && !travelTime) { + isochroneBody.travel_cost = { + max_distance: distance, + distance_step: steps, + }; + } + + if (speed) { + isochroneBody.travel_cost["speed"] = speed; + } + + if (routing.includes(",")) { + isochroneBody["routing_type"] = { + mode: routing.split(","), + egress_mode: "walk", + access_mode: "walk", + }; + + isochroneBody["time_window"] = { + weekday: "weekday", + from_time: 25200, + to_time: 32400, + }; + SendPTIsochroneRequest(isochroneBody); + } else if (routing === "car_peak") { + SendCarIsochroneRequest(isochroneBody); + } else { + SendIsochroneRequest(isochroneBody); + } + } + }; + + return ( + + + + Isochrones illustrate reachable areas within a set travel time or + distance from a specific point, aiding in spatial analysis and route + planning. + + + {routing ? ( + + ) : null} + {startingType && startingPoint ? ( + + ) : null} + + + + + + + ); +}; + +export default Isochrone; diff --git a/apps/web/components/map/panels/isochrone/IsochroneSettings.tsx b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/IsochroneSettings.tsx similarity index 70% rename from apps/web/components/map/panels/isochrone/IsochroneSettings.tsx rename to apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/IsochroneSettings.tsx index 92d6a268..68725ef0 100644 --- a/apps/web/components/map/panels/isochrone/IsochroneSettings.tsx +++ b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/IsochroneSettings.tsx @@ -10,17 +10,23 @@ import { TextField, Tab, Autocomplete, + Checkbox, } from "@mui/material"; import { TabContext, TabPanel, TabList } from "@mui/lab"; import { useTranslation } from "@/i18n/client"; import { v4 } from "uuid"; +import { useDispatch } from "react-redux"; +import { removeMarker } from "@/lib/store/styling/slice"; +import { ptModes, routingModes } from "@/public/assets/data/isochroneModes"; import type { SelectChangeEvent } from "@mui/material"; -import type { RoutingTypes } from "@/types/map/isochrone"; +import type { RoutingTypes, PTModeTypes } from "@/types/map/isochrone"; interface PickLayerProps { routing: RoutingTypes | undefined; setRouting: (value: RoutingTypes) => void; + ptModes: PTModeTypes[] | undefined; + setPtModes: (value: PTModeTypes[]) => void; speed: number | undefined; setSpeed: (value: number) => void; distance: number | undefined; @@ -35,6 +41,8 @@ const IsochroneSettings = (props: PickLayerProps) => { const { routing, setRouting, + ptModes: getPtModes, + setPtModes, speed, setSpeed, distance, @@ -46,55 +54,12 @@ const IsochroneSettings = (props: PickLayerProps) => { } = props; const [tab, setTab] = useState<"time" | "distance">("time"); + const [ptOpen, setPtOpen] = useState(false); + const { t } = useTranslation("maps"); const theme = useTheme(); - const routingModes = [ - { - name: t("panels.isochrone.routing.modes.bus"), - value: "bus", - }, - { - name: t("panels.isochrone.routing.modes.tram"), - value: "tram", - }, - { - name: t("panels.isochrone.routing.modes.rail"), - value: "rail", - }, - { - name: t("panels.isochrone.routing.modes.subway"), - value: "subway", - }, - { - name: t("panels.isochrone.routing.modes.ferry"), - value: "ferry", - }, - { - name: t("panels.isochrone.routing.modes.cable_car"), - value: "cable_car", - }, - { - name: t("panels.isochrone.routing.modes.gondola"), - value: "gondola", - }, - { - name: t("panels.isochrone.routing.modes.funicular"), - value: "funicular", - }, - { - name: t("panels.isochrone.routing.modes.walk"), - value: "walk", - }, - { - name: t("panels.isochrone.routing.modes.bicycle"), - value: "bicycle", - }, - { - name: t("panels.isochrone.routing.modes.car"), - value: "car", - }, - ]; + const dispatch = useDispatch(); const allowedNumbers = [ ...Array.from({ length: 25 }, (_, index) => index + 1).map((label) => ({ @@ -128,6 +93,7 @@ const IsochroneSettings = (props: PickLayerProps) => { size="small" disabled={!routing ? true : false} options={allowedNumbers} + value={speed ? speed : ""} sx={{ margin: `${theme.spacing(1)} 0`, width: "45%", @@ -135,9 +101,7 @@ const IsochroneSettings = (props: PickLayerProps) => { onChange={(_, value) => { setSpeed(value ? value.label : 1); }} - renderInput={(params) => ( - - )} + renderInput={(params) => } /> { 20000) : false} + value={distance ? distance : ""} + error={distance ? distance % 50 !== 0 || distance > 20000 : false} + helperText={ + distance && (distance % 50 !== 0 || distance > 20000) + ? "Invalid distance value" + : "" + } size="small" disabled={!routing ? true : false} type="number" @@ -233,6 +202,7 @@ const IsochroneSettings = (props: PickLayerProps) => { id="combo-box-demo" size="small" disabled={!routing ? true : false} + value={travelTime ? travelTime : ""} options={allowedMaxTravelTimeNumbers} onChange={(_, value) => { setTravelTime(value ? value.label : 1); @@ -242,7 +212,7 @@ const IsochroneSettings = (props: PickLayerProps) => { width: "100%", }} renderInput={(params) => ( - + )} /> @@ -250,6 +220,10 @@ const IsochroneSettings = (props: PickLayerProps) => { } function stepFunctionality() { + const isValidStep = steps + ? steps % 5 !== 0 || steps > (travelTime ? travelTime : 0) + : false; + return ( { { {t("panels.isochrone.routing.routing")} + + Chose a routing type for the isochrone. + { + {/*--------------------------PT Options--------------------------*/} + {ptOpen ? ( + option.name} + renderOption={(props, option, { selected }) => ( +
  • + + {t(`panels.isochrone.routing.modes.${option.name}`)} +
  • + )} + fullWidth + size="small" + renderInput={(params) => ( + + )} + onChange={(_, value) => { + // setPtModes(value.map((val) => val.value).join(",")); + setPtModes(value.map((val) => val.value) as PTModeTypes[]); + }} + /> + ) : null} {/*--------------------------------------------------------------*/} - + { @@ -333,13 +351,15 @@ const IsochroneSettings = (props: PickLayerProps) => { - {speedFunctionality()} + {routing !== ("pt" as RoutingTypes) ? speedFunctionality() : null} {travelTimeFunctionality()} {stepFunctionality()} diff --git a/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/StartingPoint.tsx b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/StartingPoint.tsx new file mode 100644 index 00000000..2d208eda --- /dev/null +++ b/apps/web/components/map/panels/toolbox/tools/accessibility_indicators/isochrone/StartingPoint.tsx @@ -0,0 +1,342 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { + Box, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, + debounce, + useTheme, + TextField, + Button, + Autocomplete, +} from "@mui/material"; +import { useTranslation } from "@/i18n/client"; +import { v4 } from "uuid"; +import { useProjectLayers } from "@/lib/api/projects"; +import { useMap } from "react-map-gl"; +import { Icon, ICON_NAME } from "@p4b/ui/components/Icon"; +import search from "@/lib/services/geocoder"; +import { testForCoordinates } from "@/components/map/controls/Geocoder"; +import { MAPBOX_TOKEN } from "@/lib/constants"; +import { useParams } from "next/navigation"; +import { useDispatch } from "react-redux"; +import { addMarker } from "@/lib/store/styling/slice"; + +import type { StartingPointType } from "@/types/map/isochrone"; +import type { SelectChangeEvent } from "@mui/material"; +import type { Result } from "@/types/map/controllers"; +import type { FeatureCollection } from "geojson"; +import type { RoutingTypes } from "@/types/map/isochrone"; + +interface PickLayerProps { + routing: RoutingTypes | undefined; + startingType: StartingPointType | undefined; + setStartingType: (value: StartingPointType) => void; + startingPoint: string[] | undefined; + setStartingPoint: (value: string[] | undefined) => void; +} + +const isochroneMarkerIcons = { + "walking": ICON_NAME.RUN, + "padelec": ICON_NAME.PEDELEC, + "bicycle": ICON_NAME.BICYCLE, + "pt": ICON_NAME.BUS, + "car_peak": ICON_NAME.CAR +} + +const StartingPoint = (props: PickLayerProps) => { + const { routing, startingType, setStartingPoint, startingPoint, setStartingType } = + props; + // const { projectLayers } = useProjectLayers(); + const { projectId } = useParams(); + const { layers: projectLayers } = useProjectLayers( + typeof projectId === "string" ? projectId : "", + ); + const [getCoordinates, setGetCoordinates] = useState(false); + const [value, setValue] = useState(null); + const [options, setOptions] = useState([]); + const [inputValue, setInputValue] = useState(""); + + const dispatch = useDispatch(); + const theme = useTheme(); + const { map } = useMap(); + const { t } = useTranslation("maps"); + + const fetch = useMemo( + () => + debounce( + ( + request: { value: string }, + onresult: (_error: Error, fc: FeatureCollection) => void, + ) => { + search( + "https://api.mapbox.com", + "mapbox.places", + MAPBOX_TOKEN, + request.value, + onresult, + ); + }, + 400, + ), + [], + ); + + useEffect(() => { + const handleMapClick = (event) => { + if (getCoordinates) { + console.log(startingPoint) + if (!startingPoint) { + setStartingPoint([`${event.lngLat.lat},${event.lngLat.lng}`]); + } else { + setStartingPoint([ + ...startingPoint, + `${event.lngLat.lat},${event.lngLat.lng}`, + ]); + } + dispatch( + addMarker({ + id: `isochrone-${(startingPoint ? startingPoint?.length : 0) + 1}`, + lat: event.lngLat.lat, + long: event.lngLat.lng, + iconName: isochroneMarkerIcons[routing], + }), + ); + } + }; + + map.on("click", handleMapClick); + + return () => { + map.off("click", handleMapClick); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getCoordinates, routing]); + + useEffect(() => { + let active = true; + if (inputValue === "") { + setOptions(value ? [value] : []); + return undefined; + } + const resultCoordinates = testForCoordinates(inputValue); + if (resultCoordinates[0]) { + const [_, latitude, longitude] = resultCoordinates; + setOptions([ + { + feature: { + id: "", + type: "Feature", + place_type: ["coordinate"], + relevance: 1, + properties: { + accuracy: "point", + }, + text: "", + place_name: "", + center: [longitude, latitude], + geometry: { + type: "Point", + coordinates: [longitude, latitude], + interpolated: false, + }, + address: "", + context: [], + }, + label: `${latitude}, ${longitude}`, + }, + ]); + return undefined; + } + fetch({ value: inputValue }, (error: Error, fc: FeatureCollection) => { + if (active) { + if (!error && fc && fc.features) { + setOptions( + fc.features + .map((feature) => ({ + feature: feature, + label: feature.place_name, + })) + .filter((feature) => feature.label), + ); + } + } + }); + + return () => { + active = false; + }; + }, [value, inputValue, fetch]); + + return ( + + + {t("panels.isochrone.starting.starting")} + + + + + {t("panels.isochrone.starting.type")} + + + + + {/* Pick layer in browse_layer */} + {startingType === "browse_layers" ? ( + + + {t("panels.isochrone.starting.pick_layer")} + + + + + {t("panels.isochrone.starting.layer")} + + + + + + ) : null} + {/* select point on map in place_on_map */} + {startingType === "place_on_map" ? ( + + + {t("panels.isochrone.starting.pick_on_map")} + + + ) => { + setStartingPoint(event.target.value as string); + }} + sx={{ + margin: `${theme.spacing(1)} 0`, + width: "70%", + }} + /> + + + + ) : null} + {/* Pick layer in address_search */} + {startingType === "address_input" ? ( + + + {t("panels.isochrone.starting.search_location")} + + x} + options={options} + fullWidth + value={inputValue ? inputValue : ""} + sx={{ + margin: `${theme.spacing(1)} 0`, + }} + onChange={(_event: unknown, newValue: Result | null) => { + setOptions(newValue ? [newValue, ...options] : options); + setStartingPoint( + newValue?.feature.center + ? newValue?.feature.center.reverse().map((coord)=>coord.toString()) + : [], + ); + setValue(newValue); + }} + onInputChange={(_event, newInputValue) => { + setInputValue(newInputValue); + }} + renderInput={(params) => ( + + )} + /> + + ) : null} + + ); +}; + +export default StartingPoint; diff --git a/apps/web/components/map/panels/toolbox/tools/aggregate/SelectArea.tsx b/apps/web/components/map/panels/toolbox/tools/aggregate/SelectArea.tsx index ba587944..251a0cb3 100644 --- a/apps/web/components/map/panels/toolbox/tools/aggregate/SelectArea.tsx +++ b/apps/web/components/map/panels/toolbox/tools/aggregate/SelectArea.tsx @@ -9,8 +9,9 @@ import { useTheme, } from "@mui/material"; import { v4 } from "uuid"; -import { useProjectLayers } from "@/hooks/map/layersHooks"; +import { useProjectLayers } from "@/lib/api/projects"; import { useTranslation } from "@/i18n/client"; +import { useParams } from "next/navigation"; import type { SelectChangeEvent } from "@mui/material"; import type { areaSelectionTypes } from "@/types/map/toolbox"; @@ -39,7 +40,11 @@ const SelectArea = (props: SelectAreaProps) => { const theme = useTheme(); const { t } = useTranslation("maps"); - const { projectLayers } = useProjectLayers(); + const { projectId } = useParams(); + + const { layers: projectLayers } = useProjectLayers( + typeof projectId === "string" ? projectId : "", + ); return ( @@ -117,13 +122,15 @@ const SelectArea = (props: SelectAreaProps) => { setPolygonLayer(event.target.value as areaSelectionTypes) } > - {projectLayers.map((layer) => - layer.feature_layer_geometry_type === "polygon" ? ( - - {layer.name} - - ) : null, - )} + {projectLayers + ? projectLayers.map((layer) => + layer.feature_layer_geometry_type === "polygon" ? ( + + {layer.name} + + ) : null, + ) + : null}
    diff --git a/apps/web/components/map/panels/toolbox/tools/aggregate/Statistics.tsx b/apps/web/components/map/panels/toolbox/tools/aggregate/Statistics.tsx index fc1a2afc..554ecb42 100644 --- a/apps/web/components/map/panels/toolbox/tools/aggregate/Statistics.tsx +++ b/apps/web/components/map/panels/toolbox/tools/aggregate/Statistics.tsx @@ -11,8 +11,8 @@ import { Checkbox, } from "@mui/material"; import { v4 } from "uuid"; -import { useLayerHook } from "@/hooks/map/LayerHooks"; import { useTranslation } from "@/i18n/client"; +import { useGetLayerKeys } from "@/hooks/map/ToolsHooks"; import type { SelectChangeEvent } from "@mui/material"; import type { ColumStatisticsOperation } from "@/types/map/toolbox"; @@ -70,22 +70,22 @@ const Statistics = (props: StatisticsProps) => { }, ]; - const pointLayerKeys = useLayerHook( - typeof pointLayerId === "string" ? pointLayerId : "", + const pointLayerKeys = useGetLayerKeys( + `user_data.${ + typeof pointLayerId === "string" ? pointLayerId.split("-").join("") : "" + }`, ); function checkType() { - return methodsKeys.map((key) => - key.types.includes( - pointLayerKeys - .getLayerKeys() - .keys.filter((layerKey) => layerKey.name === field)[0].type, - ) ? ( - - {key.name} - - ) : null, - ); + return methodsKeys.map((key) => + key.types.includes( + pointLayerKeys.keys.filter((layerKey) => layerKey.name === field)[0].type, + ) ? ( + + {key.name} + + ) : null, + ); } return ( @@ -118,7 +118,7 @@ const Statistics = (props: StatisticsProps) => { setFieldSelected(event.target.value as string) } > - {pointLayerKeys.getLayerKeys().keys.map((key) => ( + {pointLayerKeys.keys.map((key) => ( {key.name} @@ -169,7 +169,7 @@ const Statistics = (props: StatisticsProps) => { setGroupedFields(event.target.value as string[]) } > - {pointLayerKeys.getLayerKeys().keys.map((key) => ( + {pointLayerKeys.keys.map((key) => ( { const theme = useTheme(); - const firstLayerKeys = useLayerHook(firstLayerId); - const secondLayerKeys = useLayerHook(firstLayerId); - + + const firstLayerKeys = useGetLayerKeys(`user_data.${firstLayerId.split("-").join("")}`); + const secondLayerKeys = useGetLayerKeys(`user_data.${secondLayerId.split("-").join("")}`); + return ( @@ -75,7 +76,7 @@ const FieldsToMatch = (props: FieldsToMatchProps) => { }} > {firstLayerId.length - ? firstLayerKeys.getLayerKeys().keys.map((key) => ( + ? firstLayerKeys.keys.map((key) => ( {key.name} @@ -116,7 +117,7 @@ const FieldsToMatch = (props: FieldsToMatchProps) => { }} > {secondLayerId.length - ? secondLayerKeys.getLayerKeys().keys.map((key) => ( + ? secondLayerKeys.keys.map((key) => ( {key.name} diff --git a/apps/web/components/map/panels/toolbox/tools/join/Statistics.tsx b/apps/web/components/map/panels/toolbox/tools/join/Statistics.tsx index 831f2661..980d9660 100644 --- a/apps/web/components/map/panels/toolbox/tools/join/Statistics.tsx +++ b/apps/web/components/map/panels/toolbox/tools/join/Statistics.tsx @@ -10,8 +10,8 @@ import { MenuItem, TextField, } from "@mui/material"; -import { useLayerHook } from "@/hooks/map/LayerHooks"; import { useTranslation } from "@/i18n/client"; +import { useGetLayerKeys } from "@/hooks/map/ToolsHooks"; import type { SelectChangeEvent } from "@mui/material"; import type { ColumStatisticsOperation } from "@/types/map/toolbox"; @@ -71,10 +71,10 @@ const Statistics = (props: StatisticsProps) => { }, ]; - const saveFieldKeys = useLayerHook(secondLayerId); + const saveFieldKeys = useGetLayerKeys(`user_data.${secondLayerId.split("-").join("")}`); function checkType() { - return saveFieldKeys.getLayerKeys().keys.map((key) => + return saveFieldKeys.keys.map((key) => methods .filter((meth) => meth.name === method)[0] .types.includes(key.type) ? ( diff --git a/apps/web/hooks/map/LayerHooks.ts b/apps/web/hooks/map/LayerHooks.ts deleted file mode 100644 index b87fe416..00000000 --- a/apps/web/hooks/map/LayerHooks.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useGetLayerKeys } from "@/lib/api/filter"; - -export const useLayerHook = (layerId: string) => { - - const { data, isLoading } = useGetLayerKeys(layerId); - - const getLayerKeys = () => { - if(layerId){ - return { - keys: isLoading - ? [] - : Object.keys(data.properties) - .filter((key) => "name" in data.properties[key]) - .map((key) => ({ - name: data.properties[key].name, - type: data.properties[key].type, - })) - } - }else { - return { - keys: [] - } - } - }; - - return { getLayerKeys }; -}; diff --git a/apps/web/hooks/map/LayerPanelHooks.ts b/apps/web/hooks/map/LayerPanelHooks.ts new file mode 100644 index 00000000..cca59f80 --- /dev/null +++ b/apps/web/hooks/map/LayerPanelHooks.ts @@ -0,0 +1,54 @@ +import type { PopperMenuItem } from "@/components/common/PopperMenu"; +import { useTranslation } from "@/i18n/client"; +import type { Layer as ProjectLayer } from "@/lib/validations/layer"; +import { ContentActions, MapLayerActions } from "@/types/common"; +import { ICON_NAME } from "@p4b/ui/components/Icon"; +import { useState } from "react"; + +export const useLayerSettingsMoreMenu = () => { + const { t } = useTranslation(["map", "common"]); + const layerMoreMenuOptions: PopperMenuItem[] = [ + { + id: ContentActions.INFO, + label: t("common:info"), + icon: ICON_NAME.CIRCLEINFO, + }, + { + id: MapLayerActions.DUPLICATE, + label: t("maps:duplicate"), + icon: ICON_NAME.COPY, + }, + { + id: MapLayerActions.RENAME, + label: t("common:rename"), + icon: ICON_NAME.EDIT, + }, + { + id: ContentActions.DELETE, + label: t("common:remove"), + icon: ICON_NAME.TRASH, + color: "error.main", + }, + ]; + + const [activeLayer, setActiveLayer] = useState(); + const [moreMenuState, setMoreMenuState] = useState(); + + const closeMoreMenu = () => { + setActiveLayer(undefined); + setMoreMenuState(undefined); + }; + + const openMoreMenu = (menuItem: PopperMenuItem, layerItem: ProjectLayer) => { + setActiveLayer(layerItem); + setMoreMenuState(menuItem); + }; + + return { + layerMoreMenuOptions, + activeLayer, + moreMenuState, + closeMoreMenu, + openMoreMenu, + }; +}; diff --git a/apps/web/hooks/map/ToolsHooks.ts b/apps/web/hooks/map/ToolsHooks.ts new file mode 100644 index 00000000..f2ceaf15 --- /dev/null +++ b/apps/web/hooks/map/ToolsHooks.ts @@ -0,0 +1,20 @@ +import useSWR from "swr"; +import { fetcher } from "@/lib/api/fetcher"; +import { useLayerKeys } from "@/lib/api/layers"; + +export const useGetLayerKeys = (layerId: string) => { + + const { isLoading, error, data } = useLayerKeys(layerId); + + return { + keys: + isLoading || error || !data + ? [] + : Object.keys(data.properties) + .filter((key) => "name" in data.properties[key]) + .map((key) => ({ + name: data.properties[key].name, + type: data.properties[key].type, + })), + }; +}; diff --git a/apps/web/hooks/map/layersHooks.ts b/apps/web/hooks/map/layersHooks.ts deleted file mode 100644 index b0cd5a3f..00000000 --- a/apps/web/hooks/map/layersHooks.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useState } from "react"; -import { getProjectLayers } from "@/lib/api/projects"; -import { useDispatch } from "react-redux"; -import { setLayers } from "@/lib/store/layer/slice"; -import { useParams } from "next/navigation"; -import type { Layer } from "@/lib/validations/layer"; - -export const useProjectLayers = () => { - const [projectLayers, setProjectlayers] = useState([]); - - const dispatch = useDispatch(); - const params = useParams(); - - useEffect(() => { - getProjectLayers(typeof params.projectId === "string" ? params.projectId : "").then((data) => { - const layers = data.map((layer) => ({ ...layer, active: false })); - setProjectlayers(layers); - dispatch(setLayers(layers)); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return {projectLayers}; -}; diff --git a/apps/web/hooks/useDebounce.tsx b/apps/web/hooks/useDebounce.tsx deleted file mode 100644 index fec4d67a..00000000 --- a/apps/web/hooks/useDebounce.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect, useState } from "react"; - -export const useDebounce = (value: string, delay: number) => { - const [debouncedValue, setDebouncedValue] = useState(value); - - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - return () => { - clearTimeout(timer); - }; - }, [value, delay]); - - return String(debouncedValue).toLowerCase(); -}; diff --git a/apps/web/i18n/locales/en/common.json b/apps/web/i18n/locales/en/common.json index fb39f9ef..aefcd1ed 100644 --- a/apps/web/i18n/locales/en/common.json +++ b/apps/web/i18n/locales/en/common.json @@ -9,5 +9,7 @@ "edit": "Edit", "remove": "Remove", "upload": "Upload", - "update": "Update" + "update": "Update", + "info": "Info", + "rename": "Rename" } \ No newline at end of file diff --git a/apps/web/i18n/locales/en/maps.json b/apps/web/i18n/locales/en/maps.json index b7a6434c..bfbca1af 100644 --- a/apps/web/i18n/locales/en/maps.json +++ b/apps/web/i18n/locales/en/maps.json @@ -27,6 +27,7 @@ "isochrone": "Isochrone", "routing": { "routing": "Routing", + "pt_type": "PT type", "modes": { "bus": "Bus", "tram": "Tram", @@ -38,7 +39,9 @@ "funicular": "Funicular", "walk": "Walk", "bicycle": "Bicycle", - "car": "Car" + "pedelec": "Pedelec", + "car": "Car", + "pt": "PT" } }, "speed": "Speed", @@ -98,6 +101,10 @@ "hexagon_bin": "Hexagon bin", "polygon_layer": "Polygon Layer", "unit": "Unit" + }, + "accessibility_indicators": { + "accessibility_indicators": "Accessibility Indicators", + "isochrone": "Isochrone" } }, "filter": { @@ -129,5 +136,13 @@ "select_field": "Select a field containing the data of interest to display", "select_option": "Select Option" } - } + }, + "show_layer": "Show Layer", + "hide_layer": "Hide Layer", + "layer_more_options": "More Options", + "duplicate": "Duplicate", + "feature": "Feature", + "external_vector_tile": "External Vector Tile", + "table": "Table", + "external_imagery": "External Image" } diff --git a/apps/web/lib/api/filter.ts b/apps/web/lib/api/filter.ts deleted file mode 100644 index a1e76097..00000000 --- a/apps/web/lib/api/filter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import useSWR from "swr"; -import { fetcher } from "./fetcher"; - -export const PROJECTS_API_BASE_URL = new URL( - "api/v2/project", - process.env.NEXT_PUBLIC_API_URL, -).href; - -export const useGetLayerKeys = (layerId) => { - const { data, isLoading } = useSWR( - `${process.env.NEXT_PUBLIC_GEOAPI_URL}/collections/${`user_data.${layerId.replace(/-/g, "")}`}/queryables`, - fetcher, - ); - - return {data, isLoading} -}; - -export const useFilterQueryExpressions = ( - projectId: string, -) => { - const { data, mutate, isLoading, error } = useSWR( - `${PROJECTS_API_BASE_URL}/${projectId}/layer`, - fetcher, - ); - return {data, mutate, isLoading, error} -}; \ No newline at end of file diff --git a/apps/web/lib/api/isochrone.ts b/apps/web/lib/api/isochrone.ts new file mode 100644 index 00000000..f43f3a5d --- /dev/null +++ b/apps/web/lib/api/isochrone.ts @@ -0,0 +1,47 @@ +import type { PostIsochrone } from "@/lib/validations/isochrone"; +import { fetchWithAuth } from "@/lib/api/fetcher"; + +const ACCESSIBILITY_ISOCHRONE_API_BASE_URL = new URL( + "api/v2/active-mobility/isochrone", + process.env.NEXT_PUBLIC_API_URL, +).href; + +const PT_ISOCHRONE_API_BASE_URL = new URL( + "api/v2/motorized-mobility/pt/isochrone", + process.env.NEXT_PUBLIC_API_URL, +).href; + +const CAR_ISOCHRONE_API_BASE_URL = new URL( + "api/v2/motorized-mobility/car/isochrone", + process.env.NEXT_PUBLIC_API_URL, +).href; + +export const SendIsochroneRequest = async (body: PostIsochrone) => { + await fetchWithAuth(ACCESSIBILITY_ISOCHRONE_API_BASE_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + .then((res) => res.json()) + .then((data) => console.log(data)); +}; + +export const SendPTIsochroneRequest = async (body: PostIsochrone) => { + await fetchWithAuth(PT_ISOCHRONE_API_BASE_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + .then((res) => res.json()) + .then((data) => console.log(data)); +}; + +export const SendCarIsochroneRequest = async (body: PostIsochrone) => { + await fetchWithAuth(CAR_ISOCHRONE_API_BASE_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + .then((res) => res.json()) + .then((data) => console.log(data)); +}; diff --git a/apps/web/lib/api/layers.ts b/apps/web/lib/api/layers.ts index aadcb883..77c84cf5 100644 --- a/apps/web/lib/api/layers.ts +++ b/apps/web/lib/api/layers.ts @@ -1,13 +1,21 @@ import useSWR from "swr"; import { fetchWithAuth, fetcher } from "@/lib/api/fetcher"; import type { GetContentQueryParams } from "@/lib/validations/common"; -import type { CreateNewDatasetLayer, LayerPaginated } from "@/lib/validations/layer"; +import type { + CreateNewDatasetLayer, + LayerPaginated, +} from "@/lib/validations/layer"; export const LAYERS_API_BASE_URL = new URL( "api/v2/layer", process.env.NEXT_PUBLIC_API_URL, ).href; +export const LAYER_KEYS_API_BASE_URL = new URL( + "collections", + process.env.NEXT_PUBLIC_GEOAPI_URL, +).href; + export const useLayers = (queryParams?: GetContentQueryParams) => { const { data, isLoading, error, mutate, isValidating } = useSWR([`${LAYERS_API_BASE_URL}`, queryParams], fetcher); @@ -56,4 +64,13 @@ export const layerUploadValidateFile = async (file: File) => { throw new Error("Failed to upload folder"); } return await response.json(); +}; + +export const useLayerKeys = (layerId: string) => { + const { data, isLoading, error} = + useSWR( + [`${LAYER_KEYS_API_BASE_URL}/${layerId}/queryables`], + fetcher, + ); + return {data, isLoading, error} } \ No newline at end of file diff --git a/apps/web/lib/api/projects.ts b/apps/web/lib/api/projects.ts index 36979447..441ca6e4 100644 --- a/apps/web/lib/api/projects.ts +++ b/apps/web/lib/api/projects.ts @@ -6,6 +6,7 @@ import type { ProjectPaginated, Project, PostProject, + ProjectViewState, } from "@/lib/validations/project"; import type { GetContentQueryParams } from "@/lib/validations/common"; import type { Layer } from "@/lib/validations/layer"; @@ -30,9 +31,9 @@ export const useProjects = (queryParams?: GetContentQueryParams) => { }; }; -export const useProject = (id: string) => { +export const useProject = (projectId: string) => { const { data, isLoading, error, mutate, isValidating } = useSWR( - [`${PROJECTS_API_BASE_URL}/${id}`], + [`${PROJECTS_API_BASE_URL}/${projectId}`], fetcher, ); return { @@ -44,37 +45,154 @@ export const useProject = (id: string) => { }; }; -export const createProject = async ( - payload: PostProject, -): Promise => { - const response = await fetchWithAuth(`${PROJECTS_API_BASE_URL}`, { - method: "POST", - headers: { - "Content-Type": "application/json", +export const useProjectLayers = (projectId: string) => { + const { data, isLoading, error, mutate, isValidating } = useSWR( + [`${PROJECTS_API_BASE_URL}/${projectId}/layer`], + fetcher, + ); + return { + layers: data, + isLoading: isLoading, + isError: error, + mutate, + isValidating, + }; +}; + +export const useProjectInitialViewState = (projectId: string) => { + const { data, isLoading, error, mutate, isValidating } = + useSWR( + [`${PROJECTS_API_BASE_URL}/${projectId}/initial-view-state`], + fetcher, + ); + return { + initialView: data, + isLoading: isLoading, + isError: error, + mutate, + isValidating, + }; +}; + +export const updateProjectInitialViewState = async ( + projectId: string, + payload: ProjectViewState, +) => { + const response = await fetchWithAuth( + `${PROJECTS_API_BASE_URL}/${projectId}/initial-view-state`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + ); if (!response.ok) { - throw new Error("Failed to create project"); + throw new Error("Failed to update project initial view state"); } return await response.json(); }; +export const updateProjectLayer = async ( + projectId: string, + layerId: number, + payload: Layer, +) => { + const response = await fetchWithAuth( + `${PROJECTS_API_BASE_URL}/${projectId}/layer/${layerId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }, + ); + if (!response.ok) { + throw new Error("Failed to update project layer"); + } + return await response.json(); +}; -export const getProjectLayers = async (id: string) => { +export const deleteProjectLayer = async ( + projectId: string, + layerId: number, +) => { try { - const data: Promise = ( - await fetchWithAuth(`${PROJECTS_API_BASE_URL}/${id}/layer`) - ).json(); - return data; + await fetchWithAuth( + `${PROJECTS_API_BASE_URL}/${projectId}/layer/?layer_project_id=${layerId}`, + { + method: "DELETE", + }, + ); } catch (error) { console.error(error); throw Error( - `error: make sure you are connected to an internet connection!`, + `deleteProjectLayer: unable to delete layer with id ${layerId}`, ); } }; +export const addProjectLayers = async ( + projectId: string, + layerIds: string[], +) => { + //todo: fix the api for this. This structure doesn't make sense. + //layer_ids=1&layer_ids=2&layer_ids=3 + const layerIdsParams = layerIds.map((layerId) => { + return `layer_ids=${layerId}`; + }); + + const response = await fetchWithAuth( + `${PROJECTS_API_BASE_URL}/${projectId}/layer?${layerIdsParams.join("&")}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, + ); + if (!response.ok) { + throw new Error("Failed to add layers to project"); + } + return await response.json(); +}; + +export const updateProject = async ( + projectId: string, + payload: PostProject, +) => { + const response = await fetchWithAuth( + `${PROJECTS_API_BASE_URL}/${projectId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }, + ); + if (!response.ok) { + throw new Error("Failed to update project"); + } + return await response.json(); +}; + +export const createProject = async (payload: PostProject): Promise => { + const response = await fetchWithAuth(`${PROJECTS_API_BASE_URL}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + throw new Error("Failed to create project"); + } + return await response.json(); +}; + export const deleteProject = async (id: string) => { try { await fetchWithAuth(`${PROJECTS_API_BASE_URL}/${id}`, { diff --git a/apps/web/lib/api/tools.ts b/apps/web/lib/api/tools.ts index 1789e1e0..2ea34390 100644 --- a/apps/web/lib/api/tools.ts +++ b/apps/web/lib/api/tools.ts @@ -1,3 +1,4 @@ +import { fetchWithAuth } from "@/lib/api/fetcher"; import type { PostJoin, PostAggregate } from "@/lib/validations/tools"; const PROJECTS_API_BASE_URL = new URL( @@ -6,7 +7,7 @@ const PROJECTS_API_BASE_URL = new URL( ).href; export const SendJoinFeatureRequest = async (body: PostJoin) => { - await fetch( + await fetchWithAuth( `${PROJECTS_API_BASE_URL}/join`, { method: "POST", @@ -17,7 +18,7 @@ export const SendJoinFeatureRequest = async (body: PostJoin) => { } export const SendAggregateFeatureRequest = async (body: PostAggregate) => { - await fetch( + await fetchWithAuth( `${PROJECTS_API_BASE_URL}/aggregate-points`, { method: "POST", diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts index f7c7f9ef..34a7e6b7 100644 --- a/apps/web/lib/constants.ts +++ b/apps/web/lib/constants.ts @@ -4,3 +4,4 @@ export const MAPBOX_TOKEN = export const THEME_COOKIE_NAME = "client_theme"; export const ORG_DEFAULT_AVATAR = "https://assets.plan4better.de/img/no-org-thumb.jpg"; +export const GEOAPI_BASE_URL = process.env.NEXT_PUBLIC_GEOAPI_URL; diff --git a/apps/web/lib/services/dashboard.ts b/apps/web/lib/services/dashboard.ts deleted file mode 100644 index f9839bbe..00000000 --- a/apps/web/lib/services/dashboard.ts +++ /dev/null @@ -1,20 +0,0 @@ -import mapData from "@/lib/utils/project_layers_demo_update"; -import contentData from "@/lib/utils/template_content"; - -export const contentDataFetcher = () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(contentData); - }, 1000); // Simulate a 1-second delay - }); -}; - -// Function to simulate fetching data asynchronously -export const mapDataFetcher = () => { - return new Promise((resolve) => { - // Simulate an asynchronous delay (e.g., 1 second) - setTimeout(() => { - resolve(mapData); - }, 1000); // Simulate a 1-second delay - }); -}; diff --git a/apps/web/lib/store/styling/actions.ts b/apps/web/lib/store/styling/actions.ts deleted file mode 100644 index ec1a9d70..00000000 --- a/apps/web/lib/store/styling/actions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TLayer } from '@/lib/store/styling/slice' -import { stylesObj } from "@/lib/utils/mockLayerData"; -import { createAsyncThunk } from "@reduxjs/toolkit"; - -export const layerDataFetcher = (id: string) => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(stylesObj[id]); - }, 1000); // Simulate a 1-second delay - }); -}; - -export const fetchLayerData = createAsyncThunk( - "styling/fetchLayerData", - async (id: string) => { - try { - return await layerDataFetcher(id) as TLayer; - } catch (error) { - throw error; - } - }, -); diff --git a/apps/web/lib/store/styling/slice.ts b/apps/web/lib/store/styling/slice.ts index 63be6cba..851afb4b 100644 --- a/apps/web/lib/store/styling/slice.ts +++ b/apps/web/lib/store/styling/slice.ts @@ -1,6 +1,5 @@ import type { PayloadAction } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit"; -import { fetchLayerData } from "@/lib/store/styling/actions"; import type { AnyLayer } from "react-map-gl"; interface IViewState { @@ -101,7 +100,7 @@ const initialState: IStylingState = { markers: [], //todo need get layer from db mapLayer: null as TLayer, - changeIcon: null + changeIcon: null, }; const stylingSlice = createSlice({ @@ -109,7 +108,7 @@ const stylingSlice = createSlice({ initialState, reducers: { setIcon: (state, action: PayloadAction<(src: string) => void>) => { - state.changeIcon = action.payload + state.changeIcon = action.payload; }, setTabValue: (state, action: PayloadAction) => { state.tabValue = action.payload; @@ -120,6 +119,9 @@ const stylingSlice = createSlice({ addMarker: (state, action: PayloadAction) => { state.markers.push(action.payload); }, + removeMarker : (state) => { + state.markers = []; + }, editeMarkerPosition: (state, action: PayloadAction) => { state.markers = state.markers.map((item) => { if (item.id === action.payload.id) { @@ -174,15 +176,12 @@ const stylingSlice = createSlice({ } } }, - setLayerSymbolSize: ( - state, - action: PayloadAction<{ val: number }>, - ) => { + setLayerSymbolSize: (state, action: PayloadAction<{ val: number }>) => { const mapLayer = state.mapLayer as TLayer; if (mapLayer) { mapLayer.layout = mapLayer.layout ?? {}; - mapLayer.layout['icon-size'] = action.payload.val; + mapLayer.layout["icon-size"] = action.payload.val; } }, setIconFillColor: (state, action: PayloadAction) => { @@ -191,24 +190,7 @@ const stylingSlice = createSlice({ if (mapLayer?.paint) { mapLayer.paint["icon-color"] = action.payload; } - } - // setLayerIconImage: (state, action: PayloadAction) => { - // state.mapLayer.layers[0].layout["icon-image"] = action.payload; - // }, - // setLayerIconSize: (state, action: PayloadAction) => { - // state.mapLayer.layers[0].layout["icon-size"] = action.payload; - // }, - }, - extraReducers: (builder) => { - builder.addCase(fetchLayerData.pending, (state) => { - state.mapLayer = null; - }); - builder.addCase(fetchLayerData.fulfilled, (state, { payload }) => { - state.mapLayer = payload; - }); - builder.addCase(fetchLayerData.rejected, (state) => { - state.mapLayer = null; - }); + }, }, }); @@ -218,6 +200,7 @@ export const { setTabValue, setActiveBasemapIndex, addMarker, + removeMarker, setLayerFillColor, setLayerFillOutLineColor, saveStyles, diff --git a/apps/web/lib/utils/layer_demo.json b/apps/web/lib/utils/layer_demo.json deleted file mode 100644 index 72a7efd3..00000000 --- a/apps/web/lib/utils/layer_demo.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", - "name": "My new project", - "description": "My new project description", - "thumbnail_url": "https://assets.plan4better.de/api/thumbnail/1.png", - "tags": [ - "tag1", - "tag2" - ], - "created_by": "elias.pajares@plan4better.de", - "updated_by": "elias.pajares@plan4better.de", - "created_at": "2021-03-03T09:00:00.000000Z", - "updated_at": "2021-03-03T09:00:00.000000Z", - "shared_with": [ - { - "group_id": 1, - "group_name": "My Group 1", - "image_url": "https://assets.plan4better.de/api/thumbnail/1.png" - }, - { - "group_id": 2, - "group_name": "My Group 2", - "image_url": "https://assets.plan4better.de/api/thumbnail/2.png" - } - ], - "initial_view_state": { - "latitude": 48.1502132, - "longitude": 11.5696284, - "zoom": 10, - "min_zoom": 0, - "max_zoom": 20, - "bearing": 0, - "pitch": 0 - }, - "reports": [], - "layers": [ - { - "id": "123e4567-e89b-12d3-a456-426614174000", - "name": "Edge Layer", - "group": "Example Group 1", - "description": "This is an example for a streets layer (line)", - "type": "tile_layer", - "created_at": "2023-07-11T00:00:00", - "created_by": "example_user", - "updated_at": "2023-07-11T00:00:00", - "updated_by": "example_user", - "active": "True", - "data_source_name": "Example Data Source", - "data_reference_year": 2020, - "url": "https://api.mapbox.com/v4/eliaspajares.cljxyjs6x02672oqimtbmde3u-92yjl/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - "style": "https://api.mapbox.com/styles/v1/eliaspajares/cljxzoemb003y01qr59fx3mpq?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - "data_type": "mvt" - }, - { - "id": "123e4567-e89b-12d3-a456-426614174001", - "name": "POI Layer", - "group": "Opportunities", - "description": "This is an example for a point layer (point of interests)", - "type": "tile_layer", - "created_at": "2023-07-11T00:00:00", - "created_by": "example_user", - "updated_at": "2023-07-11T00:00:00", - "updated_by": "example_user", - "active": "True", - "data_source_name": "Example Data Source", - "data_reference_year": 2020, - "url": "https://api.mapbox.com/v4/eliaspajares.cljxyaynj02532aqi9rh1kz0g-77qff/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - "style": "https://api.mapbox.com/styles/v1/eliaspajares/cljxz3bl1003v01qy7k5m0apj?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - "data_type": "mvt" - }, - { - "id": "123e4567-e89b-12d3-a456-426614174002", - "name": "AOI Layer", - "group": "Opportunities", - "description": "This is an example for a areas of interest layer (polygon)", - "type": "tile_layer", - "created_at": "2023-07-11T00:00:00", - "created_by": "example_user", - "updated_at": "2023-07-11T00:00:00", - "updated_by": "example_user", - "active": "False", - "data_source_name": "Example Data Source", - "data_reference_year": 2021, - "url": "https://api.mapbox.com/v4/eliaspajares.cljxc2rek01ow2alyl0cy0y2j-63c9z/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - "style": "https://api.mapbox.com/styles/v1/eliaspajares/cljyel7yl005r01pfcd4h0epj?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - "data_type": "mvt" - } - ] -} diff --git a/apps/web/lib/utils/mockLayerData.js b/apps/web/lib/utils/mockLayerData.js deleted file mode 100644 index 5f0cdeca..00000000 --- a/apps/web/lib/utils/mockLayerData.js +++ /dev/null @@ -1,59 +0,0 @@ -export const stylesObj = { - poi: { - name: "poi", - sources: { - composite: { - url: "mapbox://eliaspajares.cljxyaynj02532aqi9rh1kz0g-77qff", - type: "vector", - }, - }, - id: "cljxz3bl1003v01qy7k5m0apj", - type: "symbol", - paint: { - 'icon-color': '#316940', - }, - source: "composite", - "source-layer": "poi", - layout: { - "icon-image": "dentist-15", - }, - }, - aoi: { - name: "aoi", - sources: { - composite: { - url: "mapbox://eliaspajares.cljxc2rek01ow2alyl0cy0y2j-63c9z", - type: "vector", - }, - }, - id: "cljyel7yl005r01pfcd4h0epj", - type: "fill", - paint: { - "fill-color": "#316940", - "fill-outline-color": "#000", - "fill-opacity": 1.0, - "fill-antialias": true, // - }, - layout: {}, - source: "composite", - "source-layer": "aoi", - }, - edge: { - name: "edge", - sources: { - composite: { - url: "mapbox://eliaspajares.cljxyjs6x02672oqimtbmde3u-92yjl", - type: "vector", - }, - }, - id: "cljxzoemb003y01qr59fx3mpq", - type: "line", - layout: {}, - source: "composite", - "source-layer": "edge", - paint: { - "line-color": "#ff003b", - "line-width": 3, - }, - }, -}; diff --git a/apps/web/lib/utils/project_layers_demo_update.js b/apps/web/lib/utils/project_layers_demo_update.js deleted file mode 100644 index 89825b73..00000000 --- a/apps/web/lib/utils/project_layers_demo_update.js +++ /dev/null @@ -1,132 +0,0 @@ -const data = { - id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", - name: "My new project", - description: "My new project description", - thumbnail_url: "https://assets.plan4better.de/api/thumbnail/1.png", - tags: ["tag1", "tag2"], - created_by: "elias.pajares@plan4better.de", - updated_by: "elias.pajares@plan4better.de", - created_at: "2021-03-03T09:00:00.000000Z", - updated_at: "2021-03-03T09:00:00.000000Z", - shared_with: [ - { - group_id: 1, - group_name: "My Group 1", - image_url: "https://assets.plan4better.de/api/thumbnail/1.png", - }, - { - group_id: 2, - group_name: "My Group 2", - image_url: "https://assets.plan4better.de/api/thumbnail/2.png", - }, - ], - scale_show: false, - navigation_control: false, - MAP_ACCESS_TOKEN: - "pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - map_style: "mapbox://styles/mapbox/streets-v11", - initial_view_state: { - latitude: 48.1502132, - longitude: 11.5696284, - zoom: 10, - min_zoom: 0, - max_zoom: 20, - bearing: 0, - pitch: 0, - }, - reports: [], - layers: [ - { - id: "123e4567-e89b-12d3-a456-426614174000", - name: "Edge Layer", - group: "Example Group 1", - description: "This is an example for a streets layer (line)", - type: "tile_layer", - created_at: "2023-07-11T00:00:00", - created_by: "example_user", - updated_at: "2023-07-11T00:00:00", - updated_by: "example_user", - active: "True", - data_source_name: "Example Data Source", - data_reference_year: 2020, - url: "https://api.mapbox.com/v4/eliaspajares.cljxyjs6x02672oqimtbmde3u-92yjl/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - style: - "https://api.mapbox.com/styles/v1/eliaspajares/cljxzoemb003y01qr59fx3mpq?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - data_type: "mvt", - }, - { - id: "123e4567-e89b-12d3-a456-426614174001", - name: "POI Layer", - group: "Opportunities", - description: "This is an example for a point layer (point of interests)", - type: "tile_layer", - created_at: "2023-07-11T00:00:00", - created_by: "example_user", - updated_at: "2023-07-11T00:00:00", - updated_by: "example_user", - active: "True", - data_source_name: "Example Data Source", - data_reference_year: 2020, - url: "https://api.mapbox.com/v4/eliaspajares.cljxyaynj02532aqi9rh1kz0g-77qff/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - style: - "https://api.mapbox.com/styles/v1/eliaspajares/cljxz3bl1003v01qy7k5m0apj?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - data_type: "mvt", - }, - { - id: "123e4567-e89b-12d3-a456-426614174002", - name: "AOI Layer", - group: "Opportunities", - description: "This is an example for a areas of interest layer (polygon)", - type: "tile_layer", - created_at: "2023-07-11T00:00:00", - created_by: "example_user", - updated_at: "2023-07-11T00:00:00", - updated_by: "example_user", - active: "False", - data_source_name: "Example Data Source", - data_reference_year: 2021, - url: "https://api.mapbox.com/v4/eliaspajares.cljxc2rek01ow2alyl0cy0y2j-63c9z/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - style: - "https://api.mapbox.com/styles/v1/eliaspajares/cljyel7yl005r01pfcd4h0epj?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", - data_type: "mvt", - }, - { - id: "123e4567-e89b-12d3-a456-426614174003", - name: "Example Image Layer", - group: "Example Group 2", - description: "This is an example for a image layer", - type: "image_layer", - created_at: "2023-07-11T00:00:00", - created_by: "example_user", - updated_at: "2023-07-11T00:00:00", - updated_by: "example_user", - active: "True", - data_source_name: "Example Data Source", - data_reference_year: 2020, - url: "https://www.lfu.bayern.de/gdi/wms/laerm/hauptverkehrsstrassen?LAYERS=mroadbylden2022,mroadbyln2022", - legend_urls: [ - "https://www.lfu.bayern.de/gdi/wms/laerm/hauptverkehrsstrassen?request=GetLegendGraphic&version=1.3.0&format=image/png&layer=mroadbyln&SERVICE=WMS&SLD_VERSION=1.1.0&STYLE=&TRANSPARENT=true", - "https://www.lfu.bayern.de/gdi/wms/laerm/hauptverkehrsstrassen?request=GetLegendGraphic&version=1.3.0&format=image/png&layer=mroadbyln&SERVICE=WMS&SLD_VERSION=1.1.0&STYLE=&TRANSPARENT=true", - ], - data_type: "wms", - }, - { - id: "123e4567-e89b-12d3-a456-426614174004", - name: "Example Image Layer XYZ", - group: "Example Group 2", - description: "This is an example for a image layer", - type: "image_layer", - created_at: "2023-07-11T00:00:00", - created_by: "example_user", - updated_at: "2023-07-11T00:00:00", - updated_by: "example_user", - active: "True", - data_source_name: "Example Data Source", - data_reference_year: 2020, - url: "https://tile.thunderforest.com/transport/{z}/{x}/{y}.png?apikey=402ce1ca8eb54457bdf65e2b261c5132", - data_type: "xyz", - }, - ], -}; - -export default data; diff --git a/apps/web/lib/utils/template_content.js b/apps/web/lib/utils/template_content.js deleted file mode 100644 index bcf49a0d..00000000 --- a/apps/web/lib/utils/template_content.js +++ /dev/null @@ -1,128 +0,0 @@ -const data = { - items: [ - { - content_id: "1", - name: "test", - tags: ["tag1", "tag2", "tag3"], - description: "test description", - url: "https://geoservice.plan4better.de/tiles/xxxxx", - thumbnail_url: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", - owner: { - id: "1", - first_name: "test", - last_name: "test", - email: "test@plan4better.de", - }, - metadata: { - size: "123456", - updated_by: "test@plan4better.de", - created_at: "2020-01-01 00:00:00", - updated_at: "2020-01-01 00:00:00", - }, - shared_with: { - groups: [ - { - group_id: 1, - group_name: "My Group 1", - image_url: "https://assets.plan4better.de/api/thumbnail/1.png", - permissions: ["view", "edit", "download"], - }, - { - group_id: 2, - group_name: "My Group 2", - image_url: "https://assets.plan4better.de/api/thumbnail/2.png", - permissions: ["view", "edit", "download"], - }, - ], - public: { - url: "https://geoservice.plan4better.de/tiles/xxxxx", - expiration_date: "2024-01-01 00:00:00", - password_enabled: true, - }, - }, - type: "layer", - layer_type: "feature_layer", - feature_layer_type: "indicator", - data_type: "mvt", - style_id: "1234", - }, - { - content_id: "2", - name: "test", - tags: ["tag1", "tag2", "tag3"], - description: "test description", - url: "https://geoservice.plan4better.de/wms/xxxxx", - thumbnail_url: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", - owner: { - id: "1", - first_name: "test", - last_name: "test", - email: "test@plan4better.de", - }, - metadata: { - size: "123456", - updated_by: "test@plan4better.de", - created_at: "2020-01-01 00:00:00", - updated_at: "2020-01-01 00:00:00", - }, - shared_with: { - organization: { - permissions: ["view", "edit", "download"], - }, - public: { - url: "https://geoservice.plan4better.de/tiles/xxxxx", - expiration_date: "2024-01-01 00:00:00", - password_enabled: true, - }, - }, - type: "layer", - layer_type: "imagery_layer", - data_type: "wms", - legend_urls: [ - "https://geoservice.plan4better.de/wms/xxxxx?request=GetLegendGraphic&format=image/png&width=20&height=20&layer=xxxxx", - ], - }, - { - content_id: "3", - name: "test_project", - tags: ["tag1", "tag2", "tag3"], - description: "test description", - thumbnail_url: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", - owner: { - id: "1", - first_name: "test", - last_name: "test", - email: "test@plan4better.de", - }, - metadata: { - size: 1000, - updated_by: "test@plan4better.de", - created_at: "2020-01-01 00:00:00", - updated_at: "2020-01-01 00:00:00", - }, - shared_with: { - groups: [ - { - group_id: 1, - group_name: "My Group 1", - image_url: "https://assets.plan4better.de/api/thumbnail/1.png", - permissions: ["view", "edit", "download"], - }, - { - group_id: 2, - group_name: "My Group 2", - image_url: "https://assets.plan4better.de/api/thumbnail/2.png", - permissions: ["view", "edit", "download"], - }, - ], - }, - type: "project", - }, - ], - total: 0, - page: 1, - size: 1, - pages: 0, -}; - -export default data; diff --git a/apps/web/lib/utils/template_project.js b/apps/web/lib/utils/template_project.js deleted file mode 100644 index 8a3ea07c..00000000 --- a/apps/web/lib/utils/template_project.js +++ /dev/null @@ -1,179 +0,0 @@ -// const data = { -// id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", -// name: "My new project", -// description: "My new project description", -// thumbnail_url: "https://assets.plan4better.de/api/thumbnail/1.png", -// tags: ["tag1", "tag2"], -// created_by: "elias.pajares@plan4better.de", -// updated_by: "elias.pajares@plan4better.de", -// created_at: "2021-03-03T09:00:00.000000Z", -// updated_at: "2021-03-03T09:00:00.000000Z", -// shared_with: [ -// { -// group_id: 1, -// group_name: "My Group 1", -// image_url: "https://assets.plan4better.de/api/thumbnail/1.png", -// }, -// { -// group_id: 2, -// group_name: "My Group 2", -// image_url: "https://assets.plan4better.de/api/thumbnail/2.png", -// }, -// ], -// initial_view_state: { -// latitude: 48.1502132, -// longitude: 11.5696284, -// zoom: 10, -// min_zoom: 0, -// max_zoom: 20, -// bearing: 0, -// pitch: 0, -// }, -// reports: [], -// layers: [ -// { -// id: "123e4567-e89b-12d3-a456-426614174000", -// name: "Edge Layer", -// group: "Example Group 1", -// description: "This is an example for a streets layer (line)", -// type: "tile_layer", -// created_at: "2023-07-11T00:00:00", -// created_by: "example_user", -// updated_at: "2023-07-11T00:00:00", -// updated_by: "example_user", -// active: false, -// data_source_name: "Example Data Source", -// data_reference_year: 2020, -// url: "https://api.mapbox.com/v4/eliaspajares.cljxyjs6x02672oqimtbmde3u-92yjl/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", -// style: { -// id: "edge", -// type: "line", -// layout: {}, -// source: "composite", -// "source-layer": "edge", -// paint: { -// "line-color": [ -// "interpolate", -// ["linear"], -// ["get", "class_id"], -// 101, -// [ -// "match", -// ["get", "class_id"], -// [101, 102, 108, 109], -// "hsl(0, 100%, 47%)", -// [110, 111, 112, 113, 114, 117, 118, 119], -// "hsl(0, 23%, 67%)", -// "#000000", -// ], -// 401, -// [ -// "match", -// ["get", "class_id"], -// [101, 102, 108, 109], -// "hsl(0, 100%, 47%)", -// [110, 111, 112, 113, 114, 117, 118, 119], -// "hsl(0, 23%, 67%)", -// [120, 122, 123, 124], -// "hsl(58, 100%, 71%)", -// "#000000", -// ], -// 701, -// [ -// "match", -// ["get", "class_id"], -// [101, 102, 108, 109], -// "hsl(0, 100%, 47%)", -// [110, 111, 112, 113, 114, 117, 118, 119], -// "hsl(0, 23%, 67%)", -// "#000000", -// ], -// ], -// "line-width": [ -// "match", -// ["get", "class_id"], -// [101, 102, 108, 109, 110], -// 3, -// [111, 112, 113, 114, 117, 118, 119], -// 2, -// 1, -// ], -// }, -// }, -// data_type: "mvt", -// }, -// { -// id: "123e4567-e89b-12d3-a456-426614174001", -// name: "POI Layer", -// group: "Opportunities", -// description: "This is an example for a point layer (point of interests)", -// type: "tile_layer", -// created_at: "2023-07-11T00:00:00", -// created_by: "example_user", -// updated_at: "2023-07-11T00:00:00", -// updated_by: "example_user", -// active: true, -// data_source_name: "Example Data Source", -// data_reference_year: 2020, -// url: "https://api.mapbox.com/v4/eliaspajares.cljxyaynj02532aqi9rh1kz0g-77qff/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", -// style: { -// id: "poi", -// type: "symbol", -// paint: {}, -// source: "composite", -// "source-layer": "poi", -// layout: { -// "icon-image": [ -// "match", -// ["get", "category"], -// ["dentist"], -// "dentist-15", -// ["bakery"], -// "bakery-11", -// ["nursery"], -// "hospital-15", -// "", -// ], -// }, -// }, -// data_type: "mvt", -// }, -// { -// id: "123e4567-e89b-12d3-a456-426614174002", -// name: "AOI Layer", -// group: "Opportunities", -// description: "This is an example for a areas of interest layer (polygon)", -// type: "tile_layer", -// created_at: "2023-07-11T00:00:00", -// created_by: "example_user", -// updated_at: "2023-07-11T00:00:00", -// updated_by: "example_user", -// active: false, -// data_source_name: "Example Data Source", -// data_reference_year: 2021, -// url: "https://api.mapbox.com/v4/eliaspajares.cljxc2rek01ow2alyl0cy0y2j-63c9z/{z}/{x}/{y}.mvt?access_token=pk.eyJ1IjoiZWxpYXNwYWphcmVzIiwiYSI6ImNqOW1scnVyOTRxcWwzMm5yYWhta2N2cXcifQ.aDCgidtC9cjf_O75frn9lA", -// style: { -// id: "aoi", -// type: "fill", -// paint: { -// "fill-color": [ -// "match", -// ["get", "category"], -// ["forest"], -// "hsl(137, 37%, 30%)", -// ["park"], -// "hsl(135, 69%, 70%)", -// "#000000", -// ], -// }, -// layout: {}, -// source: "composite", -// "source-layer": "aoi", -// }, -// data_type: "mvt", -// }, -// ], -// }; - - -// export default data; \ No newline at end of file diff --git a/apps/web/lib/validations/isochrone.ts b/apps/web/lib/validations/isochrone.ts new file mode 100644 index 00000000..ea8abbfc --- /dev/null +++ b/apps/web/lib/validations/isochrone.ts @@ -0,0 +1,63 @@ +import * as z from "zod"; + +export const IsochroneBaseSchema = z.object({ + starting_points: z.object({ + latitude: z.array(z.number()), + longitude: z.array(z.number()) + }).or(z.object({ + layer_id: z.string() + })), + routing_type: z.string(), + travel_cost: z.object({ + max_traveltime: z.number(), + traveltime_step: z.number(), + speed: z.number().optional(), + }).or(z.object({ + max_distance: z.number(), + distance_step: z.number() + })), + time_window: z.object({ + weekday: z.string(), + from_time: z.number(), + to_time: z.number(), + }).optional(), + result_target: z.object({ + layer_name: z.string(), + folder_id: z.string(), + project_id: z.string().optional() + }) +}); + +export const IsochronePTSchema = z.object({ + starting_points: z.object({ + latitude: z.array(z.number()), + longitude: z.array(z.number()) + }).or(z.object({ + layer_id: z.string() + })), + routing_type: z.object({ + mode: z.array(z.string()), + egress_mode: z.string(), + access_mode: z.string() + }), + travel_cost: z.object({ + max_traveltime: z.number(), + traveltime_step: z.number(), + speed: z.number().optional(), + }).or(z.object({ + max_distance: z.number(), + distance_step: z.number() + })), + time_window: z.object({ + weekday: z.string(), + from_time: z.number(), + to_time: z.number(), + }).optional(), + result_target: z.object({ + layer_name: z.string(), + folder_id: z.string(), + project_id: z.string().optional() + }) +}); + +export type PostIsochrone = z.infer; diff --git a/apps/web/lib/validations/layer.ts b/apps/web/lib/validations/layer.ts index 87cdcdf9..74e1f8be 100644 --- a/apps/web/lib/validations/layer.ts +++ b/apps/web/lib/validations/layer.ts @@ -1,11 +1,15 @@ import * as z from "zod"; import { responseSchema } from "@/lib/validations/response"; -import { contentMetadataSchema, getContentQueryParamsSchema } from "@/lib/validations/common"; +import { + contentMetadataSchema, + getContentQueryParamsSchema, +} from "@/lib/validations/common"; +import type { AnyLayer as MapLayerStyle } from "react-map-gl"; export const layerType = z.enum([ - "feature_layer", - "imagery_layer", - "tile_layer", + "feature", + "external_imagery", + "external_vector_tile", "table", ]); @@ -24,19 +28,24 @@ export const layerMetadataSchema = contentMetadataSchema.extend({ }); export const layerSchema = layerMetadataSchema.extend({ - active: z.boolean().optional(), - name: z.string().optional(), + id: z.number(), + properties: z.object({}), + total_count: z.number(), updated_at: z.string(), created_at: z.string(), - query: z.object({}), extent: z.string(), folder_id: z.string(), - id: z.string().uuid(), + data_source: z.string().optional(), + query: z.object({}), + layer_id: z.string().uuid(), + project_id: z.string().uuid(), user_id: z.string().uuid(), type: layerType, size: z.number().optional(), - style: z.object({}).optional(), + z_index: z.number().min(0).optional(), + extra_properties: z.object({}).optional(), url: z.string().optional(), + feature_layer_type: featureLayerType.optional(), feature_layer_geometry_type: z.string(), data_type: data_type.optional(), legend_urls: z.array(z.string()).optional(), @@ -86,17 +95,17 @@ export const createNewExternalTileLayerSchema = createLayerBaseSchema.extend({ export const createNewDatasetLayerSchema = z.object({ file: z.any(), - layer_in: z.union([ - createNewTableLayerSchema, - createNewStandardLayerSchema - ]), + layer_in: z.union([createNewTableLayerSchema, createNewStandardLayerSchema]), }); export const layerResponseSchema = responseSchema(layerSchema); export const layerTypesArray = Object.values(layerType.Values); export const featureLayerTypesArray = Object.values(featureLayerType.Values); -export type Layer = z.infer; +export type LayerZod = z.infer; +export type Layer = Omit & { + properties: MapLayerStyle; +}; export type LayerPaginated = z.infer; export type LayerType = z.infer; export type LayerMetadata = z.infer; diff --git a/apps/web/lib/validations/project.ts b/apps/web/lib/validations/project.ts index 8f37b287..22dc29ed 100644 --- a/apps/web/lib/validations/project.ts +++ b/apps/web/lib/validations/project.ts @@ -1,5 +1,8 @@ import * as z from "zod"; -import { contentMetadataSchema, getContentQueryParamsSchema } from "@/lib/validations/common"; +import { + contentMetadataSchema, + getContentQueryParamsSchema, +} from "@/lib/validations/common"; import { responseSchema } from "@/lib/validations/response"; export const projectSchema = contentMetadataSchema.extend({ @@ -7,8 +10,11 @@ export const projectSchema = contentMetadataSchema.extend({ created_at: z.string(), folder_id: z.string(), id: z.string(), + layer_order: z.array(z.number()), }); + + export const projectBaseSchema = z.object({ folder_id: z.string(), name: z.string().optional(), @@ -32,18 +38,18 @@ export const projectLayerSchema = projectBaseSchema.extend({ query: z.record(z.string()), }); -export const projectInitialViewStateSchema = z.object({ - latitude: z.number(), - longitude: z.number(), - zoom: z.number(), - min_zoom: z.number(), - max_zoom: z.number(), - bearing: z.number(), - pitch: z.number(), +export const projectViewStateSchema = z.object({ + latitude: z.number().min(-90).max(90), + longitude: z.number().min(-180).max(180), + zoom: z.number().min(0).max(24), + min_zoom: z.number().min(0).max(24), + max_zoom: z.number().min(0).max(24), + bearing: z.number().min(0).max(360), + pitch: z.number().min(0).max(60), }); export const postProjectSchema = projectBaseSchema.extend({ - initial_view_state: projectInitialViewStateSchema.optional(), + initial_view_state: projectViewStateSchema.optional(), }); const getProjectsQueryParamsSchema = getContentQueryParamsSchema.extend({}); @@ -55,7 +61,10 @@ export type Project = z.infer; export type ProjectLayers = z.infer; export type ProjectPaginated = z.infer; export type PostProject = z.infer; -export type ProjectLayersPaginated = z.infer; +export type ProjectViewState = z.infer; +export type ProjectLayersPaginated = z.infer< + typeof projectLayersResponseSchema +>; export type GetProjectsQueryParams = z.infer< typeof getProjectsQueryParamsSchema >; diff --git a/apps/web/package.json b/apps/web/package.json index 881769fb..382940ca 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,10 @@ "typecheck": "tsc --project ./tsconfig.json --pretty --noEmit" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/cache": "latest", "@emotion/core": "^11.0.0", "@emotion/react": "latest", diff --git a/apps/web/public/assets/data/isochroneModes.ts b/apps/web/public/assets/data/isochroneModes.ts new file mode 100644 index 00000000..78b314bb --- /dev/null +++ b/apps/web/public/assets/data/isochroneModes.ts @@ -0,0 +1,57 @@ +export const ptModes = [ + { + name: "bus", + value: "bus", + }, + { + name: "tram", + value: "tram", + }, + { + name: "rail", + value: "rail", + }, + { + name: "subway", + value: "subway", + }, + { + name: "ferry", + value: "ferry", + }, + { + name: "cable_car", + value: "cable_car", + }, + { + name: "gondola", + value: "gondola", + }, + { + name: "funicular", + value: "funicular", + }, +]; + +export const routingModes = [ + { + name: "pt", + value: "pt", + }, + { + name: "walk", + value: "walking", + }, + { + name: "bicycle", + value: "bicycle", + }, + { + name: "pedelec", + value: "pedelec", + }, + { + name: "car", + value: "car_peak", + }, +]; \ No newline at end of file diff --git a/apps/web/types/common/index.ts b/apps/web/types/common/index.ts index 79e30be7..af5225db 100644 --- a/apps/web/types/common/index.ts +++ b/apps/web/types/common/index.ts @@ -7,6 +7,11 @@ export enum ContentActions { DELETE = "delete", } +export enum MapLayerActions { + DUPLICATE = "duplicate", + RENAME = "rename", +} + export enum OrgMemberActions { EDIT = "edit", DELETE = "delete", diff --git a/apps/web/types/map/common.ts b/apps/web/types/map/common.ts index 6c20852e..e69de29b 100644 --- a/apps/web/types/map/common.ts +++ b/apps/web/types/map/common.ts @@ -1,4 +0,0 @@ -export enum DataType { - "mvt" = "mvt", - "geojson" = "geojson", -} diff --git a/apps/web/types/map/isochrone.ts b/apps/web/types/map/isochrone.ts index d1b4e3d9..a2f2c2d6 100644 --- a/apps/web/types/map/isochrone.ts +++ b/apps/web/types/map/isochrone.ts @@ -12,6 +12,17 @@ export type RoutingTypes = | "cable_car" | "gondola" | "funicular" - | "walk" + | "walking" | "bicycle" - | "car"; + | "pedelec" + | "car_peak"; + +export type PTModeTypes = + | "bus" + | "tram" + | "rail" + | "subway" + | "ferry" + | "cable_car" + | "gondola" + | "funicular"; diff --git a/apps/web/types/map/layer.d.ts b/apps/web/types/map/layer.d.ts deleted file mode 100644 index f52ac54c..00000000 --- a/apps/web/types/map/layer.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface XYZ_Layer { - id: string, - sourceUrl: string; - color: string; -} \ No newline at end of file diff --git a/apps/web/types/map/layers.d.ts b/apps/web/types/map/layers.d.ts deleted file mode 100644 index f8c622d4..00000000 --- a/apps/web/types/map/layers.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { DataType } from "@/types/map/common"; - -export type SourceProps = { - data_type: DataType; - url: string; - data_source_name: string; - data_reference_year: number; -}; - -interface GroupedLayer { - url: string; - data_type: string; - data_source_name: string; - data_reference_year: number; - layers: LayerProps[]; -} diff --git a/packages/ui/components/Icon.tsx b/packages/ui/components/Icon.tsx index f9995f81..7b8d653c 100644 --- a/packages/ui/components/Icon.tsx +++ b/packages/ui/components/Icon.tsx @@ -39,6 +39,7 @@ import { faCaretUp, faCaretDown, faLocationDot, + faLocationPin, faCross, faCircle, faUsers, @@ -70,6 +71,10 @@ import { faCrown, faArrowRightArrowLeft, faBullseye, + faCopy, + faPersonBiking, + faBicycle, + faCar, } from "@fortawesome/free-solid-svg-icons"; import { @@ -133,6 +138,7 @@ export enum ICON_NAME { STEPUP = "step-up", STEPDOWN = "step-down", LOCATION = "location", + LOCATION_MARKER = "location-marker", CROSS = "cross", CIRCLE = "circle", CIRCLE_PLUS = "circle-plus", @@ -151,7 +157,7 @@ export enum ICON_NAME { SAVE = "save", DATABASE = "database", SORT_ALPHA_ASC = "sort-alpha-asc", - SORT_ALPHA_DESC = "sort-alpha-desc", + SORT_ALPHA_DESC = "sort-alpha-desc", CLOCK = "clock", DOWNLOAD = "download", UPLOAD = "upload", @@ -161,6 +167,10 @@ export enum ICON_NAME { RULES_COMBINED = "rules-combined", CROWN = "crown", BULLSEYE="bullsey", + COPY = "copy", + PEDELEC = "pedelec", + BICYCLE = "bicycle", + CAR = "car", // Brand icons GOOGLE = "google", MICROSOFT = "microsoft", @@ -219,6 +229,7 @@ const nameToIcon: { [k in ICON_NAME]: IconDefinition } = { [ICON_NAME.STEPUP]: faCaretUp, [ICON_NAME.STEPDOWN]: faCaretDown, [ICON_NAME.LOCATION]: faLocationDot, + [ICON_NAME.LOCATION_MARKER]: faLocationPin, [ICON_NAME.CROSS]: faCross, [ICON_NAME.CIRCLE]: faCircle, [ICON_NAME.CIRCLE_PLUS]: faCirclePlus, @@ -248,6 +259,10 @@ const nameToIcon: { [k in ICON_NAME]: IconDefinition } = { [ICON_NAME.CROWN]: faCrown, [ICON_NAME.REVERSE]: faArrowRightArrowLeft, [ICON_NAME.BULLSEYE]: faBullseye, + [ICON_NAME.COPY]: faCopy, + [ICON_NAME.PEDELEC]: faPersonBiking, + [ICON_NAME.BICYCLE]: faBicycle, + [ICON_NAME.CAR]: faCar, // Brand icons [ICON_NAME.GOOGLE]: faGoogle, [ICON_NAME.MICROSOFT]: faMicrosoft, @@ -259,7 +274,7 @@ const nameToIcon: { [k in ICON_NAME]: IconDefinition } = { [ICON_NAME.LINKEDIN]: faLinkedin, [ICON_NAME.INSTAGRAM]: faInstagram, [ICON_NAME.BITBUCKET]: faBitbucket, - [ICON_NAME.PAYPAL]: faPaypal + [ICON_NAME.PAYPAL]: faPaypal, }; interface BrandColors { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0575aba2..1b64a754 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,6 +148,18 @@ importers: apps/web: dependencies: + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/modifiers': + specifier: ^7.0.0 + version: 7.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.2.0) '@emotion/cache': specifier: latest version: 11.11.0 @@ -165,13 +177,13 @@ importers: version: 3.1.1(react-hook-form@7.45.2) '@mui/icons-material': specifier: latest - version: 5.14.12(@mui/material@5.14.12)(@types/react@18.2.18)(react@18.2.0) + version: 5.14.16(@mui/material@5.14.17)(@types/react@18.2.18)(react@18.2.0) '@mui/lab': specifier: 5.0.0-alpha.132 - version: 5.0.0-alpha.132(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.12)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.0-alpha.132(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.17)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) '@mui/material': specifier: latest - version: 5.14.12(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + version: 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) '@mui/private-theming': specifier: ^5.14.5 version: 5.14.5(@types/react@18.2.18)(react@18.2.0) @@ -180,7 +192,7 @@ importers: version: 5.13.7(react@18.2.0) '@mui/x-tree-view': specifier: 6.0.0-alpha.3 - version: 6.0.0-alpha.3(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.12)(@mui/system@5.14.14)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + version: 6.0.0-alpha.3(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.17)(@mui/system@5.14.17)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) '@p4b/types': specifier: workspace:* version: link:../../packages/types @@ -267,7 +279,7 @@ importers: version: 7.1.2(mapbox-gl@2.15.0)(maplibre-gl@3.1.0)(react-dom@18.2.0)(react@18.2.0) react-material-ui-carousel: specifier: ^3.4.2 - version: 3.4.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/icons-material@5.14.12)(@mui/material@5.14.12)(@mui/system@5.14.14)(react-dom@18.2.0)(react@18.2.0) + version: 3.4.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/icons-material@5.14.16)(@mui/material@5.14.17)(@mui/system@5.14.17)(react-dom@18.2.0)(react@18.2.0) react-redux: specifier: ^8.1.1 version: 8.1.1(@types/react-dom@18.2.7)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) @@ -291,7 +303,7 @@ importers: version: 1.6.0 tss-react: specifier: latest - version: 4.9.2(@emotion/react@11.11.1)(@mui/material@5.14.12)(react@18.2.0) + version: 4.9.3(@emotion/react@11.11.1)(@mui/material@5.14.17)(react@18.2.0) uuid: specifier: ^9.0.0 version: 9.0.0 @@ -522,10 +534,10 @@ importers: version: 5.13.7(react@18.2.0) '@mui/x-data-grid': specifier: ^6.7.0 - version: 6.7.0(@mui/material@5.14.12)(@mui/system@5.14.14)(react-dom@18.2.0)(react@18.2.0) + version: 6.7.0(@mui/material@5.14.12)(@mui/system@5.14.17)(react-dom@18.2.0)(react@18.2.0) '@mui/x-date-pickers': specifier: ^6.9.2 - version: 6.9.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/base@5.0.0-beta.20)(@mui/material@5.14.12)(@mui/system@5.14.14)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + version: 6.9.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/base@5.0.0-beta.23)(@mui/material@5.14.12)(@mui/system@5.14.17)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) '@reduxjs/toolkit': specifier: ^1.9.5 version: 1.9.5(react-redux@8.1.1)(react@18.2.0) @@ -2761,6 +2773,61 @@ packages: engines: {node: '>=10.0.0'} dev: true + /@dnd-kit/accessibility@3.1.0(react@18.2.0): + resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@dnd-kit/accessibility': 3.1.0(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + dev: false + + /@dnd-kit/modifiers@7.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-BG/ETy3eBjFap7+zIti53f0PCLGDzNXyTmn6fSdrudORf+OH04MxrW4p5+mPu4mgMk9kM41iYONjc3DOUWTcfg==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/utilities@3.2.2(react@18.2.0): + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + /@emotion/babel-plugin@11.11.0: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: @@ -3744,8 +3811,8 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) - '@mui/types': 7.2.6(@types/react@18.2.18) - '@mui/utils': 5.14.14(@types/react@18.2.18)(react@18.2.0) + '@mui/types': 7.2.8(@types/react@18.2.18) + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.18 clsx: 2.0.0 @@ -3767,8 +3834,31 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) - '@mui/types': 7.2.6(@types/react@18.2.18) - '@mui/utils': 5.14.14(@types/react@18.2.18)(react@18.2.0) + '@mui/types': 7.2.8(@types/react@18.2.18) + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.18 + clsx: 2.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@mui/base@5.0.0-beta.23(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-9L8SQUGAWtd/Qi7Qem26+oSSgpY7f2iQTuvcz/rsGpyZjSomMMO6lwYeQSA0CpWM7+aN7eGoSY/WV6wxJiIxXw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.8(@types/react@18.2.18) + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.18 clsx: 2.0.0 @@ -3790,8 +3880,8 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@emotion/is-prop-valid': 1.2.1 - '@mui/types': 7.2.6(@types/react@18.2.18) - '@mui/utils': 5.13.7(react@18.2.0) + '@mui/types': 7.2.8(@types/react@18.2.18) + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.18 clsx: 1.2.1 @@ -3805,6 +3895,10 @@ packages: resolution: {integrity: sha512-Rw/xKiTOUgXD8hdKqj60aC6QcGprMipG7ne2giK6Mz7b4PlhL/xog9xLeclY3BxsRLkZQ05egFnIEY1CSibTbw==} dev: false + /@mui/core-downloads-tracker@5.14.17: + resolution: {integrity: sha512-eE0uxrpJAEL2ZXkeGLKg8HQDafsiXY+6eNpP4lcv3yIjFfGbU6Hj9/P7Adt8jpU+6JIhmxvILGj2r27pX+zdrQ==} + dev: false + /@mui/icons-material@5.14.12(@mui/material@5.14.12)(@types/react@18.2.18)(react@18.2.0): resolution: {integrity: sha512-aFm6g/AIB3RQN9h/4MKoBoBybLZXeR3aDHWNx6KzemEpIlElUxv5uXRX5Qk1VC6v/YPkhbaPsiLLjsRSTiZF3w==} engines: {node: '>=12.0.0'} @@ -3822,6 +3916,23 @@ packages: react: 18.2.0 dev: false + /@mui/icons-material@5.14.16(@mui/material@5.14.17)(@types/react@18.2.18)(react@18.2.0): + resolution: {integrity: sha512-wmOgslMEGvbHZjFLru8uH5E+pif/ciXAvKNw16q6joK6EWVWU5rDYWFknDaZhCvz8ZE/K8ZnJQ+lMG6GgHzXbg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@mui/material': ^5.0.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.18 + react: 18.2.0 + dev: false + /@mui/lab@5.0.0-alpha.132(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.12)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-MCkQlZZSsRumgCD1rDhtkyaJmCHg20zaneJhSB55X4Rr6dA9p4vK70HvCEQKQibK3RqvEuWuRUYNKWD5SlFVww==} engines: {node: '>=12.0.0'} @@ -3856,6 +3967,40 @@ packages: react-is: 18.2.0 dev: false + /@mui/lab@5.0.0-alpha.132(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.17)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-MCkQlZZSsRumgCD1rDhtkyaJmCHg20zaneJhSB55X4Rr6dA9p4vK70HvCEQKQibK3RqvEuWuRUYNKWD5SlFVww==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material': ^5.0.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0) + '@mui/base': 5.0.0-beta.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) + '@mui/types': 7.2.6(@types/react@18.2.18) + '@mui/utils': 5.13.7(react@18.2.0) + '@types/react': 18.2.18 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + dev: false + /@mui/material@5.14.12(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EelF2L46VcVqhg3KjzIGBBpOtcBgRh0MMy9Efuk6Do81QdcZsFC9RebCVAflo5jIdbHiBmxBs5/l5Q9NjONozg==} engines: {node: '>=12.0.0'} @@ -3892,18 +4037,54 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false - /@mui/private-theming@5.14.14(@types/react@18.2.18)(react@18.2.0): - resolution: {integrity: sha512-n77au3CQj9uu16hak2Y+rvbGSBaJKxziG/gEbOLVGrAuqZ+ycVSkorCfN6Y/4XgYOpG/xvmuiY3JwhAEOzY3iA==} + /@mui/material@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+y0VeOLWfEA4Z98We/UH6KCo8+f2HLZDK45FY+sJf8kSojLy3VntadKtC/u0itqnXXb1Pr4wKB2tSIBW02zY4Q==} engines: {node: '>=12.0.0'} peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 '@types/react': ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true '@types/react': optional: true dependencies: '@babel/runtime': 7.23.2 - '@mui/utils': 5.14.14(@types/react@18.2.18)(react@18.2.0) + '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0) + '@mui/base': 5.0.0-beta.23(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.17 + '@mui/system': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) + '@mui/types': 7.2.8(@types/react@18.2.18) + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) + '@types/react': 18.2.18 + '@types/react-transition-group': 4.4.8 + clsx: 2.0.0 + csstype: 3.1.2 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + + /@mui/private-theming@5.14.17(@types/react@18.2.18)(react@18.2.0): + resolution: {integrity: sha512-u4zxsCm9xmQrlhVPug+Ccrtsjv7o2+rehvrgHoh0siSguvVgVQq5O3Hh10+tp/KWQo2JR4/nCEwquSXgITS1+g==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) '@types/react': 18.2.18 prop-types: 15.8.1 react: 18.2.0 @@ -3926,8 +4107,8 @@ packages: react: 18.2.0 dev: false - /@mui/styled-engine@5.14.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-sF3DS2PVG+cFWvkVHQQaGFpL1h6gSwOW3L91pdxPLQDHDZ5mZ/X0SlXU5XA+WjypoysG4urdAQC7CH/BRvUiqg==} + /@mui/styled-engine@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-AqpVjBEA7wnBvKPW168bNlqB6EN7HxTjLOY7oi275AzD/b1C7V0wqELy6NWoJb2yya5sRf7ENf4iNi3+T5cOgw==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -3967,10 +4148,40 @@ packages: '@babel/runtime': 7.23.2 '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0) - '@mui/private-theming': 5.14.14(@types/react@18.2.18)(react@18.2.0) - '@mui/styled-engine': 5.14.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) - '@mui/types': 7.2.6(@types/react@18.2.18) - '@mui/utils': 5.14.14(@types/react@18.2.18)(react@18.2.0) + '@mui/private-theming': 5.14.17(@types/react@18.2.18)(react@18.2.0) + '@mui/styled-engine': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.8(@types/react@18.2.18) + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) + '@types/react': 18.2.18 + clsx: 2.0.0 + csstype: 3.1.2 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/system@5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0): + resolution: {integrity: sha512-Ccz3XlbCqka6DnbHfpL3o3TfOeWQPR+ewvNAgm8gnS9M0yVMmzzmY6z0w/C1eebb+7ZP7IoLUj9vojg/GBaTPg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0) + '@mui/private-theming': 5.14.17(@types/react@18.2.18)(react@18.2.0) + '@mui/styled-engine': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.8(@types/react@18.2.18) + '@mui/utils': 5.14.17(@types/react@18.2.18)(react@18.2.0) '@types/react': 18.2.18 clsx: 2.0.0 csstype: 3.1.2 @@ -3989,6 +4200,17 @@ packages: '@types/react': 18.2.18 dev: false + /@mui/types@7.2.8(@types/react@18.2.18): + resolution: {integrity: sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.18 + dev: false + /@mui/utils@5.13.7(react@18.2.0): resolution: {integrity: sha512-/3BLptG/q0u36eYED7Nhf4fKXmcKb6LjjT7ZMwhZIZSdSxVqDqSTmATW3a56n3KEPQUXCU9TpxAfCBQhs6brVA==} engines: {node: '>=12.0.0'} @@ -4021,7 +4243,25 @@ packages: react-is: 18.2.0 dev: false - /@mui/x-data-grid@6.7.0(@mui/material@5.14.12)(@mui/system@5.14.14)(react-dom@18.2.0)(react@18.2.0): + /@mui/utils@5.14.17(@types/react@18.2.18)(react@18.2.0): + resolution: {integrity: sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@types/prop-types': 15.7.9 + '@types/react': 18.2.18 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/x-data-grid@6.7.0(@mui/material@5.14.12)(@mui/system@5.14.17)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-gkMjvIT2oMKs7VOxLXxu0NkSKFBhlh/AepHB89xKcf+XVMkKBEwq8VT2GtrccAO+7Lr7ciZhcX2EmiDHsXdhbA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4032,7 +4272,7 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@mui/material': 5.14.12(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) + '@mui/system': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) '@mui/utils': 5.13.7(react@18.2.0) clsx: 1.2.1 prop-types: 15.8.1 @@ -4041,7 +4281,7 @@ packages: reselect: 4.1.8 dev: false - /@mui/x-date-pickers@6.9.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/base@5.0.0-beta.20)(@mui/material@5.14.12)(@mui/system@5.14.14)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers@6.9.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/base@5.0.0-beta.23)(@mui/material@5.14.12)(@mui/system@5.14.17)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-WoXRBUwb/nN/Hg+rArefkuRe30eVTN29dbjTN/vTydBFoNIp8vCaSeqRIEP6Zq2NaDHFWW9FMGChuuFEdROxWA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4082,9 +4322,9 @@ packages: '@babel/runtime': 7.23.2 '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0) - '@mui/base': 5.0.0-beta.20(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@mui/base': 5.0.0-beta.23(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) '@mui/material': 5.14.12(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) + '@mui/system': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) '@mui/utils': 5.13.7(react@18.2.0) '@types/react-transition-group': 4.4.8 clsx: 1.2.1 @@ -4095,7 +4335,7 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false - /@mui/x-tree-view@6.0.0-alpha.3(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.12)(@mui/system@5.14.14)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-tree-view@6.0.0-alpha.3(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.17)(@mui/system@5.14.17)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-j6vPD3e4Y4dd8v19bnDkPBXdAD8Qo5pbSEAwB3XYh60tprphBU+YVjCfUo4ZZkYEqhGBen6gKsJhFz98H2v2kg==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4110,8 +4350,8 @@ packages: '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0) '@mui/base': 5.0.0-beta.20(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) - '@mui/material': 5.14.12(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) + '@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) '@mui/utils': 5.14.14(@types/react@18.2.18)(react@18.2.0) '@types/react-transition-group': 4.4.8 clsx: 2.0.0 @@ -17146,7 +17386,7 @@ packages: - supports-color dev: false - /react-material-ui-carousel@3.4.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/icons-material@5.14.12)(@mui/material@5.14.12)(@mui/system@5.14.14)(react-dom@18.2.0)(react@18.2.0): + /react-material-ui-carousel@3.4.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/icons-material@5.14.16)(@mui/material@5.14.17)(@mui/system@5.14.17)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-jUbC5aBWqbbbUOOdUe3zTVf4kMiZFwKJqwhxzHgBfklaXQbSopis4iWAHvEOLcZtSIJk4JAGxKE0CmxDoxvUuw==} peerDependencies: '@emotion/react': ^11.4.1 @@ -17159,9 +17399,9 @@ packages: dependencies: '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0) - '@mui/icons-material': 5.14.12(@mui/material@5.14.12)(@types/react@18.2.18)(react@18.2.0) - '@mui/material': 5.14.12(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.14(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) + '@mui/icons-material': 5.14.16(@mui/material@5.14.17)(@types/react@18.2.18)(react@18.2.0) + '@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0) framer-motion: 4.1.17(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -19488,6 +19728,27 @@ packages: react: 18.2.0 dev: false + /tss-react@4.9.3(@emotion/react@11.11.1)(@mui/material@5.14.17)(react@18.2.0): + resolution: {integrity: sha512-TqI0kBFmgW0f5YIOD2PMdHu6FnqSxVDUf5uJ7+gVkhemtMfwdlFpvXpddgSesktizr9PU9hY2nZ+kNnf0KQb9A==} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/server': ^11.4.0 + '@mui/material': ^5.0.0 + react: ^16.8.0 || ^17.0.2 || ^18.0.0 + peerDependenciesMeta: + '@emotion/server': + optional: true + '@mui/material': + optional: true + dependencies: + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0) + '@emotion/serialize': 1.1.2 + '@emotion/utils': 1.2.1 + '@mui/material': 5.14.17(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /tsutils@3.21.0(typescript@5.2.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'}