Merge pull request #13 from muerwre/feature/title-dialog

Feature/title dialog
This commit is contained in:
muerwre 2019-03-29 11:35:13 +07:00 committed by GitHub
commit b9cdaed8c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 307 additions and 115 deletions

View file

@ -2,11 +2,9 @@ const mongoose = require('mongoose');
const { Schema } = mongoose; const { Schema } = mongoose;
const RouteSchema = new Schema( const RouteSchema = new Schema({
{
_id: { type: String, required: true }, _id: { type: String, required: true },
title: { type: String, default: '' }, title: { type: String, default: '' },
// address: { type: String, required: true },
version: { type: Number, default: 2 }, version: { type: Number, default: 2 },
route: { type: Array, default: [] }, route: { type: Array, default: [] },
stickers: { type: Array, default: [] }, stickers: { type: Array, default: [] },
@ -19,7 +17,7 @@ const RouteSchema = new Schema(
updated_at: { type: Date, default: Date.now() }, updated_at: { type: Date, default: Date.now() },
logo: { type: String, default: 'DEFAULT' }, logo: { type: String, default: 'DEFAULT' },
provider: { type: String, default: 'DEFAULT' }, provider: { type: String, default: 'DEFAULT' },
}, description: { type: String, default: '' },
); });
module.exports.RouteSchema = RouteSchema; module.exports.RouteSchema = RouteSchema;

View file

@ -1,14 +1,21 @@
const { User, Route } = require('../../models'); const { User, Route } = require('../../models');
const { parseRoute, parseStickers, parseString, parseNumber, parseAddress } = require('../../utils/parse'); const {
parseRoute, parseStickers, parseString, parseNumber, parseAddress
} = require('../../utils/parse');
module.exports = async (req, res) => { module.exports = async (req, res) => {
const { body, body: { id, token, force } } = req; const { body, body: { id, token, force } } = req;
const owner = await User.findOne({ _id: id, token }).populate('routes'); const owner = await User.findOne({ _id: id, token }).populate('routes');
if (!owner) return res.send({ success: false, reason: 'unauthorized', id, token }); if (!owner) {
return res.send({
success: false, reason: 'unauthorized', id, token
});
}
const title = parseString(body.title, 64); const title = parseString(body.title, 64);
const description = parseString(body.description, 256);
const address = parseAddress(body.address, 32); const address = parseAddress(body.address, 32);
const route = parseRoute(body.route); const route = parseRoute(body.route);
const stickers = parseStickers(body.stickers); const stickers = parseStickers(body.stickers);
@ -28,23 +35,23 @@ module.exports = async (req, res) => {
if (exists) { if (exists) {
await exists.set({ await exists.set({
title, route, stickers, logo, distance, updated_at: Date.now(), provider, is_public, title, route, stickers, logo, distance, updated_at: Date.now(), provider, is_public, description,
}).save(); }).save();
return res.send({ return res.send({
success: true, title, address, route, stickers, mode: 'overwrited', is_public, success: true, title, address, route, stickers, mode: 'overwrited', is_public, description,
}); });
} }
const created = await Route.create({ const created = await Route.create({
_id: address, title, route, stickers, owner, logo, distance, provider, is_public, _id: address, title, route, stickers, owner, logo, distance, provider, is_public, description,
}); });
await owner.routes.push(created); await owner.routes.push(created);
await owner.save(); await owner.save();
return res.send({ return res.send({
success: true, title, address, route, stickers, provider, is_public, success: true, title, address, route, stickers, provider, is_public, description,
}); });
}; };

7
package-lock.json generated
View file

@ -10749,6 +10749,13 @@
"scheduler": "^0.13.1" "scheduler": "^0.13.1"
} }
}, },
"react-expandable-textarea": {
"version": "github:muerwre/react-expandable-textarea#0cbcbbd875439090a2d48e711da241f2a83dd6b2",
"from": "github:muerwre/react-expandable-textarea",
"requires": {
"classnames": "^2.2.6"
}
},
"react-hot-loader": { "react-hot-loader": {
"version": "4.6.5", "version": "4.6.5",
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.6.5.tgz", "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.6.5.tgz",

View file

@ -90,6 +90,7 @@
"rc-slider": "8.5.0", "rc-slider": "8.5.0",
"react": "16.8.1", "react": "16.8.1",
"react-dom": "16.8.1", "react-dom": "16.8.1",
"react-expandable-textarea": "github:muerwre/react-expandable-textarea",
"react-hot-loader": "^4.1.1", "react-hot-loader": "^4.1.1",
"react-infinite-scroller": "^1.2.2", "react-infinite-scroller": "^1.2.2",
"react-rangeslider": "^2.2.0", "react-rangeslider": "^2.2.0",

View file

@ -1,16 +1,20 @@
import * as React from 'react'; import * as React from 'react';
import { copyToClipboard, getUrlData } from '$utils/history'; import { copyToClipboard, getUrlData } from '$utils/history';
import { toTranslit } from '$utils/format'; import { toTranslit, parseDesc } from '$utils/format';
import { TIPS } from '$constants/tips'; import { TIPS } from '$constants/tips';
import { MODES } from '$constants/modes'; import { MODES } from '$constants/modes';
import { Icon } from '$components/panels/Icon'; import { Icon } from '$components/panels/Icon';
import { Switch } from '$components/Switch'; import { Switch } from '$components/Switch';
import classnames from 'classnames'; import classnames from 'classnames';
import { IRootState } from "$redux/user/reducer";
import { sendSaveRequest, setMode } from "$redux/user/actions"; import { sendSaveRequest, setMode } from "$redux/user/actions";
import ExpandableTextarea from 'react-expandable-textarea';
interface Props {
address: string,
title: string,
is_public: boolean,
interface Props extends IRootState {
width: number, width: number,
setMode: typeof setMode, setMode: typeof setMode,
sendSaveRequest: typeof sendSaveRequest, sendSaveRequest: typeof sendSaveRequest,
@ -25,6 +29,7 @@ interface State {
address: string, address: string,
title: string, title: string,
is_public: boolean, is_public: boolean,
description: string,
} }
export class SaveDialog extends React.Component<Props, State> { export class SaveDialog extends React.Component<Props, State> {
@ -35,6 +40,7 @@ export class SaveDialog extends React.Component<Props, State> {
address: props.address || '', address: props.address || '',
title: props.title || '', title: props.title || '',
is_public: props.is_public || false, is_public: props.is_public || false,
description: props.description || '',
}; };
} }
@ -46,22 +52,23 @@ export class SaveDialog extends React.Component<Props, State> {
}; };
setTitle = ({ target: { value } }) => this.setState({ title: ((value && value.substr(0, 64)) || '') }); setTitle = ({ target: { value } }) => this.setState({ title: ((value && value.substr(0, 64)) || '') });
setAddress = ({ target: { value } }) => this.setState({ address: (value && value.substr(0, 32) || '') }); setAddress = ({ target: { value } }) => this.setState({ address: (value && value.substr(0, 32) || '') });
setDescription = ({ target: { value } }) => this.setState({ description: (value && value.substr(0, 256) || '') });
cancelSaving = () => this.props.setMode(MODES.NONE);
sendSaveRequest = (e, force = false) => { sendSaveRequest = (e, force = false) => {
const { title, is_public } = this.state; const { title, is_public, description } = this.state;
const address = this.getAddress(); const address = this.getAddress();
this.props.sendSaveRequest({ this.props.sendSaveRequest({
title, address, force, is_public title, address, force, is_public, description,
}); });
}; };
forceSaveRequest = e => this.sendSaveRequest(e, true); forceSaveRequest = e => this.sendSaveRequest(e, true);
cancelSaving = () => this.props.setMode(MODES.NONE);
onCopy = e => { onCopy = e => {
e.preventDefault(); e.preventDefault();
const { host, protocol } = getUrlData(); const { host, protocol } = getUrlData();
@ -73,7 +80,7 @@ export class SaveDialog extends React.Component<Props, State> {
}; };
render() { render() {
const { title, is_public } = this.state; const { title, is_public, description } = this.state;
const { save_error, save_finished, save_overwriting, width, save_loading } = this.props; const { save_error, save_finished, save_overwriting, width, save_loading } = this.props;
const { host, protocol } = getUrlData(); const { host, protocol } = getUrlData();
@ -104,6 +111,15 @@ export class SaveDialog extends React.Component<Props, State> {
</div> </div>
</div> </div>
<div className="save-textarea">
<ExpandableTextarea
minRows={2}
maxRows={5}
placeholder="Описание маршрута"
value={parseDesc(description)}
onChange={this.setDescription}
/>
</div>
<div className="save-text"> <div className="save-text">
{ save_error || TIPS.SAVE_INFO } { save_error || TIPS.SAVE_INFO }
</div> </div>

View file

@ -3,36 +3,105 @@ import { bindActionCreators } from "redux";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import { getStyle } from "$utils/dom";
import { nearestInt } from "$utils/geom";
import { IRootState } from "$redux/user/reducer";
import { parseDesc } from "$utils/format";
interface ITitleDialogProps { interface ITitleDialogProps {
editing: boolean, editing: IRootState['editing'],
title?: string, title?: IRootState['title'],
description?: IRootState['description'],
minLines?: number,
maxLines?: number,
} }
interface ITitleDialogState { interface ITitleDialogState {
raised: boolean; raised: boolean;
height: number;
height_raised: number;
} }
export class Component extends React.PureComponent<ITitleDialogProps, ITitleDialogState> { export class Component extends React.PureComponent<ITitleDialogProps, ITitleDialogState> {
state = { state = {
raised: false, raised: false,
height: 0,
height_raised: 0,
}; };
onHover = () => this.setState({ raised: true }); onHover = () => this.setState({ raised: true });
onLeave = () => this.setState({ raised: false }); onLeave = () => this.setState({ raised: false });
componentDidMount() {
this.setMaxHeight();
}
componentDidUpdate() {
this.setMaxHeight();
}
setMaxHeight = (): number => {
if (!this.ref_sizer || !this.ref_title || !this.ref_text) return 0;
const { height: sizer_height } = this.ref_sizer.getBoundingClientRect();
const { height: title_height } = this.ref_title.getBoundingClientRect();
const { height: text_height } = this.ref_text.getBoundingClientRect();
if (text_height === 0) {
this.setState({ height: 0, height_raised: 0 });
return;
}
const title_margin = parseInt(getStyle(this.ref_title, 'margin-bottom'), 10) || 0;
const text_margins = (parseInt(getStyle(this.ref_text, 'margin-top'), 10) || 0) +
parseInt(getStyle(this.ref_text, 'margin-bottom'), 10) || 0;;
const text_line = parseInt(getStyle(this.ref_text, 'line-height'), 10) || 0;
const container_height = sizer_height - title_height - title_margin - text_margins;
const min_height = (this.props.minLines || 5) * text_line;
const max_height = (this.props.maxLines || 20) * text_line;
const height = nearestInt(Math.min(container_height, Math.min(text_height, min_height)), text_line) + text_margins;
const height_raised = nearestInt(Math.min(container_height, Math.min(text_height, max_height)), text_line) + text_margins;
this.setState({ height, height_raised });
};
render() { render() {
const { editing, title } = this.props; const { editing, title, description } = this.props;
const { raised, height, height_raised } = this.state;
return ( return (
<div className="title-dialog-wrapper"> <div className="title-dialog-wrapper">
<div className="title-dialog-sizer" ref={el => { this.sizer = el; }}> <div className="title-dialog-sizer" ref={el => { this.ref_sizer = el; }}>
<div className={classnames('title-dialog', { active: title && !editing })}> <div
<div className="title-dialog-pane title-dialog-name"> className={classnames('title-dialog', { active: title && !editing })}
onMouseOver={this.onHover}
onMouseOut={this.onLeave}
>
<div
className="title-dialog-pane title-dialog-name"
ref={el => { this.ref_title = el; }}
>
<h2>{title}</h2> <h2>{title}</h2>
</div> </div>
<div className="title-dialog-pane title-dialog-text">
Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации "Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.." Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по <div
className={classnames("title-dialog-pane title-dialog-text", { has_shade: height_raised > height })}
style={{
height: (raised ? height_raised : height),
marginBottom: height === 0 ? 0 : 15,
}}
ref={el => { this.ref_overflow = el; }}
>
<div
ref={el => { this.ref_text = el; }}
>
{
parseDesc(description)
}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -40,11 +109,13 @@ export class Component extends React.PureComponent<ITitleDialogProps, ITitleDial
) )
} }
text; ref_sizer;
sizer; ref_title;
ref_text;
ref_overflow;
} }
const mapStateToProps = ({ user: { editing, title } }) => ({ editing, title }); const mapStateToProps = ({ user: { editing, title, description } }) => ({ editing, title, description });
const mapDispatchToProps = dispatch => bindActionCreators({ }, dispatch); const mapDispatchToProps = dispatch => bindActionCreators({ }, dispatch);
export const TitleDialog = connect(mapStateToProps, mapDispatchToProps)(Component); export const TitleDialog = connect(mapStateToProps, mapDispatchToProps)(Component);

View file

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { IModes, MODES } from '$constants/modes'; import { MODES } from '$constants/modes';
import { RouterDialog } from '$components/dialogs/RouterDialog'; import { RouterDialog } from '$components/dialogs/RouterDialog';
import { StickersDialog } from '$components/dialogs/StickersDialog'; import { StickersDialog } from '$components/dialogs/StickersDialog';

View file

@ -14,6 +14,7 @@ import { CLIENT } from '$config/frontend';
import { DIALOGS } from '$constants/dialogs'; import { DIALOGS } from '$constants/dialogs';
import { IRootState } from "$redux/user/reducer"; import { IRootState } from "$redux/user/reducer";
import { Tooltip } from "$components/panels/Tooltip"; import { Tooltip } from "$components/panels/Tooltip";
import { TitleDialog } from "$components/dialogs/TitleDialog";
interface Props extends IRootState { interface Props extends IRootState {
userLogout: typeof userLogout, userLogout: typeof userLogout,
@ -95,7 +96,7 @@ export class Component extends React.PureComponent<Props, State> {
return ( return (
<div> <div>
{ {
// <TitleDialog /> <TitleDialog />
} }
<div className="panel active panel-user"> <div className="panel active panel-user">
<div className="user-panel"> <div className="user-panel">

View file

@ -7,7 +7,7 @@
todo tower sticker todo tower sticker
todo route description todo route description
todo polyline editing only in manual mode (or by click) skip polyline editing only in manual mode (or by click)
todo selecting logo on crop todo selecting logo on crop
todo network operations notify todo network operations notify

View file

@ -12,14 +12,14 @@ import {
resetSaveDialog, resetSaveDialog,
setActiveSticker, setActiveSticker,
setAddress, setAddress,
setChanged, setChanged, setDescription,
setDistance, setDistance,
setIsEmpty, setIsRouting, setIsEmpty, setIsRouting,
setLogo, setLogo,
setMarkersShown, setMarkersShown,
setMode, setMode,
setPublic, setPublic,
setRouterPoints, setRouterPoints, setStarred,
setTitle, setTitle,
} from '$redux/user/actions'; } from '$redux/user/actions';
import { DEFAULT_PROVIDER, IProvider, PROVIDERS } from '$constants/providers'; import { DEFAULT_PROVIDER, IProvider, PROVIDERS } from '$constants/providers';
@ -37,15 +37,17 @@ interface IEditor {
owner: { id: string }; owner: { id: string };
initialData: { initialData: {
version: number, version: number,
title: string, title: IRootState['title'],
owner: { id: string }, owner: { id: string },
address: string, address: IRootState['address'],
path: any, path: any,
route: any, route: any,
stickers: any, stickers: any,
provider: string, provider: IRootState['provider'],
is_public: boolean, is_public: IRootState['is_public'],
logo: string, is_starred: IRootState['is_starred'],
description: IRootState['description'],
logo: IRootState['logo'],
}; };
activeSticker: IRootState['activeSticker']; activeSticker: IRootState['activeSticker'];
mode: IRootState['mode']; mode: IRootState['mode'];
@ -156,7 +158,9 @@ export class Editor {
route: null, route: null,
stickers: null, stickers: null,
provider: null, provider: null,
is_public: null, is_public: false,
is_starred: false,
description: '',
logo: null, logo: null,
}; };
activeSticker: IEditor['activeSticker']; activeSticker: IEditor['activeSticker'];
@ -183,8 +187,10 @@ export class Editor {
setRouterPoints: typeof setRouterPoints = value => store.dispatch(setRouterPoints(value)); setRouterPoints: typeof setRouterPoints = value => store.dispatch(setRouterPoints(value));
setActiveSticker: typeof setActiveSticker = value => store.dispatch(setActiveSticker(value)); setActiveSticker: typeof setActiveSticker = value => store.dispatch(setActiveSticker(value));
setTitle: typeof setTitle = value => store.dispatch(setTitle(value)); setTitle: typeof setTitle = value => store.dispatch(setTitle(value));
setDescription: typeof setDescription = value => store.dispatch(setDescription(value));
setAddress: typeof setAddress = value => store.dispatch(setAddress(value)); setAddress: typeof setAddress = value => store.dispatch(setAddress(value));
setPublic: typeof setPublic = value => store.dispatch(setPublic(value)); setPublic: typeof setPublic = value => store.dispatch(setPublic(value));
setStarred: typeof setStarred = value => store.dispatch(setStarred(value));
setIsEmpty: typeof setIsEmpty = value => store.dispatch(setIsEmpty(value)); setIsEmpty: typeof setIsEmpty = value => store.dispatch(setIsEmpty(value));
setIsRouting: typeof setIsRouting = value => store.dispatch(setIsRouting(value)); setIsRouting: typeof setIsRouting = value => store.dispatch(setIsRouting(value));
@ -305,7 +311,16 @@ export class Editor {
}; };
setData = ({ setData = ({
route = [], stickers = [], owner, title, address, provider = DEFAULT_PROVIDER, logo = DEFAULT_LOGO, is_public, route = [],
stickers = [],
owner,
title,
address,
provider = DEFAULT_PROVIDER,
logo = DEFAULT_LOGO,
is_public,
is_starred,
description,
}: IEditor['initialData']): void => { }: IEditor['initialData']): void => {
this.setTitle(title || ''); this.setTitle(title || '');
const { id } = this.getUser(); const { id } = this.getUser();
@ -332,6 +347,9 @@ export class Editor {
} }
this.setPublic(is_public); this.setPublic(is_public);
this.setStarred(is_starred);
this.setDescription(description);
this.setLogo((logo && LOGOS[DEFAULT_LOGO] && logo) || DEFAULT_LOGO); this.setLogo((logo && LOGOS[DEFAULT_LOGO] && logo) || DEFAULT_LOGO);
this.setProvider((provider && PROVIDERS[provider] && provider) || DEFAULT_PROVIDER); this.setProvider((provider && PROVIDERS[provider] && provider) || DEFAULT_PROVIDER);
@ -348,7 +366,7 @@ export class Editor {
setInitialData = (): void => { setInitialData = (): void => {
const { path } = getUrlData(); const { path } = getUrlData();
const { id } = this.getUser(); const { id } = this.getUser();
const { is_public, logo } = this.getState(); const { is_public, logo, is_starred , description} = this.getState();
const { route, stickers, provider } = this.dumpData(); const { route, stickers, provider } = this.dumpData();
this.initialData = { this.initialData = {
@ -362,6 +380,8 @@ export class Editor {
provider, provider,
is_public, is_public,
logo, logo,
is_starred,
description,
}; };
}; };
@ -371,8 +391,6 @@ export class Editor {
this.setInitialData(); this.setInitialData();
this.owner = { id }; this.owner = { id };
// todo: implement
// if (this.poly.latlngs && this.poly.latlngs.length > 1) this.poly.poly.editor.enable();
this.poly.enableEditor(); this.poly.enableEditor();
this.stickers.startEditing(); this.stickers.startEditing();
}; };

View file

@ -81,16 +81,6 @@ export class Stickers {
dumpData = (): Array<IStickerDump> => this.stickers.map(sticker => sticker.dumpData()); dumpData = (): Array<IStickerDump> => this.stickers.map(sticker => sticker.dumpData());
// onSpiderify = (): void => {
// console.log('spider?');
// // todo: it has markers passed as argument. Update only them.
// if (this.editor.getEditing()) {
// this.startEditing();
// } else {
// this.stopEditing();
// }
// };
startEditing = (): void => { startEditing = (): void => {
this.stickers.map(sticker => sticker.startEditing()); this.stickers.map(sticker => sticker.startEditing());
}; };

View file

@ -1,20 +1,23 @@
import { ACTIONS } from '$redux/user/constants'; import { ACTIONS } from '$redux/user/constants';
import { IUser } from "$constants/auth"; import { IUser } from "$constants/auth";
import { IRootState } from "$redux/user/reducer";
export const setUser = (user: IUser) => ({ type: ACTIONS.SET_USER, user }); export const setUser = (user: IUser) => ({ type: ACTIONS.SET_USER, user });
export const userLogout = () => ({ type: ACTIONS.USER_LOGOUT }); export const userLogout = () => ({ type: ACTIONS.USER_LOGOUT });
export const setEditing = editing => ({ type: ACTIONS.SET_EDITING, editing }); export const setEditing = (editing: IRootState['editing']) => ({ type: ACTIONS.SET_EDITING, editing });
export const setMode = mode => ({ type: ACTIONS.SET_MODE, mode }); export const setMode = (mode: IRootState['mode']) => ({ type: ACTIONS.SET_MODE, mode });
export const setDistance = distance => ({ type: ACTIONS.SET_DISTANCE, distance }); export const setDistance = (distance: IRootState['distance']) => ({ type: ACTIONS.SET_DISTANCE, distance });
export const setChanged = changed => ({ type: ACTIONS.SET_CHANGED, changed }); export const setChanged = (changed: IRootState['changed']) => ({ type: ACTIONS.SET_CHANGED, changed });
export const setRouterPoints = routerPoints => ({ type: ACTIONS.SET_ROUTER_POINTS, routerPoints }); export const setRouterPoints = routerPoints => ({ type: ACTIONS.SET_ROUTER_POINTS, routerPoints });
export const setActiveSticker = activeSticker => ({ type: ACTIONS.SET_ACTIVE_STICKER, activeSticker }); export const setActiveSticker = activeSticker => ({ type: ACTIONS.SET_ACTIVE_STICKER, activeSticker });
export const setLogo = logo => ({ type: ACTIONS.SET_LOGO, logo }); export const setLogo = logo => ({ type: ACTIONS.SET_LOGO, logo });
export const setTitle = title => ({ type: ACTIONS.SET_TITLE, title }); export const setTitle = title => ({ type: ACTIONS.SET_TITLE, title });
export const setDescription = description => ({ type: ACTIONS.SET_DESCRIPTION, description });
export const setAddress = address => ({ type: ACTIONS.SET_ADDRESS, address }); export const setAddress = address => ({ type: ACTIONS.SET_ADDRESS, address });
export const setAddressOrigin = address_origin => ({ type: ACTIONS.SET_ADDRESS_ORIGIN, address_origin }); export const setAddressOrigin = address_origin => ({ type: ACTIONS.SET_ADDRESS_ORIGIN, address_origin });
export const setPublic = is_public => ({ type: ACTIONS.SET_PUBLIC, is_public }); export const setPublic = is_public => ({ type: ACTIONS.SET_PUBLIC, is_public });
export const setStarred = is_starred => ({ type: ACTIONS.SET_STARRED, is_starred });
export const setSpeed = speed => ({ type: ACTIONS.SET_SPEED, speed }); export const setSpeed = speed => ({ type: ACTIONS.SET_SPEED, speed });
export const startEditing = () => ({ type: ACTIONS.START_EDITING }); export const startEditing = () => ({ type: ACTIONS.START_EDITING });
@ -28,12 +31,31 @@ export const clearStickers = () => ({ type: ACTIONS.CLEAR_STICKERS });
export const clearAll = () => ({ type: ACTIONS.CLEAR_ALL }); export const clearAll = () => ({ type: ACTIONS.CLEAR_ALL });
export const clearCancel = () => ({ type: ACTIONS.CLEAR_CANCEL }); export const clearCancel = () => ({ type: ACTIONS.CLEAR_CANCEL });
export const sendSaveRequest = payload => ({ type: ACTIONS.SEND_SAVE_REQUEST, ...payload }); export const sendSaveRequest = (payload: {
title: IRootState['title'],
address: IRootState['address'],
is_public: IRootState['is_public'],
description: IRootState['description'],
force: boolean,
}) => ({
type: ACTIONS.SEND_SAVE_REQUEST,
...payload,
});
export const resetSaveDialog = () => ({ type: ACTIONS.RESET_SAVE_DIALOG }); export const resetSaveDialog = () => ({ type: ACTIONS.RESET_SAVE_DIALOG });
export const setSaveLoading = save_loading => ({ type: ACTIONS.SET_SAVE_LOADING, save_loading }); export const setSaveLoading = (save_loading: IRootState['save_loading']) => ({ type: ACTIONS.SET_SAVE_LOADING, save_loading });
export const setSaveSuccess = payload => ({ type: ACTIONS.SET_SAVE_SUCCESS, ...payload });
export const setSaveError = save_error => ({ type: ACTIONS.SET_SAVE_ERROR, save_error }); export const setSaveSuccess = (payload: {
address: IRootState['address'],
title: IRootState['address'],
is_public: IRootState['is_public'],
description: IRootState['description'],
save_error: string,
}) => ({ type: ACTIONS.SET_SAVE_SUCCESS, ...payload });
export const setSaveError = (save_error: IRootState['save_error']) => ({ type: ACTIONS.SET_SAVE_ERROR, save_error });
export const setSaveOverwrite = () => ({ type: ACTIONS.SET_SAVE_OVERWRITE }); export const setSaveOverwrite = () => ({ type: ACTIONS.SET_SAVE_OVERWRITE });
export const hideRenderer = () => ({ type: ACTIONS.HIDE_RENDERER }); export const hideRenderer = () => ({ type: ACTIONS.HIDE_RENDERER });

View file

@ -17,6 +17,8 @@ export const ACTIONS: IActions = {
SET_ADDRESS: 'SET_ADDRESS', SET_ADDRESS: 'SET_ADDRESS',
SET_ADDRESS_ORIGIN: 'SET_ADDRESS_ORIGIN', SET_ADDRESS_ORIGIN: 'SET_ADDRESS_ORIGIN',
SET_PUBLIC: 'SET_PUBLIC', SET_PUBLIC: 'SET_PUBLIC',
SET_STARRED: 'SET_STARRED',
SET_DESCRIPTION: 'SET_DESCRIPTION',
START_EDITING: 'START_EDITING', START_EDITING: 'START_EDITING',
STOP_EDITING: 'STOP_EDITING', STOP_EDITING: 'STOP_EDITING',

View file

@ -26,6 +26,7 @@ export interface IRootReducer {
logo: keyof typeof LOGOS, logo: keyof typeof LOGOS,
routerPoints: number, routerPoints: number,
distance: number, distance: number,
description: string,
estimated: number, estimated: number,
speed: number, speed: number,
activeSticker: { set?: keyof IStickers, sticker?: string }, activeSticker: { set?: keyof IStickers, sticker?: string },
@ -36,6 +37,7 @@ export interface IRootReducer {
provider: keyof typeof PROVIDERS, provider: keyof typeof PROVIDERS,
markers_shown: boolean, markers_shown: boolean,
is_starred: boolean,
is_public: boolean, is_public: boolean,
is_empty: boolean, is_empty: boolean,
is_routing: boolean, is_routing: boolean,
@ -140,6 +142,11 @@ const setTitle: ActionHandler<typeof ActionCreators.setTitle> = (state, { title
title title
}); });
const setDescription: ActionHandler<typeof ActionCreators.setDescription> = (state, { description }) => ({
...state,
description
});
const setAddress: ActionHandler<typeof ActionCreators.setAddress> = (state, { address }) => ({ const setAddress: ActionHandler<typeof ActionCreators.setAddress> = (state, { address }) => ({
...state, ...state,
address address
@ -172,7 +179,11 @@ const setSaveOverwrite: ActionHandler<typeof ActionCreators.setSaveOverwrite> =
}); });
const setSaveSuccess: ActionHandler<typeof ActionCreators.setSaveSuccess> = (state, { save_error }) => ({ const setSaveSuccess: ActionHandler<typeof ActionCreators.setSaveSuccess> = (state, { save_error }) => ({
...state, save_overwriting: false, save_finished: true, save_processing: false, save_error ...state,
save_overwriting: false,
save_finished: true,
save_processing: false,
save_error,
}); });
const resetSaveDialog: ActionHandler<typeof ActionCreators.resetSaveDialog> = (state) => ({ const resetSaveDialog: ActionHandler<typeof ActionCreators.resetSaveDialog> = (state) => ({
@ -276,6 +287,8 @@ const searchSetLoading: ActionHandler<typeof ActionCreators.searchSetLoading> =
}); });
const setPublic: ActionHandler<typeof ActionCreators.setPublic> = (state, { is_public = false }) => ({ ...state, is_public }); const setPublic: ActionHandler<typeof ActionCreators.setPublic> = (state, { is_public = false }) => ({ ...state, is_public });
const setStarred: ActionHandler<typeof ActionCreators.setStarred> = (state, { is_starred = false }) => ({ ...state, is_starred });
const setSpeed: ActionHandler<typeof ActionCreators.setSpeed> = (state, { speed = 15 }) => ({ const setSpeed: ActionHandler<typeof ActionCreators.setSpeed> = (state, { speed = 15 }) => ({
...state, ...state,
speed, speed,
@ -330,6 +343,7 @@ const HANDLERS = ({
[ACTIONS.SET_ACTIVE_STICKER]: setActiveSticker, [ACTIONS.SET_ACTIVE_STICKER]: setActiveSticker,
[ACTIONS.SET_LOGO]: setLogo, [ACTIONS.SET_LOGO]: setLogo,
[ACTIONS.SET_TITLE]: setTitle, [ACTIONS.SET_TITLE]: setTitle,
[ACTIONS.SET_DESCRIPTION]: setDescription,
[ACTIONS.SET_ADDRESS]: setAddress, [ACTIONS.SET_ADDRESS]: setAddress,
[ACTIONS.SET_ADDRESS_ORIGIN]: setAddressOrigin, [ACTIONS.SET_ADDRESS_ORIGIN]: setAddressOrigin,
@ -356,6 +370,7 @@ const HANDLERS = ({
[ACTIONS.SEARCH_PUT_ROUTES]: searchPutRoutes, [ACTIONS.SEARCH_PUT_ROUTES]: searchPutRoutes,
[ACTIONS.SEARCH_SET_LOADING]: searchSetLoading, [ACTIONS.SEARCH_SET_LOADING]: searchSetLoading,
[ACTIONS.SET_PUBLIC]: setPublic, [ACTIONS.SET_PUBLIC]: setPublic,
[ACTIONS.SET_STARRED]: setStarred,
[ACTIONS.SET_SPEED]: setSpeed, [ACTIONS.SET_SPEED]: setSpeed,
[ACTIONS.SET_MARKERS_SHOWN]: setMarkersShown, [ACTIONS.SET_MARKERS_SHOWN]: setMarkersShown,
@ -376,6 +391,7 @@ export const INITIAL_STATE: IRootReducer = {
logo: DEFAULT_LOGO, logo: DEFAULT_LOGO,
routerPoints: 0, routerPoints: 0,
distance: 0, distance: 0,
description: '',
estimated: 0, estimated: 0,
speed: 15, speed: 15,
activeSticker: { set: null, sticker: null }, activeSticker: { set: null, sticker: null },
@ -387,6 +403,8 @@ export const INITIAL_STATE: IRootReducer = {
markers_shown: true, markers_shown: true,
changed: false, changed: false,
editing: false, editing: false,
is_starred: false,
is_public: false, is_public: false,
is_empty: true, is_empty: true,
is_routing: false, is_routing: false,

View file

@ -32,7 +32,7 @@ import {
setProvider, setProvider,
changeProvider, changeProvider,
setSaveLoading, setSaveLoading,
mapsSetShift, searchChangeDistance, clearAll, setFeature, searchSetTitle, setRouteStarred, mapsSetShift, searchChangeDistance, clearAll, setFeature, searchSetTitle, setRouteStarred, setDescription,
} from '$redux/user/actions'; } from '$redux/user/actions';
import { getUrlData, parseQuery, pushLoaderState, pushNetworkInitError, pushPath, replacePath } from '$utils/history'; import { getUrlData, parseQuery, pushLoaderState, pushNetworkInitError, pushPath, replacePath } from '$utils/history';
import { editor } from '$modules/Editor'; import { editor } from '$modules/Editor';
@ -307,7 +307,7 @@ function* clearSaga({ type }) {
} }
function* sendSaveRequestSaga({ function* sendSaveRequestSaga({
title, address, force, is_public title, address, force, is_public, description,
}: ReturnType<typeof ActionCreators.sendSaveRequest>) { }: ReturnType<typeof ActionCreators.sendSaveRequest>) {
if (editor.isEmpty) return yield put(setSaveError(TIPS.SAVE_EMPTY)); if (editor.isEmpty) return yield put(setSaveError(TIPS.SAVE_EMPTY));
@ -319,7 +319,7 @@ function* sendSaveRequestSaga({
const { result, timeout, cancel } = yield race({ const { result, timeout, cancel } = yield race({
result: postMap({ result: postMap({
id, token, route, stickers, title, force, address, logo, distance, provider, is_public id, token, route, stickers, title, force, address, logo, distance, provider, is_public, description,
}), }),
timeout: delay(10000), timeout: delay(10000),
cancel: take(ACTIONS.RESET_SAVE_DIALOG), cancel: take(ACTIONS.RESET_SAVE_DIALOG),
@ -333,17 +333,15 @@ function* sendSaveRequestSaga({
if (timeout || !result || !result.success || !result.address) return yield put(setSaveError(TIPS.SAVE_TIMED_OUT)); if (timeout || !result || !result.success || !result.address) return yield put(setSaveError(TIPS.SAVE_TIMED_OUT));
return yield put(setSaveSuccess({ return yield put(setSaveSuccess({
address: result.address, save_error: TIPS.SAVE_SUCCESS, title, is_public: result.is_public address: result.address,
title: result.title,
is_public: result.is_public,
description: result.description,
save_error: TIPS.SAVE_SUCCESS,
})); }));
} }
// function* refreshUserData() {
// const user = yield select(getUser);
// const data = yield call(checkUserToken, user);
//
// return yield put(setUser(data));
// }
function* getRenderData() { function* getRenderData() {
yield put(setRenderer({ info: 'Загрузка тайлов', progress: 0.1 })); yield put(setRenderer({ info: 'Загрузка тайлов', progress: 0.1 }));
@ -570,7 +568,7 @@ function* searchSetTabSaga() {
} }
function* setSaveSuccessSaga({ function* setSaveSuccessSaga({
address, title, is_public address, title, is_public, description,
}: ReturnType<typeof ActionCreators.setSaveSuccess>) { }: ReturnType<typeof ActionCreators.setSaveSuccess>) {
const { id } = yield select(getUser); const { id } = yield select(getUser);
const { dialog_active } = yield select(getState); const { dialog_active } = yield select(getState);
@ -580,6 +578,7 @@ function* setSaveSuccessSaga({
yield put(setTitle(title)); yield put(setTitle(title));
yield put(setAddress(address)); yield put(setAddress(address));
yield put(setPublic(is_public)); yield put(setPublic(is_public));
yield put(setDescription(description));
yield put(setChanged(false)); yield put(setChanged(false));
yield editor.owner = { id }; yield editor.owner = { id };

View file

@ -641,27 +641,31 @@
.title-dialog { .title-dialog {
z-index: 2; z-index: 2;
opacity: 0;
transition: opacity 500ms, transform 1s; transition: opacity 500ms, transform 1s;
transform: translate(0, 68px); transform: translate(0, 68px);
user-select: none; user-select: none;
pointer-events: all; pointer-events: all;
touch-action: auto; touch-action: auto;
display: flex;
flex-direction: column;
opacity: 0;
@media(max-width: @mobile_breakpoint) {
display: none;
}
&.active { &.active {
opacity: 0.5;
transform: translate(0, 0);
&:hover {
opacity: 1; opacity: 1;
} transform: translate(0, 0);
} }
.title-dialog-pane { .title-dialog-pane {
margin-bottom: 10px; margin-bottom: 10px;
padding: 10px; padding: 10px;
background: #111111; background: @dialog_background;
color: fade(white, 50%); color: fade(white, 50%);
font-size: 13px; font-size: 13px;
box-sizing: border-box;
h2 { h2 {
margin: 0; margin: 0;
@ -671,4 +675,38 @@
color: white; color: white;
} }
} }
.title-dialog-text {
overflow: hidden;
transition: height 500ms;
line-height: 14px;
padding: 0;
position: relative;
> div {
margin: 10px;
white-space: pre-line;
}
&.has_shade {
::after {
content: ' ';
width: 100%;
height: 40px;
background: linear-gradient(fade(@dialog_background, 0), @dialog_background);
position: absolute;
bottom: 0;
left: 0;
transition: opacity 250ms;
pointer-events: none;
touch-action: none;
}
&:hover {
::after {
opacity: 0;
}
}
}
}
} }

View file

@ -46,7 +46,7 @@
&.active { &.active {
opacity: 1; opacity: 1;
touch-action: all; touch-action: auto;
pointer-events: all; pointer-events: all;
svg { svg {
@ -90,6 +90,7 @@
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
margin-bottom: 5px; margin-bottom: 5px;
input { input {
padding: 5px 5px 5px 2px; padding: 5px 5px 5px 2px;
background: transparent; background: transparent;
@ -150,14 +151,15 @@
.save-description { .save-description {
textarea { textarea {
background: rgba(0,0,0,0.3); background: rgba(0,0,0,0.3);
outline: none;
border: none; border: none;
border-radius: 3px; border-radius: 3px;
width: 100%; width: 100%;
resize: none; resize: none;
color: inherit; color: inherit;
font: inherit; font: inherit;
height: 5.5em; padding: 5px 10px;
padding: 0.25em; font-size: 14px;
} }
} }

View file

@ -21,6 +21,7 @@ interface IPostMap {
distance: IRootState['distance'], distance: IRootState['distance'],
provider: IRootState['provider'], provider: IRootState['provider'],
is_public: IRootState['is_public'], is_public: IRootState['is_public'],
description: IRootState['description'],
} }
interface IGetRouteList { interface IGetRouteList {
@ -67,7 +68,7 @@ export const getStoredMap = (
)); ));
export const postMap = ({ export const postMap = ({
title, address, route, stickers, id, token, force, logo, distance, provider, is_public, title, address, route, stickers, id, token, force, logo, distance, provider, is_public, description,
}: IPostMap) => axios.post(API.POST_MAP, { }: IPostMap) => axios.post(API.POST_MAP, {
title, title,
address, address,
@ -80,6 +81,7 @@ export const postMap = ({
distance, distance,
provider, provider,
is_public, is_public,
description,
}).then(result => (result && result.data && result.data)); }).then(result => (result && result.data && result.data));
export const checkIframeToken = ( export const checkIframeToken = (

9
src/utils/dom.ts Normal file
View file

@ -0,0 +1,9 @@
export const getStyle = (oElm: any, strCssRule: string): string => {
if(document.defaultView && document.defaultView.getComputedStyle){
return document.defaultView.getComputedStyle(oElm, '').getPropertyValue(strCssRule);
} else if(oElm.currentStyle){
return oElm.currentStyle[strCssRule.replace(/\-(\w)/g, (strMatch, p1) => p1.toUpperCase())];
}
return '';
};

View file

@ -13,3 +13,5 @@ export const toHours = (info: number): string => {
export const toTranslit = (string: string): string => ( export const toTranslit = (string: string): string => (
removeGarbage(ru.reduce((text, el, i) => (text.replace(new RegExp(ru[i], 'g'), en[i])), (String(string) || ''))) removeGarbage(ru.reduce((text, el, i) => (text.replace(new RegExp(ru[i], 'g'), en[i])), (String(string) || '')))
); );
export const parseDesc = (text: string): string => text.replace(/(\n{2,})/ig, "\n\n");

View file

@ -90,25 +90,14 @@ const distToSegmentSquared = (A: LatLng, B: LatLng, C: LatLng): number => {
}; };
export const distToSegment = (A: LatLng, B: LatLng, C: LatLng): number => Math.sqrt(distToSegmentSquared(A, B, C)); export const distToSegment = (A: LatLng, B: LatLng, C: LatLng): number => Math.sqrt(distToSegmentSquared(A, B, C));
// if C between A and B
export const pointBetweenPoints = (A: LatLng, B: LatLng, C: LatLng): boolean => (distToSegment(A, B, C) < 0.01); export const pointBetweenPoints = (A: LatLng, B: LatLng, C: LatLng): boolean => (distToSegment(A, B, C) < 0.01);
export const angleBetweenPoints = (A: Point, B: Point): number => parseFloat(((Math.atan2(B.y - A.y, B.x - A.x))* 180 / Math.PI).toFixed()); export const angleBetweenPoints = (A: Point, B: Point): number => parseFloat(((Math.atan2(B.y - A.y, B.x - A.x))* 180 / Math.PI).toFixed());
export const angleBetweenPointsRad = (A: Point, B: Point): number => ((Math.atan2(B.x - A.x, B.y - A.y))); export const angleBetweenPointsRad = (A: Point, B: Point): number => ((Math.atan2(B.x - A.x, B.y - A.y)));
// export const pointOnDistance = (A: Point, B: Point, shift: number): Point => {
// const c = Math.sqrt((((B.x - A.x) ** 2) + ((B.y - A.y) ** 2)));
// const angle = angleBetweenPointsRad(A, B);
//
// // console.log({ angle, c, shift });
// const x = Math.floor(B.x - c * Math.sin(angle) * shift);
// const y = Math.floor(B.y - c * Math.cos(angle) * shift);
//
// // console.log({ x, y });
//
// return new Point(x, y);
// };
export const allwaysPositiveAngleDeg = (angle: number): number => ( export const allwaysPositiveAngleDeg = (angle: number): number => (
(angle >= -90 && angle <= 90) ? angle : (180 + angle) (angle >= -90 && angle <= 90) ? angle : (180 + angle)
); );
export const nearestInt = (value: number, parts: number): number => (value - (value % parts));