web/src/components/PoisMobile.tsx
import { GeolocateControl } from 'maplibre-gl'
import React, { ReactElement, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { LocationType, MapViewViewport, MapFeature, PreparePoisReturn } from 'shared'
import { PoiModel } from 'shared/api'
import { ArrowBackspaceIcon } from '../assets'
import useWindowDimensions from '../hooks/useWindowDimensions'
import { getSnapPoints } from '../utils/getSnapPoints'
import BottomActionSheet, { ScrollableBottomSheetRef } from './BottomActionSheet'
import GoBack from './GoBack'
import MapView, { MapViewRef } from './MapView'
import PoiSharedChildren from './PoiSharedChildren'
import Button from './base/Button'
import Icon from './base/Icon'
const geolocatorTopOffset = 10
const ListContainer = styled.div`
padding: 0 30px;
`
const ListTitle = styled.div`
margin: 12px 0;
font-weight: 700;
`
const GoBackContainer = styled.div<{ $hidden: boolean }>`
display: flex;
flex-direction: column;
max-height: ${props => (props.$hidden ? '0' : '10vh')};
opacity: ${props => (props.$hidden ? '0' : '1')};
overflow: hidden;
transition: all 1s;
padding: 0 30px;
`
const BackNavigation = styled(Button)`
background-color: ${props => props.theme.colors.textSecondaryColor};
height: 28px;
width: 28px;
border: 1px solid ${props => props.theme.colors.textDisabledColor};
border-radius: 50px;
box-shadow: 1px 1px 2px 0 rgb(0 0 0 / 20%);
justify-content: center;
align-items: center;
display: flex;
`
const StyledIcon = styled(Icon)`
color: ${props => props.theme.colors.backgroundColor};
`
const GeocontrolContainer = styled.div<{ $height: number }>`
--max-icon-height: calc(${props => getSnapPoints(props.$height)[1]}px + ${geolocatorTopOffset}px);
position: absolute;
inset-inline-end: 10px;
bottom: min(calc(var(--rsbs-overlay-h, 0) + ${geolocatorTopOffset}px), var(--max-icon-height));
`
type PoisMobileProps = {
toolbar: ReactElement
data: PreparePoisReturn
userLocation: LocationType | null
slug: string | undefined
mapViewport?: MapViewViewport
setMapViewport: (mapViewport: MapViewViewport) => void
selectMapFeature: (mapFeature: MapFeature | null) => void
selectPoi: (poi: PoiModel) => void
deselect: () => void
MapOverlay: ReactElement
}
const PoisMobile = ({
toolbar,
data,
userLocation,
slug,
mapViewport,
setMapViewport,
selectMapFeature,
selectPoi,
deselect,
MapOverlay,
}: PoisMobileProps): ReactElement => {
const [bottomActionSheetHeight, setBottomActionSheetHeight] = useState(0)
const [scrollOffset, setScrollOffset] = useState<number>(0)
const sheetRef = useRef<ScrollableBottomSheetRef>(null)
const geocontrolPosition = useRef<HTMLDivElement>(null)
const [mapViewRef, setMapViewRef] = useState<MapViewRef | null>(null)
const { pois, poi, mapFeatures, mapFeature } = data
const { height } = useWindowDimensions()
const canDeselect = !!mapFeature || !!slug
const { t } = useTranslation('pois')
const isBottomActionSheetFullScreen = bottomActionSheetHeight >= height
const changeSnapPoint = (snapPoint: number) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sheetRef.current?.sheet?.snapTo(({ maxHeight }) => getSnapPoints(maxHeight)[snapPoint]!)
}
const handleSelectPoi = (poi: PoiModel) => {
if (sheetRef.current?.scrollElement) {
setScrollOffset(sheetRef.current.scrollElement.scrollTop)
}
selectPoi(poi)
}
const handleSelectMapFeature = (feature: MapFeature | null) => {
if (feature && sheetRef.current?.scrollElement) {
changeSnapPoint(1)
setScrollOffset(sheetRef.current.scrollElement.scrollTop)
}
selectMapFeature(feature)
}
useEffect(() => {
sheetRef.current?.scrollElement?.scrollTo({ top: mapFeature ? 0 : scrollOffset })
}, [mapFeature, scrollOffset])
useEffect(() => {
if (mapViewRef && geocontrolPosition.current) {
const geocontrol = new GeolocateControl({
positionOptions: { enableHighAccuracy: true },
trackUserLocation: true,
})
mapViewRef.setGeocontrol(geocontrol)
geocontrolPosition.current.appendChild(geocontrol._container)
}
}, [mapViewRef])
return (
<>
<MapView
ref={setMapViewRef}
viewport={mapViewport}
setViewport={setMapViewport}
selectFeature={handleSelectMapFeature}
changeSnapPoint={changeSnapPoint}
features={mapFeatures}
currentFeature={mapFeature ?? null}
Overlay={
<>
{canDeselect && (
<BackNavigation onClick={deselect} tabIndex={0} label={t('detailsHeader')}>
<StyledIcon src={ArrowBackspaceIcon} directionDependent />
</BackNavigation>
)}
{MapOverlay}
</>
}
/>
<BottomActionSheet
toolbar={toolbar}
ref={sheetRef}
setBottomActionSheetHeight={setBottomActionSheetHeight}
sibling={<GeocontrolContainer id='geolocate' ref={geocontrolPosition} $height={height} />}>
{canDeselect && (
<GoBackContainer $hidden={!isBottomActionSheetFullScreen}>
<GoBack goBack={deselect} viewportSmall text={t('detailsHeader')} />
</GoBackContainer>
)}
<ListContainer>
{!canDeselect && <ListTitle>{t('listTitle')}</ListTitle>}
<PoiSharedChildren
pois={pois}
poi={poi}
selectPoi={handleSelectPoi}
userLocation={userLocation}
slug={slug}
/>
</ListContainer>
</BottomActionSheet>
</>
)
}
export default PoisMobile