Skip to content

Commit

Permalink
SearchBox: Refactoring and searchable stars (#552)
Browse files Browse the repository at this point in the history
Co-authored-by: Pavel Zbytovský <zbytovsky@gmail.com>
  • Loading branch information
Dlurak and zbycz authored Sep 20, 2024
1 parent 3f6e8c3 commit e01f753
Show file tree
Hide file tree
Showing 22 changed files with 382 additions and 217 deletions.
33 changes: 5 additions & 28 deletions src/components/Directions/DirectionsAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,19 @@ import { useStarsContext } from '../utils/StarsContext';
import React, { useEffect, useRef, useState } from 'react';
import { abortFetch } from '../../services/fetch';
import {
buildPhotonAddress,
fetchGeocoderOptions,
GEOCODER_ABORTABLE_QUEUE,
useInputValueState,
} from '../SearchBox/options/geocoder';
import { getStarsOptions } from '../SearchBox/options/stars';
import styled from '@emotion/styled';
import {
Autocomplete,
InputAdornment,
InputBase,
TextField,
} from '@mui/material';
import { Autocomplete, InputAdornment, TextField } from '@mui/material';
import { useMapCenter } from '../SearchBox/utils';
import { useUserThemeContext } from '../../helpers/theme';
import { renderOptionFactory } from '../SearchBox/renderOptionFactory';
import PlaceIcon from '@mui/icons-material/Place';
import { SearchOption } from '../SearchBox/types';
import { useRouter } from 'next/router';
import { destroyRouting } from './routing/handleRouting';
import { Option, splitByFirstTilda } from './helpers';
import { LonLat } from '../../services/types';
import { Option } from '../SearchBox/types';
import { getOptionLabel } from '../SearchBox/getOptionLabel';

const StyledTextField = styled(TextField)`
input::placeholder {
Expand Down Expand Up @@ -78,32 +69,18 @@ const useOptions = (inputValue: string, setOptions) => {
abortFetch(GEOCODER_ABORTABLE_QUEUE);

if (inputValue === '') {
setOptions(getStarsOptions(stars));
setOptions(getStarsOptions(stars, inputValue));
return;
}

fetchGeocoderOptions(inputValue, view, setOptions, [], []);
await fetchGeocoderOptions(inputValue, view, setOptions, [], []);
})();
}, [inputValue, stars]); // eslint-disable-line react-hooks/exhaustive-deps
};
const Row = styled.div`
width: 100%;
`;

export const getOptionLabel = (option) =>
option == null
? ''
: option.properties?.name ||
(option.star && option.star.label) ||
(option.properties && buildPhotonAddress(option.properties)) ||
'';

export const getOptionToLonLat = (option) => {
const lonLat =
(option.star && option.star.center) || option.geometry.coordinates;
return lonLat as LonLat;
};

type Props = {
label: string;
value: Option;
Expand Down
18 changes: 6 additions & 12 deletions src/components/Directions/DirectionsBox.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import styled from '@emotion/styled';
import {
DirectionsAutocomplete,
getOptionToLonLat,
} from './DirectionsAutocomplete';
import { DirectionsAutocomplete } from './DirectionsAutocomplete';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Stack } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
Expand All @@ -17,19 +14,16 @@ import {
import { getLabel } from '../../helpers/featureLabel';
import { getLastFeature } from '../../services/lastFeatureStorage';
import { Result, StyledPaper } from './Result';
import {
buildUrl,
CloseButton,
getStarOption,
Option,
parseUrlParts,
} from './helpers';
import { buildUrl, CloseButton, parseUrlParts } from './helpers';
import { PointsTooFarError, Profile, RoutingResult } from './routing/types';
import { useBoolState, useMobileMode } from '../helpers';
import { LoadingButton } from '@mui/lab';
import { type Severity, useSnackbar } from '../utils/SnackbarContext';
import { FetchError } from '../../services/helpers';
import * as Sentry from '@sentry/nextjs';
import { Option } from '../SearchBox/types';
import { getCoordsOption } from '../SearchBox/options/coords';
import { getOptionToLonLat } from '../SearchBox/getOptionToLonLat';

const Wrapper = styled(Stack)`
position: absolute;
Expand Down Expand Up @@ -86,7 +80,7 @@ const useReactToUrl = (

const lastFeature = getLastFeature();
if (lastFeature) {
setTo(getStarOption(lastFeature.center, getLabel(lastFeature)));
setTo(getCoordsOption(lastFeature.center, getLabel(lastFeature)));
}
}

Expand Down
16 changes: 5 additions & 11 deletions src/components/Directions/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { LonLat } from '../../services/types';
import { encodeUrl } from '../../helpers/utils';
import { getOptionLabel, getOptionToLonLat } from './DirectionsAutocomplete';
import Router from 'next/router';
import { IconButton } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import React from 'react';
import { Option } from '../SearchBox/types';
import { getCoordsOption } from '../SearchBox/options/coords';
import { getOptionToLonLat } from '../SearchBox/getOptionToLonLat';
import { getOptionLabel } from '../SearchBox/getOptionLabel';

export const splitByFirstTilda = (str: string) => {
if (!str) {
Expand All @@ -17,15 +20,6 @@ export const splitByFirstTilda = (str: string) => {
return [str.slice(0, index), str.slice(index + 1)];
};

export type Option = Record<string, any>; // TODO once we have types in SearchBox

export const getStarOption = (center: LonLat, label?: string): Option => ({
star: {
center,
label: label || center,
},
});

const getOptionToUrl = (point: Option) => {
const lonLat = getOptionToLonLat(point);
return `${lonLat.join(',')}~${getOptionLabel(point)}`;
Expand All @@ -42,7 +36,7 @@ const urlCoordsToLonLat = (coords: string): LonLat =>
export const parseUrlParts = (urlParts: string[]): Option[] =>
urlParts.map((urlPart) => {
const [coords, label] = splitByFirstTilda(urlPart);
return getStarOption(urlCoordsToLonLat(coords), label);
return getCoordsOption(urlCoordsToLonLat(coords), label);
});

const close = () => {
Expand Down
6 changes: 3 additions & 3 deletions src/components/Map/behaviour/PersistedScaleControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { isImperial, toggleImperial } from '../../helpers';

// https://github.com/maplibre/maplibre-gl-js/blob/afe4377706429a6b4e708e62a3c39a795ae8f28e/src/ui/control/scale_control.js#L36-L83

class ClickableScaleControl extends (ScaleControl as any) {
private onClick;
class ClickableScaleControl extends ScaleControl {
private onClick: () => void;

private getHoverText;
private getHoverText: () => string;

constructor({ onClick, getHoverText, ...options }) {
super(options);
Expand Down
66 changes: 28 additions & 38 deletions src/components/SearchBox/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,55 @@ import { onSelectedFactory } from './onSelectedFactory';
import { useUserThemeContext } from '../../helpers/theme';
import { useMapStateContext } from '../utils/MapStateContext';
import { onHighlightFactory } from './onHighlightFactory';
import { buildPhotonAddress } from './options/geocoder';
import { useMapCenter } from './utils';
import { useSnackbar } from '../utils/SnackbarContext';
import { useKeyDown } from '../../helpers/hooks';
import { Option } from './types';
import { getOptionLabel } from './getOptionLabel';

const useFocusOnSlash = () => {
const SearchBoxInput = ({ params, setInputValue, autocompleteRef }) => {
const inputRef = React.useRef<HTMLInputElement>(null);

useEffect(() => {
const onKeydown = (e) => {
if (e.key === '/') {
e.preventDefault();
inputRef.current?.focus();
}
};
window.addEventListener('keydown', onKeydown);

return () => {
window.removeEventListener('keydown', onKeydown);
};
}, []);

return inputRef;
};
useKeyDown('/', (e) => {
const isInput = e.target instanceof HTMLInputElement;
const isTextarea = e.target instanceof HTMLTextAreaElement;
if (isInput || isTextarea) {
return;
}
e.preventDefault();
inputRef.current?.focus();
});

const SearchBoxInput = ({ params, setInputValue, autocompleteRef }) => {
// const inputRef = useFocusOnSlash();
const { InputLabelProps, InputProps, ...restParams } = params;

useEffect(() => {
// @ts-ignore
params.InputProps.ref(autocompleteRef.current);
}, []); // eslint-disable-line react-hooks/exhaustive-deps

const onChange = (e) => setInputValue(e.target.value);
const onFocus = (e) => e.target.select();

return (
<InputBase
{...restParams} // eslint-disable-line react/jsx-props-no-spreading
sx={{
height: '47px',
}}
// inputRef={inputRef}
inputRef={inputRef}
placeholder={t('searchbox.placeholder')}
onChange={onChange}
onFocus={onFocus}
onChange={({ target }) => setInputValue(target.value)}
onFocus={({ target }) => target.select()}
/>
);
};

export const AutocompleteInput = ({
type AutocompleteInputProps = {
inputValue: string;
setInputValue: (value: string) => void;
options: Option[];
autocompleteRef: React.MutableRefObject<undefined>;
setOverpassLoading: React.Dispatch<React.SetStateAction<boolean>>;
};

export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
inputValue,
setInputValue,
options,
Expand All @@ -75,16 +73,8 @@ export const AutocompleteInput = ({
options={options}
// we need null to be able to select the same again (eg. category search)
value={null}
filterOptions={(x) => x}
getOptionLabel={(option) =>
option.properties?.name ||
option.preset?.presetForSearch?.name ||
option.overpass?.inputValue ||
(option.star && option.star.label) ||
(option.loader && '') ||
(option.properties && buildPhotonAddress(option.properties)) ||
''
}
filterOptions={(o) => o}
getOptionLabel={getOptionLabel}
getOptionKey={(option) => JSON.stringify(option)}
onChange={onSelectedFactory(
setFeature,
Expand All @@ -94,7 +84,7 @@ export const AutocompleteInput = ({
setOverpassLoading,
)}
onHighlightChange={onHighlightFactory(setPreview)}
getOptionDisabled={(o) => o.loader}
getOptionDisabled={(o) => o.type === 'loader'}
autoComplete
disableClearable
autoHighlight
Expand Down
7 changes: 3 additions & 4 deletions src/components/SearchBox/SearchBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { useFeatureContext } from '../utils/FeatureContext';
import { AutocompleteInput } from './AutocompleteInput';
import { t } from '../../services/intl';
import { ClosePanelButton } from '../utils/ClosePanelButton';
import { useMobileMode, useToggleState } from '../helpers';
import { useMobileMode } from '../helpers';
import { useInputValueState } from './options/geocoder';
import { useOptions } from './useOptions';
import { useGetOptions } from './useGetOptions';
import { HamburgerMenu } from '../Map/TopMenu/HamburgerMenu';
import { UserMenu } from '../Map/TopMenu/UserMenu';
import { DirectionsButton } from '../Directions/DirectionsButton';
Expand Down Expand Up @@ -44,12 +44,11 @@ const SearchBox = () => {
const isMobileMode = useMobileMode();
const { featureShown } = useFeatureContext();
const { inputValue, setInputValue } = useInputValueState();
const [options, setOptions] = useState([]);
const [overpassLoading, setOverpassLoading] = useState(false);
const autocompleteRef = useRef();
const onClosePanel = useOnClosePanel();

useOptions(inputValue, setOptions);
const options = useGetOptions(inputValue);

return (
<TopPanel $isMobileMode={isMobileMode}>
Expand Down
21 changes: 21 additions & 0 deletions src/components/SearchBox/getOptionLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Option } from './types';
import { buildPhotonAddress } from './options/geocoder';

export const getOptionLabel = (option: Option | undefined) => {
if (option == null) {
return '';
}

return (
(option.type === 'geocoder' && option.geocoder.properties?.name) ||
(option.type === 'preset' && option.preset?.presetForSearch?.name) ||
(option.type === 'overpass' && option.overpass?.inputValue) ||
(option.type === 'star' && option.star.label) ||
(option.type === 'coords' && option.coords.label) ||
(option.type === 'loader' && '') ||
(option.type === 'geocoder' &&
option.geocoder.properties &&
buildPhotonAddress(option.geocoder.properties)) ||
''
);
};
17 changes: 17 additions & 0 deletions src/components/SearchBox/getOptionToLonLat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Option } from './types';

export const getOptionToLonLat = (option: Option) => {
if (option.type === 'coords') {
return option.coords.center;
}

if (option.type === 'star') {
return option.star.center;
}

if (option.type === 'geocoder') {
return option.geocoder.geometry.coordinates;
}

throw new Error(`Unsupported option type: ${option.type}`);
};
36 changes: 19 additions & 17 deletions src/components/SearchBox/onHighlightFactory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Feature } from '../../services/types';
import { GeocoderOption, Option } from './types';

const getElementType = (osmType) => {
const getElementType = (osmType: string) => {
switch (osmType) {
case 'R':
return 'relation';
Expand All @@ -13,31 +14,32 @@ const getElementType = (osmType) => {
}
};

export const getSkeleton = (option): Feature => {
const center = option.geometry.coordinates;
const { osm_id: id, osm_type: osmType, name } = option.properties;
export const getSkeleton = ({ geocoder }: GeocoderOption): Feature => {
const center = geocoder.geometry.coordinates;
const { osm_id: id, osm_type: osmType, name } = geocoder.properties;
const type = getElementType(osmType);
const [lon, lat] = center;

return {
center,
type: 'Feature',
skeleton: true,
nonOsmObject: false,
osmMeta: { type, id: parseInt(id, 10) },
center: [parseFloat(lon), parseFloat(lat)],
tags: { name },
properties: { class: option.class, subclass: '' },
properties: { class: geocoder.properties.class, subclass: '' },
};
};

export const onHighlightFactory = (setPreview) => (e, option) => {
if (option?.star?.center) {
const { center } = option.star;
setPreview({ center });
return;
}
export const onHighlightFactory =
(setPreview: (feature: unknown) => void) => (_: never, option: Option) => {
if (!option) return;
if (option.type === 'star' && option.star.center) {
const { center } = option.star;
setPreview({ center });
return;
}

if (option?.geometry?.coordinates) {
setPreview(getSkeleton(option));
}
};
if (option.type === 'geocoder' && option.geocoder.geometry?.coordinates) {
setPreview(getSkeleton(option));
}
};
Loading

0 comments on commit e01f753

Please sign in to comment.