diff --git a/.flowconfig b/.flowconfig index a8f2043..b44871f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -2,17 +2,17 @@ module.system.node.resolve_dirname=node_modules module.system.node.resolve_dirname=src module.name_mapper='^$redux/\([-a-zA-Z0-9$_/]+\)$' -> '/src/redux/\1' -module.name_mapper='^components\/\(.*\)$' ->'/src/components/\1' -module.name_mapper='^containers\/\(.*\)$' ->'/src/containers/\1' -module.name_mapper='^constants\/\(.*\)$' ->'/src/constants/\1' -module.name_mapper='^sprites\/\(.*\)$' ->'/src/sprites/\1' -module.name_mapper='^config\/\(.*\)$' ->'/src/config/\1' -module.name_mapper='^styles\/\(.*\)$' ->'/src/styles/\1' -module.name_mapper='^utils\/\(.*\)$' ->'/src/utils/\1' +module.name_mapper='^$config/\([-a-zA-Z0-9$_/]+\)$' -> '/config/\1' +module.name_mapper='^$components\/\(.*\)$' ->'/src/components/\1' +module.name_mapper='^$containers\/\(.*\)$' ->'/src/containers/\1' +module.name_mapper='^$constants\/\(.*\)$' ->'/src/constants/\1' +module.name_mapper='^$sprites\/\(.*\)$' ->'/src/sprites/\1' +module.name_mapper='^$config\/\(.*\)$' ->'/src/config/\1' +module.name_mapper='^$styles\/\(.*\)$' ->'/src/styles/\1' +module.name_mapper='^$utils\/\(.*\)$' ->'/src/utils/\1' [ignore] .*/node_modules -.*/node_modules/styled-components/.* [include] public diff --git a/backend/models/Route.js b/backend/models/Route.js index 3e3686f..318b8f1 100644 --- a/backend/models/Route.js +++ b/backend/models/Route.js @@ -13,6 +13,7 @@ const RouteSchema = new Schema( owner: { type: Schema.Types.ObjectId, ref: 'User' }, logo: { type: String, default: 'DEFAULT' }, distance: { type: Number, default: 0 }, + public: { type: Boolean, default: true }, }, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } diff --git a/backend/routes/route/get.js b/backend/routes/route/get.js index 57fbd48..fc14681 100644 --- a/backend/routes/route/get.js +++ b/backend/routes/route/get.js @@ -1,3 +1,4 @@ +const { generateRandomUrl } = require('../auth/guest'); const { Route } = require('../../models'); module.exports = async (req, res) => { @@ -9,6 +10,7 @@ module.exports = async (req, res) => { if (!exists) return res.send({ success: false, mode: 'not_found_2' }); const data = exists.toObject(); + const random_url = await generateRandomUrl(); return res.send({ success: true, @@ -17,7 +19,8 @@ module.exports = async (req, res) => { owner: { ...data.owner, id: data.owner._id, - } + }, + random_url, }); }; diff --git a/src/components/dialogs/MapListDialog.jsx b/src/components/dialogs/MapListDialog.jsx new file mode 100644 index 0000000..c65d605 --- /dev/null +++ b/src/components/dialogs/MapListDialog.jsx @@ -0,0 +1,35 @@ +// @flow +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { RouteRow } from '$components/maps/RouteRow'; +import type { Route } from '$constants/types'; + +type Props = { + routes: { [id: String]: Route }, + editing: Boolean, +}; + +const Component = ({ routes, editing }: Props) => ( +
+ { + Object.keys(routes).map(id => ( + + )) + } +
+); + +const mapStateToProps = ({ user: { editing, user: { routes } } }) => ({ + routes, editing, +}); + +const mapDispatchToProps = dispatch => bindActionCreators({ + +}, dispatch); + +export const MapListDialog = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/maps/RouteRow.jsx b/src/components/maps/RouteRow.jsx new file mode 100644 index 0000000..245bdcd --- /dev/null +++ b/src/components/maps/RouteRow.jsx @@ -0,0 +1,38 @@ +// @flow +import React from 'react'; +import { Icon } from '$components/panels/Icon'; +import { pushPath } from '$utils/history'; + +type Props = { + title: String, + distance: Number, + created_at: String, + _id: String, + editing: Boolean, +}; + +export const RouteRow = ({ + title, distance, created_at, _id, editing +}: Props) => ( +
pushPath(`/${_id}/${editing ? 'edit' : ''}`)} + > +
+ {title || _id} +
+
+ + + {_id} + +
+
+ + + {(distance && `${distance} km`) || 'N/A'} + +
+ +
+); diff --git a/src/components/panels/UserPanel.jsx b/src/components/panels/UserPanel.jsx index d6d9da8..538f778 100644 --- a/src/components/panels/UserPanel.jsx +++ b/src/components/panels/UserPanel.jsx @@ -4,7 +4,7 @@ import { GuestButton } from '$components/user/GuestButton'; import { DEFAULT_USER, ROLES } from '$constants/auth'; import { UserButton } from '$components/user/UserButton'; import { UserMenu } from '$components/user/UserMenu'; -import { setUser, userLogout, takeAShot } from '$redux/user/actions'; +import { setUser, userLogout, takeAShot, setDialog } from '$redux/user/actions'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import type { UserType } from '$constants/types'; @@ -12,12 +12,14 @@ import { Icon } from '$components/panels/Icon'; import classnames from 'classnames'; import { CLIENT } from '$config/frontend'; +import { DIALOGS } from '$constants/dialogs'; type Props = { user: UserType, userLogout: Function, setUser: Function, - takeAShot: Function, + setDialog: Function, + dialog: String, }; export class Component extends React.PureComponent { @@ -54,6 +56,9 @@ export class Component extends React.PureComponent { } setMenuOpened = () => this.setState({ menuOpened: !this.state.menuOpened }); + openMapsDialog = () => { + this.props.setDialog({ dialog: DIALOGS.MAP_LIST }); + }; openOauthFrame = () => { const width = parseInt(window.innerWidth, 10); @@ -78,7 +83,7 @@ export class Component extends React.PureComponent { return (
-
+
{ !user || user.role === ROLES.guest @@ -96,7 +101,7 @@ export class Component extends React.PureComponent {
@@ -108,7 +113,12 @@ export class Component extends React.PureComponent { } -const mapStateToProps = ({ user: { user } }) => ({ user }); -const mapDispatchToProps = dispatch => bindActionCreators({ setUser, userLogout, takeAShot }, dispatch); +const mapStateToProps = ({ user: { dialog, user } }) => ({ dialog, user }); +const mapDispatchToProps = dispatch => bindActionCreators({ + setUser, + userLogout, + takeAShot, + setDialog, +}, dispatch); export const UserPanel = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/constants/dialogs.js b/src/constants/dialogs.js new file mode 100644 index 0000000..65d9fa6 --- /dev/null +++ b/src/constants/dialogs.js @@ -0,0 +1,5 @@ +// @flow +export const DIALOGS = ({ + NONE: 'NONE', + MAP_LIST: 'MAP_LIST', +}: { [key: String]: String }); diff --git a/src/constants/modes.js b/src/constants/modes.js index 32c0ee4..1be7253 100644 --- a/src/constants/modes.js +++ b/src/constants/modes.js @@ -1,4 +1,6 @@ -export const MODES = { +// @flow + +export const MODES = ({ POLY: 'POLY', STICKERS: 'STICKERS', ROUTER: 'ROUTER', @@ -9,4 +11,4 @@ export const MODES = { SAVE: 'SAVE', CONFIRM_CANCEL: 'CONFIRM_CANCEL', PROVIDER: 'PROVIDER', -}; +}: { [key: String]: String }); diff --git a/src/constants/types.js b/src/constants/types.js index 6ea581d..0f1ca0b 100644 --- a/src/constants/types.js +++ b/src/constants/types.js @@ -14,3 +14,18 @@ export type UserType = { photo: String, } }; + +type Path = Array<{ lat: Number, lng: Number }>; +type Stickers = Array; + +export type Route = { + _id: String, + title: String, + version: Number, + stickers: Array, + route: Array, + logo: String, + distance: Number, + created_at: String, + updated_at: String, +} diff --git a/src/containers/App.jsx b/src/containers/App.jsx index 6583714..9bf6897 100644 --- a/src/containers/App.jsx +++ b/src/containers/App.jsx @@ -12,11 +12,15 @@ import { hot } from 'react-hot-loader'; import { Renderer } from '$components/renderer/Renderer'; import { hideRenderer } from '$redux/user/actions'; import { Cursor } from '$components/Cursor'; +import { LeftDialog } from '$containers/LeftDialog'; +import { DIALOGS } from '$constants/dialogs'; type Props = { renderer_active: Boolean, hideRenderer: Function, mode: String, + dialog: String, + dialog_active: Boolean, } const Component = (props: Props) => ( @@ -26,15 +30,20 @@ const Component = (props: Props) => ( + - { props.renderer_active && } + { props.renderer_active && + + } ); - -const mapStateToProps = ({ user }) => ({ - renderer_active: user.renderer.renderer_active, - mode: user.mode, +const mapStateToProps = ({ user: { mode, dialog, dialog_active, renderer } }) => ({ + renderer_active: renderer.renderer_active, + mode, + dialog, + dialog_active, }); + const mapDispatchToProps = dispatch => bindActionCreators({ hideRenderer }, dispatch); export const App = connect(mapStateToProps, mapDispatchToProps)(hot(module)(Component)); diff --git a/src/containers/LeftDialog.jsx b/src/containers/LeftDialog.jsx new file mode 100644 index 0000000..3d1e54a --- /dev/null +++ b/src/containers/LeftDialog.jsx @@ -0,0 +1,16 @@ +// @flow +import React from 'react'; +import { DIALOGS } from '$constants/dialogs'; +import { MapListDialog } from '$components/dialogs/MapListDialog'; +import classnames from 'classnames'; + +type Props = { + dialog: String, + dialog_active: Boolean, +} + +export const LeftDialog = ({ dialog, dialog_active }: Props) => ( +
+ { dialog === DIALOGS.MAP_LIST && } +
+); diff --git a/src/modules/Editor.js b/src/modules/Editor.js index 029429f..f4c8bec 100644 --- a/src/modules/Editor.js +++ b/src/modules/Editor.js @@ -218,7 +218,6 @@ export class Editor { this.stickers.clearAll(); }; - setData = ({ route, stickers, version = 1, owner, title, address }) => { @@ -272,19 +271,21 @@ export class Editor { this.setInitialData(); - const url = (this.owner && this.owner.id === id) ? path : random_url; + // const url = (this.owner && this.owner.id === id) ? path : random_url; + this.owner = { id }; - pushPath(`/${url}/edit`); + // console.log('PATH IS', path); + // pushPath(`/${url}/edit`); if (this.poly.latlngs && this.poly.latlngs.length > 1) this.poly.poly.enableEdit(); - this.stickers.startEditing(); }; stopEditing = () => { - const { path } = getUrlData(); + // const { path } = getUrlData(); - pushPath(`/${(this.initialData && this.initialData.path) || path}`); + // console.log('PATH IS', path, this.initialData.path); + // pushPath(`/${(this.initialData && this.initialData.path) || path}`); this.poly.poly.disableEdit(); this.stickers.stopEditing(); diff --git a/src/redux/store.js b/src/redux/store.js index 1cbbbb4..d73b687 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -6,6 +6,8 @@ import createSagaMiddleware from 'redux-saga'; import { userReducer } from '$redux/user/reducer'; import { userSaga } from '$redux/user/sagas'; +import { createBrowserHistory } from 'history'; +import { locationChanged } from '$redux/user/actions'; const userPersistConfig = { key: 'user', @@ -42,3 +44,8 @@ export function configureStore() { return { store, persistor }; } + +export const history = createBrowserHistory(); +export const historyListener = history.listen(location => { + store.dispatch(locationChanged(location.pathname)); +}); diff --git a/src/redux/user/actions.js b/src/redux/user/actions.js index d244384..e33b80a 100644 --- a/src/redux/user/actions.js +++ b/src/redux/user/actions.js @@ -39,3 +39,7 @@ export const takeAShot = () => ({ type: ACTIONS.TAKE_A_SHOT }); export const cropAShot = payload => ({ type: ACTIONS.CROP_A_SHOT, ...payload }); export const setProvider = provider => ({ type: ACTIONS.SET_PROVIDER, provider }); + +export const setDialog = ({ dialog, dialog_active }) => ({ type: ACTIONS.SET_DIALOG, dialog }); +export const locationChanged = location => ({ type: ACTIONS.LOCATION_CHANGED, location }); +export const setReady = ready => ({ type: ACTIONS.SET_READY, ready }); diff --git a/src/redux/user/constants.js b/src/redux/user/constants.js index b66cb41..ab38e09 100644 --- a/src/redux/user/constants.js +++ b/src/redux/user/constants.js @@ -1,4 +1,6 @@ -export const ACTIONS = { +// @flow + +export const ACTIONS = ({ SET_USER: 'SET_USER', USER_LOGOUT: 'USER_LOGOUT', @@ -38,4 +40,8 @@ export const ACTIONS = { CROP_A_SHOT: 'CROP_A_SHOT', SET_PROVIDER: 'SET_PROVIDER', -}; + + SET_DIALOG: 'SET_DIALOG', + LOCATION_CHANGED: 'LOCATION_CHANGED', + SET_READY: 'SET_READY', +}: { [key: String]: String }); diff --git a/src/redux/user/reducer.js b/src/redux/user/reducer.js index dc70f6b..86081bf 100644 --- a/src/redux/user/reducer.js +++ b/src/redux/user/reducer.js @@ -1,3 +1,4 @@ +// @flow import { createReducer } from 'reduxsauce'; import { ACTIONS } from '$redux/user/constants'; import { DEFAULT_USER } from '$constants/auth'; @@ -5,6 +6,7 @@ import { MODES } from '$constants/modes'; import { DEFAULT_LOGO } from '$constants/logos'; import { TIPS } from '$constants/tips'; import { DEFAULT_PROVIDER } from '$constants/providers'; +import { DIALOGS } from '$constants/dialogs'; const getEstimated = distance => { const time = (distance && (distance / 15)) || 0; @@ -73,7 +75,18 @@ const setRenderer = (state, { payload }) => ({ const setProvider = (state, { provider }) => ({ ...state, provider }); -const HANDLERS = { +const setDialog = (state, { dialog, dialog_active }) => ({ + ...state, + dialog: dialog || state.dialog, + dialog_active: typeof dialog_active !== 'undefined' ? dialog_active : !state.dialog_active, +}); + +const setReady = (state, { ready = true }) => ({ + ...state, + ready, +}); + +const HANDLERS = ({ [ACTIONS.SET_USER]: setUser, [ACTIONS.SET_EDITING]: setEditing, [ACTIONS.SET_CHANGED]: setChanged, @@ -96,9 +109,13 @@ const HANDLERS = { [ACTIONS.SET_RENDERER]: setRenderer, [ACTIONS.SET_PROVIDER]: setProvider, -}; + + [ACTIONS.SET_DIALOG]: setDialog, + [ACTIONS.SET_READY]: setReady, +}: { [key: String]: Function }); export const INITIAL_STATE = { + ready: false, user: { ...DEFAULT_USER }, editing: false, mode: MODES.NONE, @@ -117,6 +134,9 @@ export const INITIAL_STATE = { save_overwriting: false, save_processing: false, + dialog: DIALOGS.NONE, + dialog_active: false, + renderer: { data: '', width: 0, diff --git a/src/redux/user/sagas.js b/src/redux/user/sagas.js index 8f6879e..bf5b90f 100644 --- a/src/redux/user/sagas.js +++ b/src/redux/user/sagas.js @@ -7,7 +7,7 @@ import { setActiveSticker, setAddress, setChanged, setEditing, - setMode, setRenderer, + setMode, setReady, setRenderer, setSaveError, setSaveOverwrite, setSaveSuccess, setTitle, setUser @@ -62,12 +62,15 @@ function* startEmptyEditorSaga() { } function* startEditingSaga() { - yield editor.startEditing(); - yield put(setEditing(true)); + // yield put(setEditing(true)); + // yield editor.startEditing(); + const { path } = getUrlData(); + yield pushPath(`/${path}/edit`); } function* stopEditingSaga() { const { changed, editing, mode } = yield select(getState); + const { path } = getUrlData(); if (!editing) return; if (changed && mode !== MODES.CONFIRM_CANCEL) { @@ -76,40 +79,59 @@ function* stopEditingSaga() { } yield editor.cancelEditing(); - yield put(setMode(MODES.NONE)); - yield put(setChanged(false)); - yield put(setEditing(editor.hasEmptyHistory)); // don't close editor if no previous map + + yield pushPath(`/${path}/`); +} + +function* loadMapSaga(path) { + const map = yield call(getStoredMap, { name: path }); + + if (map) { + yield editor.setData(map); + yield editor.fitDrawing(); + yield put(setChanged(false)); + } + + return map; } function* mapInitSaga() { const { path, mode } = getUrlData(); if (path) { - const map = yield call(getStoredMap, { name: path }); + const map = yield call(loadMapSaga, path); + // const map = yield call(getStoredMap, { name: path }); if (map) { - yield editor.setData(map); - yield editor.fitDrawing(); - yield put(setChanged(false)); + // yield editor.setData(map); + // yield editor.fitDrawing(); + // yield put(setChanged(false)); if (mode && mode === 'edit') { - yield call(startEditingSaga); + yield put(setEditing(true)); + editor.startEditing(); + // yield call(startEditingSaga); // <-- this is working // yield put(setEditing(true)); // editor.startEditing(); } else { - yield put(setEditing(false)); + // yield put(setEditing(false)); // <-- this is working // yield call(stopEditingSaga); + yield put(setEditing(false)); editor.stopEditing(); } - return hideLoader(); + yield put(setReady(true)); + hideLoader(); + return true; } } - return yield call(startEmptyEditorSaga); + yield call(startEmptyEditorSaga); + yield put(setReady(true)); + return true; } function* authChechSaga() { @@ -120,7 +142,6 @@ function* authChechSaga() { if (user) { yield put(setUser(user)); - return yield call(mapInitSaga); } } @@ -294,9 +315,37 @@ function setProviderSaga({ provider }) { return put(setMode(MODES.NONE)); } -export function* userSaga() { - // ASYNCHRONOUS!!! :-) +function* locationChangeSaga({ location }) { + const { address, editing, ready, user: { id, random_url } } = yield select(getState); + if (!ready) return; + + const { path, mode } = getUrlData(location); + + if (address !== path) { + const map = yield call(loadMapSaga, path); + + if (map && map.owner && mode === 'edit' && map.owner.id !== id) { + pushPath(`/${map.random_url}/edit`); + return; + } + } else if (mode === 'edit' && editor.owner.id !== id) { + pushPath(`/${random_url}/edit`); + return; + } + + if (editing !== (mode === 'edit')) { + if (mode === 'edit') { + yield put(setEditing(true)); + editor.startEditing(); + } else { + yield put(setEditing(false)); + editor.stopEditing(); + } + } +} + +export function* userSaga() { yield takeLatest(REHYDRATE, authChechSaga); yield takeEvery(ACTIONS.SET_MODE, setModeSaga); @@ -322,4 +371,5 @@ export function* userSaga() { yield takeLatest(ACTIONS.CROP_A_SHOT, cropAShotSaga); yield takeEvery(ACTIONS.SET_PROVIDER, setProviderSaga); + yield takeLatest(ACTIONS.LOCATION_CHANGED, locationChangeSaga); } diff --git a/src/sprites/icon.svg b/src/sprites/icon.svg index 6d844b5..d7fdac0 100644 --- a/src/sprites/icon.svg +++ b/src/sprites/icon.svg @@ -118,6 +118,22 @@ + + + + + + + + + + + + + + + +