From fe311e7839763e711fb91084aed4c8cc43f39b54 Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Thu, 8 Apr 2021 16:25:25 +0700 Subject: [PATCH] fixed typescript errors --- src/components/StickerDesc.tsx | 7 +- src/components/dialogs/MapListDialog.tsx | 13 +- src/components/dialogs/ProviderDialog.tsx | 4 +- src/components/dialogs/TitleDialog.tsx | 2 +- src/components/maps/RouteRowView.tsx | 2 +- src/components/panels/EditorPanel.tsx | 12 +- src/components/renderer/Renderer.tsx | 14 +- src/components/search/MapListDialogHead.tsx | 2 +- src/constants/api.ts | 20 +- src/constants/auth.ts | 12 +- src/constants/providers.ts | 17 +- src/map/Arrows/index.tsx | 6 +- src/map/CurrentLocation/index.tsx | 6 +- src/map/GpxPolyline/index.tsx | 4 +- src/map/KmMarks/index.tsx | 6 +- src/map/Map/index.tsx | 2 +- src/map/Route/index.tsx | 6 +- src/map/Router/index.tsx | 12 +- src/map/Sticker/index.tsx | 39 +- src/map/Stickers/index.tsx | 2 +- src/map/TileLayer/index.tsx | 8 +- src/redux/editor/index.ts | 4 +- src/redux/editor/sagas.ts | 21 +- src/redux/map/index.ts | 4 +- src/redux/map/sagas.ts | 271 ++++++------- src/redux/store.ts | 29 +- src/redux/user/index.ts | 4 +- src/redux/user/sagas.ts | 428 +++++++++++--------- src/utils/api.ts | 258 ------------ src/utils/api/index.ts | 214 ++++++++++ src/utils/api/instance.ts | 6 + src/utils/context.ts | 4 +- src/utils/gpx.ts | 6 +- src/utils/history.ts | 10 +- src/utils/map/ArrowsLayer.ts | 9 +- src/utils/map/InteractivePoly.ts | 42 +- src/utils/marks.ts | 8 +- src/utils/middleware.ts | 27 +- src/utils/renderer.ts | 10 +- src/utils/window.ts | 10 +- tsconfig.json | 2 +- 41 files changed, 786 insertions(+), 777 deletions(-) delete mode 100644 src/utils/api.ts create mode 100644 src/utils/api/index.ts create mode 100644 src/utils/api/instance.ts diff --git a/src/components/StickerDesc.tsx b/src/components/StickerDesc.tsx index ab20af4..40477bb 100644 --- a/src/components/StickerDesc.tsx +++ b/src/components/StickerDesc.tsx @@ -23,10 +23,15 @@ class StickerDesc extends React.PureComponent { blockMouse = e => { e.preventDefault(); e.stopPropagation(); + + if (!this.input) { + return + } + this.input.focus(); }; - input: HTMLTextAreaElement; + input: HTMLTextAreaElement | null = null; render() { const { value: text } = this.props; diff --git a/src/components/dialogs/MapListDialog.tsx b/src/components/dialogs/MapListDialog.tsx index 557f10f..48935b7 100644 --- a/src/components/dialogs/MapListDialog.tsx +++ b/src/components/dialogs/MapListDialog.tsx @@ -64,9 +64,8 @@ export interface State { class MapListDialogUnconnected extends PureComponent { state = { - menu_target: null, - editor_target: null, - + menu_target: '', + editor_target: '', is_editing: false, is_dropping: false, }; @@ -74,7 +73,7 @@ class MapListDialogUnconnected extends PureComponent { startEditing = (editor_target: IRouteListItem['address']): void => this.setState({ editor_target, - menu_target: null, + menu_target: '', is_editing: true, is_dropping: false, }); @@ -86,19 +85,19 @@ class MapListDialogUnconnected extends PureComponent { hideMenu = (): void => this.setState({ - menu_target: null, + menu_target: '', }); showDropCard = (editor_target: IRouteListItem['address']): void => this.setState({ editor_target, - menu_target: null, + menu_target: '', is_editing: false, is_dropping: true, }); stopEditing = (): void => { - this.setState({ editor_target: null }); + this.setState({ editor_target: '' }); }; setTitle = ({ target: { value } }: { target: { value: string } }): void => { diff --git a/src/components/dialogs/ProviderDialog.tsx b/src/components/dialogs/ProviderDialog.tsx index 29ac463..e564458 100644 --- a/src/components/dialogs/ProviderDialog.tsx +++ b/src/components/dialogs/ProviderDialog.tsx @@ -27,7 +27,7 @@ const ProviderDialogUnconnected = ({ provider, mapSetProvider }: Props) => ( backgroundImage: `url(${replaceProviderUrl(item, { x: 5980, y: 2589, zoom: 13 })})`, }} onMouseDown={() => mapSetProvider(item)} - key={PROVIDERS[item].name} + key={PROVIDERS[item]?.name} > { provider === item && @@ -44,4 +44,4 @@ const ProviderDialogUnconnected = ({ provider, mapSetProvider }: Props) => ( const ProviderDialog = connect(mapStateToProps, mapDispatchToProps)(ProviderDialogUnconnected) -export { ProviderDialog } \ No newline at end of file +export { ProviderDialog } diff --git a/src/components/dialogs/TitleDialog.tsx b/src/components/dialogs/TitleDialog.tsx index 75fd328..d626b06 100644 --- a/src/components/dialogs/TitleDialog.tsx +++ b/src/components/dialogs/TitleDialog.tsx @@ -42,7 +42,7 @@ export class TitleDialogUnconnected extends React.PureComponent { this.setMaxHeight(); } - setMaxHeight = (): number => { + setMaxHeight = () => { if (!this.ref_sizer || !this.ref_title || !this.ref_text) return 0; const { height: sizer_height } = this.ref_sizer.getBoundingClientRect(); diff --git a/src/components/maps/RouteRowView.tsx b/src/components/maps/RouteRowView.tsx index 072c80a..181ec61 100644 --- a/src/components/maps/RouteRowView.tsx +++ b/src/components/maps/RouteRowView.tsx @@ -40,7 +40,7 @@ export const RouteRowView = ({ is_admin, is_published, toggleStarred -}: Props): ReactElement => ( +}: Props): ReactElement => (
{(tab === TABS.PENDING || tab === TABS.STARRED) && is_admin && (
diff --git a/src/components/panels/EditorPanel.tsx b/src/components/panels/EditorPanel.tsx index 5b017ea..61bd25c 100644 --- a/src/components/panels/EditorPanel.tsx +++ b/src/components/panels/EditorPanel.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent, useState, useCallback } from 'react'; +import React, { PureComponent } from 'react'; import { MODES } from '~/constants/modes'; import classnames from 'classnames'; @@ -7,12 +7,12 @@ import { EditorDialog } from '~/components/panels/EditorDialog'; import { connect } from 'react-redux'; import { editorChangeMode, + editorKeyPressed, + editorRedo, editorStartEditing, editorStopEditing, editorTakeAShot, - editorKeyPressed, editorUndo, - editorRedo, } from '~/redux/editor/actions'; import { Tooltip } from '~/components/panels/Tooltip'; import { IState } from '~/redux/store'; @@ -47,6 +47,10 @@ type Props = ReturnType & typeof mapDispatchToProps & {} class EditorPanelUnconnected extends PureComponent { componentDidMount() { + if (!this.panel) { + return; + } + window.addEventListener('keydown', this.onKeyPress as any); const obj = document.getElementById('control-dialog'); @@ -57,7 +61,7 @@ class EditorPanelUnconnected extends PureComponent { obj.style.width = String(width); } - panel: HTMLElement = null; + panel: HTMLDivElement | null = null; componentWillUnmount() { window.removeEventListener('keydown', this.onKeyPress as any); diff --git a/src/components/renderer/Renderer.tsx b/src/components/renderer/Renderer.tsx index a245da5..ea7acf3 100644 --- a/src/components/renderer/Renderer.tsx +++ b/src/components/renderer/Renderer.tsx @@ -31,6 +31,10 @@ class Component extends React.Component { }; onImageLoaded = () => { + if (!this.image) { + return + } + this.croppr = new Croppr(this.image, { onInitialize: this.onCropInit, }); @@ -57,12 +61,12 @@ class Component extends React.Component { regionEl.append(this.logo); }; - croppr: Croppr; - logo: HTMLDivElement; - image: HTMLImageElement; - logoImg: HTMLImageElement; + croppr?: Croppr; + logo: HTMLDivElement | null = null; + image: HTMLImageElement | null = null; + logoImg: HTMLImageElement | null = null; - getImage = () => this.props.editorCropAShot(this.croppr.getValue()); + getImage = () => this.props.editorCropAShot(this.croppr?.getValue()); render() { const { data } = this.props.editor.renderer; diff --git a/src/components/search/MapListDialogHead.tsx b/src/components/search/MapListDialogHead.tsx index cd8edf9..11ce7f2 100644 --- a/src/components/search/MapListDialogHead.tsx +++ b/src/components/search/MapListDialogHead.tsx @@ -7,7 +7,7 @@ interface Props { max: number; search: string; distance: [number, number]; - onDistanceChange: (val: [number, number]) => void; + onDistanceChange: (val: number[]) => void; onSearchChange: ChangeEventHandler; } diff --git a/src/constants/api.ts b/src/constants/api.ts index 5febdcf..dd1f73e 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,16 +1,14 @@ -import { CLIENT } from '~/config/frontend'; - export const API = { - GET_GUEST: `${CLIENT.API_ADDR}/api/auth/`, - CHECK_TOKEN: `${CLIENT.API_ADDR}/api/auth/`, - IFRAME_LOGIN_VK: `${CLIENT.API_ADDR}/api/auth/vk`, - GET_MAP: `${CLIENT.API_ADDR}/api/route/`, - POST_MAP: `${CLIENT.API_ADDR}/api/route/`, - GET_ROUTE_LIST: tab => `${CLIENT.API_ADDR}/api/route/list/${tab}`, + GET_GUEST: `/api/auth/`, + CHECK_TOKEN: `/api/auth/`, + IFRAME_LOGIN_VK: `/api/auth/vk`, + GET_MAP: `/api/route/`, + POST_MAP: `/api/route/`, + GET_ROUTE_LIST: tab => `/api/route/list/${tab}`, - DROP_ROUTE: `${CLIENT.API_ADDR}/api/route/`, - MODIFY_ROUTE: `${CLIENT.API_ADDR}/api/route/`, - SET_STARRED: `${CLIENT.API_ADDR}/api/route/publish`, + DROP_ROUTE: `/api/route/`, + MODIFY_ROUTE: `/api/route/`, + SET_STARRED: `/api/route/publish`, }; export const API_RETRY_INTERVAL = 10; diff --git a/src/constants/auth.ts b/src/constants/auth.ts index 8e011b8..8b70e58 100644 --- a/src/constants/auth.ts +++ b/src/constants/auth.ts @@ -11,7 +11,7 @@ export interface IUser { role: IRoles[keyof IRoles]; routes: {}; success: boolean; - id?: string; + id: string; uid: string; token?: string; photo: string; @@ -31,9 +31,9 @@ export const DEFAULT_USER: IUser = { role: ROLES.guest, routes: {}, success: false, - id: null, - token: null, - photo: null, - name: null, - uid: null, + id: '', + token: undefined, + photo: '', + name: '', + uid: '', }; diff --git a/src/constants/providers.ts b/src/constants/providers.ts index 17ad1cf..a075539 100644 --- a/src/constants/providers.ts +++ b/src/constants/providers.ts @@ -5,21 +5,6 @@ export interface IProvider { } export type ITileMaps = Record -// { - // WATERCOLOR: IProvider, - // DGIS: IProvider, - // DEFAULT: IProvider, - // DARQ: IProvider, - // BLANK: IProvider, - // HOT: IProvider, - // YSAT: IProvider, - // YMAP: IProvider, - // SAT: IProvider, - // ESAT: IProvider, - // CACHE_OSM: IProvider, - // CACHE_CARTO: IProvider, -// } - // Стили карт const TILEMAPS: ITileMaps = { @@ -53,7 +38,7 @@ const TILEMAPS: ITileMaps = { const ENABLED: Array = ['BLANK', 'DEFAULT', 'DGIS', 'HOT', 'ESAT']; export const DEFAULT_PROVIDER: keyof ITileMaps = ENABLED[1]; -export const PROVIDERS: Partial = ENABLED.reduce((obj, provider) => ({ +export const PROVIDERS: ITileMaps = ENABLED.reduce((obj, provider) => ({ ...obj, [provider]: TILEMAPS[provider], }), {}); diff --git a/src/map/Arrows/index.tsx b/src/map/Arrows/index.tsx index fa4dbb0..1a10263 100644 --- a/src/map/Arrows/index.tsx +++ b/src/map/Arrows/index.tsx @@ -14,17 +14,17 @@ const mapDispatchToProps = {}; type Props = ReturnType & typeof mapDispatchToProps & {}; const ArrowsUnconnected: FC = memo(({ route }) => { - const [layer, setLayer] = useState(null); + const [layer, setLayer] = useState(null); useEffect(() => { const item = new ArrowsLayer({}).addTo(MainMap); - setLayer(item); + setLayer(item); return () => MainMap.removeLayer(item); }, [MainMap]); useEffect(() => { if (!layer) return - + layer.setLatLngs(route); }, [layer, route]) return null; diff --git a/src/map/CurrentLocation/index.tsx b/src/map/CurrentLocation/index.tsx index 745a8aa..61aac29 100644 --- a/src/map/CurrentLocation/index.tsx +++ b/src/map/CurrentLocation/index.tsx @@ -1,9 +1,9 @@ -import React, { FC, useState, useEffect, useCallback } from 'react'; -import { LatLngLiteral, marker, Marker, DivIcon } from 'leaflet'; +import React, { FC, useCallback, useEffect } from 'react'; +import { DivIcon, LatLngLiteral, Marker } from 'leaflet'; import { MainMap } from '~/constants/map'; interface IProps { - location: LatLngLiteral; + location?: LatLngLiteral; } const CurrentLocation: FC = ({ location }) => { diff --git a/src/map/GpxPolyline/index.tsx b/src/map/GpxPolyline/index.tsx index b78bdf3..f59f008 100644 --- a/src/map/GpxPolyline/index.tsx +++ b/src/map/GpxPolyline/index.tsx @@ -8,14 +8,14 @@ interface IProps { } const GpxPolyline: FC = ({ latlngs, color }) => { - const [layer, setLayer] = useState(null); + const [layer, setLayer] = useState(null); useEffect(() => { const item = new Polyline([], { color, stroke: true, opacity: 1, - weight: 7, + weight: 7, // dashArray: [12,12], }).addTo(MainMap); setLayer(item); diff --git a/src/map/KmMarks/index.tsx b/src/map/KmMarks/index.tsx index 2440e96..667d811 100644 --- a/src/map/KmMarks/index.tsx +++ b/src/map/KmMarks/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useEffect, useState, memo } from 'react'; +import React, { FC, memo, useEffect, useState } from 'react'; import { KmMarksLayer } from '~/utils/marks'; import { MainMap } from '~/constants/map'; import { selectMap } from '~/redux/map/selectors'; @@ -14,14 +14,14 @@ const mapDispatchToProps = {}; type Props = ReturnType & typeof mapDispatchToProps & {}; const KmMarksUnconnected: FC = memo(({ map: { route } }) => { - const [layer, setLayer] = useState(null); + const [layer, setLayer] = useState(null); useEffect(() => { const layer = new KmMarksLayer([]); layer.addTo(MainMap); setLayer(layer); return () => MainMap.removeLayer(layer); - }, [MainMap]); + }, []); useEffect(() => { if (!layer) return; diff --git a/src/map/Map/index.tsx b/src/map/Map/index.tsx index 44cf90b..0836afd 100644 --- a/src/map/Map/index.tsx +++ b/src/map/Map/index.tsx @@ -93,7 +93,7 @@ const MapUnconnected: React.FC = memo( enabled && )}
, - document.getElementById('canvas') + document.getElementById('canvas')! ); } ); diff --git a/src/map/Route/index.tsx b/src/map/Route/index.tsx index 9d769b2..9c7b890 100644 --- a/src/map/Route/index.tsx +++ b/src/map/Route/index.tsx @@ -1,8 +1,8 @@ -import React, { FC, useEffect, memo, useState, useCallback } from 'react'; +import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import { InteractivePoly } from '~/utils/map/InteractivePoly'; import { isMobile } from '~/utils/window'; import { LatLng } from 'leaflet'; -import { selectEditorMode, selectEditorEditing, selectEditorDirection } from '~/redux/editor/selectors'; +import { selectEditorDirection, selectEditorEditing, selectEditorMode } from '~/redux/editor/selectors'; import * as MAP_ACTIONS from '~/redux/map/actions'; import { connect } from 'react-redux'; import { selectMapRoute } from '~/redux/map/selectors'; @@ -28,7 +28,7 @@ type Props = ReturnType & typeof mapDispatchToProps & {} const RouteUnconnected: FC = memo( ({ route, editing, mode, drawing_direction, mapSetRoute, editorSetDistance, editorSetMarkersShown }) => { - const [layer, setLayer] = useState(null); + const [layer, setLayer] = useState(null); const onDistanceChange = useCallback(({ distance }) => editorSetDistance(distance), [ editorSetDistance, diff --git a/src/map/Router/index.tsx b/src/map/Router/index.tsx index de3b693..7e58edb 100644 --- a/src/map/Router/index.tsx +++ b/src/map/Router/index.tsx @@ -1,16 +1,12 @@ -import { FC, useEffect, useCallback, memo, useState } from 'react'; +import { FC, memo, useCallback, useEffect, useState } from 'react'; import { OsrmRouter } from '~/utils/map/OsrmRouter'; import { connect } from 'react-redux'; import { selectMapRoute } from '~/redux/map/selectors'; -import { - selectEditorRouter, - selectEditorMode, - selectEditorDistance, -} from '~/redux/editor/selectors'; +import { selectEditorDistance, selectEditorMode, selectEditorRouter } from '~/redux/editor/selectors'; import { MainMap } from '~/constants/map'; import * as EDITOR_ACTIONS from '~/redux/editor/actions'; import { MODES } from '~/constants/modes'; -import { LatLngLiteral, marker, divIcon } from 'leaflet'; +import { divIcon, LatLngLiteral, marker } from 'leaflet'; import classNames from 'classnames'; import { angleBetweenPoints } from '~/utils/geom'; @@ -30,7 +26,7 @@ type Props = ReturnType & typeof mapDispatchToProps & {} const RouterUnconnected: FC = memo( ({ route, mode, router: { waypoints }, editorSetRouter, distance }) => { const [dist, setDist] = useState(0); - const [end, setEnd] = useState(null); + const [end, setEnd] = useState(null); const [direction, setDirection] = useState(false); const updateWaypoints = useCallback( diff --git a/src/map/Sticker/index.tsx b/src/map/Sticker/index.tsx index 85638a5..a611388 100644 --- a/src/map/Sticker/index.tsx +++ b/src/map/Sticker/index.tsx @@ -20,8 +20,8 @@ interface IProps { mapDropSticker: (index: number) => void; } -export const getLabelDirection = (angle: number): 'left' | 'right' => - angle % Math.PI >= -(Math.PI / 2) && angle % Math.PI <= Math.PI / 2 ? 'left' : 'right'; +export const getLabelDirection = (angle?: number): 'left' | 'right' => + !!angle && angle % Math.PI >= -(Math.PI / 2) && angle % Math.PI <= Math.PI / 2 ? 'left' : 'right'; const getX = e => e.touches && e.touches.length > 0 @@ -36,50 +36,58 @@ const Sticker: React.FC = ({ mapSetSticker, mapDropSticker, }) => { - const [text, setText] = useState(sticker.text); - const [layer, setLayer] = React.useState(null); + const [text, setText] = useState(sticker.text || ''); + const [layer, setLayer] = React.useState(null); const [dragging, setDragging] = React.useState(false); - const wrapper = useRef(null); + const wrapper = useRef(null); let angle = useRef(sticker.angle); const element = React.useMemo(() => document.createElement('div'), []); - const stickerArrow = React.useRef(null); - const stickerImage = React.useRef(null); + const stickerArrow = React.useRef(null); + const stickerImage = React.useRef(null); const onChange = React.useCallback(state => mapSetSticker(index, state), [mapSetSticker, index]); const onDelete = React.useCallback(state => mapDropSticker(index), [mapSetSticker, index]); const updateAngle = useCallback( ang => { - if (!stickerImage.current || !stickerArrow.current) return; - const x = Math.cos(ang + Math.PI) * 56 - 30; const y = Math.sin(ang + Math.PI) * 56 - 30; + if (!stickerImage.current || !stickerArrow.current) { + return; + } + stickerImage.current.style.left = String(6 + x); stickerImage.current.style.top = String(6 + y); stickerArrow.current.style.transform = `rotate(${ang + Math.PI}rad)`; }, - [stickerArrow, stickerImage, angle] + [stickerArrow, stickerImage] ); const onDragStart = React.useCallback(() => { + if (!layer?.dragging) { + return + } + layer.dragging.disable(); MainMap.dragging.disable(); MainMap.disableClicks(); setDragging(true); - }, [setDragging, layer, MainMap]); + }, [setDragging, layer]); const onDragStop = React.useCallback( event => { event.stopPropagation(); event.preventDefault(); - if (!layer) return; + if (!layer?.dragging) { + return; + } setDragging(false); onChange({ @@ -134,7 +142,9 @@ const Sticker: React.FC = ({ }); }, [text, onChange, sticker]); - const direction = React.useMemo(() => getLabelDirection(sticker.angle), [sticker.angle]); + const direction = React.useMemo(() => { + getLabelDirection(sticker?.angle) + }, [sticker.angle]); useEffect(() => { updateAngle(sticker.angle); @@ -148,7 +158,8 @@ const Sticker: React.FC = ({ useEffect(() => { if (!layer) return; - setText(sticker.text); + + setText(sticker.text || ''); }, [layer, sticker.text]); useEffect(() => { diff --git a/src/map/Stickers/index.tsx b/src/map/Stickers/index.tsx index 3cee214..fbd4442 100644 --- a/src/map/Stickers/index.tsx +++ b/src/map/Stickers/index.tsx @@ -14,7 +14,7 @@ interface IProps { } const Stickers: FC = memo(({ stickers, is_editing, mapSetSticker, mapDropSticker }) => { - const [layer, setLayer] = useState(null); + const [layer, setLayer] = useState(null); const [zoom, setZoom] = useState(MainMap.getZoom()); const onZoomChange = useCallback( diff --git a/src/map/TileLayer/index.tsx b/src/map/TileLayer/index.tsx index f01b676..18c9382 100644 --- a/src/map/TileLayer/index.tsx +++ b/src/map/TileLayer/index.tsx @@ -10,7 +10,7 @@ type IProps = React.HTMLAttributes & { }; const TileLayer: React.FC = React.memo(({ children, provider, map }) => { - const [layer, setLayer] = React.useState(null); + const [layer, setLayer] = React.useState(undefined); React.useEffect(() => { if (!map) return; @@ -34,7 +34,11 @@ const TileLayer: React.FC = React.memo(({ children, provider, map }) => layer.setUrl(url); }, [layer, provider]); - return {children}; + return ( + + {children} + + ); }); export { TileLayer }; diff --git a/src/redux/editor/index.ts b/src/redux/editor/index.ts index 383d4ac..5417ecf 100644 --- a/src/redux/editor/index.ts +++ b/src/redux/editor/index.ts @@ -41,7 +41,7 @@ export interface IEditorState { distance: number; estimated: number; speed: number; - activeSticker: { set?: string; sticker?: string }; + activeSticker: { set: string; sticker: string }; is_empty: boolean; is_published: boolean; is_routing: boolean; @@ -134,7 +134,7 @@ export const EDITOR_INITIAL_STATE = { }, save: { - error: null, + error: '', finished: false, overwriting: false, processing: false, diff --git a/src/redux/editor/sagas.ts b/src/redux/editor/sagas.ts index 39275bc..8521125 100644 --- a/src/redux/editor/sagas.ts +++ b/src/redux/editor/sagas.ts @@ -69,8 +69,14 @@ import uuid from 'uuid'; import { getRandomColor, getAdaptiveScale } from '~/utils/dom'; const hideLoader = () => { - document.getElementById('loader').style.opacity = String(0); - document.getElementById('loader').style.pointerEvents = 'none'; + const el = document.getElementById('loader'); + + if (!el) { + return true; + } + + el.style.opacity = String(0); + el.style.pointerEvents = 'none'; return true; }; @@ -125,6 +131,10 @@ function* getRenderData() { canvas.height = window.innerHeight; const ctx = canvas.getContext('2d'); + if (!ctx) { + return + } + const geometry = getTilePlacement(); const points = getPolyPlacement(route); const sticker_points = getStickersPlacement(stickers); @@ -184,6 +194,11 @@ function* getCropData({ x, y, width, height }) { canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); + + if (!ctx) { + return + } + const image = yield imageFetcher(data); ctx.drawImage(image, -x, -y); @@ -281,7 +296,7 @@ function* mapClick({ latlng }: ReturnType) { function* routerSubmit() { const route: ReturnType = yield select(selectMapRoute); - const latlngs: LatLng[] = path(['_routes', 0, 'coordinates'], OsrmRouter); + const latlngs: LatLng[] = path(['_routes', 0, 'coordinates'], OsrmRouter) || []; const coordinates = simplify(latlngs); diff --git a/src/redux/map/index.ts b/src/redux/map/index.ts index 1084456..ffd2ecb 100644 --- a/src/redux/map/index.ts +++ b/src/redux/map/index.ts @@ -28,9 +28,9 @@ export const MAP_INITIAL_STATE: IMapReducer = { address: '', address_origin: '', description: '', - owner: { id: null }, + owner: { id: '' }, is_public: false, zoom: 13, } -export const map = createReducer(MAP_INITIAL_STATE, MAP_HANDLERS) \ No newline at end of file +export const map = createReducer(MAP_INITIAL_STATE, MAP_HANDLERS) diff --git a/src/redux/map/sagas.ts b/src/redux/map/sagas.ts index 718a242..1572bf6 100644 --- a/src/redux/map/sagas.ts +++ b/src/redux/map/sagas.ts @@ -1,40 +1,30 @@ -import { - takeEvery, - select, - put, - call, - TakeEffect, - race, - take, - takeLatest, - delay, -} from 'redux-saga/effects'; +import { call, delay, put, race, select, take, TakeEffect, takeEvery, takeLatest } from 'redux-saga/effects'; import { MAP_ACTIONS } from './constants'; import { - mapClicked, mapAddSticker, - mapSetProvider, + mapClicked, mapSet, - mapSetTitle, mapSetAddressOrigin, + mapSetProvider, mapSetRoute, mapSetStickers, + mapSetTitle, } from './actions'; -import { selectUser, selectUserUser } from '~/redux/user/selectors'; +import { selectUser } from '~/redux/user/selectors'; import { MODES } from '~/constants/modes'; import { + editorCaptureHistory, editorChangeMode, + editorClearAll, + editorSendSaveRequest, + editorSetActiveSticker, editorSetChanged, editorSetEditing, - editorSetReady, - editorSetActiveSticker, - editorSendSaveRequest, - editorSetSave, - editorClearAll, editorSetHistory, - editorCaptureHistory, + editorSetReady, + editorSetSave, } from '~/redux/editor/actions'; -import { pushLoaderState, getUrlData, pushPath } from '~/utils/history'; +import { getUrlData, pushLoaderState, pushPath } from '~/utils/history'; import { getStoredMap, postMap } from '~/utils/api'; import { Unwrap } from '~/utils/middleware'; import { selectMap, selectMapProvider, selectMapRoute, selectMapStickers } from './selectors'; @@ -70,29 +60,36 @@ export function* replaceAddressIfItsBusy(destination, original) { } export function* loadMapSaga(path) { - const { - data: { route, error, random_url }, - }: Unwrap = yield call(getStoredMap, { name: path }); + try { + const { + data: { + route, error, random_url, + }, + }: Unwrap = yield call(getStoredMap, { name: path }); - if (route && !error) { - yield put( - mapSet({ - provider: route.provider, - route: route.route, - stickers: route.stickers, - title: route.title, - address: route.address, - description: route.description, - is_public: route.is_public, - logo: route.logo, - }) - ); + if (route && !error) { + yield put( + mapSet({ + provider: route.provider, + route: route.route, + stickers: route.stickers, + title: route.title, + address: route.address, + description: route.description, + is_public: route.is_public, + logo: route.logo, + }), + ); - yield put(editorSetHistory({ records: [{ route: route.route, stickers: route.stickers }] })); - return { route, random_url }; + yield put(editorSetHistory({ records: [{ route: route.route, stickers: route.stickers }] })); + return { route, random_url }; + } + + return null; + } catch (e) { + console.log(e); + yield call(startEmptyEditorSaga); } - - return null; } export function* startEmptyEditorSaga() { @@ -142,10 +139,10 @@ export function* mapInitSaga() { yield put(mapSetProvider(provider)); if (hash && /^#map/.test(hash)) { - const [, newUrl] = hash.match(/^#map[:/?!](.*)$/); + const matches = hash.match(/^#map[:/?!](.*)$/); - if (newUrl) { - yield pushPath(`/${newUrl}`); + if (matches && matches[1]) { + yield pushPath(`/${matches[1]}`); yield call(setReadySaga); return; } @@ -161,7 +158,7 @@ function* setActiveStickerSaga() { yield put(editorChangeMode(MODES.STICKERS)); } -function* setTitleSaga({ title }: ReturnType) { +function setTitleSaga({ title }: ReturnType) { if (title) { document.title = `${title} | Редактор маршрутов`; } @@ -216,7 +213,7 @@ function* clearSaga({ type }) { const { mode, activeSticker }: ReturnType = yield select(selectEditor); if (activeSticker && activeSticker.set && activeSticker.sticker) { - yield put(editorSetActiveSticker(null)); + yield put(editorSetActiveSticker({ set: '', sticker: '' })); } if (mode !== MODES.NONE) { @@ -231,94 +228,96 @@ function* sendSaveRequestSaga({ is_public, description, }: ReturnType) { - const { route, stickers, provider }: ReturnType = yield select(selectMap); + try { + const { route, stickers, provider }: ReturnType = yield select(selectMap); - if (!route.length && !stickers.length) { - return yield put( - editorSetSave({ error: TIPS.SAVE_EMPTY, loading: false, overwriting: false, finished: false }) + if (!route.length && !stickers.length) { + return yield put( + editorSetSave({ error: TIPS.SAVE_EMPTY, loading: false, overwriting: false, finished: false }), + ); + } + + const { logo }: ReturnType = yield select(selectMap); + const { distance }: ReturnType = yield select(selectEditor); + + yield put(editorSetSave({ loading: true, overwriting: false, finished: false, error: '' })); + + const { + result, + timeout, + cancel, + }: { + result: Unwrap; + timeout: boolean; + cancel: TakeEffect; + } = yield race({ + result: postMap({ + route, + stickers, + title, + force, + address, + logo, + distance, + provider, + is_public, + description, + }), + timeout: delay(10000), + cancel: take(EDITOR_ACTIONS.RESET_SAVE_DIALOG), + }); + + yield put(editorSetSave({ loading: false })); + + if (cancel) return yield put(editorChangeMode(MODES.NONE)); + + if (result && result.data.code === 'already_exist') + return yield put(editorSetSave({ overwriting: true })); + + if (result && result.data.code === 'conflict') + return yield put( + editorSetSave({ + error: TIPS.SAVE_EXISTS, + loading: false, + overwriting: false, + finished: false, + }), + ); + + if (timeout || !result || !result.data.route || !result.data.route.address) + return yield put( + editorSetSave({ + error: TIPS.SAVE_TIMED_OUT, + loading: false, + overwriting: false, + finished: false, + }), + ); + + yield put( + mapSet({ + address: result.data.route.address, + title: result.data.route.title, + is_public: result.data.route.is_public, + description: result.data.route.description, + }), ); + + yield put(editorSetReady(false)); + pushPath(`/${address}/edit`); + yield put(editorSetReady(true)); + + yield put( + editorSetSave({ + error: TIPS.SAVE_SUCCESS, + loading: false, + overwriting: false, + finished: true, + }), + ); + } catch (e) { + console.log(e); } - - const { logo }: ReturnType = yield select(selectMap); - const { distance }: ReturnType = yield select(selectEditor); - const { token }: ReturnType = yield select(selectUserUser); - - yield put(editorSetSave({ loading: true, overwriting: false, finished: false, error: null })); - - const { - result, - timeout, - cancel, - }: { - result: Unwrap; - timeout: boolean; - cancel: TakeEffect; - } = yield race({ - result: postMap({ - token, - route, - stickers, - title, - force, - address, - logo, - distance, - provider, - is_public, - description, - }), - timeout: delay(10000), - cancel: take(EDITOR_ACTIONS.RESET_SAVE_DIALOG), - }); - - yield put(editorSetSave({ loading: false })); - - if (cancel) return yield put(editorChangeMode(MODES.NONE)); - - if (result && result.data.code === 'already_exist') - return yield put(editorSetSave({ overwriting: true })); - - if (result && result.data.code === 'conflict') - return yield put( - editorSetSave({ - error: TIPS.SAVE_EXISTS, - loading: false, - overwriting: false, - finished: false, - }) - ); - - if (timeout || !result || !result.data.route || !result.data.route.address) - return yield put( - editorSetSave({ - error: TIPS.SAVE_TIMED_OUT, - loading: false, - overwriting: false, - finished: false, - }) - ); - - yield put( - mapSet({ - address: result.data.route.address, - title: result.data.route.title, - is_public: result.data.route.is_public, - description: result.data.route.description, - }) - ); - - yield put(editorSetReady(false)); - pushPath(`/${address}/edit`); - yield put(editorSetReady(true)); - - yield put( - editorSetSave({ - error: TIPS.SAVE_SUCCESS, - loading: false, - overwriting: false, - finished: true, - }) - ); } function* setChanged() { @@ -328,14 +327,10 @@ function* setChanged() { yield put(editorSetChanged(true)); } -function* onZoomChange() { - -} - export function* mapSaga() { yield takeEvery( [MAP_ACTIONS.SET_ROUTE, MAP_ACTIONS.SET_STICKER, MAP_ACTIONS.SET_STICKERS], - setChanged + setChanged, ); yield takeEvery(EDITOR_ACTIONS.START_EDITING, startEditingSaga); @@ -351,6 +346,6 @@ export function* mapSaga() { EDITOR_ACTIONS.CLEAR_ALL, EDITOR_ACTIONS.CLEAR_CANCEL, ], - clearSaga + clearSaga, ); } diff --git a/src/redux/store.ts b/src/redux/store.ts index ccdee48..e8212ab 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -18,9 +18,12 @@ import { map, IMapReducer } from '~/redux/map'; import { mapSaga } from '~/redux/map/sagas'; import { watchLocation, getLocation } from '~/utils/window'; import { LatLngLiteral } from 'leaflet'; -import { setUserLocation } from './user/actions'; +import { setUserLocation, userLogout } from './user/actions'; import { MainMap } from '~/constants/map'; import { mapZoomChange } from './map/actions'; +import { assocPath } from 'ramda'; +import { AxiosError } from 'axios'; +import { api } from '~/utils/api/instance'; const userPersistConfig: PersistConfig = { key: 'user', @@ -64,6 +67,28 @@ export function configureStore(): { store: Store; persistor: Persistor } { const persistor = persistStore(store); + // Pass token to axios + api.interceptors.request.use(options => { + const token = store.getState().user.token; + + if (!token) { + return options; + } + + return assocPath(['headers', 'authorization'], `Bearer ${token}`, options); + }); + + // Logout on 401 + api.interceptors.response.use(undefined, (error: AxiosError<{ error: string }>) => { + if (error.response?.status === 401) { + store.dispatch(userLogout()); + } + + error.message = error?.response?.data?.error || error?.response?.statusText || error.message; + + throw error; + }); + return { store, persistor }; } @@ -74,5 +99,5 @@ history.listen((location, action) => { store.dispatch(editorLocationChanged(location.pathname)); }); -watchLocation((location: LatLngLiteral) => store.dispatch(setUserLocation(location))); +watchLocation((location: LatLngLiteral | undefined) => store.dispatch(setUserLocation(location))); MainMap.on('zoomend', event => store.dispatch(mapZoomChange(event.target._zoom))) diff --git a/src/redux/user/index.ts b/src/redux/user/index.ts index 162d8d0..51ef671 100644 --- a/src/redux/user/index.ts +++ b/src/redux/user/index.ts @@ -15,7 +15,7 @@ export interface IRouteListItem { export interface IRootReducer { // ready: boolean, user: IUser; - location: LatLngLiteral; + location?: LatLngLiteral; routes: { limit: 0; loading: boolean; @@ -38,7 +38,7 @@ export type IRootState = Readonly; export const INITIAL_STATE: IRootReducer = { user: { ...DEFAULT_USER }, - location: null, + location: undefined, routes: { limit: 0, loading: false, // <-- maybe delete this diff --git a/src/redux/user/sagas.ts b/src/redux/user/sagas.ts index e366860..f9ee41a 100644 --- a/src/redux/user/sagas.ts +++ b/src/redux/user/sagas.ts @@ -1,5 +1,5 @@ import { REHYDRATE, RehydrateAction } from 'redux-persist'; -import { takeLatest, select, call, put, takeEvery, delay } from 'redux-saga/effects'; +import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; import { checkIframeToken, checkUserToken, @@ -9,15 +9,16 @@ import { modifyRoute, sendRouteStarred, } from '~/utils/api'; +import * as ActionCreators from '~/redux/user/actions'; import { - searchSetTab, - setUser, mapsSetShift, searchChangeDistance, searchPutRoutes, searchSetLoading, + searchSetTab, searchSetTitle, setRouteStarred, + setUser, userLogin, } from '~/redux/user/actions'; @@ -26,8 +27,6 @@ import { USER_ACTIONS } from '~/redux/user/constants'; import { DEFAULT_USER } from '~/constants/auth'; import { DIALOGS, TABS } from '~/constants/dialogs'; - -import * as ActionCreators from '~/redux/user/actions'; import { Unwrap } from '~/utils/middleware'; import { selectUser, selectUserUser } from './selectors'; import { mapInitSaga } from '~/redux/map/sagas'; @@ -35,64 +34,71 @@ import { editorSetDialog, editorSetDialogActive } from '../editor/actions'; import { selectEditor } from '../editor/selectors'; function* generateGuestSaga() { - const { - data: { user, random_url }, - }: Unwrap = yield call(getGuestToken); + try { + const { + data: { user, random_url }, + }: Unwrap = yield call(getGuestToken); - yield put(setUser({ ...user, random_url })); + yield put(setUser({ ...user, random_url })); - return { ...user, random_url }; + return { ...user, random_url }; + } catch(e) { + console.log(e) + } } function* authCheckSaga({ key }: RehydrateAction) { - if (key !== 'user') return; + try { + if (key !== 'user') return; - pushLoaderState(70); + pushLoaderState(70); - const { id, token }: ReturnType = yield select(selectUserUser); - const { ready }: ReturnType = yield select(selectEditor); + const { id, token }: ReturnType = yield select(selectUserUser); + const { ready }: ReturnType = yield select(selectEditor); - if (window.location.search || true) { - const { viewer_id, auth_key } = yield parseQuery(window.location.search); + if (window.location.search || true) { + const { viewer_id, auth_key } = yield parseQuery(window.location.search); - if (viewer_id && auth_key && id !== `vk:${viewer_id}`) { - const user = yield call(checkIframeToken, { viewer_id, auth_key }); + if (viewer_id && auth_key && id !== `vk:${viewer_id}`) { + const user = yield call(checkIframeToken, { viewer_id, auth_key }); + + if (user) { + yield put(setUser(user)); + + pushLoaderState(99); + + return yield call(mapInitSaga); + } + } + } + + if (id && token) { + const { + data: { user, random_url }, + }: Unwrap = yield call(checkUserToken, { + id, + }); if (user) { - yield put(setUser(user)); + yield put(setUser({ ...user, random_url })); pushLoaderState(99); return yield call(mapInitSaga); + } else if (!ready) { + pushNetworkInitError(); + return; } } + + yield call(generateGuestSaga); + + pushLoaderState(80); + + return yield call(mapInitSaga); + } catch (e) { + console.log(e); } - - if (id && token) { - const { - data: { user, random_url }, - }: Unwrap = yield call(checkUserToken, { - id, - token, - }); - - if (user) { - yield put(setUser({ ...user, random_url })); - - pushLoaderState(99); - - return yield call(mapInitSaga); - } else if (!ready) { - pushNetworkInitError(); - return; - } - } - - yield call(generateGuestSaga); - - pushLoaderState(80); - - return yield call(mapInitSaga); } function* gotVkUserSaga({ user: u }: ReturnType) { @@ -105,58 +111,63 @@ function* gotVkUserSaga({ user: u }: ReturnType } function* searchGetRoutes() { - const { token }: ReturnType = yield select(selectUserUser); + try { + const { + routes: { + step, + shift, + filter: { title, distance, tab }, + }, + }: ReturnType = yield select(selectUser); - const { - routes: { + const result: Unwrap = yield getRouteList({ + search: title, + min: distance[0], + max: distance[1], step, shift, - filter: { title, distance, tab }, - }, - }: ReturnType = yield select(selectUser); + tab, + }); - const result: Unwrap = yield getRouteList({ - token, - search: title, - min: distance[0], - max: distance[1], - step, - shift, - tab, - }); - - return result; + return result; + } catch (e) { + console.log(e); + } } export function* searchSetSagaWorker() { - const { - routes: { filter }, - }: ReturnType = yield select(selectUser); + try { + const { + routes: { filter }, + }: ReturnType = yield select(selectUser); - const { - data: { - routes, - limits: { min, max, count: limit }, - filter: { shift, step }, - }, - }: Unwrap = yield call(searchGetRoutes); + const { + data: { + routes, + limits: { min, max, count: limit }, + filter: { shift, step }, + }, + }: Unwrap = yield call(searchGetRoutes); - yield put(searchPutRoutes({ list: routes, min, max, limit, shift, step })); + yield put(searchPutRoutes({ list: routes, min, max, limit, shift, step })); - // change distange range if needed and load additional data - if ( - (filter.min > min && filter.distance[0] <= filter.min) || - (filter.max < max && filter.distance[1] >= filter.max) - ) { - yield put( - searchChangeDistance([ - filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0], - filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1], - ]) - ); + // change distange range if needed and load additional data + if ( + (filter.min > min && filter.distance[0] <= filter.min) || + (filter.max < max && filter.distance[1] >= filter.max) + ) { + yield put( + searchChangeDistance([ + filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0], + filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1], + ]), + ); + } + + return yield put(searchSetLoading(false)); + } catch (e) { + console.log(e); } - - return yield put(searchSetLoading(false)); } function* searchSetSaga() { @@ -167,26 +178,30 @@ function* searchSetSaga() { } function* openMapDialogSaga({ tab }: ReturnType) { - const { - routes: { - filter: { tab: current }, - }, - }: ReturnType = yield select(selectUser); + try { + const { + routes: { + filter: { tab: current }, + }, + }: ReturnType = yield select(selectUser); - const { dialog_active }: ReturnType = yield select(selectEditor); + const { dialog_active }: ReturnType = yield select(selectEditor); - if (dialog_active && tab === current) { - return yield put(editorSetDialogActive(false)); + if (dialog_active && tab === current) { + return yield put(editorSetDialogActive(false)); + } + + if (tab !== current) { + yield put(searchSetTab(tab)); + } + + yield put(editorSetDialog(DIALOGS.MAP_LIST)); + yield put(editorSetDialogActive(true)); + + return tab; + } catch (e) { + console.log(e); } - - if (tab !== current) { - yield put(searchSetTab(tab)); - } - - yield put(editorSetDialog(DIALOGS.MAP_LIST)); - yield put(editorSetDialogActive(true)); - - return tab; } function* searchSetTabSaga() { @@ -210,78 +225,85 @@ function* setUserSaga() { } function* mapsLoadMoreSaga() { - const { - routes: { limit, list, shift, step, loading, filter }, - }: ReturnType = yield select(selectUser); + try { + const { + routes: { limit, list, shift, step, loading, filter }, + }: ReturnType = yield select(selectUser); - if (loading || list.length >= limit || limit === 0) return; + if (loading || list.length >= limit || limit === 0) return; - yield delay(50); + yield delay(50); - yield put(searchSetLoading(true)); - yield put(mapsSetShift(shift + step)); + yield put(searchSetLoading(true)); + yield put(mapsSetShift(shift + step)); - const { - data: { - limits: { min, max, count }, - filter: { shift: resp_shift, step: resp_step }, - routes, - }, - }: Unwrap = yield call(searchGetRoutes); + const { + data: { + limits: { min, max, count }, + filter: { shift: resp_shift, step: resp_step }, + routes, + }, + }: Unwrap = yield call(searchGetRoutes); + + if ( + (filter.min > min && filter.distance[0] <= filter.min) || + (filter.max < max && filter.distance[1] >= filter.max) + ) { + yield put( + searchChangeDistance([ + filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0], + filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1], + ]), + ); + } - if ( - (filter.min > min && filter.distance[0] <= filter.min) || - (filter.max < max && filter.distance[1] >= filter.max) - ) { yield put( - searchChangeDistance([ - filter.min > min && filter.distance[0] <= filter.min ? min : filter.distance[0], - filter.max < max && filter.distance[1] >= filter.max ? max : filter.distance[1], - ]) + searchPutRoutes({ + min, + max, + limit: count, + shift: resp_shift, + step: resp_step, + list: [...list, ...routes], + }), ); + yield put(searchSetLoading(false)); + } catch (e) { + console.log(e); } - - yield put( - searchPutRoutes({ - min, - max, - limit: count, - shift: resp_shift, - step: resp_step, - list: [...list, ...routes], - }) - ); - yield put(searchSetLoading(false)); } function* dropRouteSaga({ address }: ReturnType) { - const { token }: ReturnType = yield select(selectUserUser); - const { - routes: { - list, - step, - shift, - limit, - filter: { min, max }, - }, - }: ReturnType = yield select(selectUser); - - const index = list.findIndex(el => el.address === address); - - if (index >= 0) { - yield put( - searchPutRoutes({ - list: list.filter(el => el.address !== address), - min, - max, + try { + const { + routes: { + list, step, - shift: shift > 0 ? shift - 1 : 0, - limit: limit > 0 ? limit - 1 : limit, - }) - ); - } + shift, + limit, + filter: { min, max }, + }, + }: ReturnType = yield select(selectUser); - return yield call(dropRoute, { address, token }); + const index = list.findIndex(el => el.address === address); + + if (index >= 0) { + yield put( + searchPutRoutes({ + list: list.filter(el => el.address !== address), + min, + max, + step, + shift: shift > 0 ? shift - 1 : 0, + limit: limit > 0 ? limit - 1 : limit, + }), + ); + } + + return yield call(dropRoute, { address }); + } catch (e) { + console.log(e); + } } function* modifyRouteSaga({ @@ -289,53 +311,59 @@ function* modifyRouteSaga({ title, is_public, }: ReturnType) { - const { token }: ReturnType = yield select(selectUserUser); - const { - routes: { - list, - step, - shift, - limit, - filter: { min, max }, - }, - }: ReturnType = yield select(selectUser); - - const index = list.findIndex(el => el.address === address); - - if (index >= 0) { - yield put( - searchPutRoutes({ - list: list.map(el => (el.address !== address ? el : { ...el, title, is_public })), - min, - max, + try { + const { + routes: { + list, step, - shift: shift > 0 ? shift - 1 : 0, - limit: limit > 0 ? limit - 1 : limit, - }) - ); - } + shift, + limit, + filter: { min, max }, + }, + }: ReturnType = yield select(selectUser); - return yield call(modifyRoute, { address, token, title, is_public }); + const index = list.findIndex(el => el.address === address); + + if (index >= 0) { + yield put( + searchPutRoutes({ + list: list.map(el => (el.address !== address ? el : { ...el, title, is_public })), + min, + max, + step, + shift: shift > 0 ? shift - 1 : 0, + limit: limit > 0 ? limit - 1 : limit, + }), + ); + } + + return yield call(modifyRoute, { address, title, is_public }); + } catch (e) { + console.log(e); + } } function* toggleRouteStarredSaga({ address, }: ReturnType) { - const { token }: ReturnType = yield select(selectUserUser); - const { - routes: { list }, - }: ReturnType = yield select(selectUser); + try { + const { + routes: { list }, + }: ReturnType = yield select(selectUser); - const route = list.find(el => el.address === address); + const route = list.find(el => el.address === address); - yield put(setRouteStarred(address, !route.is_published)); - const result = yield sendRouteStarred({ - token, - address, - is_published: !route.is_published, - }); + yield put(setRouteStarred(address, !route?.is_published)); - if (!result) return yield put(setRouteStarred(address, route.is_published)); + const result = yield sendRouteStarred({ + address, + is_published: !route?.is_published, + }); + + if (!result) return yield put(setRouteStarred(address, !!route?.is_published)); + } catch (e) { + console.log(e); + } } export function* updateUserRoutes() { @@ -350,7 +378,7 @@ export function* userSaga() { yield takeLatest( [USER_ACTIONS.SEARCH_SET_TITLE, USER_ACTIONS.SEARCH_SET_DISTANCE], - searchSetSaga + searchSetSaga, ); yield takeLatest(USER_ACTIONS.OPEN_MAP_DIALOG, openMapDialogSaga); diff --git a/src/utils/api.ts b/src/utils/api.ts deleted file mode 100644 index b6d8c45..0000000 --- a/src/utils/api.ts +++ /dev/null @@ -1,258 +0,0 @@ -import axios from 'axios/index'; -import { API } from '~/constants/api'; -import { IRootState, IRouteListItem } from '~/redux/user'; -import { IUser } from '~/constants/auth'; -import { CLIENT } from '~/config/frontend'; -import { LatLngLiteral } from 'leaflet'; -import { - resultMiddleware, - errorMiddleware, - IResultWithStatus, - configWithToken, -} from './middleware'; -import { IRoute } from '~/redux/map/types'; -import { INominatimResult } from '~/redux/types'; -import { MainMap } from '~/constants/map'; - -const arrayToObject = (array: any[], key: string): {} => - array.reduce((obj, el) => ({ ...obj, [el[key]]: el }), {}); - -interface IGetRouteList { - min: number; - max: number; - tab: string; - search: string; - step: IRootState['routes']['step']; - shift: IRootState['routes']['step']; - token: IRootState['user']['token']; -} - -interface IGetRouteListResult { - min: IRootState['routes']['filter']['min']; - max: IRootState['routes']['filter']['max']; - limit: IRootState['routes']['limit']; - step: IRootState['routes']['step']; - shift: IRootState['routes']['shift']; - list: IRootState['routes']['list']; -} - -export const checkUserToken = ({ - id, - token, -}: { - id: IRootState['user']['id']; - token: IRootState['user']['token']; -}): Promise> => - axios - .get(API.CHECK_TOKEN, { - params: { id, token }, - }) - .then(resultMiddleware) - .catch(errorMiddleware); - -export const getGuestToken = (): Promise> => - axios - .get(API.GET_GUEST) - .then(resultMiddleware) - .catch(errorMiddleware); - -export const getStoredMap = ({ - name, -}: { - name: IRoute['address']; -}): Promise> => - axios - .get(API.GET_MAP, { - params: { name }, - }) - .then(resultMiddleware) - .catch(errorMiddleware); - -export const postMap = ({ - title, - address, - route, - stickers, - force, - logo, - distance, - provider, - is_public, - description, - token, -}: Partial & { - force: boolean; - token: string; -}): Promise> => - axios - .post( - API.POST_MAP, - { - route: { - title, - address, - route, - stickers, - logo, - distance, - provider, - is_public, - description, - }, - force, - }, - configWithToken(token) - ) - .then(resultMiddleware) - .catch(errorMiddleware); - -export const checkIframeToken = ({ - viewer_id, - auth_key, -}: { - viewer_id: string; - auth_key: string; -}) => - axios - .get(API.IFRAME_LOGIN_VK, { - params: { viewer_id, auth_key }, - }) - .then(result => result && result.data && result.data.success && result.data.user) - .catch(() => false); - -export const getRouteList = ({ - search, - min, - max, - tab, - token, - step, - shift, -}: IGetRouteList): Promise> => - axios - .get( - API.GET_ROUTE_LIST(tab), - configWithToken(token, { - params: { - search, - min, - max, - token, - step, - shift, - }, - }) - ) - .then(resultMiddleware) - .catch(errorMiddleware); - -export const checkOSRMService = (bounds: LatLngLiteral[]): Promise => - CLIENT && - CLIENT.OSRM_URL && - axios - .get(CLIENT.OSRM_TEST_URL(bounds)) - .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', - dedupe: 1, - }, - }) - .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 } })) - .then(resultMiddleware) - .catch(errorMiddleware); - -export const modifyRoute = ({ - address, - token, - title, - is_public, -}: { - address: string; - token: string; - title: string; - is_public: boolean; -}): Promise> => - axios - .patch(API.MODIFY_ROUTE, { address, token, is_public, title }, configWithToken(token)) - .then(resultMiddleware) - .catch(errorMiddleware); - -export const sendRouteStarred = ({ - token, - address, - is_published, -}: { - token: string; - address: string; - is_published: boolean; -}): Promise> => - axios - .post(API.SET_STARRED, { address, is_published }, configWithToken(token)) - .then(resultMiddleware) - .catch(errorMiddleware); diff --git a/src/utils/api/index.ts b/src/utils/api/index.ts new file mode 100644 index 0000000..fae0f68 --- /dev/null +++ b/src/utils/api/index.ts @@ -0,0 +1,214 @@ +import { API } from '~/constants/api'; +import { IRootState, IRouteListItem } from '~/redux/user'; +import { IUser } from '~/constants/auth'; +import { CLIENT } from '~/config/frontend'; +import { LatLngLiteral } from 'leaflet'; +import { IRoute } from '~/redux/map/types'; +import { INominatimResult } from '~/redux/types'; +import { api } from './instance'; + +interface IGetRouteList { + min: number; + max: number; + tab: string; + search: string; + step: IRootState['routes']['step']; + shift: IRootState['routes']['step']; +} + +export const checkUserToken = ({ + id, +}: { + id: IRootState['user']['id']; +}) => + api + .get<{ + user: IUser; + random_url: string; + routes: IRouteListItem[]; + }>(API.CHECK_TOKEN, { + params: { id }, + }); + +export const getGuestToken = () => + api + .get<{ + user: IUser; + random_url: string; + }>(API.GET_GUEST); + +export const getStoredMap = ({ + name, +}: { + name: IRoute['address']; +}) => + api + .get<{ + route: IRoute; + error?: string; + random_url: string; + }>(API.GET_MAP, { + params: { name }, + }); + +export const postMap = ({ + title, + address, + route, + stickers, + force, + logo, + distance, + provider, + is_public, + description, +}: Partial & { + force: boolean; +}) => + api + .post<{ + route: IRoute; + error?: string; + code?: string; + }>( + API.POST_MAP, + { + route: { + title, + address, + route, + stickers, + logo, + distance, + provider, + is_public, + description, + }, + force, + }, + ); + +export const checkIframeToken = ({ + viewer_id, + auth_key, +}: { + viewer_id: string; + auth_key: string; +}) => + api + .get<{ + success: boolean, + user: IUser, + }>(API.IFRAME_LOGIN_VK, { + params: { viewer_id, auth_key }, + }) + .then(result => !!result.data.success && !!result.data.user) + .catch(() => false); + +export const getRouteList = ({ + search, + min, + max, + tab, + step, + shift, +}: IGetRouteList) => + api + .get<{ + routes: IRoute[]; + limits: { + min: number; + max: number; + count: number; + }; + filter: { + min: number; + max: number; + shift: number; + step: number; + }; + }>( + API.GET_ROUTE_LIST(tab), + { + params: { + search, + min, + max, + step, + shift, + }, + }, + ); + +export const checkOSRMService = (bounds: LatLngLiteral[]) => + !!CLIENT && + !!CLIENT.OSRM_URL && + api + .get(CLIENT.OSRM_TEST_URL(bounds)) + .then(() => true) + .catch(() => false); + +export const checkNominatimService = () => + !!CLIENT && + !!CLIENT.NOMINATIM_TEST_URL && + api + .get(CLIENT.NOMINATIM_TEST_URL) + .then(() => true) + .catch(() => false); + +export const searchNominatim = (query: string) => + CLIENT && + CLIENT.NOMINATIM_URL && + api + .get(`${CLIENT.NOMINATIM_URL} ${query}`, { + params: { + format: 'json', + country_code: 'ru', + 'accept-language': 'ru_RU', + dedupe: 1, + }, + }) + .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 }: { address: string }) => + api + .delete(API.DROP_ROUTE, { data: { address } }); + +export const modifyRoute = ({ + address, + title, + is_public, +}: { + address: string; + title: string; + is_public: boolean; +}) => + api + .patch<{ + route: IRoute; + }>(API.MODIFY_ROUTE, { address, is_public, title }); + +export const sendRouteStarred = ({ + address, + is_published, +}: { + address: string; + is_published: boolean; +}) => + api + .post<{ route: IRoute }>(API.SET_STARRED, { address, is_published }); diff --git a/src/utils/api/instance.ts b/src/utils/api/instance.ts new file mode 100644 index 0000000..ee2bb46 --- /dev/null +++ b/src/utils/api/instance.ts @@ -0,0 +1,6 @@ +import axios from 'axios'; +import { CLIENT } from '~/config/frontend'; + +export const api = axios.create({ + baseURL: CLIENT.API_ADDR, +}) diff --git a/src/utils/context.ts b/src/utils/context.ts index 6631da9..2310b3a 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -1,5 +1,5 @@ import React from 'react'; import { Map, TileLayer } from 'leaflet'; -export const MapContext = React.createContext(null); -export const TileContext = React.createContext(null) \ No newline at end of file +export const MapContext = React.createContext(undefined); +export const TileContext = React.createContext(undefined) diff --git a/src/utils/gpx.ts b/src/utils/gpx.ts index 67e3560..240b806 100644 --- a/src/utils/gpx.ts +++ b/src/utils/gpx.ts @@ -39,7 +39,7 @@ export const getGPXString = ({ ${title || 'GPX Track'} - ${stickers.reduce( + ${(stickers || []).reduce( (cat, { latlng: { lat, lng }, text }) => `${cat} @@ -93,12 +93,12 @@ export const importGpxTrack = async (file: File) => { return trkseg.trkpt ? [ ...trkseg_res, - ...trkseg.trkpt.map(pnt => ({ lat: pnt['$'].lat, lng: pnt['$'].lon })), + ...trkseg.trkpt.map(pnt => new LatLng(pnt['$'].lat, pnt['$'].lon)), ] : trkseg_res; }, trk_res) : trk_res; - }, []); + }, [] as LatLng[]); return [ { diff --git a/src/utils/history.ts b/src/utils/history.ts index c23be92..c2435c9 100644 --- a/src/utils/history.ts +++ b/src/utils/history.ts @@ -36,20 +36,20 @@ export const parseQuery = (queryString: string) => { }; export const pushLoaderState = (state: number) => { - document.getElementById('loader-bar').style.width = `${state}%`; + document.getElementById('loader-bar')!.style.width = `${state}%`; }; export const countDownToRefresh = (left: number = API_RETRY_INTERVAL): void => { if (left <= 0) return document.location.reload(); - document.getElementById('loader-bar').style.width = `${(left / API_RETRY_INTERVAL) * 100}%`; + document.getElementById('loader-bar')!.style.width = `${(left / API_RETRY_INTERVAL) * 100}%`; setTimeout(() => countDownToRefresh(left - 0.25), 1000); }; export const pushNetworkInitError = () => { - document.getElementById('loader-bar').classList.add('is_failed'); - document.getElementById('loader-bar').style.width = '100%'; - document.getElementById('loader-error').style.opacity = String(1); + document.getElementById('loader-bar')!.classList.add('is_failed'); + document.getElementById('loader-bar')!.style.width = '100%'; + document.getElementById('loader-error')!.style.opacity = String(1); countDownToRefresh(); }; diff --git a/src/utils/map/ArrowsLayer.ts b/src/utils/map/ArrowsLayer.ts index ef46715..435e8c2 100644 --- a/src/utils/map/ArrowsLayer.ts +++ b/src/utils/map/ArrowsLayer.ts @@ -9,6 +9,7 @@ import { LatLng, LatLngLiteral, LayerGroup, Map, Marker } from 'leaflet'; import { arrowClusterIcon, createArrow } from '~/utils/arrow'; import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js'; import { angleBetweenPoints, dist2, middleCoord } from '~/utils/geom'; +import { MainMap } from '~/constants/map'; class ArrowsLayer extends LayerGroup { constructor(props) { @@ -46,13 +47,13 @@ class ArrowsLayer extends LayerGroup { ), ] : res, - [] + [] as Marker[] ); this.arrowLayer.addLayers(midpoints); }; - map: Map; + map: Map = MainMap; arrowLayer = new MarkerClusterGroup({ spiderfyOnMaxZoom: false, showCoverageOnHover: false, @@ -62,10 +63,10 @@ class ArrowsLayer extends LayerGroup { iconCreateFunction: arrowClusterIcon, }); - layers: Marker[] = []; + layers: Marker[] = []; } -ArrowsLayer.addInitHook(function() { +ArrowsLayer.addInitHook(function(this: ArrowsLayer) { this.once('add', event => { if (event.target instanceof ArrowsLayer) { this.map = event.target._map; diff --git a/src/utils/map/InteractivePoly.ts b/src/utils/map/InteractivePoly.ts index de5b064..fa445bc 100644 --- a/src/utils/map/InteractivePoly.ts +++ b/src/utils/map/InteractivePoly.ts @@ -36,9 +36,9 @@ class InteractivePoly extends Polyline { this.constraintsStyle = { ...this.constraintsStyle, - ...options.constraintsStyle, + ...(options?.constraintsStyle || {}), }; - this.maxMarkers = options.maxMarkers || this.maxMarkers; + this.maxMarkers = options?.maxMarkers || this.maxMarkers; this.constrLine = new Polyline([], this.constraintsStyle); @@ -162,10 +162,12 @@ class InteractivePoly extends Polyline { ? { ...obj, hidden: [...obj.hidden, marker] } : { ...obj, visible: [...obj.visible, marker] }; }, - { visible: [], hidden: [] } + { visible: [], hidden: [] } as Record<'visible' | 'hidden', Marker[]> ); - if (visible.length > this.maxMarkers) return this.hideAllMarkers(); + if (visible.length > (this.maxMarkers || 2)) { + return this.hideAllMarkers(); + } this.showAllMarkers(); @@ -337,11 +339,11 @@ class InteractivePoly extends Polyline { onMarkerDrag = ({ target }: { target: Marker }) => { const coords = new Array(0) - .concat((this.vertex_index > 0 && this.markers[this.vertex_index - 1].getLatLng()) || []) + .concat((this.vertex_index! > 0 && this.markers[this.vertex_index! - 1].getLatLng()) || []) .concat(target.getLatLng()) .concat( - (this.vertex_index < this.markers.length - 1 && - this.markers[this.vertex_index + 1].getLatLng()) || + (this.vertex_index! < this.markers.length - 1 && + this.markers[this.vertex_index! + 1].getLatLng()) || [] ); @@ -369,17 +371,17 @@ class InteractivePoly extends Polyline { onMarkerDragEnd = ({ target }: { target: Marker }): void => { const latlngs = this.getLatLngs() as LatLngLiteral[]; this.markerDragChangeDistance( - this.vertex_index, - latlngs[this.vertex_index], + this.vertex_index!, + latlngs[this.vertex_index!], target.getLatLng() ); - this.replaceLatlng(target.getLatLng(), this.vertex_index); + this.replaceLatlng(target.getLatLng(), this.vertex_index!); this.is_dragging = false; this.constrLine.removeFrom(this._map); - this.vertex_index = null; + this.vertex_index = 0; if (this.is_drawing) this.startDrawing(); @@ -496,7 +498,7 @@ class InteractivePoly extends Polyline { this.constrLine.setLatLngs(coords); }; - setDirection = (direction: 'forward' | 'backward') => { + setDirection = (direction: 'forward' | 'backward') => { this.drawing_direction = direction; this.updateConstraintsToLatLngs(this.getLatLngs() as LatLngExpression[]); } @@ -568,27 +570,27 @@ class InteractivePoly extends Polyline { is_drawing: boolean = false; drawing_direction: 'forward' | 'backward' = 'forward'; - vertex_index?: number = null; + vertex_index: number = 0; - hint_prev_marker: number = null; + hint_prev_marker: number = 0; distance: number = 0; } -InteractivePoly.addInitHook(function() { +InteractivePoly.addInitHook(function(this: InteractivePoly) { this.once('add', event => { if (event.target instanceof InteractivePoly) { - this.map = event.target._map; + this._map = event.target._map; - this.map.on('touch', console.log); + this._map.on('touch', console.log); this.markerLayer.addTo(event.target._map); this.hintMarker.addTo(event.target._map); this.constrLine.addTo(event.target._map); this.touchHinter.addTo(event.target._map); - this.map.on('moveend', this.updateMarkers); + this._map.on('moveend', this.updateMarkers); - this.on('latlngschange', this.updateTouchHinter); + this.on('latlngschange' as any, this.updateTouchHinter as any); if (this.touchHinter && window.innerWidth < 768) { try { @@ -605,7 +607,7 @@ InteractivePoly.addInitHook(function() { this.constrLine.removeFrom(this._map); this.touchHinter.removeFrom(this._map); - this.map.off('moveend', this.updateMarkers); + this._map.off('moveend', this.updateMarkers); } }); }); diff --git a/src/utils/marks.ts b/src/utils/marks.ts index f3df6cf..961e7fe 100644 --- a/src/utils/marks.ts +++ b/src/utils/marks.ts @@ -3,6 +3,7 @@ import { arrowClusterIcon } from '~/utils/arrow'; import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js'; import { allwaysPositiveAngleDeg, angleBetweenPoints, distKmHaversine } from '~/utils/geom'; import classNames from 'classnames'; +import { MainMap } from '~/constants/map'; const arrow_image = '/images/arrow.svg'; @@ -43,8 +44,7 @@ class KmMarksLayer extends LayerGroup { }; drawMiddleMarkers = (latlngs: LatLngLiteral[]) => { - const marks = []; - const arrows = []; + const marks: Marker[] = []; let last_km_mark = 0; this.distance = latlngs.reduce((dist, current, index) => { @@ -160,7 +160,7 @@ class KmMarksLayer extends LayerGroup { }; options: KmMarksOptions; - map: Map; + map: Map = MainMap; marksLayer: MarkerClusterGroup = new MarkerClusterGroup({ spiderfyOnMaxZoom: false, showCoverageOnHover: false, @@ -173,7 +173,7 @@ class KmMarksLayer extends LayerGroup { distance: number = 0; } -KmMarksLayer.addInitHook(function() { +KmMarksLayer.addInitHook(function(this: KmMarksLayer) { this.once('add', event => { if (event.target instanceof KmMarksLayer) { this.map = event.target._map; diff --git a/src/utils/middleware.ts b/src/utils/middleware.ts index 1a2fa1f..7504f3e 100644 --- a/src/utils/middleware.ts +++ b/src/utils/middleware.ts @@ -1,4 +1,4 @@ -import { AxiosRequestConfig } from "axios"; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; export type Unwrap = T extends (...args: any[]) => Promise ? U : T; @@ -23,28 +23,3 @@ export const HTTP_RESPONSES = { NOT_FOUND: 404, TOO_MANY_REQUESTS: 429, }; - -export const resultMiddleware = (({ - status, - data, -}: { - status: number; - data: T; -}): { status: number; data: T } => ({ status, data })); - -export const errorMiddleware = (debug): IResultWithStatus => (debug && debug.response - ? debug.response - : { - status: HTTP_RESPONSES.CONNECTION_REFUSED, - data: {}, - debug, - error: 'Ошибка сети', - }); - -export const configWithToken = ( - token: string, - config: AxiosRequestConfig = {}, -): AxiosRequestConfig => ({ - ...config, - headers: { ...(config.headers || {}), Authorization: `${token}` }, -}); \ No newline at end of file diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 4c6ffb0..f76f0bc 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -133,7 +133,7 @@ export const fetchImages = ( ): Promise<{ x: number; y: number; image: HTMLImageElement }[]> => { const { minX, maxX, minY, maxY, zoom } = geometry; - const images = []; + const images: { x: number; y: number; source: string }[] = []; for (let x = minX; x <= maxX; x += 1) { for (let y = minY; y <= maxY; y += 1) { images.push({ x, y, source: getImageSource({ x, y, zoom }, provider) }); @@ -173,7 +173,7 @@ export const composePoly = ({ ctx, color = 'gradient', weight = CLIENT.STROKE_WIDTH, - dash = null, + dash = [], }: { points: Point[]; ctx: CanvasRenderingContext2D; @@ -472,14 +472,14 @@ export const composeStickers = async ({ if (!stickers || stickers.length < 0) return; stickers.map(({ x, y, angle, text }) => { - composeStickerArrow(ctx, x, y, angle, zoom); + composeStickerArrow(ctx, x, y, angle || 0, zoom); - if (text) composeStickerText(ctx, x, y, angle, text, zoom); + if (text) composeStickerText(ctx, x, y, angle || 0, text, zoom); }); await Promise.all( stickers.map(({ x, y, angle, set, sticker }) => - composeStickerImage(ctx, x, y, angle, set, sticker, zoom) + composeStickerImage(ctx, x, y, angle || 0, set, sticker, zoom) ) ); }; diff --git a/src/utils/window.ts b/src/utils/window.ts index aef72df..96aa295 100644 --- a/src/utils/window.ts +++ b/src/utils/window.ts @@ -3,10 +3,10 @@ import { LatLngLiteral } from 'leaflet'; export const isMobile = (): boolean => window.innerWidth <= MOBILE_BREAKPOINT; -export const getLocation = (callback: (pos: LatLngLiteral) => void) => { +export const getLocation = (callback: (pos: LatLngLiteral | undefined) => void) => { window.navigator.geolocation.getCurrentPosition(position => { if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude) - return callback(null); + return callback(undefined); const { latitude: lat, longitude: lng } = position.coords; @@ -15,18 +15,18 @@ export const getLocation = (callback: (pos: LatLngLiteral) => void) => { }); }; -export const watchLocation = (callback: (pos: LatLngLiteral) => void): number => { +export const watchLocation = (callback: (pos: LatLngLiteral | undefined) => void): number => { return window.navigator.geolocation.watchPosition( position => { if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude) - return callback(null); + return callback(undefined); const { latitude: lat, longitude: lng } = position.coords; callback({ lat, lng }); return; }, - () => callback(null), + () => callback(undefined), { timeout: 30, } diff --git a/tsconfig.json b/tsconfig.json index 5e8c822..9660a61 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext",