diff --git a/package-lock.json b/package-lock.json index 6a614e0..f644eb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10130,6 +10130,11 @@ "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", "optional": true }, + "pt-sans-cyrillic": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/pt-sans-cyrillic/-/pt-sans-cyrillic-0.0.4.tgz", + "integrity": "sha512-QbXgUHp5pbSbxbLdfpe5/MzuYPufqv36UMQUUI7QwceaaCJA8NQilysjlexjHLyK0GFv7NB5kl6ZAcIMBBBRXA==" + }, "public-encrypt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -10209,6 +10214,11 @@ "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==", "dev": true }, + "raleway-cyrillic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raleway-cyrillic/-/raleway-cyrillic-4.0.2.tgz", + "integrity": "sha1-HcKzrqYwKwhTbs7jGIyS0li4jOE=" + }, "ramda": { "version": "0.24.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz", diff --git a/package.json b/package.json index fbb4f67..02ce515 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,8 @@ "leaflet-routing-machine": "muerwre/leaflet-routing-machine#no-osrm-text", "less": "^3.8.1", "lodash": "^4.17.10", + "pt-sans-cyrillic": "0.0.4", + "raleway-cyrillic": "^4.0.2", "react": "^16.3.2", "react-dom": "^16.3.2", "react-hot-loader": "^4.1.1", diff --git a/src/components/panels/EditorDialog.jsx b/src/components/panels/EditorDialog.jsx index e8a4565..60a54ce 100644 --- a/src/components/panels/EditorDialog.jsx +++ b/src/components/panels/EditorDialog.jsx @@ -5,15 +5,19 @@ import { RouterDialog } from '$components/router/RouterDialog'; import { StickersDialog } from '$components/stickers/StickersDialog'; import { TrashDialog } from '$components/trash/TrashDialog'; import { LogoDialog } from '$components/logo/LogoDialog'; +import { SaveDialog } from '$components/save/SaveDialog'; +import { CancelDialog } from '$components/save/CancelDialog'; export const EditorDialog = ({ - mode, routerPoints, editor, activeSticker, logo + mode, routerPoints, editor, activeSticker, logo, user, title, address, }) => { const showDialog = ( mode === MODES.ROUTER || (mode === MODES.STICKERS && !activeSticker) || mode === MODES.TRASH || mode === MODES.LOGO + || mode === MODES.SAVE + || mode === MODES.CONFIRM_CANCEL ); return ( @@ -23,6 +27,8 @@ export const EditorDialog = ({ { mode === MODES.STICKERS && } { mode === MODES.TRASH && } { mode === MODES.LOGO && } + { mode === MODES.SAVE && } + { mode === MODES.CONFIRM_CANCEL && } ); }; diff --git a/src/components/panels/EditorPanel.jsx b/src/components/panels/EditorPanel.jsx index 97ee003..381fc6e 100644 --- a/src/components/panels/EditorPanel.jsx +++ b/src/components/panels/EditorPanel.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { MODES } from '$constants/modes'; import classnames from 'classnames'; -import { toHours } from '$utils/time'; +import { toHours } from '$utils/format'; import { Icon } from '$components/panels/Icon'; import { EditorDialog } from '$components/panels/EditorDialog'; @@ -21,9 +21,21 @@ export class EditorPanel extends React.PureComponent { startLogoMode = () => this.props.editor.changeMode(MODES.LOGO); + startSaveMode = () => this.props.editor.changeMode(MODES.SAVE); + + stopEditing = () => { + if (!this.props.changed){ + this.props.editor.cancelEditing(); + } else { + this.props.editor.changeMode(MODES.CONFIRM_CANCEL); + } + }; + + startEditing = () => this.props.editor.startEditing(); + render() { const { - mode, routerPoints, editor, totalDistance, estimateTime, activeSticker, logo, + mode, routerPoints, editor, totalDistance, estimateTime, activeSticker, logo, user, editing, title, address, changed, } = this.props; return ( @@ -35,37 +47,41 @@ export class EditorPanel extends React.PureComponent { activeSticker={activeSticker} editor={editor} logo={logo} + user={user} + title={title} + address={address} /> -
-
- {totalDistance} км - - { - {toHours(estimateTime)} - } -
+
+ {changed && '(ch) '} + {totalDistance} км + + { + {toHours(estimateTime)} + } +
+
@@ -77,31 +93,54 @@ export class EditorPanel extends React.PureComponent { className={classnames({ active: mode === MODES.SHOTTER })} onClick={this.startShotterMode} > - + - -
+
+ +
+ + + +
+ +
+ +
+
+ +
); diff --git a/src/components/panels/UserPanel.jsx b/src/components/panels/UserPanel.jsx index f154927..1bb564c 100644 --- a/src/components/panels/UserPanel.jsx +++ b/src/components/panels/UserPanel.jsx @@ -56,13 +56,13 @@ export class UserPanel extends React.PureComponent { render() { const { - props: { user, userLogout }, + props: { user, userLogout, editor, editing }, state: { menuOpened }, } = this; return (
-
+
{ !user || user.role === ROLES.guest diff --git a/src/components/save/CancelDialog.jsx b/src/components/save/CancelDialog.jsx new file mode 100644 index 0000000..22e79a0 --- /dev/null +++ b/src/components/save/CancelDialog.jsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { MODES } from '$constants/modes'; + +export class CancelDialog extends React.Component { + cancel = () => { + this.props.editor.stopEditing(); + }; + + proceed = () => { + this.props.editor.changeMode(MODES.NONE); + }; + + save = () => { + this.props.editor.changeMode(MODES.SAVE); + }; + + render() { + return ( +
+
+
Изменения не сохранены!
+
Закрыть редактор?
+
+
+
+ Закрыть +
+
+ Продолжить +
+
+ Сохранить +
+
+
+ ); + } +} diff --git a/src/components/save/SaveDialog.jsx b/src/components/save/SaveDialog.jsx new file mode 100644 index 0000000..6c94c4f --- /dev/null +++ b/src/components/save/SaveDialog.jsx @@ -0,0 +1,139 @@ +import React from 'react'; +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'; + +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()); + }; + + setTitle = ({ target: { value } }) => this.setState({ title: (value || '') }); + + setAddress = ({ target: { value } }) => this.setState({ address: (value || '') }); + + cancelSaving = () => this.props.editor.changeMode(MODES.NONE); + + sendSaveRequest = (e, force = false) => { + const { route, stickers } = this.props.editor.dumpData(); + const { title } = this.state; + const { id, token } = this.props.user; + + postMap({ + id, + token, + route, + stickers, + title, + force, + address: this.getAddress(), + }).then(this.parseResponse).catch(console.warn); + }; + + 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 { host } = getUrlData(); + + return ( +
+
+
+ + +
+
+ +
+
+ + +
+ +
+ { + error || TIPS.SAVE_INFO + } +
+ +
+
+
+ + { !finished && +
Отмена
+ } + + { + (!sending || (sending && !overwriting && !finished)) && +
Сохранить
+ } + + { + sending && overwriting && +
Перезаписать
+ } + + { finished && +
Отлично, спасибо!
+ } + +
+
+
+ + +
+ ); + } +} diff --git a/src/constants/api.js b/src/constants/api.js index 346ec1c..e7e9e22 100644 --- a/src/constants/api.js +++ b/src/constants/api.js @@ -1,5 +1,8 @@ export const SERVER = 'http://alpha-map.vault48.org'; export const API = { COMPOSE: `${SERVER}/engine/composerOrchid.php`, - GET_GUEST: `${SERVER}/engine/auth.php`, + GET_GUEST: `${SERVER}/engine/authOrchid.php`, + CHECK_TOKEN: `${SERVER}/engine/authOrchid.php`, + GET_MAP: `${SERVER}/engine/authOrchid.php`, + POST_MAP: `${SERVER}/engine/authOrchid.php?action=store`, }; diff --git a/src/constants/auth.js b/src/constants/auth.js index 81054ca..913d249 100644 --- a/src/constants/auth.js +++ b/src/constants/auth.js @@ -10,6 +10,8 @@ export const DEFAULT_USER = { role: ROLES.guest, routes: [], success: false, + id: null, + token: null, userdata: { name: '', agent: '', diff --git a/src/constants/modes.js b/src/constants/modes.js index f7ef134..5813132 100644 --- a/src/constants/modes.js +++ b/src/constants/modes.js @@ -6,4 +6,6 @@ export const MODES = { TRASH: 'TRASH', NONE: 'NONE', LOGO: 'LOGO', + SAVE: 'SAVE', + CONFIRM_CANCEL: 'CONFIRM_CANCEL' }; diff --git a/src/constants/tips.js b/src/constants/tips.js new file mode 100644 index 0000000..1d16936 --- /dev/null +++ b/src/constants/tips.js @@ -0,0 +1,3 @@ +export const TIPS = { + SAVE_INFO: 'Вы можете задать своё название маршрута и адрес, по которому он будет доступен.' +}; diff --git a/src/containers/App.jsx b/src/containers/App.jsx index ab5e2f5..4fae52b 100644 --- a/src/containers/App.jsx +++ b/src/containers/App.jsx @@ -6,13 +6,15 @@ import { Fills } from '$components/Fills'; import { DEFAULT_LOGO } from '$constants/logos'; import { UserLocation } from '$components/UserLocation'; import { DEFAULT_USER } from '$constants/auth'; -import { getGuestToken, checkUserToken } from '$utils/api'; +import { getGuestToken, checkUserToken, getStoredMap } from '$utils/api'; import { storeData, getData } from '$utils/storage'; import { UserPanel } from '$components/panels/UserPanel'; +import { getUrlData, pushPath } from '$utils/history'; export class App extends React.Component { state = { mode: 'none', + editing: false, logo: DEFAULT_LOGO, routerPoints: 0, totalDistance: 0, @@ -21,12 +23,68 @@ export class App extends React.Component { user: { ...DEFAULT_USER, }, + title: '', + address: '', + changed: false, }; componentDidMount() { this.authInit(); + window.editor = this.editor; } + mapInit = () => { + const { path, mode } = getUrlData(); + if (path) { + getStoredMap({ name: path }) + .then(this.setDataOnLoad) + .then(() => { + if (mode && mode === 'edit') { + this.editor.startEditing(); + } else { + this.editor.stopEditing(); + } + }) + .catch(this.startEmptyEditor); + } else { + // this.hideLoader(); + this.startEmptyEditor(); + } + }; + + startEmptyEditor = () => { + const { user } = this.state; + if (!user || !user.random_url || !user.id) return; + + pushPath(`/${user.random_url}/edit`); + + this.editor.owner = user.id; + this.editor.startEditing(); + + this.hideLoader(); + + this.clearChanged(); + }; + + setTitle = title => this.setState({ title }); + setAddress = address => { + console.log('SAT', address); + this.setState({ address }); + }; + + getTitle = () => this.state.title; + + setDataOnLoad = data => { + this.clearChanged(); + this.editor.setData(data); + this.hideLoader(); + }; + + hideLoader = () => { + document.getElementById('loader').style.opacity = 0; + document.getElementById('loader').style.pointerEvents = 'none'; + }; + setMode = mode => { this.setState({ mode }); }; @@ -49,6 +107,23 @@ export class App extends React.Component { this.setState({ logo }); }; + setEditing = editing => { + this.setState({ editing }); + }; + + getUser = () => this.state.user; + + triggerOnChange = () => { + if (!this.state.editing) return; + console.log('CHANGED!'); + this.setState({ changed: true }); + }; + + clearChanged = () => { + console.log('clearing'); + this.setState({ changed: false }); + }; + editor = new Editor({ container: 'map', mode: this.state.mode, @@ -57,29 +132,41 @@ export class App extends React.Component { setTotalDist: this.setTotalDist, setActiveSticker: this.setActiveSticker, setLogo: this.setLogo, + setEditing: this.setEditing, + setTitle: this.setTitle, + setAddress: this.setAddress, + getUser: this.getUser, + triggerOnChange: this.triggerOnChange, + clearChanged: this.clearChanged, + getTitle: this.getTitle, }); authInit = () => { const user = this.getUserData(); const { id, token } = (user || {}); - const fallback = () => getGuestToken({ callback: this.setUser }); if (id && token) { checkUserToken({ - callback: this.setUser, - fallback, id, token - }); + }) + .then(this.setUser) + .then(this.mapInit); } else { - getGuestToken({ callback: fallback }); + getGuestToken() + .then(this.setUser) + .then(this.mapInit); } }; setUser = user => { if (!user.token || !user.id) return; + if (this.state.user.id === this.editor.owner) { + this.editor.owner = user.id; + } + this.setState({ user: { ...DEFAULT_USER, @@ -94,25 +181,25 @@ export class App extends React.Component { storeData('user', this.state.user); }; - getUserData = () => { - return getData('user') || null; - }; + getUserData = () => getData('user') || null; userLogout = () => { + if (this.state.user.id === this.editor.owner) { + this.editor.owner = null; + } + // this.setState({ - user: { - ...DEFAULT_USER, - } + user: DEFAULT_USER, }); - this.storeUserData(); + setTimeout(this.storeUserData, 0); }; render() { const { editor, state: { - mode, routerPoints, totalDistance, estimateTime, activeSticker, logo, user, + mode, routerPoints, totalDistance, estimateTime, activeSticker, logo, user, editing, title, address, changed, }, } = this; @@ -124,6 +211,7 @@ export class App extends React.Component {
); diff --git a/src/index.html b/src/index.html index 0db381b..067fea9 100644 --- a/src/index.html +++ b/src/index.html @@ -9,9 +9,21 @@ + - +
-
+
diff --git a/src/index.js b/src/index.js index ebc4153..e6f3bb6 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,8 @@ import ReactDOM from 'react-dom'; import { App } from '$containers/App'; import '$styles/main.less'; +import 'raleway-cyrillic'; + // import { Provider } from 'react-redux'; // import { ConnectedRouter } from 'react-router-redux'; // import { PersistGate } from 'redux-persist/integration/react'; diff --git a/src/modules/Editor.js b/src/modules/Editor.js index 4203b68..3c4d00b 100644 --- a/src/modules/Editor.js +++ b/src/modules/Editor.js @@ -6,6 +6,9 @@ import { Router } from '$modules/Router'; import { Shotter } from '$modules/Shotter'; import { DEFAULT_LOGO } from '$constants/logos'; +import { parseStickerAngle, parseStickerStyle } from '$utils/import'; +import { getUrlData, pushPath } from '$utils/history'; + export class Editor { constructor({ container, @@ -15,19 +18,27 @@ export class Editor { setTotalDist, setActiveSticker, setLogo, + setEditing, + setTitle, + setAddress, + getUser, + triggerOnChange, + clearChanged, + getTitle, }) { this.logo = DEFAULT_LOGO; - + this.owner = null; this.map = new Map({ container }); + this.initialData = {}; const { lockMapClicks, routerMoveStart, changeMode, pushPolyPoints, map: { map } } = this; this.poly = new Poly({ - map, routerMoveStart, lockMapClicks, setTotalDist + map, routerMoveStart, lockMapClicks, setTotalDist, triggerOnChange }); - this.stickers = new Stickers({ map, lockMapClicks }); + this.stickers = new Stickers({ map, lockMapClicks, triggerOnChange }); this.router = new Router({ map, lockMapClicks, setRouterPoints, changeMode, pushPolyPoints }); @@ -49,6 +60,9 @@ export class Editor { }, [MODES.TRASH]: { toggle: this.clearAll, + }, + [MODES.CONFIRM_CANCEL]: { + toggle: this.cancelEditing, } }; @@ -58,10 +72,16 @@ export class Editor { }; this.activeSticker = null; + this.clearChanged = clearChanged; this.setActiveSticker = setActiveSticker; this.setLogo = setLogo; this.setMode = setMode; + this.setEditing = setEditing; + this.setTitle = setTitle; + this.setAddress = setAddress; + this.getUser = getUser; this.mode = mode; + this.getTitle = getTitle; map.addEventListener('mouseup', this.onClick); map.addEventListener('dragstart', () => lockMapClicks(true)); @@ -158,11 +178,118 @@ export class Editor { this.setSticker(null); this.changeMode(MODES.NONE); + + this.clearChanged(); }; changeLogo = logo => { this.logo = logo; this.setLogo(logo); this.changeMode(MODES.NONE); + }; + + setData = ({ route, stickers, version = 1, owner, title, address }) => { + this.setTitle(title || ''); + const { id } = this.getUser(); + + if (address && id && owner && id === owner) this.setAddress(address); + + if (route) { + this.poly.setPoints(route); + } + + if (stickers) { + stickers.map(sticker => this.stickers.createSticker({ + latlng: sticker.latlng, + angle: parseStickerAngle({ sticker, version }), + sticker: parseStickerStyle({ sticker, version }), + })); + } + + if (owner) { + this.owner = owner; + } + + if (!route || route.length <= 1) return; + + const bounds = this.poly.poly.getBounds(); + + if (Object.values(bounds)) this.map.map.fitBounds(bounds); + }; + + setInitialData = () => { + const { path } = getUrlData(); + const { id } = this.getUser(); + const { route, stickers } = this.dumpData(); + + this.initialData = { + version: 2, + title: this.getTitle(), + owner: this.owner, + address: this.owner === id ? path : null, + path: path, + route, + stickers, + }; + }; + + startEditing = () => { + const { path } = getUrlData(); + const { random_url, id } = this.getUser(); + + this.setInitialData(); + + const url = (this.owner && this.owner === id) ? path : random_url; + + pushPath(`/${url}/edit`); + + if (this.poly.latlngs && this.poly.latlngs.length > 1) this.poly.poly.enableEdit(); + + this.stickers.startEditing(); + this.setEditing(true); + + console.log(this.initialData); + }; + + stopEditing = () => { + const { path } = getUrlData(); + pushPath(`/${(this.initialData && this.initialData.path) || path}`); + + this.changeMode(MODES.NONE); + this.poly.poly.disableEdit(); + this.stickers.stopEditing(); + this.setEditing(false); + }; + + cancelEditing = () => { + this.stopEditing(); + + console.log('trying to set initial data'); + + if (this.hasEmptyHistory()) { + this.clearAll(); + this.startEditing(); + } else { + this.setData(this.initialData); + } + + this.clearChanged(); + }; + + dumpData = () => ({ + route: this.poly.dumpData(), + stickers: this.stickers.dumpData(), + }); + + isEmpty = () => { + const { route, stickers } = this.dumpData(); + + return (route.length > 1 && stickers.length > 0); + }; + + hasEmptyHistory = () => { + const { route, stickers } = this.initialData; + + return (!route || route.length < 1) && (!stickers || stickers.length <= 0); } } diff --git a/src/modules/Poly.js b/src/modules/Poly.js index b3e69aa..bec868e 100644 --- a/src/modules/Poly.js +++ b/src/modules/Poly.js @@ -12,7 +12,7 @@ const polyStyle = { export class Poly { constructor({ - map, routerMoveStart, lockMapClicks, setTotalDist + map, routerMoveStart, lockMapClicks, setTotalDist, triggerOnChange, }) { this.poly = L.polyline([], polyStyle); @@ -23,6 +23,7 @@ export class Poly { this.routerMoveStart = routerMoveStart; this.setTotalDist = setTotalDist; + this.triggerOnChange = triggerOnChange; this.lockMapClicks = lockMapClicks; this.bindEvents(); @@ -63,7 +64,10 @@ export class Poly { this.setTotalDist(kilometers); this.routerMoveStart(); + this.drawArrows(); + + if (coords.length > 1) this.triggerOnChange(); }; bindEvents = () => { @@ -124,6 +128,13 @@ export class Poly { this.lockMapClicks(true); }; + setPoints = latlngs => { + if (!latlngs || latlngs.length <= 1) return; + this.poly.setLatLngs(latlngs); + + this.updateMarks(); + }; + pushPoints = latlngs => { const { map } = this; const simplified = simplify({ map, latlngs }); @@ -141,8 +152,11 @@ export class Poly { clearAll = () => { this.poly.setLatLngs([]); this.poly.disableEdit(); + this.updateMarks(); }; clearArrows = () => this.arrows.clearLayers(); + + dumpData = () => this.latlngs; } diff --git a/src/modules/Sticker.js b/src/modules/Sticker.js index 123921c..0d93a2f 100644 --- a/src/modules/Sticker.js +++ b/src/modules/Sticker.js @@ -7,11 +7,15 @@ import stickers from '$sprites/stickers.svg'; export class Sticker { constructor({ - latlng, deleteSticker, map, lockMapClicks, sticker + latlng, deleteSticker, map, lockMapClicks, sticker, triggerOnChange, angle = 2.2 }) { - this.angle = 2.2; + this.latlng = latlng; + this.angle = angle; this.isDragging = false; this.map = map; + this.sticker = sticker; + this.editable = true; + this.triggerOnChange = triggerOnChange; this.deleteSticker = deleteSticker; this.lockMapClicks = lockMapClicks; @@ -37,18 +41,23 @@ export class Sticker { className: 'sticker-container', }); - this.sticker = marker(latlng, { icon: mark }); + this.marker = marker(latlng, { icon: mark }); - this.setAngle(this.angle); + this.setAngle(angle); this.stickerImage.addEventListener('mousedown', this.onDragStart); this.stickerImage.addEventListener('mouseup', this.onDragStop); this.element.addEventListener('mouseup', this.preventPropagations); this.stickerDelete.addEventListener('click', this.onDelete); + + this.marker.addEventListener('dragend', this.triggerOnChange); + + this.triggerOnChange(); } onDelete = () => { + this.triggerOnChange(); if (!this.isDragging) this.deleteSticker(this); }; @@ -56,7 +65,7 @@ export class Sticker { this.preventPropagations(e); this.isDragging = true; - this.sticker.disableEdit(); + this.marker.disableEdit(); this.lockMapClicks(true); @@ -74,8 +83,9 @@ export class Sticker { onDragStop = e => { this.preventPropagations(e); + this.triggerOnChange(); this.isDragging = false; - this.sticker.enableEdit(); + this.marker.enableEdit(); window.removeEventListener('mousemove', this.onDrag); window.removeEventListener('mouseup', this.onDragStop); @@ -89,6 +99,7 @@ export class Sticker { }; estimateAngle = e => { + console.log('est'); const { x, y } = this.element.getBoundingClientRect(); const { pageX, pageY } = e; this.angle = Math.atan2((y - pageY), (x - pageX)); @@ -97,8 +108,6 @@ export class Sticker { }; setAngle = angle => { - // $(active_sticker.container).css('left',6+x-parseInt(active_sticker.ctrl.css('left'))).css('top',6+y-parseInt(active_sticker.ctrl.css('top'))); - // const rad = 44; const mrad = 76; const x = ((Math.cos(angle + 3.14) * rad) - 30); @@ -114,7 +123,7 @@ export class Sticker { this.stickerDelete.style.top = ay; this.stickerArrow.style.transform = `rotate(${angle + 3.14}rad)`; - } + }; generateStickerSVG = sticker => ( ` @@ -122,5 +131,19 @@ export class Sticker { ` - ) + ); + + dumpData = () => ({ + angle: this.angle, + latlng: { ...this.marker.getLatLng() }, + sticker: this.sticker, + }); + + stopEditing = () => { + this.element.className = 'sticker-container inactive'; + }; + + startEditing = () => { + this.element.className = 'sticker-container'; + }; } diff --git a/src/modules/Stickers.js b/src/modules/Stickers.js index f7fd935..49142cc 100644 --- a/src/modules/Stickers.js +++ b/src/modules/Stickers.js @@ -2,9 +2,10 @@ import { layerGroup } from 'leaflet'; import { Sticker } from '$modules/Sticker'; export class Stickers { - constructor({ map, lockMapClicks }) { + constructor({ map, lockMapClicks, triggerOnChange }) { this.map = map; this.layer = layerGroup(); + this.triggerOnChange = triggerOnChange; this.lockMapClicks = lockMapClicks; this.stickers = []; @@ -19,18 +20,20 @@ export class Stickers { // this.createSticker({ latlng }); // }; - createSticker = ({ latlng, sticker }) => { + createSticker = ({ latlng, sticker, angle = 2.2 }) => { const marker = new Sticker({ latlng, + angle, deleteSticker: this.deleteStickerByReference, map: this.map, lockMapClicks: this.lockMapClicks, sticker, + triggerOnChange: this.triggerOnChange, }); this.stickers.push(marker); - marker.sticker.addTo(this.map); - marker.sticker.enableEdit(); + marker.marker.addTo(this.map); + marker.marker.enableEdit(); }; deleteStickerByReference = ref => { @@ -38,7 +41,7 @@ export class Stickers { if (index < 0) return; - this.map.removeLayer(ref.sticker); + this.map.removeLayer(ref.marker); this.stickers.splice(index, 1); }; @@ -48,5 +51,15 @@ export class Stickers { this.deleteStickerByReference(sticker); return true; }); + }; + + dumpData = () => this.stickers.map(sticker => sticker.dumpData()); + + startEditing = () => { + this.stickers.map(sticker => sticker.startEditing()); + } + + stopEditing = () => { + this.stickers.map(sticker => sticker.stopEditing()); } } diff --git a/src/sprites/favicon.png b/src/sprites/favicon.png index f88434c..22b09fe 100644 Binary files a/src/sprites/favicon.png and b/src/sprites/favicon.png differ diff --git a/src/sprites/icon.svg b/src/sprites/icon.svg index e1564ae..7dfdb90 100644 --- a/src/sprites/icon.svg +++ b/src/sprites/icon.svg @@ -138,6 +138,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sprites/icons_draft.svg b/src/sprites/icons_draft.svg index cfa3fd2..221768d 100644 --- a/src/sprites/icons_draft.svg +++ b/src/sprites/icons_draft.svg @@ -7,6 +7,7 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="600" @@ -17,7 +18,29 @@ inkscape:version="0.92.2 5c3e80d, 2017-08-06" sodipodi:docname="icons_draft.svg"> + id="defs2"> + + + + + + + inkscape:snap-global="true" /> @@ -47,7 +70,7 @@ image/svg+xml - + @@ -58,7 +81,7 @@ transform="translate(0,-288.53332)" /> + transform="translate(-192)"> - + style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2.73455501;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 10.922683,16.585837 4.189515,4.189516 6.606544,-6.606544" + id="path7854" + inkscape:connector-curvature="0" /> + id="icon-trash-2" + transform="translate(-96)"> + + width="3.1245625" + height="3.0925183" + x="2.6705999" + y="3.7506495" + rx="0.35952863" + ry="0.40317342" /> + width="3.6630149" + height="0.86975819" + x="2.4013736" + y="2.422653" + rx="0.43487909" + ry="0.43487909" /> + width="2.9243309" + height="1.3062081" + x="2.7707155" + y="1.6977544" + rx="0.65310407" + ry="0.65310407" /> - - - + width="1.9327896" + height="0.52264619" + x="3.2664864" + y="2.1137509" + rx="0.26132309" + ry="0.26132309" /> + + + + - - - - - - - - - - - - - - - - - - - - - + id="icon-cycle" + transform="translate(-256)"> + id="icon-arrow" + transform="translate(-96)"> @@ -441,7 +386,7 @@ + transform="translate(-320)"> + id="icon-sticker-2" + transform="translate(-160)"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.34301686;stroke-miterlimit:4;stroke-dasharray:6.6860338, 3.3430169;stroke-dashoffset:0;stroke-opacity:1" + id="rect6617" + width="19.5" + height="13.890411" + x="6.25" + y="9.8327999" + rx="1.0866628" + ry="1.0866628" /> + + + + + + + + + + + + + + + + + + diff --git a/src/styles/button.less b/src/styles/button.less index 28a4383..f358e21 100644 --- a/src/styles/button.less +++ b/src/styles/button.less @@ -20,6 +20,10 @@ &.danger { background: #ed2f3b; } + + &.success { + background: #17bf6d; + } } .button-group { diff --git a/src/styles/colors.less b/src/styles/colors.less index c8553f4..0759894 100644 --- a/src/styles/colors.less +++ b/src/styles/colors.less @@ -8,3 +8,9 @@ @dialog_background: #222222; @location_line: #ff3344; + +@green_primary: #abc837; +@green_secondary: #009c80; + +@red_primary: #ff7034; +@red_secondary: #ff3344; diff --git a/src/styles/main.less b/src/styles/main.less index b95038c..a280a4f 100644 --- a/src/styles/main.less +++ b/src/styles/main.less @@ -7,9 +7,10 @@ @import 'button.less'; @import 'logo.less'; @import 'user-button.less'; +@import 'save.less'; body { - font-family: sans-serif; + font-family: 'Raleway', sans-serif; font-size: 14px; } diff --git a/src/styles/map.less b/src/styles/map.less index e849169..d6b1848 100644 --- a/src/styles/map.less +++ b/src/styles/map.less @@ -64,9 +64,9 @@ width: 32px; height: 32px; position: fixed; - top: 80px; + top: 10px; left: 10px; - border-radius: 2px; + border-radius: 2px 0 0 2px; z-index: 3; box-shadow: @bar_shadow; cursor: pointer; @@ -110,3 +110,7 @@ position: absolute; } } + +.leaflet-top { + top: 42px; +} diff --git a/src/styles/panel.less b/src/styles/panel.less index 79a6f5a..6b01ad7 100644 --- a/src/styles/panel.less +++ b/src/styles/panel.less @@ -1,21 +1,25 @@ .control-bar { background: @bar_background; - border-radius: 3px; + border-radius: 4px; display: flex; box-shadow: @bar_shadow; } .control-dist { - height: 44px; background: #222222; padding: 0 10px; display: flex; align-items: center; - border-radius: 3px 0 0 3px; font-weight: 200; color: #cccccc; user-select: none; box-shadow: @bar_shadow; + position: fixed; + top: 10px; + left: 42px; + z-index: 2; + height: 32px; + border-radius: 0 3px 3px 0; svg { fill: #cccccc; @@ -36,6 +40,12 @@ color: white; display: flex; align-items: center; + transform: translateY(100px); + transition: transform 500ms; + + &.active { + transform: translateY(0); + } &.right { left: auto; @@ -54,6 +64,7 @@ transition: background-color 500ms; height: 48px; box-sizing: border-box; + user-select: none; &:hover { background: rgba(100, 100, 100, 0.2); @@ -66,6 +77,10 @@ margin-left: 8px; } + &:first-child { + border-radius: 4px 0 0 4px; + } + &:last-child { border-radius: 0 4px 4px 0; } @@ -78,10 +93,27 @@ } &.highlighted { + background: #555555; + } + + &.cancel { + background: linear-gradient(270deg, #0f5871, #444444 60%); + } + + &.primary { background: linear-gradient(150deg, @blue_primary, @blue_secondary) 50% 50% no-repeat; background-size: 100% 100%; } + &.danger { + background: linear-gradient(150deg, @red_primary, @red_secondary) 50% 50% no-repeat; + background-size: 100% 100%; + } + + &.single { + border-radius: 3px; + } + svg { fill: white; stroke: white; diff --git a/src/styles/save.less b/src/styles/save.less new file mode 100644 index 0000000..0f2361f --- /dev/null +++ b/src/styles/save.less @@ -0,0 +1,84 @@ +.save-helper { + width: 443px; + padding: 0; + flex-direction: column; +} + +.save-title { + padding: 10px; + width: 100%; + background: linear-gradient(160deg, @green_primary, @green_secondary); + flex-direction: column; + border-radius: 3px 3px 0 0; + font-weight: 200; + box-sizing: border-box; +} + +.save-description { + padding: 10px; +} + +.save-title-input { + background: rgba(0, 0, 0, 0.2); + border-radius: 2px; + display: flex; + + input { + width: 100%; + padding: 5px; + background: transparent; + border: none; + outline: none; + color: white; + + font-family: inherit; + font-size: 14px; + font-weight: 200; + } +} + +.save-title-label { + display: flex; + padding: 5px 10px; + background: rgba(0,0,0,0.1); + height: 100%; +} + +.save-address-input { + background: rgba(0, 0, 0, 0.2); + border-radius: 2px; + display: flex; + + input { + width: 100%; + padding: 5px 5px 5px 2px; + background: transparent; + border: none; + outline: none; + color: white; + + font-family: inherit; + font-size: 14px; + font-weight: 200; + } +} + +.save-address-label { + display: flex; + padding: 5px 0 5px 10px; + height: 100%; + opacity: 0.5; +} + +.save-text { + padding: 10px; +} + +.save-buttons { + display: flex; + padding: 10px; +} + +.save-buttons-text { + flex: 1; +} diff --git a/src/styles/stickers.less b/src/styles/stickers.less index 457adbf..2e871f7 100644 --- a/src/styles/stickers.less +++ b/src/styles/stickers.less @@ -1,11 +1,16 @@ .sticker-container { outline: none; position: relative; + transition: transform 250ms; + cursor: pointer; + + &.leaflet-drag-target { + transition: none !important; + } &:before { content: ' '; - box-shadow: 0 0 10px 1px #ff3344; - background: #ff334422; + background: @red_secondary; width: 48px; height: 48px; left: -24px; @@ -13,8 +18,8 @@ position: absolute; border-radius: 40px; opacity: 0; + transform: scale(0.5); transition: opacity 250ms, transform 500ms; - transform: scale(0); } &:hover, &:active { @@ -24,10 +29,18 @@ } &:before { - opacity: 1; + opacity: 0.3; transform: scale(1); } } + + &.inactive { + pointer-events: none; + + .sticker-delete { + display: none; + } + } } .sticker-label { diff --git a/src/utils/api.js b/src/utils/api.js index 8975171..a00036a 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -4,25 +4,19 @@ import { API } from '$constants/api'; const report = console.warn; -export const checkUserToken = ({ - callback, fallback, id, token -}) => ( - axios.get(API.GET_GUEST, { - params: { action: 'check_token', id, token } - }) - .then(result => (result && result.data)) - .then(data => ({ ...data, id, token })) - .then(callback) - .catch(fallback) -); -export const getGuestToken = ({ callback }) => ( - axios.get(API.GET_GUEST, { - params: { action: 'gen_guest_token' } - }) - .then(result => (result && result.data)) - .then(callback) - .catch(report) -); +export const checkUserToken = ({ id, token }) => axios.get(API.CHECK_TOKEN, { + params: { + id, + token, + action: 'check_token', + } +}).then(result => (result && result.data && { ...result.data, id, token })) + +export const getGuestToken = () => axios.get(API.GET_GUEST, { + params: { + action: 'gen_guest_token' + } +}).then(result => (result && result.data)); export const getMergedImage = ({ placement, callback }) => ( axios.get(API.COMPOSE, { @@ -31,3 +25,27 @@ export const getMergedImage = ({ placement, callback }) => ( .then(callback) .catch(report) ); + +export const getStoredMap = ({ name }) => axios.get(API.GET_MAP, { + params: { + name, + action: 'load' + } +}).then(result => (result && result.data && result.data.data && { + ...result.data.data, + owner: (result.data.owner || null), + address: (result.data.name || name), +})); + +export const postMap = ({ + title, address, route, stickers, id, token, force, +}) => axios.post(API.POST_MAP, { + action: 'store', + title, + address, + route, + stickers, + id, + token, + force, +}).then(result => (result && result.data && result.data)); diff --git a/src/utils/format.js b/src/utils/format.js new file mode 100644 index 0000000..8789d43 --- /dev/null +++ b/src/utils/format.js @@ -0,0 +1,12 @@ +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']; + +export const toHours = (info) => { + const hrs = parseInt(Number(info), 10); + const min = Math.round((Number(info) - hrs) * 60); + const lmin = min < 10 ? '0' + min : min; + return `${hrs}:${lmin}`; +}; + + +export const toTranslit = string => ru.reduce((text, el, i) => (text.replace(new RegExp(ru[i], 'g'), en[i])), (String(string) || '')); diff --git a/src/utils/history.js b/src/utils/history.js new file mode 100644 index 0000000..4eb9172 --- /dev/null +++ b/src/utils/history.js @@ -0,0 +1,13 @@ +export const getPath = () => (window.location && window.location.pathname && + window.location.pathname.replace(/^\//, '')); + +export const pushPath = url => window.history.pushState(url, 'Редактирование маршрута', url); + +export const getUrlData = () => { + const url = getPath(); + + const [path, mode] = url.split('/'); + const { host } = window.location; + + return { path, mode, host }; +}; diff --git a/src/utils/import.js b/src/utils/import.js new file mode 100644 index 0000000..d5a47e0 --- /dev/null +++ b/src/utils/import.js @@ -0,0 +1,15 @@ +/* + functions to parse old maps data + */ + +export const parseStickerAngle = ({ sticker, version }) => { + return sticker && version && parseInt(version, 10) === 2 + ? parseFloat(sticker.angle) + : parseFloat(sticker.ang - 3.14); +}; + +export const parseStickerStyle = ({ sticker, version }) => ( + sticker && version && parseInt(version, 10) === 2 + ? sticker.sticker + : 'basic' +); diff --git a/src/utils/time.js b/src/utils/time.js deleted file mode 100644 index d9b5c15..0000000 --- a/src/utils/time.js +++ /dev/null @@ -1,6 +0,0 @@ -export const toHours = (info) => { - const hrs = parseInt(Number(info), 10); - const min = Math.round((Number(info) - hrs) * 60); - const lmin = min < 10 ? '0' + min : min; - return `${hrs}:${lmin}`; -}; diff --git a/webpack.config.js b/webpack.config.js index 1f0a38f..d236671 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -105,10 +105,10 @@ module.exports = () => { resolve, plugins, entry: { - // loader: './src/loader.js', app: './src/index.js', }, output: { + publicPath: '/', filename: '[name].bundle.[githash].js', }, optimization: {