From 8fcca6587eb846ca6825cc8f199d4ef35da7fbb2 Mon Sep 17 00:00:00 2001
From: muerwre <gotham48@gmail.com>
Date: Tue, 27 Nov 2018 14:48:57 +0700
Subject: [PATCH] save: save dialog and sagas

---
 src/components/panels/EditorDialog.jsx |   5 +-
 src/components/save/SaveDialog.jsx     | 101 ++++++++++---------------
 src/constants/tips.js                  |   5 +-
 src/modules/Editor.js                  |  23 +++---
 src/redux/user/actions.js              |  10 ++-
 src/redux/user/constants.js            |   7 ++
 src/redux/user/reducer.js              |  25 +++++-
 src/redux/user/sagas.js                |  53 ++++++++++++-
 src/styles/save.less                   |  18 +++--
 src/utils/format.js                    |   2 +-
 10 files changed, 161 insertions(+), 88 deletions(-)

diff --git a/src/components/panels/EditorDialog.jsx b/src/components/panels/EditorDialog.jsx
index 7733fff..8cb6263 100644
--- a/src/components/panels/EditorDialog.jsx
+++ b/src/components/panels/EditorDialog.jsx
@@ -11,7 +11,6 @@ import { CancelDialog } from '$components/save/CancelDialog';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
-
 import {
   setMode,
   setLogo,
@@ -24,12 +23,13 @@ import {
   clearCancel,
   stopEditing,
   setEditing,
+  sendSaveRequest,
 } from '$redux/user/actions';
 
 type Props = {
   mode: String,
   activeSticker: String,
-  windth: Number,
+  width: Number,
 }
 
 export const Component = (props: Props) => {
@@ -83,6 +83,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
   stopEditing,
   setEditing,
   setMode,
+  sendSaveRequest,
 }, dispatch);
 
 export const EditorDialog = connect(
diff --git a/src/components/save/SaveDialog.jsx b/src/components/save/SaveDialog.jsx
index 6c94c4f..e8c3798 100644
--- a/src/components/save/SaveDialog.jsx
+++ b/src/components/save/SaveDialog.jsx
@@ -3,86 +3,63 @@ import { getUrlData, pushPath } from '$utils/history';
 import { toTranslit } from '$utils/format';
 import { TIPS } from '$constants/tips';
 import { MODES } from '$constants/modes';
-import { postMap } from '$utils/api';
 
-import classnames from 'classnames';
+type Props = {
+  address: String, // initial?
+  title: String, // initial?
 
-export class SaveDialog extends React.Component {
+  save_error: String,
+  save_finished: Boolean,
+  save_overwriting: Boolean,
+  save_processing: Boolean,
+
+  setMode: Function,
+  sendSaveRequest: Function,
+};
+
+type State = {
+  address: String,
+  title: String,
+};
+
+export class SaveDialog extends React.Component<Props, State> {
   constructor(props) {
     super(props);
 
     this.state = {
       address: props.address || '',
       title: props.title || '',
-      error: '',
-      sending: false,
-      finished: false,
-      overwriting: false,
     };
   }
 
   getAddress = () => {
     const { path } = getUrlData();
     const { title, address } = this.state;
-    return toTranslit(address.trim()) || toTranslit(title.trim()) || toTranslit(path.trim());
+
+    return toTranslit(address.trim()) || toTranslit(title.trim().toLowerCase()) || toTranslit(path.trim());
   };
 
   setTitle = ({ target: { value } }) => this.setState({ title: (value || '') });
 
   setAddress = ({ target: { value } }) => this.setState({ address: (value || '') });
 
-  cancelSaving = () => this.props.editor.changeMode(MODES.NONE);
+  // cancelSaving = () => this.props.editor.changeMode(MODES.NONE);
+  cancelSaving = () => this.props.setMode(MODES.NONE);
 
   sendSaveRequest = (e, force = false) => {
-    const { route, stickers } = this.props.editor.dumpData();
     const { title } = this.state;
-    const { id, token } = this.props.user;
+    const address = this.getAddress();
 
-    postMap({
-      id,
-      token,
-      route,
-      stickers,
-      title,
-      force,
-      address: this.getAddress(),
-    }).then(this.parseResponse).catch(console.warn);
+    this.props.sendSaveRequest({
+      title, address, force,
+    });
   };
 
   forceSaveRequest = e => this.sendSaveRequest(e, true);
 
-  parseResponse = data => {
-    if (data.success) return this.setSuccess(data);
-    if (data.mode === 'overwriting') return this.setOverwrite(data.description);
-    return this.setError(data.description);
-  };
-
-  setSuccess = ({ address, description }) => {
-    pushPath(`/${address}/edit`);
-
-    console.log('addr?', address);
-    this.props.editor.setAddress(address);
-    this.props.editor.owner = this.props.user.id;
-
-    this.props.editor.setInitialData();
-
-    this.setState({
-      error: description, finished: true, sending: true, overwriting: false
-    });
-  };
-
-  setOverwrite = error => this.setState({
-    error, finished: false, sending: true, overwriting: true
-  });
-
-  setError = error => this.setState({
-    error, finished: false, sending: true, overwriting: false
-  });
-
   render() {
-    const {
-      title, error, finished, overwriting, sending
-    } = this.state;
+    const { title } = this.state;
+    const { save_error, save_finished, save_overwriting, save_processing } = this.props;
     const { host } = getUrlData();
 
     return (
@@ -90,42 +67,40 @@ export class SaveDialog extends React.Component {
         <div className="save-title">
           <div className="save-title-input">
             <label className="save-title-label">Название</label>
-            <input type="text" value={title} onChange={this.setTitle} autoFocus />
+            <input type="text" value={title} onChange={this.setTitle} autoFocus readOnly={save_finished} />
           </div>
         </div>
 
         <div className="save-description">
           <div className="save-address-input">
             <label className="save-address-label">http://{host}/</label>
-            <input type="text" value={this.getAddress().substr(0, 32)} onChange={this.setAddress} />
+            <input type="text" value={this.getAddress().substr(0, 32)} onChange={this.setAddress} readOnly={save_finished} />
           </div>
 
           <div className="save-text">
-            {
-              error || TIPS.SAVE_INFO
-            }
+            { save_error || TIPS.SAVE_INFO }
           </div>
 
           <div className="save-buttons">
             <div className="save-buttons-text" />
-            <div className={classnames({ 'button-group': !finished })}>
+            <div>
 
-              { !finished &&
-              <div className="button" onClick={this.cancelSaving}>Отмена</div>
+              { !save_finished &&
+                  <div className="button" onClick={this.cancelSaving}>Отмена</div>
               }
 
               {
-                (!sending || (sending && !overwriting && !finished)) &&
+                !save_finished && !save_overwriting &&
                   <div className="button primary" onClick={this.sendSaveRequest}>Сохранить</div>
               }
 
               {
-                sending && overwriting &&
+                save_overwriting &&
                   <div className="button danger" onClick={this.forceSaveRequest}>Перезаписать</div>
               }
 
-              { finished &&
-              <div className="button success" onClick={this.cancelSaving}>Отлично, спасибо!</div>
+              { save_finished &&
+                  <div className="button success" onClick={this.cancelSaving}>Отлично, спасибо!</div>
               }
 
             </div>
diff --git a/src/constants/tips.js b/src/constants/tips.js
index 1d16936..5fc206a 100644
--- a/src/constants/tips.js
+++ b/src/constants/tips.js
@@ -1,3 +1,6 @@
 export const TIPS = {
-  SAVE_INFO: 'Вы можете задать своё название маршрута и адрес, по которому он будет доступен.'
+  SAVE_INFO: 'Никто, кроме вас не сможет изменить маршрут - только создать его копию и сохранить по другому адресу',
+  SAVE_TIMED_OUT: 'Сервер не ответил на запрос, попробуйте позже',
+  SAVE_EMPTY: 'Этот маршрут пуст, нарисуйте что-нибудь для начала',
+  SAVE_SUCCESS: 'Маршрут сохранен - он будет доступен по указанному адресу'
 };
diff --git a/src/modules/Editor.js b/src/modules/Editor.js
index a7c6989..683ee2e 100644
--- a/src/modules/Editor.js
+++ b/src/modules/Editor.js
@@ -95,20 +95,23 @@ export class Editor {
   getEditing = () => store.getState().user.editing;
   getChanged = () => store.getState().user.changed;
   getRouterPoints = () => store.getState().user.routerPoints;
+  getDistance = () => store.getState().user.distance;
 
   setMode = value => store.dispatch(setMode(value));
-  setDistance = value => store.dispatch(setDistance(value));
   setChanged = value => store.dispatch(setChanged(value));
   setRouterPoints = value => store.dispatch(setRouterPoints(value));
   setActiveSticker = value => store.dispatch(setActiveSticker(value));
   setTitle = value => store.dispatch(setTitle(value));
   setAddress = value => store.dispatch(setAddress(value));
 
+  setDistance = value => {
+    if (this.getDistance() !== value) store.dispatch(setDistance(value));
+  };
+
   clearMode = () => this.setMode(MODES.NONE);
   clearChanged = () => store.dispatch(setChanged(false));
 
   startPoly = () => {
-    console.log(this.getRouterPoints());
     if (this.getRouterPoints()) this.router.clearAll();
 
     this.poly.continue();
@@ -121,16 +124,15 @@ export class Editor {
   };
 
   createStickerOnClick = (e) => {
-    // todo: move to sagas?
     if (!e || !e.latlng || !this.activeSticker) return;
     const { latlng } = e;
 
     this.stickers.createSticker({ latlng, sticker: this.activeSticker });
     this.setActiveSticker(null);
+    this.setChanged(true);
   };
 
   changeMode = mode => {
-    // todo: check if TOGGLING works (we changing MODE from the sagas now)
     if (this.mode === mode) {
       if (this.switches[mode] && this.switches[mode].toggle) {
         // if we have special function on mode when it clicked again
@@ -202,14 +204,9 @@ export class Editor {
   };
 
   clearAll = () => {
-    // todo: move to sagas
     this.poly.clearAll();
     this.router.clearAll();
     this.stickers.clearAll();
-
-    // this.setActiveSticker(null);
-    // this.setMode(MODES.NONE);
-    // this.clearChanged();
   };
 
 
@@ -308,11 +305,17 @@ export class Editor {
   //   return (route.length > 1 && stickers.length > 0);
   // };
 
+  isEmpty = () => {
+    const { route, stickers } = this.dumpData();
+
+    return (!route || route.length < 1) && (!stickers || stickers.length <= 0);
+  };
+
   hasEmptyHistory = () => {
     const { route, stickers } = this.initialData;
 
     return (!route || route.length < 1) && (!stickers || stickers.length <= 0);
-  }
+  };
 }
 
 export const editor = new Editor({});
diff --git a/src/redux/user/actions.js b/src/redux/user/actions.js
index ad7458c..2edd440 100644
--- a/src/redux/user/actions.js
+++ b/src/redux/user/actions.js
@@ -1,7 +1,7 @@
 import { ACTIONS } from '$redux/user/constants';
 
 export const setUser = user => ({ type: ACTIONS.SET_USER, user });
-export const userLogout = user => ({ type: ACTIONS.USER_LOGOUT });
+export const userLogout = () => ({ type: ACTIONS.USER_LOGOUT });
 
 
 export const setEditing = editing => ({ type: ACTIONS.SET_EDITING, editing });
@@ -24,3 +24,11 @@ export const clearPoly = () => ({ type: ACTIONS.CLEAR_POLY });
 export const clearStickers = () => ({ type: ACTIONS.CLEAR_STICKERS });
 export const clearAll = () => ({ type: ACTIONS.CLEAR_ALL });
 export const clearCancel = () => ({ type: ACTIONS.CLEAR_CANCEL });
+
+export const sendSaveRequest = payload => ({ type: ACTIONS.SEND_SAVE_REQUEST, ...payload });
+export const cancelSaveRequest = () => ({ type: ACTIONS.CANCEL_SAVE_REQUEST });
+
+export const setSaveSuccess = payload => ({ type: ACTIONS.SET_SAVE_SUCCESS, ...payload });
+export const setSaveError = save_error => ({ type: ACTIONS.SET_SAVE_ERROR, save_error });
+export const setSaveOverwrite = () => ({ type: ACTIONS.SET_SAVE_OVERWRITE });
+
diff --git a/src/redux/user/constants.js b/src/redux/user/constants.js
index 4152501..f61d967 100644
--- a/src/redux/user/constants.js
+++ b/src/redux/user/constants.js
@@ -22,4 +22,11 @@ export const ACTIONS = {
   CLEAR_STICKERS: 'CLEAR_STICKERS',
   CLEAR_ALL: 'CLEAR_ALL',
   CLEAR_CANCEL: 'CLEAR_CANCEL',
+
+  SEND_SAVE_REQUEST: 'SEND_SAVE_REQUEST',
+  CANCEL_SAVE_REQUEST: 'CANCEL_SAVE_REQUEST',
+
+  SET_SAVE_SUCCESS: 'SET_SAVE_SUCCESS',
+  SET_SAVE_ERROR: 'SET_SAVE_ERROR',
+  SET_SAVE_OVERWRITE: 'SET_SAVE_OVERWRITE',
 };
diff --git a/src/redux/user/reducer.js b/src/redux/user/reducer.js
index 4394095..e6510cb 100644
--- a/src/redux/user/reducer.js
+++ b/src/redux/user/reducer.js
@@ -18,7 +18,9 @@ const setUser = (state, { user }) => ({
 });
 
 const setEditing = (state, { editing }) => ({ ...state, editing });
-const setChanged = (state, { changed }) => ({ ...state, changed });
+const setChanged = (state, { changed }) => ({
+  ...state, changed, ...state, save_overwriting: false, save_finished: false, save_processing: false, save_error: '',
+});
 const setMode = (state, { mode }) => ({ ...state, mode });
 const setDistance = (state, { distance }) => ({
   ...state,
@@ -33,6 +35,17 @@ const setLogo = (state, { logo }) => ({ ...state, logo });
 const setTitle = (state, { title }) => ({ ...state, title });
 const setAddress = (state, { address }) => ({ ...state, address });
 
+const sendSaveRequest = state => ({ ...state, save_processing: true, });
+const setSaveError = (state, { save_error }) => ({
+  ...state, save_error, save_finished: false, save_processing: false
+});
+const setSaveOverwrite = state => ({
+  ...state, save_overwriting: true, save_finished: false, save_processing: false
+});
+const setSaveSuccess = (state, { save_error }) => ({
+  ...state, save_overwriting: false, save_finished: true, save_processing: false, save_error
+});
+
 const HANDLERS = {
   [ACTIONS.SET_USER]: setUser,
   [ACTIONS.SET_EDITING]: setEditing,
@@ -44,6 +57,11 @@ const HANDLERS = {
   [ACTIONS.SET_LOGO]: setLogo,
   [ACTIONS.SET_TITLE]: setTitle,
   [ACTIONS.SET_ADDRESS]: setAddress,
+
+  [ACTIONS.SET_SAVE_ERROR]: setSaveError,
+  [ACTIONS.SET_SAVE_OVERWRITE]: setSaveOverwrite,
+  [ACTIONS.SET_SAVE_SUCCESS]: setSaveSuccess,
+  [ACTIONS.SEND_SAVE_REQUEST]: sendSaveRequest,
 };
 
 export const INITIAL_STATE = {
@@ -58,6 +76,11 @@ export const INITIAL_STATE = {
   title: 0,
   address: '',
   changed: false,
+
+  save_error: '',
+  save_finished: false,
+  save_overwriting: false,
+  save_processing: false,
 };
 
 export const userReducer = createReducer(INITIAL_STATE, HANDLERS);
diff --git a/src/redux/user/sagas.js b/src/redux/user/sagas.js
index 37f9103..9910c79 100644
--- a/src/redux/user/sagas.js
+++ b/src/redux/user/sagas.js
@@ -1,12 +1,22 @@
 import { REHYDRATE } from 'redux-persist';
-import { takeLatest, select, call, put, takeEvery } from 'redux-saga/effects';
-import { checkUserToken, getGuestToken, getStoredMap } from '$utils/api';
-import { setActiveSticker, setChanged, setEditing, setMode, setUser } from '$redux/user/actions';
+import { delay } from 'redux-saga';
+import { takeLatest, select, call, put, takeEvery, race, take } from 'redux-saga/effects';
+import { checkUserToken, getGuestToken, getStoredMap, postMap } from '$utils/api';
+import {
+  setActiveSticker, setAddress,
+  setChanged,
+  setEditing,
+  setMode,
+  setSaveError,
+  setSaveOverwrite, setSaveSuccess, setTitle,
+  setUser
+} from '$redux/user/actions';
 import { getUrlData, pushPath } from '$utils/history';
 import { editor } from '$modules/Editor';
 import { ACTIONS } from '$redux/user/constants';
 import { MODES } from '$constants/modes';
 import { DEFAULT_USER } from '$constants/auth';
+import { TIPS } from '$constants/tips';
 
 const getUser = state => (state.user.user);
 const getState = state => (state.user);
@@ -172,6 +182,41 @@ function* clearSaga({ type }) {
   yield put(setMode(MODES.NONE));
 }
 
+function* sendSaveRequestSaga({ title, address, force }) {
+  if (editor.isEmpty()) {
+    return yield put(setSaveError(TIPS.SAVE_EMPTY));
+  }
+
+  const { route, stickers } = editor.dumpData();
+  const { id, token } = yield select(getUser);
+
+  const { result, timeout, cancel } = yield race({
+    result: postMap({
+      id, token, route, stickers, title, force, address
+    }),
+    timeout: delay(10000),
+    cancel: take(ACTIONS.CANCEL_SAVE_REQUEST),
+  });
+
+  if (cancel) return yield put(setMode(MODES.NONE));
+  if (result && result.mode === 'overwriting') return yield put(setSaveOverwrite());
+  if (timeout || !result || !result.success || !result.address) return yield put(setSaveError(TIPS.SAVE_TIMED_OUT));
+
+  return yield put(setSaveSuccess({ address: result.address, save_error: TIPS.SAVE_SUCCESS, title }));
+}
+
+function* setSaveSuccessSaga({ address, title }) {
+  const { id } = yield select(getUser);
+
+  pushPath(`/${address}/edit`);
+  yield put(setTitle(title));
+  yield put(setAddress(address));
+  // yield editor.setAddress(address);
+  yield editor.owner = id;
+
+  return yield editor.setInitialData();
+}
+
 export function* userSaga() {
   // ASYNCHRONOUS!!! :-)
 
@@ -194,4 +239,6 @@ export function* userSaga() {
     ACTIONS.CLEAR_CANCEL,
   ], clearSaga);
 
+  yield takeLatest(ACTIONS.SEND_SAVE_REQUEST, sendSaveRequestSaga);
+  yield takeLatest(ACTIONS.SET_SAVE_SUCCESS, setSaveSuccessSaga);
 }
diff --git a/src/styles/save.less b/src/styles/save.less
index 0f2361f..0442c4b 100644
--- a/src/styles/save.less
+++ b/src/styles/save.less
@@ -1,5 +1,4 @@
 .save-helper {
-  width: 443px;
   padding: 0;
   flex-direction: column;
 }
@@ -7,9 +6,9 @@
 .save-title {
   padding: 10px;
   width: 100%;
-  background: linear-gradient(160deg, @green_primary, @green_secondary);
+  background: linear-gradient(150deg, @green_primary, @green_secondary);
   flex-direction: column;
-  border-radius: 3px 3px 0 0;
+  border-radius: @panel_radius @panel_radius 0 0;
   font-weight: 200;
   box-sizing: border-box;
 }
@@ -19,8 +18,8 @@
 }
 
 .save-title-input {
-  background: rgba(0, 0, 0, 0.2);
-  border-radius: 2px;
+  background: rgba(0, 0, 0, 0.3);
+  border-radius: @panel_radius;
   display: flex;
 
   input {
@@ -72,11 +71,18 @@
 
 .save-text {
   padding: 10px;
+  line-height: 1.1em;
+  min-height: 2.2em;
 }
 
 .save-buttons {
   display: flex;
-  padding: 10px;
+  padding: 0px;
+  margin-top: 20px;
+
+  .button {
+    margin-left: 10px;
+  }
 }
 
 .save-buttons-text {
diff --git a/src/utils/format.js b/src/utils/format.js
index 8789d43..4956f2d 100644
--- a/src/utils/format.js
+++ b/src/utils/format.js
@@ -1,5 +1,5 @@
 const ru = [' ','\\.',',',':','\\?','#','Я','я','Ю','ю','Ч','ч','Ш','ш','Щ','щ','Ж','ж','А','а','Б','б','В','в','Г','г','Д','д','Е','е','Ё','ё','З','з','И','и','Й','й','К','к','Л','л','М','м','Н','н', 'О','о','П','п','Р','р','С','с','Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ы','ы','Ь','ь','Ъ','ъ','Э','э'];
-const en = ['_','','','','','','Ya','ya','Yu','yu','Ch','ch','Sh','sh','Sh','sh','Zh','zh','A','a','B','b','V','v','G','g','D','d','E','e','E','e','Z','z','I','i','J','j','K','k','L','l','M','m','N','n', 'O','o','P','p','R','r','S','s','T','t','U','u','F','f','H','h','C','c','Y','y','`','`','\'','\'','E', 'e'];
+const en = ['_','','','','','','Ya','ya','Yu','yu','Ch','ch','Sh','sh','Sh','sh','Zh','zh','A','a','B','b','V','v','G','g','D','d','E','e','E','e','Z','z','I','i','J','j','K','k','L','l','M','m','N','n', 'O','o','P','p','R','r','S','s','T','t','U','u','F','f','H','h','C','c','Y','y','','','','','E', 'e'];
 
 export const toHours = (info) => {
   const hrs = parseInt(Number(info), 10);