diff --git a/.env b/.env index 0c640ad12..39d0dd7f0 100644 --- a/.env +++ b/.env @@ -30,7 +30,7 @@ NEXT_PUBLIC_TYPESENSE_NODES=lj2nqsybtrcxp5kmp-1.a1.typesense.net NEXT_PUBLIC_TYPESENSE_API_KEY=nBvjNVojLn792SX9sOLhLlpQ7BQJo5ok # Mapbox Geocoder token -NEXT_PUBLIC_MAPBOX_API_KEY=pk.eyJ1IjoibWFwcGFuZGFzIiwiYSI6ImNsZG1wcnBhZTA5eXozb3FnZTY1bHg4bHcifQ.7LJGRLFeLUbntAt5twiDiw +NEXT_PUBLIC_MAPBOX_API_KEY=pk.eyJ1Ijoib3BlbmJldGFwcm9qZWN0IiwiYSI6ImNsdThtb2kzMDAwbmcya3Fubmk4cXI1MXEifQ.b1UScIUV2CFR1oX0MD2goQ # Maptiler API Key NEXT_PUBLIC_MAPTILER_API_KEY=ejjLkz58mUNz9TgNs0Ed diff --git a/package.json b/package.json index b7bf837cc..e6caf0fd5 100644 --- a/package.json +++ b/package.json @@ -26,15 +26,12 @@ "@radix-ui/react-popover": "^1.0.3", "@radix-ui/react-portal": "^1.0.1", "@radix-ui/react-slider": "^1.1.2", - "@radix-ui/react-tabs": "^1.0.1", - "@radix-ui/react-toggle": "^1.0.1", "@turf/bbox": "^6.5.0", "@turf/line-to-polygon": "^6.5.0", "@udecode/zustood": "^1.1.3", "@vercel/edge": "^1.1.1", "auth0": "^2.42.0", "awesome-debounce-promise": "^2.1.0", - "aws-sdk": "^2.1265.0", "axios": "^0.24.0", "classnames": "^2.3.1", "csvtojson": "^2.0.10", @@ -56,7 +53,6 @@ "nprogress": "^0.2.0", "paper": "^0.12.17", "paperjs-offset": "^1.0.8", - "pmtiles": "^3.0.3", "rc-slider": "^10.0.0-alpha.5", "react": "^18.2.0", "react-content-loader": "^6.2.0", @@ -67,7 +63,6 @@ "react-infinite-scroll-component": "^6.1.0", "react-map-gl": "^7.1.7", "react-markdown": "^9.0.1", - "react-paginate": "^8.1.3", "react-responsive": "^9.0.0-beta.6", "react-swipeable": "^7.0.0", "react-toastify": "^9.1.1", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index e4bccd261..a0dacb7e7 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -14,6 +14,9 @@ interface HeaderProps { showFilterBar?: boolean } +/** + * @deprecated + */ export default function Header (props: HeaderProps): JSX.Element { const { isTablet, isMobile } = useResponsive() const includeFilters = Boolean(props.showFilterBar) diff --git a/src/components/finder/CragHighlightPopover.tsx b/src/components/finder/CragHighlightPopover.tsx deleted file mode 100644 index 6c54e5def..000000000 --- a/src/components/finder/CragHighlightPopover.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { memo } from 'react' -import Link from 'next/link' -import { Popover } from '@headlessui/react' -import { XMarkIcon } from '@heroicons/react/24/outline' - -import { sanitizeName } from '../../js/utils' -import { AreaType } from '../../js/types' -import DTable from '../ui/DTable' -import { actions } from '../../js/stores' - -function CragHighlightPopover (props: AreaType | undefined): JSX.Element | null { - if (props == null) return null - const { areaName, aggregate, metadata } = props - const name = sanitizeName(areaName) - const { areaId } = metadata - return ( - - - - - - - - {name} - - { - e.preventDefault() - actions.filters.deactivateActiveMarker() - }} - className='-mt-0.5 p-1 rounded-full hover:bg-slate-200' - > - - - - - - - - - - - ) -} - -export default memo(CragHighlightPopover) diff --git a/src/components/finder/CragRow.tsx b/src/components/finder/CragRow.tsx deleted file mode 100644 index 7c1a586fc..000000000 --- a/src/components/finder/CragRow.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Link from 'next/link' -import CounterPie from '../ui/Statistics/CounterPie' -import { sanitizeName } from '../../js/utils' -import { AreaType } from '../../js/types' -import DTable from '../ui/DTable' -import { MiniCrumbs } from '../ui/BreadCrumbs' -import useResponsive from '../../js/hooks/useResponsive' -import { cragFiltersStore } from '../../js/stores' - -type CragRowProps = Pick - -export default function CragRow ({ areaName, totalClimbs, metadata, aggregate, pathTokens }: CragRowProps): JSX.Element { - const getClimbsForYou = cragFiltersStore.get.inMyRangeCount(aggregate) - const name = sanitizeName(areaName) - const { areaId } = metadata - const { isMobile } = useResponsive() - - return ( - ( - - - { - // Todo set some state to highlight this crag on the map - }} - > - - - - {name} - - - - Climbs for you - - {!isMobile && } - - - - - - - - ) - ) -} diff --git a/src/components/finder/CragTable.tsx b/src/components/finder/CragTable.tsx deleted file mode 100644 index 009eb7480..000000000 --- a/src/components/finder/CragTable.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { cragFiltersStore } from '../../js/stores' -import CragRow from './CragRow' -import { AreaType } from '../../js/types' - -export default function CragTable (): JSX.Element { - const { currentItems } = cragFiltersStore.use.pagination() - return ( - <> - - {currentItems.map( - (crag: AreaType) => - )} - > - ) -} diff --git a/src/components/finder/MobileMainView.tsx b/src/components/finder/MobileMainView.tsx deleted file mode 100644 index 50ca474b4..000000000 --- a/src/components/finder/MobileMainView.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import SegmentedControl from '../ui/SegmentedControl' - -interface MobileMainViewProps { - listView: JSX.Element | JSX.Element[] - mapView: JSX.Element | JSX.Element[] -} -export default function MobileMainView ({ listView, mapView }: MobileMainViewProps): JSX.Element { - return ( - - - {mapView} - - - {listView} - - - ) -} diff --git a/src/components/finder/Pagination.tsx b/src/components/finder/Pagination.tsx deleted file mode 100644 index 23969dd19..000000000 --- a/src/components/finder/Pagination.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import ReactPaginate from 'react-paginate' -import { actions, store, ITEMS_PER_PAGE } from '../../js/stores' - -/** - * Pagination control - */ -const Pagination = (): JSX.Element => { - const { pageCount, currentPage, itemOffset } = store.filters.pagination() - const total = store.filters.total() - return ( - - - - - ) -} -export default Pagination - -const handlePageClick = ({ selected }: { selected: number }): void => { - window.scrollTo({ top: 0, behavior: 'auto' }) - actions.filters.toPage(selected) -} - -interface FooterProps { - itemOffset: number - itemsInView: number - total: number -} - -const Footer = ({ itemOffset, total, itemsInView }: FooterProps): JSX.Element => ( - - {itemOffset + 1} – {itemsInView} of {total} crags to climb - -) diff --git a/src/components/finder/Preface.tsx b/src/components/finder/Preface.tsx deleted file mode 100644 index 6238b3c0e..000000000 --- a/src/components/finder/Preface.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import DownloadLink from './Download' - -export const Preface = ({ isLoading, total, searchText }: { isLoading: boolean, total: number, searchText: string }): JSX.Element => { - return ( - - - - {isLoading - ? `Loading crags in ${searchText}...` - : `${humanizeNumber(total)} crags near ${searchText}.`} - - Consult local climbing community and guidebooks before you visit. - - - - ) -} - -export const humanizeNumber = (n: number): string => n > 300 ? '300+' : n.toString() diff --git a/src/components/finder/TwoColumnLayout.tsx b/src/components/finder/TwoColumnLayout.tsx deleted file mode 100644 index 41d038a44..000000000 --- a/src/components/finder/TwoColumnLayout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -interface TwoColumnLayoutProps { - left: JSX.Element | JSX.Element [] - right: JSX.Element | JSX.Element [] -} - -const TwoColumnLayout = ({ left, right }: TwoColumnLayoutProps): JSX.Element => { - return ( - - - {left} - - {right} - - - - ) -} - -export default TwoColumnLayout diff --git a/src/components/layout.tsx b/src/components/layout.tsx index 26a0d9014..b714db9bc 100644 --- a/src/components/layout.tsx +++ b/src/components/layout.tsx @@ -38,4 +38,7 @@ function Layout ({ ) } +/** + * @deprecated + */ export default Layout diff --git a/src/components/maps/BaseMap.tsx b/src/components/maps/BaseMap.tsx deleted file mode 100644 index 6ee4685d5..000000000 --- a/src/components/maps/BaseMap.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useRef, useEffect } from 'react' -import { Map, MapLayerMouseEvent, MapRef, ViewStateChangeEvent } from 'react-map-gl' - -import { BBoxType, XViewStateType } from '../../js/types' - -export const DEFAULT_INITIAL_VIEWSTATE: XViewStateType = { - width: 300, - height: 1024, - padding: { top: 20, bottom: 20, left: 20, right: 20 }, - bearing: 0, - zoom: 9, - pitch: 0, - latitude: 36.079693291728546, - longitude: -115.5, - bbox: [0, 0, 0, 0] -} - -export const MAP_STYLES = { - light: 'mapbox://styles/mappandas/ckf8bb0qv18be19npofybx7yq', - dark: 'mapbox://styles/mappandas/cl0u44wo8008415pedsbgtml7' -} -interface BaseMapProps { - height: number - viewstate: XViewStateType - onViewStateChange: (vs: XViewStateType) => void - children: JSX.Element | JSX.Element[] | null - light: boolean - onClick?: (event: MapLayerMouseEvent) => void - onHover?: (event: MapLayerMouseEvent) => void - interactiveLayerIds: string[] -} - -/** - * Important! if you want MapGL to pass custom layer data to onClick/onHover, - * you need to provide the layer id to MapGL via interactiveLayerIds - * @deprecated - */ -export default function BaseMap ({ - height, - viewstate, - onViewStateChange, - children, - light, - onClick = (): void => {}, - onHover = (): void => {}, - interactiveLayerIds -}: BaseMapProps): JSX.Element { - const mapRef = useRef(null) - - // Map seems to struggle responding to height changes after initial set - useEffect(() => { - if (mapRef.current !== null) { - const map = (mapRef.current) - map.resize() - } - }, [height, mapRef]) - - const onMapLoad = React.useCallback(() => { - if (mapRef.current !== null) { - const map = mapRef.current - - const bounds = map.getBounds() - const sw = bounds.getSouthWest() - const ne = bounds.getNorthEast() - const bbox: BBoxType = [sw.lng, sw.lat, ne.lng, ne.lat] - - const center = map.getCenter() - const vs = { - bearing: 0, - zoom: map.getZoom(), - padding: map.getPadding(), - pitch: 0, - latitude: center.lat, - longitude: center.lng, - bbox - } - onViewStateChange({ ...viewstate, ...vs, bbox }) - } - }, [viewstate]) - - const onMoveHandler = (event: ViewStateChangeEvent): void => { - // console.log(event) - const { viewState } = event - const bounds = mapRef.current?.getBounds() ?? null - - let bbox: BBoxType = [0, 0, 0, 0] - if (bounds != null) { - const sw = bounds.getSouthWest() - const ne = bounds.getNorthEast() - bbox = [sw.lng, sw.lat, ne.lng, ne.lat] - } - onViewStateChange({ ...viewState, height: 0, width: 0, bbox }) - } - - return ( - - {children} - - ) -} diff --git a/src/components/maps/CragsMap.tsx b/src/components/maps/CragsMap.tsx deleted file mode 100644 index eb0f528d7..000000000 --- a/src/components/maps/CragsMap.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { useState, useCallback } from 'react' - -import BaseMap from './BaseMap' -import CragHighlightPopover from '../finder/CragHighlightPopover' -import { store, actions } from '../../js/stores' -import MarkerLayer, { InteractiveLayerIDs as markerLayerIds } from './MarkerLayer' -import HeatmapLayer from './HeatmapLayer' -import InteractiveMarker - from './InteractiveMarker' -import useAutoSizing from '../../js/hooks/finder/useMapAutoSizing' -import { MapLayerMouseEvent } from 'mapbox-gl' - -const mapElementId = 'my-area-map' -/** - * Make a map of crag markers. - * @deprecated - */ -export default function CragsMap (): JSX.Element { - const geojson = store.filters.allGeoJson() - - const [viewstate, height, setViewState] = useAutoSizing({ geojson, elementId: mapElementId }) - - // track current mouseover marker - const [hoverMarker, setHoverMarker] = useState(null) - - const onHoverHandler = useCallback((event: MapLayerMouseEvent) => { - const { features } = event - if (features == null || features.length === 0) return - const f = features[0] - // @ts-expect-error - setHoverMarker([f.properties.lng, f.properties.lat]) - }, []) - - const onClickHandler = useCallback((event: MapLayerMouseEvent) => { - const { features } = event - // @ts-expect-error - const { id, lng, lat } = features[0].properties - actions.filters.setActiveMarker(id, [lng, lat]) - }, []) - - const { areaId, lnglat } = store.filters.map().active - const area = areaId != null ? store.filters.areaById(areaId) : null - return ( - <> - - - {area != null && - } - - - - - - - - - > - ) -} diff --git a/src/components/maps/HeatmapLayer.tsx b/src/components/maps/HeatmapLayer.tsx deleted file mode 100644 index 1ea61d735..000000000 --- a/src/components/maps/HeatmapLayer.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Source, Layer, LayerProps } from 'react-map-gl' -import { Properties, FeatureCollection } from '@turf/helpers' - -export const LayerId = 'heatmap' - -const layerStyle: LayerProps = { - id: 'all-areas-heat', - type: 'heatmap', - source: 'all-areas', - maxzoom: 15, - paint: { - // Increase the heatmap weight based on frequency and property magnitude - 'heatmap-weight': [ - 'interpolate', - ['linear'], - ['get', 'totalClimbs'], - 0, - 0, - 40, - 1 - ], - // Increase the heatmap color weight weight by zoom level - // heatmap-intensity is a multiplier on top of heatmap-weight - 'heatmap-intensity': [ - 'interpolate', - ['linear'], - ['zoom'], - 0, - 1, - 9, - 3 - ], - // Color ramp for heatmap. Domain is 0 (low) to 1 (high). - // Begin color ramp at 0-stop with a 0-transparancy color - // to create a blur-like effect. - 'heatmap-color': [ - 'interpolate', - ['linear'], - ['heatmap-density'], - 0, - 'rgba(198, 219, 239,0)', - 0.2, - '#9ecae1', - 0.4, - '#6baed6', - 0.6, - '#4292c6', - 0.8, - '#2171b5', - 1, - '#084594' - ], - // Adjust the heatmap radius by zoom level - 'heatmap-radius': [ - 'interpolate', - ['linear'], - ['zoom'], - 0, - 2, - 9, - 20 - ], - // Transition from heatmap to circle layer by zoom level - 'heatmap-opacity': [ - 'interpolate', - ['linear'], - ['zoom'], - 8, - 0.45, - 16, - 1 - ] - } -} - -interface MarkerLayerProps { - geojson: FeatureCollection | undefined -} -/** - * Build a heatmap layer using native Mapbox GL style. - */ -export default function HeatmapLayer ({ geojson }: MarkerLayerProps): JSX.Element | null { - if (geojson == null) return null - return ( - - - - ) -} diff --git a/src/components/maps/InteractiveMarker.tsx b/src/components/maps/InteractiveMarker.tsx deleted file mode 100644 index c7327421d..000000000 --- a/src/components/maps/InteractiveMarker.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { memo } from 'react' -import { Marker } from 'react-map-gl' - -interface ActiveMarkerProps { - lnglat: number[] | null - hover?: boolean -} - -function InteractiveMarker ({ lnglat, hover = true }: ActiveMarkerProps): JSX.Element | null { - if (lnglat === null) return null - return ( - - - - ) -} - -export default memo(InteractiveMarker) diff --git a/src/components/maps/MarkerLayer.tsx b/src/components/maps/MarkerLayer.tsx deleted file mode 100644 index e2b615c49..000000000 --- a/src/components/maps/MarkerLayer.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Properties, FeatureCollection } from '@turf/helpers' -import { Source, Layer, LayerProps } from 'react-map-gl' - -const layerStyle: LayerProps = { - id: 'my-areas', - type: 'symbol', - source: 'areas', - filter: ['==', 'isInMyRange', true], - layout: { - 'icon-size': ['interpolate', ['linear'], ['zoom'], 8, 1.25, 12, 1], - 'symbol-spacing': 5, - 'text-field': ['get', 'name'], - 'text-variable-anchor': ['bottom', 'left', 'right'], - 'text-radial-offset': 1, - 'text-justify': 'auto', - 'icon-image': [ - 'case', - ['boolean', ['get', 'isInMyRange'], false], - 'circle', // more important - larger marker - 'dot-11' // small marker - ], - 'text-size': ['interpolate', ['linear'], ['zoom'], 8, 12, 14, 14], - 'icon-allow-overlap': false - }, - paint: { - 'text-halo-blur': 4, - 'text-halo-width': 1, - 'text-color': '#ffffff', - 'text-halo-color': '#262626' - } -} - -const closeupLayerStyle: LayerProps = { - id: 'all-markers', - type: 'symbol', - source: 'areas', - minzoom: 14, - filter: ['==', 'isInMyRange', false], - layout: { - 'icon-size': ['interpolate', ['linear'], ['zoom'], 8, 0.75, 12, 1], - 'symbol-spacing': 20, - 'text-field': ['get', 'name'], - 'text-variable-anchor': ['bottom', 'left', 'right'], - 'text-radial-offset': 1, - 'text-justify': 'auto', - 'icon-image': 'dot-11', - 'text-size': ['interpolate', ['linear'], ['zoom'], 8, 12, 14, 10], - 'icon-allow-overlap': false - }, - paint: { - 'icon-color': '#ffffff', - 'text-halo-blur': 4, - 'text-halo-width': 2, - 'text-color': '#a3a3a3', - 'text-halo-color': '#334455' - } -} - -interface MarkerLayerProps { - geojson: FeatureCollection | undefined -} - -/** - * Build a layer of crag markers using native Mapbox GL style. - */ -export default function MarkerLayer ({ geojson }: MarkerLayerProps): JSX.Element | null { - if (geojson == null) return null - return ( - - - - - ) -} - -// Important! Pass these IDs to Mapbox GL so that onClick/onHover receives -// the active Geojson object -export const InteractiveLayerIDs = [layerStyle.id, closeupLayerStyle.id] as string[] diff --git a/src/components/search/XSearch.tsx b/src/components/search/XSearch.tsx index 3576999db..990364546 100644 --- a/src/components/search/XSearch.tsx +++ b/src/components/search/XSearch.tsx @@ -2,7 +2,7 @@ import { AutocompleteClassNames } from '@algolia/autocomplete-js' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' import { Autocomplete2 } from './Autocomplete2' -import { xsearchTypesense, searchPoi } from './sources' +import { xsearchTypesense } from './sources' import { AddNewButton } from './templates/ClimbResultXSearch' import { ReactNode } from 'react' interface XSearchProps { @@ -19,10 +19,7 @@ export default function XSearch ({ placeholder = 'Try "Cat In the Hat" or "Las V placeholder={placeholder} getSources={async ({ query }) => { 'use client' - const sources = await xsearchTypesense(query) - const poiSource = await searchPoi(query) - sources.push(poiSource) - return sources + return await xsearchTypesense(query) }} classNames={CUSTOM_CLASSES} resultContainer={ResultContainer} @@ -42,10 +39,7 @@ export function XSearchMinimal ({ placeholder = 'Try "Cat In the Hat" or "Las Ve } getSources={async ({ query }) => { - const sources = await xsearchTypesense(query) - const poiSource = await searchPoi(query) - sources.push(poiSource) - return sources + return await xsearchTypesense(query) }} classNames={{ ...CUSTOM_CLASSES, detachedSearchButton: 'aa-hidden-mobile-trigger-btn' }} resultContainer={ResultContainer} @@ -82,10 +76,7 @@ export const XSearchMobile = (): JSX.Element => { getSources={async ({ query }) => { 'use client' if (query?.trim() === '') return [] - const sources = await xsearchTypesense(query) - const poiSource = await searchPoi(query) - sources.push(poiSource) - return sources + return await xsearchTypesense(query) }} resultContainer={ResultContainer} /> diff --git a/src/components/search/sources/PoiSource.tsx b/src/components/search/sources/PoiSource.tsx index 36836c6e2..39dcf8867 100644 --- a/src/components/search/sources/PoiSource.tsx +++ b/src/components/search/sources/PoiSource.tsx @@ -18,6 +18,7 @@ interface PoiDoc extends BaseItem { * match 'query'. Wrap result in Algolia.Source object to allow Autocomplete component. * See also https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/sources/ * to render the result. + * @deprecated * @param query search string */ export const searchPoi = async (query: string): Promise> => { diff --git a/src/components/ui/SegmentedControl.tsx b/src/components/ui/SegmentedControl.tsx deleted file mode 100644 index 9d2f4f0fc..000000000 --- a/src/components/ui/SegmentedControl.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Tab } from '@headlessui/react' -import classname from 'classnames' -interface SegmentedControlProps { - labels: string[] - children: JSX.Element | JSX.Element[] -} -export default function SegmentedControl ({ labels, children }: SegmentedControlProps): JSX.Element { - return ( - - - {labels.map(label => )} - - - {children} - - - ) -} - -export interface SegmentProps { - children: any -} -const Segment = ({ children }: SegmentProps): JSX.Element => { - return ({children}) -} - -SegmentedControl.Segment = Segment - -const Label: React.FC = ({ label }) => ( - - {({ selected }) => ( - - {label} - - )} - ) diff --git a/src/js/hooks/finder/useCragFinder.ts b/src/js/hooks/finder/useCragFinder.ts deleted file mode 100644 index ab9c5221b..000000000 --- a/src/js/hooks/finder/useCragFinder.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect } from 'react' -import { NextRouter } from 'next/router' - -import { actions, cragFiltersStore } from '../../stores/index' - -/* eslint-disable-next-line */ -const useCragFinder = (router: NextRouter) => { - const { query } = router - - useEffect(() => { - try { - const lnglat = parseCenterStr(query.center as string) - const placeId = query?.placeId === undefined ? lnglat.join() : (query.placeId as string) - const fetch = async (): Promise => await actions.filters.validLnglat(query.shortName as string, placeId, lnglat) - void fetch() - // Todo: clear finder.errors - } catch (e) { - // Todo: set finder.errors - } - }, [query]) - return cragFiltersStore -} - -const parseCenterStr = (s: string): [number, number] => { - const [_lng, _lat] = s.split(',') - const lng = parseFloat(_lng) - const lat = parseFloat(_lat) - if (lat < -90 || lat > 90) { - const message = 'LngLat [lat] must be within -90 to 90 degrees' - throw new Error(message) - } else if (lng < -180 || lng > 180) { - const message = 'LngLat [lng] must be within -180 to 180 degrees' - throw new Error(message) - } - return [lng, lat] -} - -export default useCragFinder diff --git a/src/js/hooks/finder/useMapAutoSizing.ts b/src/js/hooks/finder/useMapAutoSizing.ts deleted file mode 100644 index a13dff8e1..000000000 --- a/src/js/hooks/finder/useMapAutoSizing.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useCallback, useState, Dispatch, SetStateAction } from 'react' -import { FeatureCollection } from '@turf/helpers' -import { bboxFromGeoJson, bbox2Viewport } from '../../../js/GeoHelpers' -import { DEFAULT_INITIAL_VIEWSTATE } from '../../../components/maps/BaseMap' -import { store } from '../../stores' -import { getNavBarOffset } from '../../../components/Header' - -import { XViewStateType } from '../../../js/types' -/** The return type for our responsive window logic. */ -type useAutoSizingReturn = readonly [ - XViewStateType, - number, - Dispatch>, -] - -interface UseAutoSizingProps { - geojson: FeatureCollection | null - elementId: string -} - -/** - * React hook for auto detecting and calculating div height - */ -export default function useAutoSizing ({ geojson, elementId }: UseAutoSizingProps): useAutoSizingReturn { - const navbarOffset = getNavBarOffset() - const [[width, height], setWH] = useState([DEFAULT_INITIAL_VIEWSTATE.width, DEFAULT_INITIAL_VIEWSTATE.height]) - const [viewState, setViewState] = useState(DEFAULT_INITIAL_VIEWSTATE) - - const isLoading = store.filters.isLoading() - useEffect(() => { - updateDimensions() - window.addEventListener('resize', updateDimensions) - - if (geojson != null && geojson.features.length > 0) { - // Calculate new viewState based on crag search result - const bbox = bboxFromGeoJson(geojson) - const vs = bbox2Viewport(bbox, width, height) - setViewState({ ...viewState, ...vs, ...bbox }) - } else { - setViewState(previous => ({ ...previous, width, height })) - } - return () => { - window.removeEventListener('resize', updateDimensions) - } - }, [geojson, isLoading, width, height]) - - const updateDimensions = useCallback(() => { - const { width, height } = getMapDivDimensions(elementId, navbarOffset) - setWH([width, height]) - }, [width, height]) - - return [viewState, height, setViewState] as const -} - -const getMapDivDimensions = (id: string, offset: number): { width: number, height: number } => { - const div = document.getElementById(id) - let width = 200 - if (div != null) { - width = div.clientWidth - } - const height = window.innerHeight - offset - return { width, height } -} diff --git a/src/pages/finder.tsx b/src/pages/finder.tsx deleted file mode 100644 index f8d9dc17c..000000000 --- a/src/pages/finder.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useRouter } from 'next/router' -// @ts-expect-error -import NProgress from 'nprogress/nprogress' - -import useCragFinder from '../js/hooks/finder/useCragFinder' -import Layout from '../components/layout' -import CragsMap from '../components/maps/CragsMap' -import MobileMainView from '../components/finder/MobileMainView' -import CragTable from '../components/finder/CragTable' -import Pagination from '../components/finder/Pagination' -import TwoColumnLayout from '../components/finder/TwoColumnLayout' -import useResponsive from '../js/hooks/useResponsive' -import { Preface } from '../components/finder/Preface' - -NProgress.configure({ showSpinner: false, easing: 'ease-in-out', speed: 1000 }) - -export default function Finder (): JSX.Element { - const { isTablet, isMobile } = useResponsive() - const cragFiltersStore = useCragFinder(useRouter()) - const { total, searchText, isLoading } = cragFiltersStore.useStore() - if (isLoading) { - NProgress.start() - } else { - NProgress.done() - } - return ( - - {isMobile || isTablet - ? - - - - - } - mapView={} - /> - : - - - - > - } - right={} - />} - - ) -} diff --git a/yarn.lock b/yarn.lock index 0d59a351e..8f8f97ad5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2173,31 +2173,6 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" -"@radix-ui/react-tabs@^1.0.1": - version "1.0.4" - resolved "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz" - integrity sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-direction" "1.0.1" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-roving-focus" "1.0.4" - "@radix-ui/react-use-controllable-state" "1.0.1" - -"@radix-ui/react-toggle@^1.0.1": - version "1.0.3" - resolved "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz" - integrity sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-use-controllable-state" "1.0.1" - "@radix-ui/react-use-callback-ref@1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz" @@ -2754,13 +2729,6 @@ resolved "https://registry.yarnpkg.com/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz#17cc131d14ceff59dcf14e5847bd971b96f2cbe0" integrity sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA== -"@types/leaflet@^1.9.8": - version "1.9.8" - resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.8.tgz#32162a8eaf305c63267e99470b9603b5883e63e8" - integrity sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg== - dependencies: - "@types/geojson" "*" - "@types/mapbox-gl@>=1.0.0": version "2.7.14" resolved "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.14.tgz" @@ -3389,22 +3357,6 @@ awesome-only-resolves-last-promise@^1.0.3: dependencies: awesome-imperative-promise "^1.0.1" -aws-sdk@^2.1265.0: - version "2.1462.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1462.0.tgz" - integrity sha512-gEcp/YWUp0zrM/LujI3cTLbOTK6XLwGSHWQII57jjRvjsIMacLomnIcd7fGKSfREAIHr5saexISRsnXhfI+Vgw== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.16.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - util "^0.12.4" - uuid "8.0.0" - xml2js "0.5.0" - axios@^0.21.1: version "0.21.4" resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" @@ -3528,7 +3480,7 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.0.2, base64-js@^1.3.0: +base64-js@^1.3.0: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -3595,15 +3547,6 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@4.9.2: - version "4.9.2" - resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - builtins@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz" @@ -4851,11 +4794,6 @@ eventemitter3@^4.0.1: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz" - integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== - execa@^5.0.0: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" @@ -4994,11 +4932,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fflate@^0.8.0: - version "0.8.2" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" - integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" @@ -5541,12 +5474,7 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -ieee754@^1.1.12, ieee754@^1.1.4: +ieee754@^1.1.12: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -5644,7 +5572,7 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -is-arguments@^1.0.4, is-arguments@^1.1.1: +is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -5748,7 +5676,7 @@ is-generator-fn@^2.0.0: resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-generator-function@^1.0.10, is-generator-function@^1.0.7: +is-generator-function@^1.0.10: version "1.0.10" resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz" integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== @@ -5852,7 +5780,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.3, is-typed-array@^1.1.9: +is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: version "1.1.12" resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz" integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== @@ -5896,11 +5824,6 @@ is-what@^4.1.8: resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isarray@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" @@ -6362,11 +6285,6 @@ jiti@^1.19.1: resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== -jmespath@0.16.0: - version "0.16.0" - resolved "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz" - integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== - jose@^4.11.4, jose@^4.14.4: version "4.14.6" resolved "https://registry.npmjs.org/jose/-/jose-4.14.6.tgz" @@ -7692,14 +7610,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pmtiles@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/pmtiles/-/pmtiles-3.0.3.tgz#a8b128ebaaf039c050622ff80803155365f54525" - integrity sha512-tj4l3HHJd6/qf9VefzlPK2eYEQgbf+4uXPzNlrj3k7hHvLtibYSxfp51TF6ALt4YezM8MCdiOminnHvdAyqyGg== - dependencies: - "@types/leaflet" "^1.9.8" - fflate "^0.8.0" - postcss-import@^14.1.0: version "14.1.0" resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz" @@ -7842,7 +7752,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7886,11 +7796,6 @@ psl@^1.1.33: resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" - integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== - punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" @@ -7908,11 +7813,6 @@ qs@^6.10.3, qs@^6.11.0: dependencies: side-channel "^1.0.4" -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - querystringify@^2.1.1: version "2.2.0" resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" @@ -8037,13 +7937,6 @@ react-markdown@^9.0.1: unist-util-visit "^5.0.0" vfile "^6.0.0" -react-paginate@^8.1.3: - version "8.2.0" - resolved "https://registry.npmjs.org/react-paginate/-/react-paginate-8.2.0.tgz" - integrity sha512-sJCz1PW+9PNIjUSn919nlcRVuleN2YPoFBOvL+6TPgrH/3lwphqiSOgdrLafLdyLDxsgK+oSgviqacF4hxsDIw== - dependencies: - prop-types "^15" - react-remove-scroll-bar@^2.3.3: version "2.3.4" resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz" @@ -8482,16 +8375,6 @@ safe-regex-test@^1.0.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" - integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== - -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - saxes@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz" @@ -9493,14 +9376,6 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -url@0.10.3: - version "0.10.3" - resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz" - integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== - dependencies: - punycode "1.3.2" - querystring "0.2.0" - use-callback-ref@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz" @@ -9536,22 +9411,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -util@^0.12.4: - version "0.12.5" - resolved "https://registry.npmjs.org/util/-/util-0.12.5.tgz" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - -uuid@8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== - uuid@9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" @@ -9721,7 +9580,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.1" is-weakset "^2.0.1" -which-typed-array@^1.1.11, which-typed-array@^1.1.2, which-typed-array@^1.1.9: +which-typed-array@^1.1.11, which-typed-array@^1.1.9: version "1.1.11" resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz" integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== @@ -9783,19 +9642,6 @@ xml-name-validator@^4.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== -xml2js@0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz" - integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz"