diff --git a/apps/api-portal/features/user-applications/user-applications.ts b/apps/api-portal/features/user-applications/user-applications.ts index e4ca2cc5fe..929f506032 100644 --- a/apps/api-portal/features/user-applications/user-applications.ts +++ b/apps/api-portal/features/user-applications/user-applications.ts @@ -1,9 +1,9 @@ import { stringify } from 'qs' import { useMutation, useQuery, useQueryClient } from 'react-query' import { useMemo, useState } from 'react' +import type { FieldValidationError } from 'lib/types' import { GFWAPI } from '@globalfishingwatch/api-client' import type { APIPagination, UserApplication } from '@globalfishingwatch/api-types' -import type { FieldValidationError } from 'lib/types' import useUser, { checkUserApplicationPermission } from 'features/user/user' export type UserApplicationCreateArguments = Omit< diff --git a/apps/fishing-map/data/layer-library/layers-events.ts b/apps/fishing-map/data/layer-library/layers-events.ts index 85a1829480..9a0a06b2e9 100644 --- a/apps/fishing-map/data/layer-library/layers-events.ts +++ b/apps/fishing-map/data/layer-library/layers-events.ts @@ -20,24 +20,24 @@ export const LAYERS_LIBRARY_EVENTS: LibraryLayerConfig[] = [ color: '#FAE9A0', }, }, - // { - // id: LOITERING_EVENTS_SOURCE_ID, - // dataviewId: CLUSTER_LOITERING_EVENTS_DATAVIEW_SLUG, - // previewImageUrl: `${PATH_BASENAME}/images/layer-library/loitering.jpg`, - // config: { - // color: '#CEA9F9', - // }, - // onlyGFWUser: true, - // }, - // { - // id: PORT_VISITS_EVENTS_SOURCE_ID, - // dataviewId: CLUSTER_PORT_VISIT_EVENTS_DATAVIEW_SLUG, - // previewImageUrl: `${PATH_BASENAME}/images/layer-library/port-visits.jpg`, - // config: { - // color: '#9AEEFF', - // }, - // onlyGFWUser: true, - // }, + { + id: LOITERING_EVENTS_SOURCE_ID, + dataviewId: CLUSTER_LOITERING_EVENTS_DATAVIEW_SLUG, + previewImageUrl: `${PATH_BASENAME}/images/layer-library/loitering.jpg`, + config: { + color: '#CEA9F9', + }, + onlyGFWUser: true, + }, + { + id: PORT_VISITS_EVENTS_SOURCE_ID, + dataviewId: CLUSTER_PORT_VISIT_EVENTS_DATAVIEW_SLUG, + previewImageUrl: `${PATH_BASENAME}/images/layer-library/port-visits.jpg`, + config: { + color: '#9AEEFF', + }, + onlyGFWUser: true, + }, ] export const LAYER_LIBRARY_EVENTS_IDS = LAYERS_LIBRARY_EVENTS.map((layer) => layer.id) diff --git a/apps/fishing-map/features/download/downloadActivity.slice.ts b/apps/fishing-map/features/download/downloadActivity.slice.ts index b520ef2cd3..063d7a6e7e 100644 --- a/apps/fishing-map/features/download/downloadActivity.slice.ts +++ b/apps/fishing-map/features/download/downloadActivity.slice.ts @@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit' import { stringify } from 'qs' import { saveAs } from 'file-saver' +import type { RootState } from 'reducers' import type { Dataview, DownloadActivity } from '@globalfishingwatch/api-types' import { getIsConcurrentError, @@ -10,7 +11,6 @@ import { parseAPIError, } from '@globalfishingwatch/api-client' import type { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' -import type { RootState } from 'reducers' import type { AsyncError } from 'utils/async-slice' import { AsyncReducerStatus } from 'utils/async-slice' import type { AreaKeyId, AreaKeys } from 'features/areas/areas.slice' diff --git a/apps/fishing-map/features/download/downloadTrack.slice.ts b/apps/fishing-map/features/download/downloadTrack.slice.ts index 7dfe608dba..9ebaafca88 100644 --- a/apps/fishing-map/features/download/downloadTrack.slice.ts +++ b/apps/fishing-map/features/download/downloadTrack.slice.ts @@ -2,9 +2,9 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { stringify } from 'qs' import { saveAs } from 'file-saver' +import type { RootState } from 'reducers' import type { DownloadRateLimit, ThinningConfig } from '@globalfishingwatch/api-types' import { GFWAPI, parseAPIError } from '@globalfishingwatch/api-client' -import type { RootState } from 'reducers' import type { AsyncError } from 'utils/async-slice' import { AsyncReducerStatus } from 'utils/async-slice' import type { DateRange } from 'features/download/downloadActivity.slice' diff --git a/apps/fishing-map/features/editor/editor.slice.ts b/apps/fishing-map/features/editor/editor.slice.ts index ff0434b1af..54eb6af431 100644 --- a/apps/fishing-map/features/editor/editor.slice.ts +++ b/apps/fishing-map/features/editor/editor.slice.ts @@ -1,8 +1,8 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import { stringify } from 'qs' +import type { RootState } from 'reducers' import { GFWAPI, parseAPIError } from '@globalfishingwatch/api-client' import type { APIPagination, Dataview } from '@globalfishingwatch/api-types' -import type { RootState } from 'reducers' import type { AsyncError } from 'utils/async-slice' import { AsyncReducerStatus } from 'utils/async-slice' import { APP_NAME, DEFAULT_PAGINATION_PARAMS } from 'data/config' diff --git a/apps/fishing-map/features/map/controls/map-controls.slice.ts b/apps/fishing-map/features/map/controls/map-controls.slice.ts index fafd660000..deaf4f4a31 100644 --- a/apps/fishing-map/features/map/controls/map-controls.slice.ts +++ b/apps/fishing-map/features/map/controls/map-controls.slice.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSelector, createSlice } from '@reduxjs/toolkit' -import type { RulerData } from '@globalfishingwatch/deck-layers' import type { RootState } from 'reducers' +import type { RulerData } from '@globalfishingwatch/deck-layers' import type { MapAnnotation } from '../overlays/annotations/annotations.types' export type MapControl = 'annotations' | 'rulers' | 'errorNotification' diff --git a/apps/fishing-map/features/map/popups/Popup.module.css b/apps/fishing-map/features/map/popups/Popup.module.css index 88c27f80a0..e89412c2c2 100644 --- a/apps/fishing-map/features/map/popups/Popup.module.css +++ b/apps/fishing-map/features/map/popups/Popup.module.css @@ -24,7 +24,9 @@ font: var(--font-L); line-height: 1; margin: var(--space-XS); - transition: opacity 0.15s linear, color 0.15s linear; + transition: + opacity 0.15s linear, + color 0.15s linear; z-index: 1; top: -2rem; right: -2rem; @@ -283,3 +285,9 @@ overflow: hidden; text-overflow: ellipsis; } + +.portCTA { + margin-top: var(--space-S); + display: block; + width: 100%; +} diff --git a/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx b/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx index c6e6f69f2d..e3c9bb89a9 100644 --- a/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx +++ b/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx @@ -2,7 +2,7 @@ import { Fragment, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { stringify } from 'qs' import { useSelector } from 'react-redux' -import { Button, Icon, IconButton } from '@globalfishingwatch/ui-components' +import { Button, Icon } from '@globalfishingwatch/ui-components' import { DatasetTypes, DataviewCategory } from '@globalfishingwatch/api-types' import { AsyncReducerStatus } from 'utils/async-slice' import I18nDate from 'features/i18n/i18nDate' @@ -22,6 +22,7 @@ import { VESSEL_GROUP_EVENTS_DATAVIEW_IDS } from 'features/reports/vessel-groups import { getEventDescriptionComponent } from 'utils/events' import PortsReportLink from 'features/reports/ports/PortsReportLink' import { selectIsGFWUser } from 'features/user/selectors/user.selectors' +import { selectIsPortReportLocation } from 'routes/routes.selectors' import { useMapViewState } from '../../map-viewport.hooks' import type { ExtendedEventVessel, @@ -193,6 +194,8 @@ type PortVisitLayerProps = { error?: string } function PortVisitEventTooltipRow({ feature, showFeaturesDetails, error }: PortVisitLayerProps) { + const { t } = useTranslation() + const isPortReportLocation = useSelector(selectIsPortReportLocation) const { datasetId, event, color } = feature const title = getDatasetLabel({ id: datasetId! }) const isGFWUser = useSelector(selectIsGFWUser) @@ -201,15 +204,6 @@ function PortVisitEventTooltipRow({ feature, showFeaturesDetails, error }: PortV
{

{title}

} - {isGFWUser && event?.port && ( - -
- {event?.port?.name || event?.port?.id} - {event?.port?.country && ` (${formatInfoField(event.port.country, 'flag')})`} - -
-
- )} {error &&

{error}

} {showFeaturesDetails && ( )} + {isGFWUser && event?.port && !isPortReportLocation && ( + + + + )}
) diff --git a/apps/fishing-map/features/modals/modals.slice.ts b/apps/fishing-map/features/modals/modals.slice.ts index ea1fcecc1c..94d3a2a08e 100644 --- a/apps/fishing-map/features/modals/modals.slice.ts +++ b/apps/fishing-map/features/modals/modals.slice.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' -import type { DatasetGeometryType, DataviewCategory } from '@globalfishingwatch/api-types' import type { RootState } from 'reducers' +import type { DatasetGeometryType, DataviewCategory } from '@globalfishingwatch/api-types' type ModalId = | 'feedback' diff --git a/apps/fishing-map/features/reports/areas/area-reports.hooks.ts b/apps/fishing-map/features/reports/areas/area-reports.hooks.ts index b3ced38dd2..c757581906 100644 --- a/apps/fishing-map/features/reports/areas/area-reports.hooks.ts +++ b/apps/fishing-map/features/reports/areas/area-reports.hooks.ts @@ -1,11 +1,11 @@ import { useCallback, useEffect, useMemo } from 'react' import { useSelector } from 'react-redux' +import { useGetStatsByDataviewQuery } from 'queries/stats-api' import type { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import type { Dataset, Dataview } from '@globalfishingwatch/api-types' import { useLocalStorage } from '@globalfishingwatch/react-hooks' import { useGetDeckLayers } from '@globalfishingwatch/deck-layer-composer' import type { ContextFeature, ContextLayer } from '@globalfishingwatch/deck-layers' -import { useGetStatsByDataviewQuery } from 'queries/stats-api' import { useAppDispatch } from 'features/app/app.hooks' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import { diff --git a/apps/fishing-map/features/reports/ports/PortsReportLink.tsx b/apps/fishing-map/features/reports/ports/PortsReportLink.tsx index 0527dea631..583320b3bd 100644 --- a/apps/fishing-map/features/reports/ports/PortsReportLink.tsx +++ b/apps/fishing-map/features/reports/ports/PortsReportLink.tsx @@ -1,7 +1,7 @@ import { useSelector } from 'react-redux' +import cx from 'classnames' import React from 'react' import Link from 'redux-first-router-link' -import { useTranslation } from 'react-i18next' import { Tooltip } from '@globalfishingwatch/ui-components' import type { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { DataviewType } from '@globalfishingwatch/api-types' @@ -21,12 +21,12 @@ import { getPortClusterDataviewForReport } from './ports-report.utils' type PortsReportLinkProps = { port: ExtendedFeatureByVesselEventPort children: React.ReactNode + tooltip?: string } -function PortsReportLink({ children, port }: PortsReportLinkProps) { +function PortsReportLink({ children, port, tooltip }: PortsReportLinkProps) { const workspace = useSelector(selectWorkspace) const query = useSelector(selectLocationQuery) - const { t } = useTranslation() if (!workspace || !port) { return children @@ -55,7 +55,7 @@ function PortsReportLink({ children, port }: PortsReportLinkProps) { return ( - - {children} - + {children} ) } diff --git a/apps/fishing-map/features/reports/ports/ports-report.utils.ts b/apps/fishing-map/features/reports/ports/ports-report.utils.ts index a0b6f948c4..0c5905b124 100644 --- a/apps/fishing-map/features/reports/ports/ports-report.utils.ts +++ b/apps/fishing-map/features/reports/ports/ports-report.utils.ts @@ -8,21 +8,23 @@ export function isPortClusterDataviewForReport(dataview: UrlDataviewInstance) { export function getPortClusterDataviewForReport( dataview: UrlDataviewInstance, - { portId, clusterMaxZoomLevels } = {} as { + { portId, clusterMaxZoomLevels: newClusterMaxZoomLevels } = {} as { portId?: string clusterMaxZoomLevels?: ClusterMaxZoomLevelConfig } ) { if (isPortClusterDataviewForReport(dataview)) { + const { clusterMaxZoomLevels, ...restConfig } = dataview.config || {} + const { port_id, ...restFilters } = restConfig.filters || {} return { ...dataview, config: { - ...dataview.config, + ...restConfig, visible: true, - clusterMaxZoomLevels, + ...(newClusterMaxZoomLevels && { clusterMaxZoomLevels: newClusterMaxZoomLevels }), filters: { - ...(dataview.config?.filters || {}), - port_id: portId, + ...(restFilters || {}), + ...(portId && { port_id: portId }), }, }, } diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightFishing.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightFishing.tsx index 42ad940bd5..4b864fd30b 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightFishing.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightFishing.tsx @@ -2,10 +2,10 @@ import { useTranslation } from 'react-i18next' import { useState } from 'react' import cx from 'classnames' import { useSelector } from 'react-redux' +import { useGetVesselGroupInsightQuery } from 'queries/vessel-insight-api' import type { ParsedAPIError } from '@globalfishingwatch/api-client' import { Collapsable } from '@globalfishingwatch/ui-components' import { RegionType, VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' -import { useGetVesselGroupInsightQuery } from 'queries/vessel-insight-api' import InsightError from 'features/vessel/insights/InsightErrorMessage' import DataTerminology from 'features/vessel/identity/DataTerminology' import { formatInfoField } from 'utils/info' diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightMOU.tsx b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightMOU.tsx index 6e46f253ef..35e0137058 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightMOU.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/insights/VGRInsightMOU.tsx @@ -3,10 +3,10 @@ import { useState } from 'react' import cx from 'classnames' import { useSelector } from 'react-redux' import { groupBy } from 'es-toolkit' +import { useGetVesselGroupInsightQuery } from 'queries/vessel-insight-api' import type { ParsedAPIError } from '@globalfishingwatch/api-client' import { Collapsable } from '@globalfishingwatch/ui-components' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' -import { useGetVesselGroupInsightQuery } from 'queries/vessel-insight-api' import InsightError from 'features/vessel/insights/InsightErrorMessage' import DataTerminology from 'features/vessel/identity/DataTerminology' import { selectIsGuestUser } from 'features/user/selectors/user.selectors' diff --git a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts index 5bb33d04c0..9dd5f87ea8 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts @@ -1,11 +1,11 @@ import { createSelector } from '@reduxjs/toolkit' -import type { InsightType } from '@globalfishingwatch/api-types' import type { VesselGroupInsightParams } from 'queries/vessel-insight-api' import { selectVesselGroupInsight, selectVesselGroupInsightApiSlice, } from 'queries/vessel-insight-api' import type { RootState } from 'reducers' +import type { InsightType } from '@globalfishingwatch/api-types' import { selectReportVesselGroupId } from 'routes/routes.selectors' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import { dataviewHasVesselGroupId } from 'features/dataviews/dataviews.utils' diff --git a/apps/fishing-map/features/vessel/insights/InsightGapsDetails.tsx b/apps/fishing-map/features/vessel/insights/InsightGapsDetails.tsx index 5cad17adf3..9e57b5b15e 100644 --- a/apps/fishing-map/features/vessel/insights/InsightGapsDetails.tsx +++ b/apps/fishing-map/features/vessel/insights/InsightGapsDetails.tsx @@ -1,10 +1,10 @@ import { useSelector } from 'react-redux' import { Fragment } from 'react' import { useTranslation } from 'react-i18next' +import { useGetVesselEventsQuery } from 'queries/vessel-events-api' import type { InsightResponse } from '@globalfishingwatch/api-types' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import { IconButton } from '@globalfishingwatch/ui-components' -import { useGetVesselEventsQuery } from 'queries/vessel-events-api' import { getVesselIdentities } from 'features/vessel/vessel.utils' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import { removeNonTunaRFMO } from 'features/vessel/insights/insights.utils' diff --git a/apps/fishing-map/features/vessel/insights/InsightWrapper.tsx b/apps/fishing-map/features/vessel/insights/InsightWrapper.tsx index c6f4df362b..7ab709667a 100644 --- a/apps/fishing-map/features/vessel/insights/InsightWrapper.tsx +++ b/apps/fishing-map/features/vessel/insights/InsightWrapper.tsx @@ -1,8 +1,8 @@ import { useSelector } from 'react-redux' +import { useGetVesselInsightQuery } from 'queries/vessel-insight-api' import type { InsightType } from '@globalfishingwatch/api-types' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import type { ParsedAPIError } from '@globalfishingwatch/api-client' -import { useGetVesselInsightQuery } from 'queries/vessel-insight-api' import { getVesselIdentities } from 'features/vessel/vessel.utils' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import InsightMOUList from 'features/vessel/insights/InsightMOUList' diff --git a/apps/fishing-map/features/vessel/vessel.slice.ts b/apps/fishing-map/features/vessel/vessel.slice.ts index 0cfe07d333..c69b076958 100644 --- a/apps/fishing-map/features/vessel/vessel.slice.ts +++ b/apps/fishing-map/features/vessel/vessel.slice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import { HYDRATE } from 'next-redux-wrapper' +import type { RootState } from 'reducers' import type { ParsedAPIError } from '@globalfishingwatch/api-client' import { GFWAPI, parseAPIError } from '@globalfishingwatch/api-client' import type { @@ -19,7 +20,6 @@ import { DatasetTypes, ResourceStatus } from '@globalfishingwatch/api-types' import { setResource } from '@globalfishingwatch/dataviews-client' import { resolveEndpoint } from '@globalfishingwatch/datasets-client' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' -import type { RootState } from 'reducers' import { AsyncReducerStatus } from 'utils/async-slice' import { selectResources } from 'features/resources/resources.slice' import { diff --git a/apps/fishing-map/middlewares.ts b/apps/fishing-map/middlewares.ts index b78c930411..02ca639987 100644 --- a/apps/fishing-map/middlewares.ts +++ b/apps/fishing-map/middlewares.ts @@ -1,8 +1,8 @@ import type { Middleware } from 'redux' import type { ThunkDispatch } from '@reduxjs/toolkit' import { isRejectedWithValue } from '@reduxjs/toolkit' -import { isUnauthorized } from '@globalfishingwatch/api-client' import type { RootState } from 'reducers' +import { isUnauthorized } from '@globalfishingwatch/api-client' import type { AsyncError } from 'utils/async-slice' import { setLoginExpired } from 'features/user/user.slice' import { selectIsGuestUser } from 'features/user/selectors/user.selectors' diff --git a/apps/fishing-map/routes/routes.middlewares.ts b/apps/fishing-map/routes/routes.middlewares.ts index a2d64c2df9..0f84bd93e5 100644 --- a/apps/fishing-map/routes/routes.middlewares.ts +++ b/apps/fishing-map/routes/routes.middlewares.ts @@ -1,6 +1,6 @@ import type { Middleware, Dispatch } from 'redux' -import { ACCESS_TOKEN_STRING } from '@globalfishingwatch/api-client' import type { RootState } from 'reducers' +import { ACCESS_TOKEN_STRING } from '@globalfishingwatch/api-client' import { REPLACE_URL_PARAMS } from 'routes/routes.config' import { setLastWorkspaceVisited } from 'features/workspace/workspace.slice' import type { QueryParam, QueryParams } from 'types' diff --git a/apps/fishing-map/server/vessel/vessel.tsx b/apps/fishing-map/server/vessel/vessel.tsx index c8c400712d..ae372159a7 100644 --- a/apps/fishing-map/server/vessel/vessel.tsx +++ b/apps/fishing-map/server/vessel/vessel.tsx @@ -1,7 +1,7 @@ import cx from 'classnames' import { useSelector } from 'react-redux' -import { Logo } from '@globalfishingwatch/ui-components' import { serverT } from 'server/i18n' +import { Logo } from '@globalfishingwatch/ui-components' import { AsyncReducerStatus } from 'utils/async-slice' import { IDENTITY_FIELD_GROUPS } from 'features/vessel/vessel.config' import { formatInfoField } from 'utils/info' diff --git a/apps/real-time-prototype/features/map/Map.tsx b/apps/real-time-prototype/features/map/Map.tsx index d24f481842..1d6d1dab15 100644 --- a/apps/real-time-prototype/features/map/Map.tsx +++ b/apps/real-time-prototype/features/map/Map.tsx @@ -8,14 +8,14 @@ import { useAtom } from 'jotai' import { BitmapLayer } from '@deck.gl/layers' import { TileLayer } from '@deck.gl/geo-layers' import uniqBy from 'lodash/uniqBy' -import { GFWAPI } from '@globalfishingwatch/api-client' -import type { MiniglobeBounds} from '@globalfishingwatch/ui-components'; -import { MiniGlobe, Tooltip } from '@globalfishingwatch/ui-components' -import { BasemapType } from '@globalfishingwatch/layer-composer' import { useTracksLayer, useTracksSublayers } from 'layers/tracks/tracks.hooks' import { useLatestPositionsLayer } from 'layers/latest-positions/latest-positions.hooks' import { useContextsLayer } from 'layers/context/context.hooks' import { useBasemapLayer } from 'layers/basemap/basemap.hooks' +import { GFWAPI } from '@globalfishingwatch/api-client' +import type { MiniglobeBounds} from '@globalfishingwatch/ui-components'; +import { MiniGlobe, Tooltip } from '@globalfishingwatch/ui-components' +import { BasemapType } from '@globalfishingwatch/layer-composer' import { useURLViewport, useViewport } from 'features/map/map-viewport.hooks' import { hoveredFeaturesAtom } from 'features/map/map-picking.hooks' import { getDateLabel } from 'utils/dates'