From b20a3445d184d855f1bb8b5d55b127c5d125d4ba Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Mon, 20 Jan 2020 16:42:46 +0700 Subject: [PATCH] nominatim search (without working dialog) --- package-lock.json | 101 +++++++++++++++++- package.json | 2 +- src/components/dialogs/DialogLoader.tsx | 16 +++ src/components/dialogs/MapListDialog.tsx | 7 +- src/components/dialogs/NominatimDialog.tsx | 46 ++++++++ .../dialogs/NominatimSearchPanel.tsx | 37 +++++++ .../nominatim/NominatimListItem.tsx | 21 ++++ src/components/panels/EditorPanel.tsx | 10 +- src/components/panels/UserPanel.tsx | 10 +- src/constants/dialogs.ts | 17 +-- src/containers/App.tsx | 5 +- src/containers/LeftDialog.tsx | 6 +- src/map/Map/index.tsx | 8 +- src/redux/editor/actions.ts | 10 ++ src/redux/editor/constants.ts | 2 + src/redux/editor/handlers.ts | 12 +++ src/redux/editor/index.ts | 21 +++- src/redux/editor/sagas.ts | 44 +++++++- src/redux/editor/selectors.ts | 1 + src/redux/map/sagas.ts | 2 +- src/redux/types.ts | 7 ++ src/redux/user/sagas.ts | 10 +- src/sprites/icon.svg | 7 ++ src/styles/dialogs.less | 62 +++++++++-- src/styles/panel.less | 14 +++ src/utils/api.ts | 31 ++++++ src/utils/window.ts | 2 - 27 files changed, 450 insertions(+), 61 deletions(-) create mode 100644 src/components/dialogs/DialogLoader.tsx create mode 100644 src/components/dialogs/NominatimDialog.tsx create mode 100644 src/components/dialogs/NominatimSearchPanel.tsx create mode 100644 src/components/nominatim/NominatimListItem.tsx diff --git a/package-lock.json b/package-lock.json index 5a6cb34..4639a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -775,6 +775,77 @@ "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-0.2.0.tgz", "integrity": "sha1-biWYB0SqIjMflLZFpULALT/P7pc=" }, + "@redux-saga/core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", + "integrity": "sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==", + "requires": { + "@babel/runtime": "7.8.3", + "@redux-saga/deferred": "1.1.2", + "@redux-saga/delay-p": "1.1.2", + "@redux-saga/is": "1.1.2", + "@redux-saga/symbols": "1.1.2", + "@redux-saga/types": "1.1.0", + "redux": "4.0.5", + "typescript-tuple": "2.2.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", + "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", + "requires": { + "regenerator-runtime": "0.13.3" + } + }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "1.4.0", + "symbol-observable": "1.2.0" + } + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + } + } + }, + "@redux-saga/deferred": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.1.2.tgz", + "integrity": "sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==" + }, + "@redux-saga/delay-p": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.1.2.tgz", + "integrity": "sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==", + "requires": { + "@redux-saga/symbols": "1.1.2" + } + }, + "@redux-saga/is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.2.tgz", + "integrity": "sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==", + "requires": { + "@redux-saga/symbols": "1.1.2", + "@redux-saga/types": "1.1.0" + } + }, + "@redux-saga/symbols": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.2.tgz", + "integrity": "sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==" + }, + "@redux-saga/types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz", + "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==" + }, "@types/classnames": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.7.tgz", @@ -11242,9 +11313,12 @@ "integrity": "sha512-sSJAzNq7zka3qVHKce1hbvqf0Vf5DuTVm7dr4GtsqQVOexnrvbV47RWFiPxQ8fscnyiuWyD2O92DOxPl0tGCRg==" }, "redux-saga": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-0.16.2.tgz", - "integrity": "sha512-iIjKnRThI5sKPEASpUvySemjzwqwI13e3qP7oLub+FycCRDysLSAOwt958niZW6LhxfmS6Qm1BzbU70w/Koc4w==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz", + "integrity": "sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==", + "requires": { + "@redux-saga/core": "1.1.3" + } }, "reduxsauce": { "version": "1.0.1", @@ -13112,6 +13186,27 @@ "integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==", "dev": true }, + "typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "requires": { + "typescript-logic": "0.0.0" + } + }, + "typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "requires": { + "typescript-compare": "0.0.2" + } + }, "uglifyjs-webpack-plugin": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", diff --git a/package.json b/package.json index 5c86c11..6fbb42e 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "reactrangeslider": "^3.0.6", "redux": "^4.0.1", "redux-persist": "^5.10.0", - "redux-saga": "^0.16.2", + "redux-saga": "^1.0.0", "reduxsauce": "^1.0.0", "scrypt": "^6.0.3", "throttle-debounce": "^2.1.0", diff --git a/src/components/dialogs/DialogLoader.tsx b/src/components/dialogs/DialogLoader.tsx new file mode 100644 index 0000000..4dfc069 --- /dev/null +++ b/src/components/dialogs/DialogLoader.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import { Icon } from '~/components/panels/Icon'; + +interface IProps {} + +const DialogLoader: FC = ({}) => { + return ( +
+
+ +
+
+ ); +}; + +export { DialogLoader }; diff --git a/src/components/dialogs/MapListDialog.tsx b/src/components/dialogs/MapListDialog.tsx index 7bca6a3..557f10f 100644 --- a/src/components/dialogs/MapListDialog.tsx +++ b/src/components/dialogs/MapListDialog.tsx @@ -24,6 +24,7 @@ import { IRouteListItem } from '~/redux/user'; import { ROLES } from '~/constants/auth'; import { IState } from '~/redux/store'; import { MapListDialogHead } from '~/components/search/MapListDialogHead'; +import { DialogLoader } from '~/components/dialogs/DialogLoader'; const mapStateToProps = ({ editor: { editing }, @@ -165,11 +166,7 @@ class MapListDialogUnconnected extends PureComponent { return (
{list.length === 0 && loading && ( -
-
- -
-
+ )} {ready && !loading && list.length === 0 && ( diff --git a/src/components/dialogs/NominatimDialog.tsx b/src/components/dialogs/NominatimDialog.tsx new file mode 100644 index 0000000..da22cc0 --- /dev/null +++ b/src/components/dialogs/NominatimDialog.tsx @@ -0,0 +1,46 @@ +import React, { FC, Fragment, useCallback } from 'react'; +import { connect } from 'react-redux'; +import { IState } from '~/redux/store'; +import { selectEditorNominatim } from '~/redux/editor/selectors'; +import { DialogLoader } from './DialogLoader'; +import { NominatimListItem } from '~/components/nominatim/NominatimListItem'; +import { MainMap } from '~/constants/map'; +import { Scroll } from '../Scroll'; + +const mapStateToProps = (state: IState) => ({ + nominatim: selectEditorNominatim(state), +}); + +type Props = ReturnType & {}; + +const NominatimDialogUnconnected: FC = ({ nominatim: { loading, list } }) => { + const onItemClick = useCallback( + (index: number) => { + if (!list[index]) return; + + MainMap.setView(list[index].latlng, 17); + }, + [MainMap, list] + ); + + return ( + +
+ +
+ +
+ {loading && } + {list.map((item, i) => ( + + ))} +
+
+
+ + ); +}; + +const NominatimDialog = connect(mapStateToProps)(NominatimDialogUnconnected); + +export { NominatimDialog }; diff --git a/src/components/dialogs/NominatimSearchPanel.tsx b/src/components/dialogs/NominatimSearchPanel.tsx new file mode 100644 index 0000000..433b613 --- /dev/null +++ b/src/components/dialogs/NominatimSearchPanel.tsx @@ -0,0 +1,37 @@ +import React, { FC, useCallback, useState } from 'react'; +import { Icon } from '../panels/Icon'; + +interface IProps { + active: boolean; + onSearch: (search: string) => void; +} + +const NominatimSearchPanel: FC = ({ active, onSearch }) => { + const [search, setSearch] = useState('Колывань'); + + const setValue = useCallback(({ target: { value } }) => setSearch(value), [setSearch]); + + const onSubmit = useCallback(event => { + event.preventDefault(); + + if (search.length < 3) return; + + onSearch(search); + }, [search, onSearch]); + + return ( +
+
+
+ +
+ + +
+
+ ); +}; + +export { NominatimSearchPanel }; diff --git a/src/components/nominatim/NominatimListItem.tsx b/src/components/nominatim/NominatimListItem.tsx new file mode 100644 index 0000000..8c097cb --- /dev/null +++ b/src/components/nominatim/NominatimListItem.tsx @@ -0,0 +1,21 @@ +import React, { FC, useCallback } from 'react'; +import { INominatimResult } from '~/redux/types'; +import { MainMap } from '~/constants/map'; + +interface IProps { + item: INominatimResult; +} + +const NominatimListItem: FC = ({ item }) => { + const onClick = useCallback(() => { + MainMap.panTo(item.latlng); + }, [MainMap]); + + return ( +
+
{item.title}
+
+ ); +}; + +export { NominatimListItem }; diff --git a/src/components/panels/EditorPanel.tsx b/src/components/panels/EditorPanel.tsx index 54382b0..8bb8450 100644 --- a/src/components/panels/EditorPanel.tsx +++ b/src/components/panels/EditorPanel.tsx @@ -32,7 +32,7 @@ type Props = ReturnType & typeof mapDispatchToProps & {} class EditorPanelUnconnected extends PureComponent { componentDidMount() { - window.addEventListener('keydown', this.props.editorKeyPressed as any); + window.addEventListener('keydown', this.onKeyPress as any); const obj = document.getElementById('control-dialog'); const { width } = this.panel.getBoundingClientRect(); @@ -45,9 +45,15 @@ class EditorPanelUnconnected extends PureComponent { panel: HTMLElement = null; componentWillUnmount() { - window.removeEventListener('keydown', this.props.editorKeyPressed as any); + window.removeEventListener('keydown', this.onKeyPress as any); } + onKeyPress = event => { + if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') return; + + this.props.editorKeyPressed(event); + }; + startPolyMode = () => this.props.editorSetMode(MODES.POLY); startStickerMode = () => this.props.editorSetMode(MODES.STICKERS_SELECT); startRouterMode = () => this.props.editorSetMode(MODES.ROUTER); diff --git a/src/components/panels/UserPanel.tsx b/src/components/panels/UserPanel.tsx index 112e0f4..8d6765f 100644 --- a/src/components/panels/UserPanel.tsx +++ b/src/components/panels/UserPanel.tsx @@ -10,6 +10,7 @@ import { editorSetDialog, editorSetDialogActive, editorGetGPXTrack, + editorSearchNominatim, } from '~/redux/editor/actions'; import { connect } from 'react-redux'; import { Icon } from '~/components/panels/Icon'; @@ -19,11 +20,12 @@ import { CLIENT } from '~/config/frontend'; import { DIALOGS, TABS } from '~/constants/dialogs'; import { Tooltip } from '~/components/panels/Tooltip'; import { TitleDialog } from '~/components/dialogs/TitleDialog'; +import { NominatimSearchPanel } from '~/components/dialogs/NominatimSearchPanel'; import { IState } from '~/redux/store'; const mapStateToProps = ({ user: { user }, - editor: { dialog, dialog_active }, + editor: { dialog, dialog_active, features }, map: { route, stickers }, }: IState) => ({ dialog, @@ -31,6 +33,7 @@ const mapStateToProps = ({ user, route, stickers, + features, }); const mapDispatchToProps = { @@ -42,6 +45,7 @@ const mapDispatchToProps = { editorSetDialogActive, openMapDialog, editorGetGPXTrack, + editorSearchNominatim, }; type Props = ReturnType & typeof mapDispatchToProps & {}; @@ -90,6 +94,7 @@ export class UserPanelUnconnected extends PureComponent { } setMenuOpened = () => this.setState({ menuOpened: !this.state.menuOpened }); + openMapsDialog = () => { this.props.openMapDialog(TABS.MY); }; @@ -115,7 +120,7 @@ export class UserPanelUnconnected extends PureComponent { render() { const { - props: { user, dialog, dialog_active, route, stickers }, + props: { user, dialog, dialog_active, route, stickers, features }, state: { menuOpened }, } = this; @@ -124,6 +129,7 @@ export class UserPanelUnconnected extends PureComponent { return (
+
diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 1291b6c..6a71f58 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -1,22 +1,11 @@ -export interface IDialogs { - NONE: string, - MAP_LIST: string, - APP_INFO: string, -} - -export interface IMapTabs { - MY: string, - PENDING: string, - STARRED: string, -} - -export const DIALOGS: IDialogs = ({ +export const DIALOGS = ({ NONE: 'NONE', MAP_LIST: 'MAP_LIST', APP_INFO: 'APP_INFO', + NOMINATIM: 'NOMINATIM', }); -export const TABS: IMapTabs = { +export const TABS = { MY: 'my', PENDING: 'pending', STARRED: 'starred', diff --git a/src/containers/App.tsx b/src/containers/App.tsx index e109b32..972c09e 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -4,7 +4,6 @@ import { EditorPanel } from '~/components/panels/EditorPanel'; import { Fills } from '~/components/Fills'; import { UserPanel } from '~/components/panels/UserPanel'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { hot } from 'react-hot-loader'; import { Renderer } from '~/components/renderer/Renderer'; @@ -14,7 +13,7 @@ import { TopLeftPanel } from '~/components/panels/TopLeftPanel'; import { TopRightPanel } from '~/components/panels/TopRightPanel'; import { LogoPreview } from '~/components/logo/LogoPreview'; import { IStickerPack } from '~/constants/stickers'; -import { IDialogs } from '~/constants/dialogs'; +import { DIALOGS } from '~/constants/dialogs'; import { Map } from '~/map/Map'; import { IEditorState } from '~/redux/editor'; @@ -25,7 +24,7 @@ type Props = { renderer_active: boolean; mode: IEditorState['mode']; - dialog: keyof IDialogs; + dialog: keyof typeof DIALOGS; dialog_active: boolean; set: keyof IStickerPack; editorHideRenderer: typeof editorHideRenderer; diff --git a/src/containers/LeftDialog.tsx b/src/containers/LeftDialog.tsx index 4455738..f05fb3c 100644 --- a/src/containers/LeftDialog.tsx +++ b/src/containers/LeftDialog.tsx @@ -1,13 +1,14 @@ import React, { createElement, FC, memo } from 'react'; -import { DIALOGS, IDialogs } from '~/constants/dialogs'; +import { DIALOGS } from '~/constants/dialogs'; import classnames from 'classnames'; import { AppInfoDialog } from '~/components/dialogs/AppInfoDialog'; import { Icon } from '~/components/panels/Icon'; import { MapListDialog } from '~/components/dialogs/MapListDialog'; +import { NominatimDialog } from '~/components/dialogs/NominatimDialog'; import * as EDITOR_ACTIONS from '~/redux/editor/actions'; interface Props { - dialog: keyof IDialogs; + dialog: keyof typeof DIALOGS; dialog_active: Boolean; editorSetDialogActive: typeof EDITOR_ACTIONS.editorSetDialogActive; } @@ -15,6 +16,7 @@ interface Props { const LEFT_DIALOGS = { [DIALOGS.MAP_LIST]: MapListDialog, [DIALOGS.APP_INFO]: AppInfoDialog, + [DIALOGS.NOMINATIM]: NominatimDialog, }; const LeftDialog: FC = memo(({ dialog, dialog_active, editorSetDialogActive }) => ( diff --git a/src/map/Map/index.tsx b/src/map/Map/index.tsx index 95f3752..b93afec 100644 --- a/src/map/Map/index.tsx +++ b/src/map/Map/index.tsx @@ -51,8 +51,12 @@ const MapUnconnected: React.FC = ({ }) => { const onClick = React.useCallback( event => { - if (!MainMap.clickable || mode === MODES.NONE) return; - + if ( + !MainMap.clickable || + mode === MODES.NONE + ) + return; + mapClicked(event.latlng); }, [mapClicked, mode] diff --git a/src/redux/editor/actions.ts b/src/redux/editor/actions.ts index b8f5b67..e68f199 100644 --- a/src/redux/editor/actions.ts +++ b/src/redux/editor/actions.ts @@ -112,3 +112,13 @@ export const editorSetRouter = (router: Partial) => ({ type: EDITOR_ACTIONS.SET_ROUTER, router, }); + +export const editorSetNominatim = (nominatim: Partial) => ({ + type: EDITOR_ACTIONS.SET_NOMINATIM, + nominatim, +}) + +export const editorSearchNominatim = (search: IEditorState['nominatim']['search']) => ({ + type: EDITOR_ACTIONS.SEARCH_NOMINATIM, + search, +}) \ No newline at end of file diff --git a/src/redux/editor/constants.ts b/src/redux/editor/constants.ts index d28a407..fb031f1 100644 --- a/src/redux/editor/constants.ts +++ b/src/redux/editor/constants.ts @@ -44,4 +44,6 @@ export const EDITOR_ACTIONS = { KEY_PRESSED: `${P}-KEY_PRESSED`, SET_ROUTER: `${P}-SET_ROUTER`, + SET_NOMINATIM: `${P}-SET_NOMINATIM`, + SEARCH_NOMINATIM: `${P}-SEARCH_NOMINATIM`, }; diff --git a/src/redux/editor/handlers.ts b/src/redux/editor/handlers.ts index 1266457..c2d33e8 100644 --- a/src/redux/editor/handlers.ts +++ b/src/redux/editor/handlers.ts @@ -157,6 +157,17 @@ const setRouter = ( }, }); +const setNominatim = ( + state, + { nominatim }: ReturnType +): IEditorState => ({ + ...state, + nominatim: { + ...state.nominatim, + ...nominatim, + }, +}); + export const EDITOR_HANDLERS = { [EDITOR_ACTIONS.SET_EDITING]: setEditing, [EDITOR_ACTIONS.SET_CHANGED]: setChanged, @@ -184,4 +195,5 @@ export const EDITOR_HANDLERS = { [EDITOR_ACTIONS.SET_FEATURE]: setFeature, [EDITOR_ACTIONS.SET_IS_ROUTING]: setIsRouting, [EDITOR_ACTIONS.SET_ROUTER]: setRouter, + [EDITOR_ACTIONS.SET_NOMINATIM]: setNominatim, }; diff --git a/src/redux/editor/index.ts b/src/redux/editor/index.ts index 6fca121..7d7c958 100644 --- a/src/redux/editor/index.ts +++ b/src/redux/editor/index.ts @@ -1,8 +1,9 @@ import { createReducer } from '~/utils/reducer'; -import { IDialogs } from '~/constants/dialogs'; +import { DIALOGS } from '~/constants/dialogs'; import { MODES } from '~/constants/modes'; import { EDITOR_HANDLERS } from './handlers'; import { ILatLng } from '../map/types'; +import { INominatimResult } from '~/redux/types'; export interface IEditorState { changed: boolean; @@ -11,13 +12,13 @@ export interface IEditorState { markers_shown: boolean; router: { - points: ILatLng[]; + points: ILatLng[]; waypoints: ILatLng[]; }; mode: typeof MODES[keyof typeof MODES]; - dialog: IDialogs[keyof IDialogs]; + dialog: typeof DIALOGS[keyof typeof DIALOGS]; dialog_active: boolean; routerPoints: number; @@ -31,6 +32,13 @@ export interface IEditorState { features: { routing: boolean; + nominatim: boolean; + }; + + nominatim: { + search: string; + loading: boolean; + list: INominatimResult[]; }; renderer: { @@ -76,6 +84,13 @@ export const EDITOR_INITIAL_STATE = { features: { routing: false, + nominatim: false, + }, + + nominatim: { + search: '', + loading: false, + list: [], }, renderer: { diff --git a/src/redux/editor/sagas.ts b/src/redux/editor/sagas.ts index d636770..f613dc1 100644 --- a/src/redux/editor/sagas.ts +++ b/src/redux/editor/sagas.ts @@ -1,5 +1,13 @@ -import { call, put, takeEvery, takeLatest, select, race } from 'redux-saga/effects'; -import { delay, SagaIterator } from 'redux-saga'; +import { + call, + put, + takeEvery, + takeLatest, + select, + race, + takeLeading, + delay, +} from 'redux-saga/effects'; import { selectEditor, selectEditorMode } from '~/redux/editor/selectors'; import { simplify } from '~/utils/simplify'; import { @@ -14,13 +22,16 @@ import { editorLocationChanged, editorKeyPressed, editorSetSave, + editorSearchNominatim, + editorSetDialog, + editorSetNominatim, } from '~/redux/editor/actions'; import { getUrlData, pushPath } from '~/utils/history'; import { MODES } from '~/constants/modes'; -import { checkOSRMService } from '~/utils/api'; +import { checkOSRMService, checkNominatimService, searchNominatim } from '~/utils/api'; import { LatLng } from 'leaflet'; import { searchSetTab } from '../user/actions'; -import { TABS } from '~/constants/dialogs'; +import { TABS, DIALOGS } from '~/constants/dialogs'; import { EDITOR_ACTIONS } from './constants'; import { getGPXString, downloadGPXTrack } from '~/utils/gpx'; import { @@ -76,11 +87,17 @@ function* checkOSRMServiceSaga() { yield put(editorSetFeature({ routing })); } +function* checkNominatimSaga() { + const nominatim = yield call(checkNominatimService); + yield put(editorSetFeature({ nominatim })); +} + export function* setReadySaga() { yield put(editorSetReady(true)); hideLoader(); yield call(checkOSRMServiceSaga); + yield call(checkNominatimSaga); yield put(searchSetTab(TABS.MY)); } @@ -217,7 +234,7 @@ function* keyPressedSaga({ key, target }: ReturnType) { } } -function* getGPXTrackSaga(): SagaIterator { +function* getGPXTrackSaga() { const { route, stickers, title, address }: ReturnType = yield select(selectMap); if (!route.length && !stickers.length) return; @@ -259,6 +276,22 @@ function* cancelSave() { ); } +function* searchNominatimSaga({ search }: ReturnType) { + const { dialog, dialog_active }: ReturnType = yield select(selectEditor); + + if (dialog !== DIALOGS.NOMINATIM || !dialog_active) { + yield put(editorSetDialog(DIALOGS.NOMINATIM)); + yield put(editorSetDialogActive(true)); + } + + yield put(editorSetNominatim({ loading: true, search })); + const list = yield call(searchNominatim, search); + yield put(editorSetNominatim({ list })); + + yield delay(1000); // safely wait for 1s to prevent from ddosing nominatim + yield put(editorSetNominatim({ loading: false })); +} + export function* editorSaga() { yield takeEvery(EDITOR_ACTIONS.LOCATION_CHANGED, locationChangeSaga); @@ -271,4 +304,5 @@ export function* editorSaga() { yield takeLatest(MAP_ACTIONS.MAP_CLICKED, mapClick); yield takeLatest(EDITOR_ACTIONS.ROUTER_SUBMIT, routerSubmit); yield takeLatest(EDITOR_ACTIONS.CANCEL_SAVE, cancelSave); + yield takeLeading(EDITOR_ACTIONS.SEARCH_NOMINATIM, searchNominatimSaga); } diff --git a/src/redux/editor/selectors.ts b/src/redux/editor/selectors.ts index 3587f3f..c4ce029 100644 --- a/src/redux/editor/selectors.ts +++ b/src/redux/editor/selectors.ts @@ -8,3 +8,4 @@ export const selectEditorActiveSticker = (state: IState) => state.editor.activeS export const selectEditorRenderer = (state: IState) => state.editor.renderer; export const selectEditorRouter = (state: IState) => state.editor.router; export const selectEditorDistance = (state: IState) => state.editor.distance; +export const selectEditorNominatim = (state: IState) => state.editor.nominatim; diff --git a/src/redux/map/sagas.ts b/src/redux/map/sagas.ts index 8deecd2..e323c79 100644 --- a/src/redux/map/sagas.ts +++ b/src/redux/map/sagas.ts @@ -7,6 +7,7 @@ import { race, take, takeLatest, + delay, } from 'redux-saga/effects'; import { MAP_ACTIONS } from './constants'; import { @@ -36,7 +37,6 @@ import { getStoredMap, postMap } from '~/utils/api'; import { Unwrap } from '~/utils/middleware'; import { selectMap, selectMapProvider, selectMapRoute, selectMapStickers } from './selectors'; import { TIPS } from '~/constants/tips'; -import { delay } from 'redux-saga'; import { setReadySaga } from '../editor/sagas'; import { selectEditor } from '../editor/selectors'; import { EDITOR_ACTIONS } from '../editor/constants'; diff --git a/src/redux/types.ts b/src/redux/types.ts index e69de29..0f42cde 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -0,0 +1,7 @@ +import { LatLngLiteral } from 'leaflet'; + +export interface INominatimResult { + id: number; + title: string; + latlng: LatLngLiteral; +}; \ No newline at end of file diff --git a/src/redux/user/sagas.ts b/src/redux/user/sagas.ts index 32c0893..cd77bf3 100644 --- a/src/redux/user/sagas.ts +++ b/src/redux/user/sagas.ts @@ -1,6 +1,5 @@ import { REHYDRATE, RehydrateAction } from 'redux-persist'; -import { delay, SagaIterator } from 'redux-saga'; -import { takeLatest, select, call, put, takeEvery } from 'redux-saga/effects'; +import { takeLatest, select, call, put, takeEvery, delay } from 'redux-saga/effects'; import { checkIframeToken, checkUserToken, @@ -34,7 +33,6 @@ import { selectUser, selectUserUser } from './selectors'; import { mapInitSaga } from '~/redux/map/sagas'; import { editorSetDialog, editorSetDialogActive } from '../editor/actions'; import { selectEditor } from '../editor/selectors'; -import { getLocation, watchLocation } from '~/utils/window'; function* generateGuestSaga() { const { @@ -198,7 +196,7 @@ function* searchSetTabSaga() { yield put(searchSetTitle('')); } -function* userLogoutSaga(): SagaIterator { +function* userLogoutSaga() { yield put(setUser(DEFAULT_USER)); yield call(generateGuestSaga); } @@ -256,7 +254,7 @@ function* mapsLoadMoreSaga() { yield put(searchSetLoading(false)); } -function* dropRouteSaga({ address }: ReturnType): SagaIterator { +function* dropRouteSaga({ address }: ReturnType) { const { token }: ReturnType = yield select(selectUserUser); const { routes: { @@ -290,7 +288,7 @@ function* modifyRouteSaga({ address, title, is_public, -}: ReturnType): SagaIterator { +}: ReturnType) { const { token }: ReturnType = yield select(selectUserUser); const { routes: { diff --git a/src/sprites/icon.svg b/src/sprites/icon.svg index 4b93bea..7d9667f 100644 --- a/src/sprites/icon.svg +++ b/src/sprites/icon.svg @@ -422,6 +422,13 @@ + + + + + + + diff --git a/src/styles/dialogs.less b/src/styles/dialogs.less index ca518fe..bd368cb 100644 --- a/src/styles/dialogs.less +++ b/src/styles/dialogs.less @@ -36,7 +36,6 @@ background: rgba(19, 45, 53, 0.95); } } - } .dialog-close-button { @@ -94,8 +93,41 @@ } } +.dialog-flex-scroll { + display: flex; + align-items: center; + justify-content: center; +} + +.nominatim-dialog-content { + padding-bottom: 48px; + min-height: 25vh; +} + +.nominatim-list-item { + padding: 10px; + color: white; + cursor: pointer; + transition: background-color 0.25s; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 200px; + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + .title { + text-overflow: hidden; + font-size: 12px; + -webkit-line-clamp: 2; + } +} + .dialog-shader { - &::before, &::after { + &::before, + &::after { content: ' '; height: 40px; width: 100%; @@ -120,13 +152,21 @@ } @keyframes pulse { - 0% { opacity: 1; } - 100% { opacity: 0.5; } + 0% { + opacity: 1; + } + 100% { + opacity: 0.5; + } } @keyframes spin { - 0% { transform: rotate(0); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0); + } + 100% { + transform: rotate(360deg); + } } .dialog-maplist-pulse { @@ -195,11 +235,14 @@ &.has_edit { //transform: translateY(-2px); - .route-row { background: fade(@green_secondary, 30%); } + .route-row { + background: fade(@green_secondary, 30%); + } } &.is_menu_target { - .route-row, .route-row-fav { + .route-row, + .route-row-fav { transform: translateX(-120px); } @@ -337,7 +380,6 @@ } } } - } .route-title { @@ -414,7 +456,7 @@ } } - @media(max-width: @mobile_breakpoint) { + @media (max-width: @mobile_breakpoint) { height: 48px; .dialog-tab { diff --git a/src/styles/panel.less b/src/styles/panel.less index c787e50..74fecc4 100644 --- a/src/styles/panel.less +++ b/src/styles/panel.less @@ -3,6 +3,8 @@ border-radius: @panel_radius; display: flex; box-shadow: @bar_shadow; + align-items: center; + justify-content: center; @media (max-width: @mobile_breakpoint) { box-shadow: none; @@ -723,4 +725,16 @@ .location-bar { width: 32px; +} + +.nominatim-panel { + position: fixed; + bottom: 53px; + left: 10px; + width: 272px +} + +.nominatim-search-input { + padding-left: 10px; + flex: 1; } \ No newline at end of file diff --git a/src/utils/api.ts b/src/utils/api.ts index fdad497..85b53da 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -11,6 +11,7 @@ import { configWithToken, } from './middleware'; import { IRoute } from '~/redux/map/types'; +import { INominatimResult } from '~/redux/types'; const arrayToObject = (array: any[], key: string): {} => array.reduce((obj, el) => ({ ...obj, [el[key]]: el }), {}); @@ -180,6 +181,36 @@ export const checkOSRMService = (bounds: LatLngLiteral[]): Promise => .then(() => true) .catch(() => false); +export const checkNominatimService = (): Promise => + CLIENT && + CLIENT.NOMINATIM_TEST_URL && + axios + .get(CLIENT.NOMINATIM_TEST_URL) + .then(() => true) + .catch(() => false); + +export const searchNominatim = (query: string) => + CLIENT && + CLIENT.NOMINATIM_URL && + axios + .get(`${CLIENT.NOMINATIM_URL}${query}`, { params: { format: 'json', country_code: 'ru', 'accept-language': 'ru_RU' } }) + .then( + data => + data && + data.data && + data.data.map( + (item): INominatimResult => ({ + id: item.place_id, + latlng: { + lat: item.lat, + lng: item.lon, + }, + title: item.display_name, + }) + ) + ) + .catch(() => []); + export const dropRoute = ({ address, token }: { address: string; token: string }): Promise => axios .delete(API.DROP_ROUTE, configWithToken(token, { data: { address } })) diff --git a/src/utils/window.ts b/src/utils/window.ts index 0d60bb7..c137ccf 100644 --- a/src/utils/window.ts +++ b/src/utils/window.ts @@ -20,8 +20,6 @@ export const getLocation = (callback: (pos: LatLngLiteral) => void) => { export const watchLocation = (callback: (pos: LatLngLiteral) => void): number => { return window.navigator.geolocation.watchPosition( position => { - console.log('Watch?'); - if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude) return callback(null);