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 { 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 {
- +
- +
- { - error || TIPS.SAVE_INFO - } + { save_error || TIPS.SAVE_INFO }
-
+
- { !finished && -
Отмена
+ { !save_finished && +
Отмена
} { - (!sending || (sending && !overwriting && !finished)) && + !save_finished && !save_overwriting &&
Сохранить
} { - sending && overwriting && + save_overwriting &&
Перезаписать
} - { finished && -
Отлично, спасибо!
+ { save_finished && +
Отлично, спасибо!
}
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);