digitalfabrik/integreat-app

View on GitHub
web/src/components/PoisDesktop.tsx

Summary

Maintainability
A
1 hr
Test Coverage
C
79%
import React, { ReactElement, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { GeolocateControl, NavigationControl } from 'react-map-gl'
import styled, { useTheme } from 'styled-components'

import { LocationType, MapViewViewport, MapFeature, PreparePoisReturn } from 'shared'
import { CityModel, PoiModel } from 'shared/api'

import dimensions from '../constants/dimensions'
import CityContentFooter from './CityContentFooter'
import GoBack from './GoBack'
import MapView from './MapView'
import PoiPanelNavigation from './PoiPanelNavigation'
import PoiSharedChildren from './PoiSharedChildren'

const PanelContainer = styled.article`
  overflow: auto;
  height: 100%;
  display: flex;
  flex-direction: column;
  width: ${dimensions.poiDesktopPanelWidth}px;

  /* additional min-width is needed because the article would shrink to a smaller width if the content can be smaller */
  min-width: ${dimensions.poiDesktopPanelWidth}px;
`

const ListViewWrapper = styled.div<{ $panelHeights: number; $bottomBarHeight: number }>`
  padding: 16px;
  overflow: auto;
  ${({ $panelHeights: panelHeights, $bottomBarHeight: bottomBarHeight }) =>
    `height: calc(100vh - ${panelHeights}px - ${bottomBarHeight}px);`};
`

const ToolbarContainer = styled.div`
  display: flex;
  justify-content: center;
  background-color: ${props => props.theme.colors.backgroundAccentColor};
  box-shadow: 1px 0 4px 0 rgb(0 0 0 / 20%);
`

const ListHeader = styled.div`
  padding-top: clamp(16px, 1.4vh, 32px);
  padding-bottom: clamp(10px, 1vh, 20px);
  text-align: center;
  font-size: ${props => props.theme.fonts.subTitleFontSize};
  font-family: ${props => props.theme.fonts.web.decorativeFont};
  line-height: ${props => props.theme.fonts.decorativeLineHeight};
  font-weight: 600;
  border-bottom: 1px solid ${props => props.theme.colors.borderColor};
  margin-bottom: clamp(10px, 1vh, 20px);
`

const FooterContainer = styled.div`
  position: absolute;
  bottom: 0;
`

type PoisDesktopProps = {
  panelHeights: number
  toolbar: ReactElement
  cityModel: CityModel
  data: PreparePoisReturn
  selectMapFeature: (mapFeature: MapFeature | null) => void
  selectPoi: (poi: PoiModel) => void
  deselect: () => void
  userLocation: LocationType | null
  languageCode: string
  slug: string | undefined
  mapViewport?: MapViewViewport
  setMapViewport: (mapViewport: MapViewViewport) => void
  MapOverlay: ReactElement
  PanelContent?: ReactElement
}

const nextPoiIndex = (step: 1 | -1, arrayLength: number, currentIndex: number): number => {
  if (currentIndex === arrayLength - 1 && step === 1) {
    return 0
  }
  if (currentIndex === 0 && step === -1) {
    return arrayLength - 1
  }
  return currentIndex + step
}

const PoisDesktop = ({
  panelHeights,
  toolbar,
  data,
  userLocation,
  selectMapFeature,
  selectPoi,
  deselect,
  cityModel,
  languageCode,
  slug,
  mapViewport,
  setMapViewport,
  MapOverlay,
  PanelContent: PanelContentProp,
}: PoisDesktopProps): ReactElement => {
  const { t } = useTranslation('pois')
  const [scrollOffset, setScrollOffset] = useState<number>(0)
  const listRef = useRef<HTMLDivElement>(null)
  const { pois, poi, mapFeatures, mapFeature } = data
  const canDeselect = !!mapFeature || !!slug
  const { contentDirection } = useTheme()

  const handleSelectPoi = (poi: PoiModel) => {
    if (listRef.current) {
      setScrollOffset(listRef.current.scrollTop)
    }
    selectPoi(poi)
  }

  const switchPoi = (step: 1 | -1) => {
    const currentPoiIndex = pois.findIndex(it => it.slug === poi?.slug)
    const updatedIndex = nextPoiIndex(step, pois.length, currentPoiIndex)
    const newPoi = pois[updatedIndex]
    if (newPoi) {
      selectPoi(newPoi)
    }
  }

  useEffect(() => {
    listRef.current?.scrollTo({ top: mapFeature ? 0 : scrollOffset })
  }, [mapFeature, scrollOffset])

  const PanelContent = (
    <>
      <ListViewWrapper
        ref={listRef}
        $panelHeights={panelHeights}
        $bottomBarHeight={poi ? dimensions.poiDetailNavigation : dimensions.toolbarHeight}>
        {canDeselect ? (
          <GoBack goBack={deselect} text={t('detailsHeader')} />
        ) : (
          <ListHeader>{t('listTitle')}</ListHeader>
        )}

        <PoiSharedChildren
          pois={pois}
          poi={poi}
          selectPoi={handleSelectPoi}
          userLocation={userLocation}
          toolbar={toolbar}
          slug={slug}
        />
      </ListViewWrapper>
      {poi && pois.length > 0 ? (
        <PoiPanelNavigation switchPoi={switchPoi} />
      ) : (
        <ToolbarContainer>{toolbar}</ToolbarContainer>
      )}
    </>
  )

  return (
    <>
      <PanelContainer>{PanelContentProp ?? PanelContent}</PanelContainer>
      <MapView
        viewport={mapViewport}
        setViewport={setMapViewport}
        selectFeature={selectMapFeature}
        features={mapFeatures}
        currentFeature={mapFeature ?? null}
        Overlay={MapOverlay}>
        <NavigationControl showCompass={false} position={contentDirection === 'rtl' ? 'bottom-left' : 'bottom-right'} />
        <GeolocateControl
          positionOptions={{ enableHighAccuracy: true }}
          trackUserLocation
          position={contentDirection === 'rtl' ? 'bottom-left' : 'bottom-right'}
        />
        <FooterContainer>
          <CityContentFooter city={cityModel.code} language={languageCode} mode='overlay' />
        </FooterContainer>
      </MapView>
    </>
  )
}

export default PoisDesktop