Compare commits
329 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
322337edea | ||
![]() |
1dcfd7e9c0 | ||
![]() |
49a2015510 | ||
![]() |
12f27b8307 | ||
![]() |
93eefa3b3c | ||
![]() |
f48b724b9c | ||
![]() |
151b26ebd3 | ||
![]() |
ccf7b606bf | ||
![]() |
b199c99519 | ||
![]() |
738f69fff4 | ||
![]() |
bc5892120f | ||
![]() |
91fc203b0a | ||
![]() |
afc1b084e1 | ||
![]() |
11a0556f99 | ||
![]() |
a03a906648 | ||
![]() |
a3982eddae | ||
![]() |
b421005b7d | ||
![]() |
10d5fa1b98 | ||
![]() |
7b24499f49 | ||
![]() |
8346c2533f | ||
![]() |
0018465f15 | ||
![]() |
48cf0b93ee | ||
![]() |
9c436d348c | ||
![]() |
fda24c78b0 | ||
![]() |
b563b51121 | ||
![]() |
5f4d2f9cd2 | ||
![]() |
fe311e7839 | ||
![]() |
9a7a038032 | ||
![]() |
dc01dfe3c0 | ||
![]() |
c1c99b4562 | ||
![]() |
5e3c416824 | ||
![]() |
a4b8d70cc8 | ||
![]() |
4e167c9759 | ||
![]() |
3109f49cc4 | ||
![]() |
1eefa0ebbc | ||
![]() |
faed11bb79 | ||
![]() |
7735d11019 | ||
![]() |
fd65653584 | ||
![]() |
1d1eedd911 | ||
![]() |
82cd56fee1 | ||
![]() |
478c557a2b | ||
![]() |
9b52ff10e5 | ||
![]() |
4e18eba558 | ||
![]() |
c2f42ea721 | ||
![]() |
b5b7c82ea4 | ||
![]() |
6db5b62a74 | ||
![]() |
4f65aa8f89 | ||
![]() |
91c9adba83 | ||
![]() |
82bd1345a1 | ||
![]() |
ef08fadeb1 | ||
![]() |
b60c97283d | ||
![]() |
25d6ecaa17 | ||
![]() |
9017e63fac | ||
![]() |
1c62320e39 | ||
![]() |
e9be5ad612 | ||
![]() |
1e5d10098d | ||
![]() |
3d65cab965 | ||
![]() |
7d4046ddc5 | ||
![]() |
08449b10c3 | ||
![]() |
608ec84a53 | ||
![]() |
fa6f481906 | ||
![]() |
6c39d20b98 | ||
![]() |
036dae321f | ||
![]() |
b207cdd55c | ||
![]() |
ef7dd1de24 | ||
![]() |
40c4e433bc | ||
![]() |
80ae6cb375 | ||
![]() |
a2f338c377 | ||
![]() |
bb5310107b | ||
![]() |
4d828baf5c | ||
![]() |
ed796167ea | ||
![]() |
4b7bff0f30 | ||
![]() |
4cffb64bc5 | ||
![]() |
8a80569cca | ||
![]() |
9ad26ddd56 | ||
![]() |
79e1418265 | ||
![]() |
d5460ea89f | ||
![]() |
fd01a0db9d | ||
![]() |
0b7cf74266 | ||
![]() |
e33a003894 | ||
![]() |
3a23bca44e | ||
![]() |
2a8734e440 | ||
![]() |
67fa815359 | ||
![]() |
74b8e7a4c8 | ||
![]() |
cc884bcf64 | ||
![]() |
865decd7cb | ||
![]() |
750c39102d | ||
![]() |
124bffedfe | ||
![]() |
5946f1a618 | ||
![]() |
3a8d0021cd | ||
![]() |
4adacd0698 | ||
![]() |
03c85c5112 | ||
![]() |
74f7d2171b | ||
![]() |
cf2391e791 | ||
![]() |
8dfd28b346 | ||
![]() |
ae3f6d13da | ||
![]() |
d1d324a792 | ||
![]() |
caa11c1eca | ||
![]() |
b9a499d0f0 | ||
![]() |
7fa24d2fe6 | ||
![]() |
b2ba661d56 | ||
![]() |
12fa07f243 | ||
![]() |
75f76e473b | ||
![]() |
97fe205e5f | ||
![]() |
3ca211b9bb | ||
![]() |
a1c55befa2 | ||
![]() |
ef5cd0cdef | ||
![]() |
09ce310bad | ||
![]() |
008a42bd9b | ||
![]() |
a19555b429 | ||
![]() |
1d45e65434 | ||
![]() |
acca2aba14 | ||
![]() |
b03c5ce0de | ||
![]() |
5f7c036149 | ||
![]() |
067021b18a | ||
![]() |
b2357cf522 | ||
![]() |
53bb6a8443 | ||
![]() |
5ceda71589 | ||
![]() |
f41078b769 | ||
![]() |
5604d8716b | ||
![]() |
c4aede5833 | ||
![]() |
871f9bb8a2 | ||
![]() |
460440ea24 | ||
![]() |
3ffba24994 | ||
![]() |
e995b46641 | ||
![]() |
947ec69e60 | ||
![]() |
02e570bc9c | ||
![]() |
dd1c9a1d1c | ||
![]() |
ad676d5fde | ||
![]() |
bbd7d6a89a | ||
![]() |
16308996e1 | ||
![]() |
7c931f4f6d | ||
![]() |
9a08ccd95b | ||
![]() |
37003b9e85 | ||
![]() |
ba7a8f9d66 | ||
![]() |
8e0a4c6df6 | ||
![]() |
60b8fd51a2 | ||
![]() |
a1f9704c0b | ||
![]() |
4915744c84 | ||
![]() |
8df7d7d27d | ||
![]() |
3de4c085da | ||
![]() |
2d749166cf | ||
![]() |
8cac89cdd1 | ||
![]() |
d0f419c18b | ||
![]() |
5e55434772 | ||
![]() |
3c0b6cfa22 | ||
![]() |
b3adf4d556 | ||
![]() |
0fd656e5fa | ||
![]() |
3a988d23df | ||
![]() |
b20a3445d1 | ||
![]() |
c3e136cebb | ||
![]() |
30197c7f15 | ||
![]() |
5ef427cb45 | ||
![]() |
a574b7393d | ||
![]() |
5e3aa587c7 | ||
![]() |
ea8936f283 | ||
![]() |
cb02338c67 | ||
![]() |
d4307ae336 | ||
![]() |
50c2ac70e3 | ||
![]() |
d752039de9 | ||
![]() |
24641a33f7 | ||
![]() |
5db6d85e35 | ||
![]() |
72c6b99f58 | ||
![]() |
80120eb37b | ||
![]() |
71f2f7fba5 | ||
![]() |
d8d448bcf7 | ||
![]() |
973f934614 | ||
![]() |
bdcd7fd814 | ||
![]() |
4ac0bbebe2 | ||
![]() |
f4cd6bd44a | ||
![]() |
58427e7017 | ||
![]() |
de2b747a20 | ||
![]() |
69d1d749cf | ||
![]() |
b6bf317649 | ||
![]() |
23b8f5dea6 | ||
![]() |
18cbeed06e | ||
![]() |
586e09f2b5 | ||
![]() |
2dd14f4229 | ||
![]() |
7e537d87ac | ||
![]() |
a06326fb1c | ||
![]() |
d8b51e0b1a | ||
![]() |
0ea9710497 | ||
![]() |
1307ff407e | ||
![]() |
27884b1201 | ||
![]() |
8c9a7fa339 | ||
![]() |
ce128306cc | ||
![]() |
bf4ecc68c8 | ||
![]() |
0710ecdf01 | ||
![]() |
56fd8ec4cd | ||
![]() |
1a00b98ba3 | ||
![]() |
34f98fb08b | ||
![]() |
2e9b332012 | ||
![]() |
0b0b8b9cc0 | ||
![]() |
bc34cf3876 | ||
![]() |
0314edd550 | ||
![]() |
afc9654200 | ||
![]() |
65885acb75 | ||
![]() |
9760002fad | ||
![]() |
8adf64acb9 | ||
![]() |
cb07aa9fb0 | ||
![]() |
bdbe28b854 | ||
![]() |
2be073078f | ||
![]() |
42dbfb0681 | ||
![]() |
c166eee586 | ||
![]() |
7bdf07cae5 | ||
![]() |
67eeaa7293 | ||
![]() |
87670770b0 | ||
![]() |
e950d98b73 | ||
![]() |
af8d270460 | ||
![]() |
103097edbd | ||
![]() |
23c9e42bd5 | ||
![]() |
0f31144567 | ||
![]() |
0c321f2bb3 | ||
![]() |
aa8fd14517 | ||
![]() |
de5726929f | ||
![]() |
b75c028ce1 | ||
![]() |
9f8cb1a875 | ||
![]() |
848fa9fd02 | ||
![]() |
6db2809de5 | ||
![]() |
e0048d1fc3 | ||
![]() |
9c3c8cf46d | ||
![]() |
58eefd5670 | ||
![]() |
fca52df9f5 | ||
![]() |
5664291c92 | ||
![]() |
813fded927 | ||
![]() |
f814fe6c42 | ||
![]() |
570efa661d | ||
![]() |
d8705e3fb2 | ||
![]() |
01a8a44114 | ||
![]() |
ef92e4f38c | ||
![]() |
6880d155ed | ||
![]() |
6ad3d9681a | ||
![]() |
40c589063a | ||
![]() |
bbd7f801b4 | ||
![]() |
710d04a77b | ||
![]() |
ba446169a9 | ||
![]() |
d55f403ff2 | ||
![]() |
5b857c1724 | ||
![]() |
dd6f163793 | ||
![]() |
7bea7b9ed1 | ||
![]() |
288c25e32c | ||
![]() |
2144af9899 | ||
![]() |
62cb8d8e18 | ||
![]() |
1f774a8299 | ||
![]() |
f664eb53e7 | ||
![]() |
3ace7b157a | ||
![]() |
8e860f327a | ||
![]() |
577398dc17 | ||
![]() |
83b7e8dc98 | ||
![]() |
29dde732bd | ||
![]() |
dc66889a42 | ||
![]() |
8b86b2c91c | ||
![]() |
17efbba1ff | ||
![]() |
350a090ac3 | ||
![]() |
944be7f4b1 | ||
![]() |
13c829e6cc | ||
![]() |
a8ddf1e7fa | ||
![]() |
3a74299ece | ||
![]() |
8ba5d4d238 | ||
![]() |
9b4a6c45ee | ||
![]() |
cdbbaa0450 | ||
![]() |
1a5dfd29a0 | ||
![]() |
6f40f29d84 | ||
![]() |
4416fa5d64 | ||
![]() |
acedb845af | ||
![]() |
f62fce715d | ||
![]() |
45fe192564 | ||
![]() |
284c840a93 | ||
![]() |
edf819ab1a | ||
![]() |
92d50d4932 | ||
![]() |
d6fabfbb6f | ||
![]() |
0bfb1dee90 | ||
![]() |
dd2861fe5c | ||
![]() |
646c38c0ee | ||
![]() |
c1e3c6f383 | ||
![]() |
017d2c75b6 | ||
![]() |
18da8c9238 | ||
![]() |
531a8ece25 | ||
![]() |
a837614e4c | ||
![]() |
952ba32c1d | ||
![]() |
9c75f3e7c9 | ||
![]() |
e75fb2fb86 | ||
![]() |
99d287333c | ||
![]() |
7b7377f7ce | ||
![]() |
1e57cf01ae | ||
![]() |
3ed2e28161 | ||
![]() |
5065894894 | ||
![]() |
dffe378c0d | ||
![]() |
793c6440ca | ||
![]() |
30f10a7da9 | ||
![]() |
15486043d3 | ||
![]() |
0d5f8f273f | ||
![]() |
76affc56b5 | ||
![]() |
dac68e7445 | ||
![]() |
a8df7c7c9e | ||
![]() |
745d4f89c7 | ||
![]() |
2afa5c856d | ||
![]() |
293376ce03 | ||
![]() |
3713fa6909 | ||
![]() |
da67e10cd2 | ||
![]() |
fcea89b347 | ||
![]() |
a42f839d1f | ||
![]() |
9edef64f97 | ||
![]() |
8a23277311 | ||
![]() |
c1dfa24f1b | ||
![]() |
d79eed6d70 | ||
![]() |
644a15b782 | ||
![]() |
6714721adf | ||
![]() |
8dc3969906 | ||
![]() |
4d9456610f | ||
![]() |
c8b233bcb7 | ||
![]() |
27eee16e77 | ||
![]() |
2cf83e88ed | ||
![]() |
b9cdaed8c9 | ||
![]() |
8f60a5efd6 | ||
![]() |
c040e33a8a | ||
![]() |
a4b620471a | ||
![]() |
41f40a783f | ||
![]() |
3a874583bc | ||
![]() |
ea688c363b | ||
![]() |
f88bedd600 | ||
![]() |
aca177aa6d | ||
![]() |
fd7404e565 | ||
![]() |
d2beab0976 | ||
![]() |
56f5c66c79 | ||
![]() |
baa41f707d | ||
![]() |
a920217959 | ||
![]() |
53f717651e | ||
![]() |
198fc3579e |
13
.babelrc
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"presets": ["env", "react","stage-2"],
|
||||
"plugins": [
|
||||
"ramda",
|
||||
"lodash",
|
||||
"react-hot-loader/babel",
|
||||
"lodash",
|
||||
["transform-runtime", {
|
||||
"polyfill": false,
|
||||
"regenerator": true
|
||||
}]
|
||||
]
|
||||
}
|
36
.drone.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
kind: pipeline
|
||||
name: build
|
||||
type: docker
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: build-master
|
||||
image: plugins/docker
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
environment:
|
||||
REACT_APP_PUBLIC_PATH: https://map.vault48.org/
|
||||
REACT_APP_API_ADDR: https://backend-map.vault48.org
|
||||
REACT_APP_OSRM_URL: https://osrm.vault48.org/route/v1
|
||||
REACT_APP_OSRM_PROFILE: bike
|
||||
settings:
|
||||
dockerfile: docker/www/Dockerfile
|
||||
build_args_from_env:
|
||||
- REACT_APP_PUBLIC_PATH
|
||||
- REACT_APP_API_ADDR
|
||||
- REACT_APP_OSRM_URL
|
||||
- REACT_APP_OSRM_PROFILE
|
||||
tag:
|
||||
- ${DRONE_BRANCH}
|
||||
username:
|
||||
from_secret: global_docker_login
|
||||
password:
|
||||
from_secret: global_docker_password
|
||||
registry:
|
||||
from_secret: global_docker_registry
|
||||
repo:
|
||||
from_secret: docker_repo
|
4
.env.example
Normal file
|
@ -0,0 +1,4 @@
|
|||
REACT_APP_PUBLIC_PATH = https://map.vault48.org/
|
||||
REACT_APP_API_ADDR = https://backend.map.vault48.org
|
||||
REACT_APP_OSRM_URL = https://vault48.org:5001/route/v1
|
||||
REACT_APP_OSRM_PROFILE = bike
|
71
.eslintrc
|
@ -1,71 +0,0 @@
|
|||
{
|
||||
"extends": "airbnb",
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react",
|
||||
"jsx-a11y",
|
||||
"import"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": 1,
|
||||
"comma-dangle": 0,
|
||||
"no-restricted-syntax": 1,
|
||||
"new-cap": 1,
|
||||
"no-continue": 1,
|
||||
"no-underscore-dangle": 0,
|
||||
"global-require": 1,
|
||||
"react/no-multi-comp": 1,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"import/no-unresolved": 1,
|
||||
"import/prefer-default-export": 0,
|
||||
"import/extensions": 1,
|
||||
"no-return-assign": 1,
|
||||
"max-len": 1,
|
||||
"jsx-a11y/no-static-element-interactions": 0,
|
||||
"jsx-a11y/click-events-have-key-events": 0,
|
||||
"jsx-a11y/interactive-supports-focus": 0,
|
||||
"arrow-parens": 0,
|
||||
"jsx-a11y/no-autofocus": 0,
|
||||
"react/jsx-closing-tag-location": 0,
|
||||
"prefer-promise-reject-errors": 0,
|
||||
"jsx-a11y/mouse-events-have-key-events": 0,
|
||||
"camelcase": 0,
|
||||
"no-trailing-spaces": 0,
|
||||
},
|
||||
"globals": {
|
||||
"document": false,
|
||||
"window": false,
|
||||
"HTMLInputElement": false,
|
||||
"HTMLDivElement": false,
|
||||
"Headers": false,
|
||||
"FormData": false,
|
||||
"WebSocket": true,
|
||||
"Element": true,
|
||||
"localStorage": true,
|
||||
},
|
||||
"env": {},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"webpack.config.js": {
|
||||
"resolve": {
|
||||
"alias": {
|
||||
"$components": "src/components",
|
||||
"$containers": "src/containers",
|
||||
"$constants": "src/constants",
|
||||
"$sprites": "src/sprites",
|
||||
"$config": "config",
|
||||
"$styles": "src/styles",
|
||||
"$redux": "src/redux",
|
||||
"$utils": "src/utils",
|
||||
"$modules": "src/modules"
|
||||
},
|
||||
|
||||
"extensions": [".js", ".jsx", ".scss"],
|
||||
"modules": ["config", "src", "node_modules"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
.flowconfig
|
@ -1,23 +0,0 @@
|
|||
[options]
|
||||
module.system.node.resolve_dirname=node_modules
|
||||
module.system.node.resolve_dirname=src
|
||||
module.name_mapper='^$redux/\([-a-zA-Z0-9$_/]+\)$' -> '<PROJECT_ROOT>/src/redux/\1'
|
||||
module.name_mapper='^$config/\([-a-zA-Z0-9$_/]+\)$' -> '<PROJECT_ROOT>/config/\1'
|
||||
module.name_mapper='^$components\/\(.*\)$' ->'<PROJECT_ROOT>/src/components/\1'
|
||||
module.name_mapper='^$containers\/\(.*\)$' ->'<PROJECT_ROOT>/src/containers/\1'
|
||||
module.name_mapper='^$constants\/\(.*\)$' ->'<PROJECT_ROOT>/src/constants/\1'
|
||||
module.name_mapper='^$sprites\/\(.*\)$' ->'<PROJECT_ROOT>/src/sprites/\1'
|
||||
module.name_mapper='^$config\/\(.*\)$' ->'<PROJECT_ROOT>/src/config/\1'
|
||||
module.name_mapper='^$styles\/\(.*\)$' ->'<PROJECT_ROOT>/src/styles/\1'
|
||||
module.name_mapper='^$utils\/\(.*\)$' ->'<PROJECT_ROOT>/src/utils/\1'
|
||||
|
||||
[ignore]
|
||||
.*/node_modules
|
||||
|
||||
[include]
|
||||
public
|
||||
../node_modules/
|
||||
|
||||
[libs]
|
||||
flow-libs/
|
||||
|
6
.gitignore
vendored
|
@ -14,9 +14,9 @@ yarn-error.log
|
|||
/osrm/pbf
|
||||
/osrm/pbf/*
|
||||
|
||||
/config/frontend.js
|
||||
/config/backend.js
|
||||
|
||||
# Bundle
|
||||
*.js.map
|
||||
stats.json
|
||||
|
||||
.env
|
||||
build
|
||||
|
|
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
|
@ -35,10 +35,3 @@ Configs are placed in ```./config/frontend.js```
|
|||
For development launch ```npm start``` and visit ```http://localhost:8000/```
|
||||
|
||||
For production launch ```npm build```, the output will be placed at ```./dist``` folder, you should configure your http server to serve index html from that folder.
|
||||
|
||||
### Backend
|
||||
Take a look at ```./config/backend.js```. By default your api server will be spawned at ```http://localhost:3000/```
|
||||
|
||||
For development launch ```npm serve-dev```, it will launch dev server, reloading on every code update
|
||||
|
||||
For production launch ```npm serve``` and make it running in background by any desirable way, such as ```forever```
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
https://habr.com/company/ruvds/blog/321104/
|
||||
*/
|
||||
const createError = require('http-errors');
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const lessMiddleware = require('less-middleware');
|
||||
const logger = require('morgan');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const routeRouter = require('./routes/route');
|
||||
const authRouter = require('./routes/auth');
|
||||
const db = require('./config/db');
|
||||
|
||||
const app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(lessMiddleware(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, PATCH');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.json());
|
||||
|
||||
// app.use('/', indexRouter);
|
||||
// app.use('/users', usersRouter);
|
||||
app.use('/auth', authRouter);
|
||||
app.use('/route', routeRouter);
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use((err, req, res, next) => {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
const { CONFIG } = require('../../config/backend');
|
||||
|
||||
const app = require('../app');
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
if (CONFIG.HTTP.ENABLED) {
|
||||
const httpPort = CONFIG.HTTP.PORT;
|
||||
// app.set('port', httpPort);
|
||||
|
||||
const httpServer = http.createServer(app);
|
||||
httpServer.listen(httpPort);
|
||||
|
||||
httpServer.on('error', console.log);
|
||||
httpServer.on('listening', console.log);
|
||||
}
|
||||
|
||||
if (CONFIG.HTTPS.ENABLED) {
|
||||
const sslPort = CONFIG.HTTPS.PORT;
|
||||
// app.set('port', sslPort);
|
||||
|
||||
const key = fs.readFileSync(CONFIG.HTTPS.PRIVATE_KEY, 'utf8');
|
||||
const cert = fs.readFileSync(CONFIG.HTTPS.CERTIFICATE, 'utf8');
|
||||
const ca = fs.readFileSync(CONFIG.HTTPS.CA, 'utf8');
|
||||
|
||||
const sslServer = https.createServer({ key, cert, ca }, app);
|
||||
|
||||
sslServer.listen(sslPort);
|
||||
sslServer.on('error', console.log);
|
||||
sslServer.on('listening', console.log);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
const { CONFIG } = require('../../config/backend');
|
||||
|
||||
const {
|
||||
DB: {
|
||||
USER, PASSWORD, HOSTNAME, PORT, DATABASE
|
||||
}
|
||||
} = CONFIG;
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
mongoose.Promise = require('bluebird');
|
||||
|
||||
mongoose.connect(`mongodb://${USER}:${PASSWORD}@${HOSTNAME}:${PORT}/${DATABASE}`, { });
|
||||
const database = mongoose.connection;
|
||||
|
||||
database.on('error', (err) => { console.error(`Database Connection Error: ${err}`); process.exit(2); });
|
||||
database.on('connected', () => { console.info('Succesfully connected to MongoDB Database'); });
|
||||
|
||||
console.log(`DB: mongodb://${USER}:${PASSWORD}@${HOSTNAME}:${PORT}/${DATABASE}`);
|
|
@ -1,11 +0,0 @@
|
|||
// export const OAUTH_FAILED_TITLE = 'Ошибка авторизации';
|
||||
module.exports.STRINGS = {
|
||||
OAUTH: {
|
||||
ERROR_TITLE: 'Ошибка авторизации',
|
||||
ERROR_HEADING: 'Ошибка',
|
||||
ERROR_TEXT: 'Авторизация не удалась, попробуйте еще раз',
|
||||
ERROR_CLOSE_BUTTON: 'ЗАКРЫТЬ',
|
||||
|
||||
SUCCESS_TITLE: 'Успешно!',
|
||||
},
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const RouteSchema = new Schema(
|
||||
{
|
||||
_id: { type: String, required: true },
|
||||
title: { type: String, default: '' },
|
||||
// address: { type: String, required: true },
|
||||
version: { type: Number, default: 2 },
|
||||
route: { type: Array, default: [] },
|
||||
stickers: { type: Array, default: [] },
|
||||
owner: { type: Schema.Types.ObjectId, ref: 'User' },
|
||||
distance: { type: Number, default: 0 },
|
||||
is_public: { type: Boolean, default: false },
|
||||
is_deleted: { type: Boolean, default: false },
|
||||
created_at: { type: Date, default: Date.now() },
|
||||
updated_at: { type: Date, default: Date.now() },
|
||||
logo: { type: String, default: 'DEFAULT' },
|
||||
provider: { type: String, default: 'DEFAULT' },
|
||||
},
|
||||
);
|
||||
|
||||
module.exports.RouteSchema = RouteSchema;
|
|
@ -1,27 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { Schema } = mongoose;
|
||||
|
||||
// Schemas
|
||||
const UserSchema = new Schema(
|
||||
{
|
||||
_id: { type: String, required: true },
|
||||
role: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['admin', 'guest', 'user', 'vk'],
|
||||
},
|
||||
token: { type: String, required: true },
|
||||
created_at: { type: Date, required: true, default: Date.now },
|
||||
|
||||
first_name: { type: String },
|
||||
last_name: { type: String },
|
||||
photo: { type: String },
|
||||
version: { type: Number, default: 2 },
|
||||
routes: [{ type: Schema.Types.ObjectId, ref: 'Route' }]
|
||||
},
|
||||
{
|
||||
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
||||
}
|
||||
);
|
||||
|
||||
module.exports.UserSchema = UserSchema;
|
|
@ -1,8 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const { UserSchema } = require('./User');
|
||||
const { RouteSchema } = require('./Route');
|
||||
|
||||
module.exports.User = mongoose.model('User', UserSchema);
|
||||
module.exports.Route = mongoose.model('Route', RouteSchema);
|
||||
|
|
@ -1 +0,0 @@
|
|||
body{background:#6d9dc8;font:14px "Lucida Grande",Helvetica,Arial,sans-serif;color:white;margin:0;font-weight:300}div#message{padding:100px;display:flex;align-items:center;justify-content:center;flex-direction:column;height:100vh;width:100vw;margin:0;position:absolute;box-sizing:border-box;text-align:left}div.bg{background:white;color:#666666;padding:20px 40px;border-radius:4px}h1{margin-bottom:10px;text-transform:uppercase}button{height:32px;padding:0 24px;color:white;border:none;box-sizing:border-box;margin-top:40px;border-radius:3px;background:#6d9dc8;font-weight:bold;float:right;cursor:pointer}
|
|
@ -1,47 +0,0 @@
|
|||
body {
|
||||
background: #6d9dc8;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
color: white;
|
||||
margin: 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
div#message {
|
||||
padding: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.bg {
|
||||
background: white;
|
||||
color: #666666;
|
||||
padding: 20px 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 32px;
|
||||
padding: 0 24px;
|
||||
color: white;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
margin-top: 40px;
|
||||
border-radius: 3px;
|
||||
background: #6d9dc8;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
body{padding:50px;font:14px "Lucida Grande",Helvetica,Arial,sans-serif}a{color:#00B7FF}
|
|
@ -1,8 +0,0 @@
|
|||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
const express = require('express');
|
||||
const guest = require('./auth/guest');
|
||||
const list = require('./auth/list');
|
||||
const check = require('./auth/check');
|
||||
const vk = require('./auth/social/vk');
|
||||
const iframe_vk = require('./auth/iframe/vk');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', check);
|
||||
router.get('/list', list);
|
||||
router.get('/guest', guest);
|
||||
router.get('/social/vk', vk);
|
||||
router.get('/iframe/vk', iframe_vk);
|
||||
|
||||
module.exports = router;
|
|
@ -1,32 +0,0 @@
|
|||
const { User } = require('../../models');
|
||||
const { generateGuest, generateRandomUrl } = require('./guest');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { id, token } = req.query;
|
||||
|
||||
const user = await User.findOne({ _id: id, token });
|
||||
// .populate({
|
||||
// path: 'routes',
|
||||
// select: '_id title distance owner updated_at',
|
||||
// options: {
|
||||
// limit: 200,
|
||||
// sort: { updated_at: -1 },
|
||||
// }
|
||||
// })
|
||||
|
||||
const random_url = await generateRandomUrl();
|
||||
|
||||
if (user) {
|
||||
return res.send({
|
||||
success: true, ...user.toObject(), id: user._id, random_url
|
||||
});
|
||||
}
|
||||
|
||||
const guest = await generateGuest();
|
||||
const created = await User.create(guest).then(result => result.toObject());
|
||||
|
||||
return res.send({
|
||||
success: false, error: 'user not found', error_code: 1231, ...created, id: created._id, random_url
|
||||
});
|
||||
};
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
const { genRandomSequence } = require('../../utils/gen');
|
||||
const { User } = require('../../models');
|
||||
|
||||
const generateGuestToken = () => {
|
||||
const _id = `guest:${genRandomSequence(16)}`;
|
||||
|
||||
return User.find({ _id }).then(user => {
|
||||
if (user.length) return generateGuestToken();
|
||||
|
||||
return _id;
|
||||
});
|
||||
};
|
||||
|
||||
const generateUser = (_id, role = 'guest') => {
|
||||
const token = `seq:${genRandomSequence(32)}`;
|
||||
|
||||
return { _id, token, role };
|
||||
};
|
||||
|
||||
const saveUser = user => {
|
||||
const model = new User({ ...user });
|
||||
|
||||
return model.save()
|
||||
.then(() => model.toObject())
|
||||
.catch(() => ({
|
||||
...user,
|
||||
success: false,
|
||||
error: 'Error saving user model',
|
||||
error_code: 1232,
|
||||
}));
|
||||
};
|
||||
|
||||
const generateRandomUrl = () => Promise.resolve(genRandomSequence(16));
|
||||
|
||||
const generateGuest = async () => {
|
||||
const user = await generateGuestToken()
|
||||
.then(generateUser);
|
||||
const random_url = await generateRandomUrl();
|
||||
|
||||
return { ...user, random_url, first_name: '', last_name: '', photo: '' };
|
||||
};
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const user = await generateGuest().then(saveUser);
|
||||
|
||||
res.send({ success: true, ...user });
|
||||
};
|
||||
|
||||
module.exports.generateUser = generateUser;
|
||||
module.exports.generateGuest = generateGuest;
|
||||
module.exports.generateGuestToken = generateGuestToken;
|
||||
module.exports.generateRandomUrl = generateRandomUrl;
|
|
@ -1,22 +0,0 @@
|
|||
const { User } = require('../../../models');
|
||||
const { CONFIG } = require('../../../../config/backend');
|
||||
const md5 = require('js-md5');
|
||||
const { generateRandomUrl } = require('../guest');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
if (!CONFIG.SOCIAL.VK_IFRAME.ENABLED) return res.send({ success: false, error: 'Unsupported Method' });
|
||||
|
||||
const { query: { viewer_id, auth_key } } = req;
|
||||
|
||||
const checksum = md5(`${CONFIG.SOCIAL.VK_IFRAME.APP_ID}_${viewer_id}_${CONFIG.SOCIAL.VK_IFRAME.SECRET}`);
|
||||
|
||||
if (checksum !== auth_key) return res.send({ success: false, error: 'No such user (1)' });
|
||||
|
||||
const user = await User.findOne({ _id: `vk:${viewer_id}` }).populate('routes');
|
||||
|
||||
if (!user) return res.send({ success: false, error: 'No such user (2)' });
|
||||
|
||||
const random_url = await generateRandomUrl();
|
||||
return res.send({ success: true, user: { ...user.toObject(), id: user._id, random_url } });
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
const { User } = require('../../models');
|
||||
|
||||
module.exports = (req, res) => User.find((err, articles) => {
|
||||
if (!err) return res.send(articles);
|
||||
|
||||
res.statusCode = 500;
|
||||
return res.send({ error: 'Server error' });
|
||||
});
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
const { User } = require('../../../models');
|
||||
const axios = require('axios');
|
||||
const { generateUser } = require('../guest');
|
||||
const { STRINGS } = require('../../../config/strings');
|
||||
const { CONFIG } = require('../../../../config/backend');
|
||||
|
||||
const fetchUserData = async (req, res) => {
|
||||
const { query: { code } } = req;
|
||||
const host = req.get('host');
|
||||
const proto = req.connection.encrypted ? 'https' : 'http';
|
||||
|
||||
const { data: { access_token, user_id } } = await axios.get(
|
||||
'https://oauth.vk.com/access_token',
|
||||
{
|
||||
params: {
|
||||
client_id: CONFIG.SOCIAL.VK.APP_ID,
|
||||
client_secret: CONFIG.SOCIAL.VK.SECRET,
|
||||
code,
|
||||
redirect_uri: `${proto}://${host}/auth/social/vk`,
|
||||
}
|
||||
}
|
||||
).catch(() => {
|
||||
return res.render('social/vk_error', {
|
||||
title: STRINGS.OAUTH.ERROR_TITLE,
|
||||
heading: STRINGS.OAUTH.ERROR_HEADING,
|
||||
reason: STRINGS.OAUTH.ERROR_TEXT,
|
||||
button: STRINGS.OAUTH.ERROR_CLOSE_BUTTON,
|
||||
});
|
||||
});
|
||||
|
||||
const { data } = await axios.get(
|
||||
'https://api.vk.com/method/users.get',
|
||||
{
|
||||
params: {
|
||||
user_id,
|
||||
fields: 'photo',
|
||||
v: '5.67',
|
||||
access_token,
|
||||
}
|
||||
}
|
||||
).catch(() => {
|
||||
return res.render('social/vk_error', {
|
||||
title: STRINGS.OAUTH.ERROR_TITLE,
|
||||
heading: STRINGS.OAUTH.ERROR_HEADING,
|
||||
reason: STRINGS.OAUTH.ERROR_TEXT,
|
||||
button: STRINGS.OAUTH.ERROR_CLOSE_BUTTON,
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { response } = await fetchUserData(req, res);
|
||||
|
||||
const {
|
||||
first_name = '', last_name = '', photo = '', id = ''
|
||||
} = response[0];
|
||||
|
||||
const newUser = await generateUser(`vk:${id}`, 'vk');
|
||||
const name = `${first_name} ${last_name}`;
|
||||
const user = {
|
||||
...newUser, first_name, last_name, photo, name,
|
||||
};
|
||||
|
||||
const auth = await User.findOne({ _id: user._id }).populate('routes');
|
||||
|
||||
if (auth) {
|
||||
await auth.set({
|
||||
first_name, last_name, name, photo
|
||||
}).save();
|
||||
|
||||
res.render('social/success', { title: STRINGS.OAUTH.SUCCESS_TITLE, ...user, token: auth.token });
|
||||
} else {
|
||||
const created = await User.create(user, (err, result) => {
|
||||
return result.toObject();
|
||||
});
|
||||
|
||||
res.render('social/success', { title: STRINGS.OAUTH.SUCCESS_TITLE, ...user, ...created });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,16 +0,0 @@
|
|||
const express = require('express');
|
||||
const post = require('./route/post');
|
||||
const get = require('./route/get');
|
||||
const list = require('./route/list');
|
||||
const drop = require('./route/drop');
|
||||
const patch = require('./route/patch');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/', post);
|
||||
router.get('/', get);
|
||||
router.patch('/', patch);
|
||||
router.delete('/', drop);
|
||||
router.get('/list', list);
|
||||
|
||||
module.exports = router;
|
|
@ -1,19 +0,0 @@
|
|||
const { User, Route } = require('../../models');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { body: { id, token, address } } = req;
|
||||
|
||||
const owner = await User.findOne({ _id: id, token }).populate('routes');
|
||||
|
||||
if (!owner) return res.send({ success: false, reason: 'unauthorized' });
|
||||
|
||||
const exists = await Route.findOne({ _id: address }).populate('owner', '_id');
|
||||
|
||||
if (!exists) return res.send({ success: false, mode: 'not_exists' });
|
||||
if (exists && exists.owner._id !== id) return res.send({ success: false, mode: 'not_yours' });
|
||||
|
||||
await exists.set({ is_deleted: true }).save();
|
||||
|
||||
return res.send({ success: true, address });
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
const { generateRandomUrl } = require('../auth/guest');
|
||||
const { Route } = require('../../models');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { query: { name } } = req;
|
||||
|
||||
if (!name) return res.send({ success: false, mode: 'not_found_1' });
|
||||
|
||||
const exists = await Route.findOne({ _id: name, is_deleted: false }).populate('owner', '_id');
|
||||
|
||||
if (!exists) return res.send({ success: false, mode: 'not_found_2' });
|
||||
const data = exists.toObject();
|
||||
const random_url = await generateRandomUrl();
|
||||
|
||||
return res.send({
|
||||
success: true,
|
||||
...data,
|
||||
address: exists._id,
|
||||
owner: {
|
||||
...data.owner,
|
||||
id: data.owner._id,
|
||||
},
|
||||
random_url,
|
||||
});
|
||||
};
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
const { Route, User } = require('../../models');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
||||
query: {
|
||||
id, token, title, distance, author, step = 20, shift = 0,
|
||||
}
|
||||
} = req;
|
||||
|
||||
const user = await User.findOne({ _id: id, token });
|
||||
|
||||
let criteria = { is_deleted: false };
|
||||
|
||||
if (title) {
|
||||
criteria = {
|
||||
...criteria,
|
||||
$or: [
|
||||
{ title: new RegExp(title.trim(), 'ig') },
|
||||
{ _id: new RegExp(title.trim(), 'ig') },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (!author || !user || (user._id !== author)) {
|
||||
criteria = {
|
||||
...criteria,
|
||||
is_public: true,
|
||||
};
|
||||
}
|
||||
|
||||
let list = await Route.find(
|
||||
{
|
||||
...criteria,
|
||||
},
|
||||
'_id title distance owner updated_at is_public is_deleted',
|
||||
{
|
||||
limit: 9000,
|
||||
sort: { updated_at: -1 },
|
||||
}
|
||||
).populate('owner', '_id');
|
||||
|
||||
list = list.filter(item => !author || (item.owner && item.owner._id === author));
|
||||
|
||||
let limits = list.reduce(({ min, max }, { distance: dist }) => ({
|
||||
min: Math.ceil(Math.min(dist, min) / 25) * 25,
|
||||
max: Math.ceil(Math.max(dist, max) / 25) * 25,
|
||||
}), { min: 999999, max: 0 });
|
||||
|
||||
const minDist = parseInt(distance[0], 10);
|
||||
const maxDist = parseInt(distance[1], 10) === 200 ? 99999 : parseInt(distance[1], 10);
|
||||
// const maxDist = parseInt(distance[1], 10) > parseInt(distance[0], 10)
|
||||
// ? parseInt(distance[1], 10)
|
||||
// : 10000;
|
||||
|
||||
if (distance && distance.length === 2) {
|
||||
list = list.filter(item => (
|
||||
item.distance >= minDist &&
|
||||
item.distance <= maxDist
|
||||
));
|
||||
}
|
||||
|
||||
const limit = list.length;
|
||||
|
||||
if (step) {
|
||||
list = list.slice(parseInt(shift, 10), (parseInt(shift, 10) + parseInt(step, 10)));
|
||||
}
|
||||
|
||||
if (list.length === 0) {
|
||||
limits = { min: 0, max: 0 };
|
||||
} else if (limits.max === 0) {
|
||||
limits = { min: 0, max: 0 };
|
||||
} else if (limits.min === limits.max) {
|
||||
limits = { min: limits.max - 25, max: limits.max };
|
||||
} else if (limits.max > 200) {
|
||||
limits = { min: limits.min, max: 200 };
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
list,
|
||||
limit,
|
||||
step: parseInt(step, 10),
|
||||
shift: parseInt(shift, 10),
|
||||
...limits,
|
||||
});
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
const { User, Route } = require('../../models');
|
||||
|
||||
const { parseString } = require('../../utils/parse');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { body, body: { id, token, address } } = req;
|
||||
|
||||
const owner = await User.findOne({ _id: id, token }).populate('routes');
|
||||
|
||||
if (!owner) return res.send({ success: false, reason: 'unauthorized' });
|
||||
|
||||
const title = parseString(body.title, 64);
|
||||
const is_public = !!body.is_public;
|
||||
|
||||
const exists = await Route.findOne({ _id: address, is_deleted: false }).populate('owner', '_id');
|
||||
|
||||
if (!exists) return res.send({ success: false, mode: 'not_exists' });
|
||||
if (exists && exists.owner._id !== id) return res.send({ success: false, mode: 'not_yours' });
|
||||
|
||||
exists.set({ title, is_public }).save();
|
||||
|
||||
return res.send({ success: true, ...exists });
|
||||
};
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
const { User, Route } = require('../../models');
|
||||
|
||||
const { parseRoute, parseStickers, parseString, parseNumber, parseAddress } = require('../../utils/parse');
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { body, body: { id, token, force } } = req;
|
||||
|
||||
const owner = await User.findOne({ _id: id, token }).populate('routes');
|
||||
if (!owner) return res.send({ success: false, reason: 'unauthorized', id, token });
|
||||
|
||||
const title = parseString(body.title, 64);
|
||||
const address = parseAddress(body.address, 32)
|
||||
const route = parseRoute(body.route);
|
||||
const stickers = parseStickers(body.stickers);
|
||||
const logo = parseString(body.logo, 16);
|
||||
const provider = parseString(body.provider, 16) || 'DEFAULT';
|
||||
const distance = parseNumber(body.distance, 0, 1000);
|
||||
const is_public = !!body.is_public;
|
||||
|
||||
if ((!route || route.length <= 0) && (!stickers || stickers.length <= 0)) {
|
||||
return res.send({ success: false, mode: 'empty' });
|
||||
}
|
||||
|
||||
const exists = await Route.findOne({ _id: address, is_deleted: false }).populate('owner', '_id');
|
||||
|
||||
if (exists && exists.owner._id !== id) return res.send({ success: false, mode: 'exists' });
|
||||
if (exists && !force) return res.send({ success: false, mode: 'overwriting' });
|
||||
|
||||
if (exists) {
|
||||
exists.set({
|
||||
title, route, stickers, logo, distance, updated_at: Date.now(), provider, is_public,
|
||||
}).save();
|
||||
|
||||
return res.send({
|
||||
success: true, title, address, route, stickers, mode: 'overwrited', is_public,
|
||||
});
|
||||
}
|
||||
|
||||
const created = await Route.create({
|
||||
_id: address, title, route, stickers, owner, logo, distance, provider, is_public,
|
||||
});
|
||||
|
||||
await owner.routes.push(created);
|
||||
await owner.save();
|
||||
|
||||
return res.send({
|
||||
success: true, title, address, route, stickers, provider, is_public,
|
||||
});
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET users listing. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.send('respond with a resource');
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,182 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
const mysql = require('mysql');
|
||||
const { REPLACEMENT, MAPS } = require('./stickersReplacement');
|
||||
const db = require('../config/db');
|
||||
let total_dist = 0;
|
||||
const { User, Route } = require('../models');
|
||||
|
||||
const con = mysql.createConnection({
|
||||
host: 'vault48.org',
|
||||
user: 'macos_exporter',
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
const tryJSON = data => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
function deg2rad(deg) {
|
||||
return (deg * Math.PI) / 180;
|
||||
}
|
||||
|
||||
const findDistance = (t1, n1, t2, n2) => {
|
||||
// convert coordinates to radians
|
||||
const lat1 = deg2rad(t1);
|
||||
const lon1 = deg2rad(n1);
|
||||
const lat2 = deg2rad(t2);
|
||||
const lon2 = deg2rad(n2);
|
||||
|
||||
// find the differences between the coordinates
|
||||
const dlat = lat2 - lat1;
|
||||
const dlon = lon2 - lon1;
|
||||
|
||||
// here's the heavy lifting
|
||||
const a = (Math.sin(dlat / 2) ** 2) +
|
||||
(Math.cos(lat1) * Math.cos(lat2) * (Math.sin(dlon / 2) ** 2));
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); // great circle distance in radians
|
||||
// const dm = c * 3961; // great circle distance in miles
|
||||
const dk = c * 6373; // great circle distance in km
|
||||
|
||||
// round the results down to the nearest 1/1000
|
||||
// const mi = round(dm);
|
||||
return (Math.round(dk * 1000) / 1000);
|
||||
};
|
||||
|
||||
const calcPolyDistance = route => {
|
||||
let summ = 0;
|
||||
for (let i = 1; i < route.length; i += 1) {
|
||||
summ += findDistance(route[i - 1].lat, route[i - 1].lng, route[i].lat, route[i].lng);
|
||||
}
|
||||
|
||||
total_dist += summ;
|
||||
|
||||
return parseFloat(Number(summ).toFixed(2));
|
||||
};
|
||||
|
||||
const stickerAngleParser = angle => parseFloat(((Number(angle) % Math.PI) + Math.PI).toFixed(2));
|
||||
|
||||
const stickerStyleParser = style => (REPLACEMENT[style]
|
||||
? { ...REPLACEMENT[style] }
|
||||
: { set: 'base', sticker: 'chicken' });
|
||||
|
||||
const stickersParser = stickers => (
|
||||
stickers.map(({
|
||||
ang, latlng, style, text
|
||||
}) => ({
|
||||
angle: stickerAngleParser(ang), // todo: change it!
|
||||
latlng,
|
||||
text,
|
||||
...stickerStyleParser(style),
|
||||
}))
|
||||
);
|
||||
|
||||
const pointParser = points => (
|
||||
points.map(({ latlngs, text }) => ({
|
||||
text,
|
||||
latlng: latlngs[0],
|
||||
set: 'base',
|
||||
sticker: 'empty',
|
||||
angle: 2.2,
|
||||
}))
|
||||
);
|
||||
|
||||
const mapStyleParser = style => (MAPS[style]
|
||||
? MAPS[style]
|
||||
: 'DEFAULT'
|
||||
);
|
||||
|
||||
const run = async () => {
|
||||
await con.connect(err => { if (err) throw err; });
|
||||
await con.query('use neu_map');
|
||||
|
||||
const users = await new Promise(resolve => con.query('SELECT * from tokens where role = "vk"', (err, rows) => {
|
||||
resolve(rows.map(({
|
||||
login, token, created, role, data
|
||||
}) => {
|
||||
const info = tryJSON(data);
|
||||
const created_at = new Date(created * 1000);
|
||||
|
||||
return {
|
||||
_id: login,
|
||||
token,
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
name: info.name,
|
||||
first_name: (info.name && info.name.split(' ')[0]) || '',
|
||||
last_name: (info.name && info.name.split(' ')[1]) || '',
|
||||
role,
|
||||
photo: info.photo,
|
||||
version: 1,
|
||||
};
|
||||
}));
|
||||
}));
|
||||
|
||||
const routes = await new Promise(resolve => con.query('SELECT routes.*, tokens.login as login from routes LEFT JOIN tokens ON tokens.id = routes.id WHERE tokens.login IS NOT NULL AND tokens.role="vk"', (err, rows) => {
|
||||
if (err) console.log('error', err);
|
||||
|
||||
resolve(rows.map(({
|
||||
data, created, name, login
|
||||
}) => {
|
||||
const {
|
||||
map_style, route, stickers, points, logo
|
||||
} = tryJSON(data);
|
||||
const created_at = new Date(created * 1000);
|
||||
|
||||
return {
|
||||
_id: name,
|
||||
owner: { _id: login },
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
provider: mapStyleParser(map_style),
|
||||
route: route.map(({ lat, lng }) => ({ lat: Number(lat), lng: Number(lng) })),
|
||||
stickers: [
|
||||
...stickersParser(stickers),
|
||||
...pointParser(points)
|
||||
],
|
||||
logo: logo === 'default' ? 'nvs' : logo,
|
||||
title: '',
|
||||
version: 1,
|
||||
distance: calcPolyDistance(route),
|
||||
is_public: true,
|
||||
};
|
||||
}));
|
||||
}));
|
||||
|
||||
console.log('ended, got users:', users.length);
|
||||
console.log('ended, got routes:', routes.length);
|
||||
|
||||
const deletedUsers = await User.deleteMany({
|
||||
// version: 1
|
||||
});
|
||||
const deletedRoutes = await Route.deleteMany({
|
||||
// version: 1
|
||||
});
|
||||
console.log("dropped users", deletedUsers);
|
||||
console.log("dropped routes", deletedRoutes);
|
||||
console.log('inserting users');
|
||||
const addedUsers = (await Promise.all(users.map(user => User.create(user))))
|
||||
.reduce((obj, user) => ({ ...obj, [user._id]: user }), {});
|
||||
|
||||
console.log('inserting routes');
|
||||
const addedRoutes = await (await Promise.all(routes.map(route => Route.create({
|
||||
...route,
|
||||
owner: addedUsers[route.owner._id],
|
||||
}))));
|
||||
|
||||
await Promise.all(addedRoutes.map(async (route) => {
|
||||
await addedUsers[route.owner._id].routes.push(route);
|
||||
}));
|
||||
|
||||
await Promise.all(addedRoutes.map(route => new Promise(resolve => (
|
||||
addedUsers[route.owner._id].save(() => resolve())
|
||||
))));
|
||||
|
||||
console.log('ok');
|
||||
};
|
||||
|
||||
run();
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
module.exports.REPLACEMENT = {
|
||||
0: { set: 'base', sticker: 'men' },
|
||||
1: { set: 'base', sticker: 'square' },
|
||||
2: { set: 'base', sticker: 'bridge' },
|
||||
3: { set: 'base', sticker: 'ikea' },
|
||||
4: { set: 'base', sticker: 'bugr' },
|
||||
5: { set: 'base', sticker: 'monum' },
|
||||
6: { set: 'base', sticker: 'opera' },
|
||||
7: { set: 'base', sticker: 'forest' },
|
||||
8: { set: 'base', sticker: 'empty' },
|
||||
9: { set: 'base', sticker: 'empty' },
|
||||
10: { set: 'base', sticker: 'road' },
|
||||
11: { set: 'base', sticker: 'chicken' },
|
||||
12: { set: 'base', sticker: 'camp' },
|
||||
13: { set: 'base', sticker: 'fastfood' },
|
||||
14: { set: 'base', sticker: 'beer' },
|
||||
15: { set: 'base', sticker: 'pancake' },
|
||||
16: { set: 'base', sticker: 'rocks' },
|
||||
17: { set: 'real', sticker: 'swamp' },
|
||||
18: { set: 'real', sticker: 'skull' },
|
||||
19: { set: 'base', sticker: 'crap' },
|
||||
20: { set: 'pin', sticker: 'start' },
|
||||
21: { set: 'pin', sticker: 'p1' },
|
||||
22: { set: 'pin', sticker: 'p2' },
|
||||
23: { set: 'pin', sticker: 'p3' },
|
||||
24: { set: 'pin', sticker: 'p4' },
|
||||
25: { set: 'pin', sticker: 'p5' },
|
||||
26: { set: 'pin', sticker: 'p6' },
|
||||
27: { set: 'pin', sticker: 'finish' },
|
||||
29: { set: 'pin', sticker: 'danger' },
|
||||
30: { set: 'pin', sticker: 'question' },
|
||||
};
|
||||
|
||||
module.exports.MAPS = {
|
||||
'watercolor': 'BLANK',
|
||||
'darq': 'BLANK',
|
||||
'default': 'DGIS',
|
||||
'osm': 'DEFAULT',
|
||||
'hot': 'HOT',
|
||||
'blank': 'BLANK',
|
||||
'sat': 'DEFAULT',
|
||||
'ymap': 'DEFAULT',
|
||||
'ysat': 'DEFAULT',
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
module.exports.genRandomSequence = (length = 16) => {
|
||||
let sequence = '';
|
||||
const symbols = 'ABCDEFGHIJKLMOPQRSTUVXYZabcdefghijgmlopqrstuvxyz01234567890'
|
||||
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
sequence += symbols[parseInt(Math.random() * (symbols.length - 1), 10)];
|
||||
}
|
||||
|
||||
return sequence;
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
module.exports.parseRoute = route => route.filter(el => (
|
||||
Object.keys(el).length === 2
|
||||
&& el.lat
|
||||
&& parseInt(el.lat, 10) > 0
|
||||
&& parseInt(el.lat, 10) < 1000
|
||||
&& el.lng
|
||||
&& parseInt(el.lng, 10) > 0
|
||||
&& parseInt(el.lng, 10) < 1000
|
||||
));
|
||||
|
||||
module.exports.parseStickers = stickers => stickers.filter(el => (
|
||||
Object.keys(el).length === 5
|
||||
&& el.latlng
|
||||
&& Object.keys(el.latlng).length === 2
|
||||
&& el.latlng.lat
|
||||
&& parseInt(el.latlng.lat, 10) > 0
|
||||
&& parseInt(el.latlng.lat, 10) < 1000
|
||||
&& el.latlng.lng
|
||||
&& parseInt(el.latlng.lng, 10) > 0
|
||||
&& parseInt(el.latlng.lng, 10) < 1000
|
||||
));
|
||||
// .map(el => ((el.text && String(el.text).substr(0, 100)) || ''));
|
||||
|
||||
const parseString = (value, size) => (value && String(value).substr(0, size)) || '';
|
||||
module.exports.parseNumber = (value, min, max) => (value && Number(value) && Math.min(max, Math.max(min, value))) || 0;
|
||||
|
||||
module.exports.parseString = parseString;
|
||||
module.exports.parseAddress = (value, size) => (
|
||||
parseString(value, size).replace(/[^A-Za-z\-_0-9]/ig, '_').replace(/_{2,}/ig, '_')
|
||||
);
|
|
@ -1,6 +0,0 @@
|
|||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
|
@ -1,5 +0,0 @@
|
|||
extends layout
|
||||
|
||||
block content
|
||||
h1= title
|
||||
p Welcome to #{title}
|
|
@ -1,7 +0,0 @@
|
|||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
body
|
||||
block content
|
|
@ -1,17 +0,0 @@
|
|||
doctype html
|
||||
html
|
||||
body
|
||||
script.
|
||||
window.opener.postMessage({
|
||||
type: 'oauth_login',
|
||||
user: {
|
||||
id: '#{_id}',
|
||||
token: '#{token}',
|
||||
name: '#{name}',
|
||||
photo: '#{photo}',
|
||||
role: '#{role}',
|
||||
}
|
||||
}, '*');
|
||||
|
||||
window.close();
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
doctype html
|
||||
html
|
||||
head
|
||||
title Ошибка авторизации
|
||||
link(rel='stylesheet', href='/stylesheets/social/vk.css')
|
||||
|
||||
body
|
||||
block content
|
||||
div#message
|
||||
div.bg
|
||||
h1=heading
|
||||
div=reason
|
||||
button(onclick='window.close()')=button
|
|
@ -1,27 +0,0 @@
|
|||
module.exports.CONFIG = {
|
||||
HTTPS: {
|
||||
ENABLED: false,
|
||||
PORT: 3000,
|
||||
PRIVATE_KEY: '/etc/letsencrypt/live/HOSTNAME.org/privkey.pem',
|
||||
CERTIFICATE: '/etc/letsencrypt/live/HOSTNAME.org/cert.pem',
|
||||
CA: '/etc/letsencrypt/live/hostname/chain.pem',
|
||||
},
|
||||
HTTP: {
|
||||
ENABLED: false,
|
||||
PORT: 3000,
|
||||
},
|
||||
DB: { // mongo db config
|
||||
USER: 'user',
|
||||
PASSWORD: 'password',
|
||||
HOSTNAME: 'HOSTNAME.org',
|
||||
PORT: 27017,
|
||||
DATABASE: 'map',
|
||||
},
|
||||
SOCIAL: {
|
||||
VK: {
|
||||
ENABLED: false,
|
||||
SECRET: 'YOUR_VK_SECRET', // secret token
|
||||
APP_ID: 0, // numeric
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
import { DEFAULT_PROVIDER, PROVIDERS } from '$constants/providers';
|
||||
import { LatLngLiteral } from 'leaflet';
|
||||
|
||||
const API_ADDR = 'https://HOSTNAME.org:3000';
|
||||
const OSRM_URL = 'https://HOSTNAME.org:5001/route/v1';
|
||||
const OSRM_PROFILE = 'bike';
|
||||
const OSRM_TEST_URL = ([south_west, north_east]: [LatLngLiteral, LatLngLiteral]) => (
|
||||
`${OSRM_URL}/${OSRM_PROFILE}/${Object.values(south_west).join(',')};${Object.values(north_east).join(',')}`
|
||||
);
|
||||
|
||||
export const CLIENT = {
|
||||
OSRM_URL,
|
||||
API_ADDR,
|
||||
OSRM_TEST_URL,
|
||||
OSRM_PROFILE,
|
||||
STROKE_WIDTH: 5,
|
||||
};
|
||||
|
||||
export const COLORS = {
|
||||
PATH_COLOR: ['#ff7700', '#ff3344'],
|
||||
};
|
||||
|
||||
export const PROVIDER = PROVIDERS[DEFAULT_PROVIDER];
|
||||
|
||||
export const MOBILE_BREAKPOINT = 768;
|
48
craco.config.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const CracoAlias = require('craco-alias');
|
||||
// const fastRefreshCracoPlugin = require('craco-fast-refresh');
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
alias: {
|
||||
'~': `src`,
|
||||
},
|
||||
output: {
|
||||
publicPath: '/',
|
||||
},
|
||||
},
|
||||
eslint: {
|
||||
enable: false,
|
||||
mode: 'file',
|
||||
},
|
||||
jest: {
|
||||
setupTestFrameworkScriptFile: '<rootDir>/src/setupTests.js',
|
||||
configure: {
|
||||
moduleNameMapper: {
|
||||
'^~/(.*)$': '<rootDir>/src/$1',
|
||||
'^.+\\.scss$': 'identity-obj-proxy',
|
||||
},
|
||||
snapshotSerializers: ['enzyme-to-json/serializer'],
|
||||
moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'jsx', 'node'],
|
||||
verbose: true,
|
||||
roots: ['<rootDir>/src'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
'^.+\\.ts?$': 'babel-jest',
|
||||
'^.+\\.js?$': 'ts-jest',
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
},
|
||||
preset: 'ts-jest/presets/js-with-ts',
|
||||
testEnvironment: 'node',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// { plugin: fastRefreshCracoPlugin },
|
||||
{
|
||||
plugin: CracoAlias,
|
||||
options: {
|
||||
source: 'tsconfig',
|
||||
tsConfigPath: 'tsconfig.paths.json',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
11
docker-compose.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
version: '3'
|
||||
services:
|
||||
www:
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/www/Dockerfile
|
||||
ports:
|
||||
- ${EXPOSE}:80
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
28
docker/www/Dockerfile
Normal file
|
@ -0,0 +1,28 @@
|
|||
# stage1 as builder
|
||||
FROM node:erbium-alpine as builder
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN yarn
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG REACT_APP_PUBLIC_PATH
|
||||
ARG REACT_APP_API_ADDR
|
||||
ARG REACT_APP_OSRM_URL
|
||||
ARG REACT_APP_OSRM_PROFILE
|
||||
|
||||
ENV REACT_APP_PUBLIC_PATH $REACT_APP_PUBLIC_PATH
|
||||
ENV REACT_APP_API_ADDR $REACT_APP_API_ADDR
|
||||
ENV REACT_APP_OSRM_URL $REACT_APP_OSRM_URL
|
||||
ENV REACT_APP_OSRM_PROFILE $REACT_APP_OSRM_PROFILE
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY docker/www/nginx.conf /etc/nginx/nginx.conf
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=builder /build /usr/share/nginx/html
|
||||
EXPOSE ${EXPOSE} 80
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
46
docker/www/nginx.conf
Normal file
|
@ -0,0 +1,46 @@
|
|||
worker_processes 4;
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain application/xml application/javascript;
|
||||
|
||||
## All static files will be served directly.
|
||||
location ~* ^.+\.(?:css|cur|js|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg)$ {
|
||||
access_log off;
|
||||
expires 30d;
|
||||
add_header Cache-Control public;
|
||||
gzip_static on;
|
||||
|
||||
## No need to bleed constant updates. Send the all shebang in one
|
||||
## fell swoop.
|
||||
tcp_nodelay off;
|
||||
|
||||
## Set the OS file cache.
|
||||
open_file_cache max=3000 inactive=120s;
|
||||
open_file_cache_valid 45s;
|
||||
open_file_cache_min_uses 2;
|
||||
open_file_cache_errors off;
|
||||
}
|
||||
|
||||
location / {
|
||||
gzip_static on;
|
||||
try_files $uri @index;
|
||||
}
|
||||
|
||||
location @index {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
expires -1;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35201
package-lock.json
generated
126
package.json
|
@ -5,120 +5,70 @@
|
|||
"description": "Beta Map",
|
||||
"main": "./src/js/index.js",
|
||||
"scripts": {
|
||||
"start": "NODE_ENV=development webpack-dev-server --mode development --hot --open --inline --progress",
|
||||
"serve": "node ./backend/bin/www",
|
||||
"serve-dev": "nodemon ./backend/bin/www",
|
||||
"build": "NODE_ENV=production webpack --env production --config=webpack.config.js --progress -p",
|
||||
"profile": "webpack --json > stats.json"
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test"
|
||||
},
|
||||
"author": "Grigory Chervoplyas",
|
||||
"license": "ISC",
|
||||
"keywords": [],
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0-rc.1",
|
||||
"@babel/preset-env": "^7.0.0-rc.1",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-plugin-ramda": "^2.0.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-import-resolver-babel-module": "^4.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.9.0",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-babel": "^5.0.0",
|
||||
"eslint-plugin-flowtype": "^2.46.2",
|
||||
"eslint-plugin-import": "^2.11.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"flow-bin": "^0.73.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"less-loader": "^4.1.0",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"style-loader": "^0.21.0",
|
||||
"ts-node": "^8.0.1",
|
||||
"typescript": "^3.2.4",
|
||||
"uglifyjs-webpack-plugin": "^1.3.0",
|
||||
"webpack": "^4.6.0",
|
||||
"webpack-cli": "^3.2.3",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/node": "^11.9.0",
|
||||
"@types/ramda": "^0.26.39",
|
||||
"@types/react": "16.8.1",
|
||||
"axios": "^0.18.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bluebird": "^3.5.3",
|
||||
"body-parser": "^1.18.3",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"craco": "^0.0.3",
|
||||
"craco-alias": "^2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"classnames": "^2.2.6",
|
||||
"clean-webpack-plugin": "^0.1.9",
|
||||
"cookie-parser": "~1.4.3",
|
||||
"craco-fast-refresh": "^1.0.5",
|
||||
"croppr": "^2.3.1",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"file-saver": "^2.0.0",
|
||||
"history": "^4.7.2",
|
||||
"http-errors": "~1.6.2",
|
||||
"js-md5": "^0.7.3",
|
||||
"leaflet": "^1.4.0",
|
||||
"gpx-parser-builder": "^1.0.2",
|
||||
"leaflet": "1.6.0",
|
||||
"leaflet-editable": "^1.1.0",
|
||||
"leaflet-editable-polyline": "muerwre/leaflet-editable-polyline#master",
|
||||
"leaflet-geometryutil": "^0.9.0",
|
||||
"leaflet-routing-machine": "muerwre/leaflet-routing-machine#no-osrm-text",
|
||||
"leaflet-routing-machine": "^3.2.12",
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"less": "^3.8.1",
|
||||
"less-middleware": "~2.2.1",
|
||||
"lodash": "^4.17.10",
|
||||
"mongoose": "^5.3.14",
|
||||
"morgan": "~1.9.0",
|
||||
"mysql": "^2.16.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"node-sass": "^5.0.0",
|
||||
"pt-sans-cyrillic": "0.0.4",
|
||||
"pug": "2.0.0-beta11",
|
||||
"raleway-cyrillic": "^4.0.2",
|
||||
"rc-slider": "8.5.0",
|
||||
"react": "16.8.1",
|
||||
"react-dom": "16.8.1",
|
||||
"react-hot-loader": "^4.1.1",
|
||||
"ramda": "^0.26.1",
|
||||
"rc-slider": "^9.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-infinite-scroller": "^1.2.2",
|
||||
"react-rangeslider": "^2.2.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router": "^4.3.1",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-scrollbar": "^0.5.4",
|
||||
"reactrangeslider": "^3.0.6",
|
||||
"redux": "^4.0.1",
|
||||
"redux-persist": "^5.10.0",
|
||||
"redux-saga": "^0.16.2",
|
||||
"reduxsauce": "^1.0.0",
|
||||
"scrypt": "^6.0.3",
|
||||
"styled-components": "^3.2.6",
|
||||
"styled-theming": "^2.2.0",
|
||||
"throttle-debounce": "^2.1.0",
|
||||
"redux-saga": "^1.0.0",
|
||||
"tt-react-custom-scrollbars": "^4.2.1-tt2",
|
||||
"typeface-pt-sans": "0.0.54",
|
||||
"typesafe-actions": "^3.0.0",
|
||||
"webpack-git-hash": "^1.0.2"
|
||||
"typescript": "^4.2.4",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"flow-coverage-report": {
|
||||
"includeGlob": [
|
||||
"src/**/*.js",
|
||||
"src/**/*.jsx"
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"type": [
|
||||
"text",
|
||||
"html",
|
||||
"json"
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 707 B |
5
public/images/arrow.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="48" height="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="path-arrow" transform="scale(2) translate(5 -2)">
|
||||
<path d="m 2.625,3.375 h 7.5 L 10.28125,1.609375 13.5,4.25 10.484375,6.921875 10.171875,5.15625 2.625,5.125 Z" fill="#ff3344" fillRule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 297 B |
425
public/images/icon.svg
Normal file
|
@ -0,0 +1,425 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">
|
||||
<defs>
|
||||
<g id="icon-router">
|
||||
<rect x="0" y="0" width="32" height="32" fill="black" stroke="none" />
|
||||
<g transform="translate(0 0)">
|
||||
<path d="M12 5c-2.105-.003-3.587 1.238-4.3 2.354a6.454 6.454 0 0 0-.888 2.226l-.02.123v12.137h3v-11.814c.01-.045.098-.531.434-1.057.352-.549.722-.97 1.77-.969 1.073.001 1.547.446 1.945.983.367.493.47.894.49.966v12.45s-.003 1.326.624 2.723c.626 1.396 2.196 3.019 4.627 3.068 2.45.05 4.063-1.603 4.689-3.022.626-1.418.621-2.77.621-2.77v-10.949h-3v10.913s-.032.84-.365 1.595c-.334.756-.61 1.259-1.885 1.233-1.295-.027-1.617-.557-1.95-1.297a4.623 4.623 0 0 1-.36-1.526v-12.873l-.045-.177s-.256-1.071-1.035-2.121c-.78-1.05-2.273-2.193-4.352-2.196z" stroke="none" fill="white" />
|
||||
<circle cx="8.5" cy="24" r="2" fill="none" stroke-width="2" stroke="white" />
|
||||
<circle cx="23.5" cy="9" r="2" fill="none" stroke-width="2" stroke="white" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-sticker">
|
||||
<rect x="0" y="0" width="32" height="32" fill="none" stroke="none" />
|
||||
<g transform="translate(-680 -285)">
|
||||
<path d="M690.764 306.811l5.71 7.913 5.014-7.694z" stroke="none" fill="white" />
|
||||
<path transform="matrix(.88332 0 0 .88132 81.246 35.954)" d="M702.328 307.792l-6.02-.026-6.02.026-2.986-5.226-3.033-5.2 3.033-5.2 2.987-5.227 6.02.026 6.02-.026 2.986 5.226 3.033 5.2-3.033 5.2z" fill="none" stroke="white" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-poly">
|
||||
<rect x="0" y="0" width="32" height="32" fill="black" stroke="none" />
|
||||
<g>
|
||||
<path d="m6 14l9 -7l-2 16l14 -10l-3 12" fill="none" stroke-linecap="butt" stroke-linejoin="round" stroke="white" />
|
||||
<circle cx="5" cy="15" r="2" fill="none" stroke-width="2" stroke="white" />
|
||||
<circle cx="16" cy="5" r="2" fill="none" stroke-width="2" stroke="white" />
|
||||
<circle cx="12" cy="25" r="2" fill="none" stroke-width="2" stroke="white" />
|
||||
<circle cx="28" cy="11" r="2" fill="none" stroke-width="2" stroke="white" />
|
||||
<circle cx="24" cy="27" r="2" fill="none" stroke-width="2" stroke="white" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-shooter">
|
||||
<rect x="0" y="0" width="32" height="32" fill="black" stroke="none" />
|
||||
<g transform="translate(18 6)">
|
||||
<path d="m0 0l-4.391.054c-1.418.531-2.34 1.756-3.176 3.102h-5.178c-.68.317-1.351.655-1.455 2.584v11.49c.17 1.001.58 1.765 1.455 2.06h22.537c.746-.044 1.288-.426 1.68-1.06v-13.517c-.185-1.643-.916-1.65-1.68-1.557h-6.62c-.326-1.26-1.91-2.247-3.172-3.156zm-2.122 5.289c3.227 0 5.87 2.626 5.87 5.846s-2.643 5.845-5.87 5.845c-3.227 0-5.869-2.626-5.869-5.845 0-3.22 2.642-5.846 5.87-5.846zm0 1.998a3.844 3.844 0 0 0-3.869 3.848 3.842 3.842 0 0 0 3.87 3.845 3.84 3.84 0 0 0 3.866-3.845 3.842 3.842 0 0 0-3.867-3.848z" fill="white" stroke="none" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-trash">
|
||||
<g transform="matrix(4.6 0 0 4.6 -4 -4)" stroke="none">
|
||||
<path fill="white" d="M2.783 3.626h2.923v2.923H2.783z" />
|
||||
<path fill="white" d="M2.479 2.597h3.508v.748H2.479z" />
|
||||
<path fill="white" d="M3.438 1.919h1.473v.865H3.438z" />
|
||||
<path fill="black" d="M3.859 2.25h.631v.386h-.631z" />
|
||||
<path fill="black" d="M3.134 3.906h.468v2.315h-.468z" />
|
||||
<path fill="black" d="M-4.537 3.906h.444v2.315h-.444z" transform="scale(-1 1)" />
|
||||
<path fill="black" d="M4.958 3.906h.444v2.315h-.444z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-save" stroke="none">
|
||||
<path fill="black" d="M0 0h32v32H0z" />
|
||||
<path fill="white" d="M6.844 8.459V24h18.312V11.375H14.031V8.459z" />
|
||||
<rect fill="black" width="2.74" height="5.038" x="14.63" y="14.411" stroke-width="3.603" />
|
||||
<path fill="black" d="M16.866 19.73l-4.315-.06-4.314.06 2.209-3.707 2.105-3.766 2.106 3.766z" transform="matrix(.45903 -.40628 .79506 .23456 -3.467 21.088)" />
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="svg8" width="600" height="32">
|
||||
<g id="g4853" transform="translate(-29.985 30.509)">
|
||||
<rect id="rect4832" width="4.035" height="1.75" x="16.495" y="-28.417" fill="#fff" stroke-opacity=".941" stroke-width=".115" rx="0" ry="0" />
|
||||
<path id="rect4834" fill="#fff" stroke-opacity=".941" stroke-width=".247" d="M15.535-28.417h5.616v.933h-5.616z" />
|
||||
<path id="rect4836" fill="#fff" stroke-opacity=".941" stroke-width=".293" d="M16.495-29.706v1.29h.671v-.761h2.66v.76h.67v-1.29z" />
|
||||
<path id="path4841" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width=".753" d="M16.886-27.49c.092.03-.101 1.171.111 2.499.213 1.328-.477 1.846-.477 1.846" />
|
||||
<path id="path4843" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width=".753" d="M18.496-27.49c.093.03-.101 1.171.112 2.499.212 1.328-.478 1.846-.478 1.846" />
|
||||
<path id="path4845" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width=".753" d="M20.107-27.49c.092.03-.102 1.171.111 2.499s-.477 1.846-.477 1.846" />
|
||||
</g>
|
||||
<g id="g4909" transform="translate(-.054 .888)">
|
||||
<path id="rect4855" fill="#fff" stroke-opacity=".941" stroke-width=".811" d="M16.272-27.384h4.266l-.397 4.3h-3.538z" />
|
||||
<path id="rect4857" fill="#fff" stroke-opacity=".941" stroke-width=".43" d="M15.809-28.939h5.192v.992h-5.192z" />
|
||||
<path id="rect4879" fill="#fff" stroke-opacity=".941" stroke-width=".502" d="M17.246-29.945v1.123h.586v-.66h1.005v.66h.587v-1.123z" />
|
||||
<g id="g4900">
|
||||
<path id="path4885" fill-rule="evenodd" stroke-width=".265" d="M16.885-26.893l.187 3.508h.538l-.14-3.508z" />
|
||||
<path id="path4887" fill-rule="evenodd" stroke-width=".265" d="M19.983-26.893l-.187 3.508h-.538l.14-3.508z" />
|
||||
<path id="rect4889" stroke-opacity=".941" stroke-width=".821" d="M18.165-26.881h.538v3.473h-.538z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-save" stroke-opacity=".941" transform="translate(-64)">
|
||||
<path id="rect4766" stroke-width=".265" d="M0 0h32v32H0z" />
|
||||
<path id="rect4986" fill="#fff" stroke-width="2.491" d="M6.844 8.459V24h18.312V11.375H14.031V8.459z" />
|
||||
<rect id="rect4991" width="2.74" height="5.038" x="14.63" y="14.411" stroke-width="3.603" rx="0" ry="0" />
|
||||
<path id="path4993" stroke-width="3" d="M16.866 19.73l-4.315-.06-4.314.06 2.209-3.707 2.105-3.766 2.106 3.766z" transform="matrix(.45903 -.40628 .79506 .23456 -3.467 21.088)" />
|
||||
</g>
|
||||
<g id="g4982" stroke-opacity=".941" transform="translate(-32)">
|
||||
<path id="rect4964" stroke-width=".265" d="M0 0h32v32H0z" />
|
||||
<g id="g4980" transform="matrix(3.77953 0 0 3.77953 .002 -.002)">
|
||||
<path id="rect4966" fill="#fff" stroke-width=".794" d="M2.783 3.626h2.923v2.923H2.783z" />
|
||||
<path id="rect4968" fill="#fff" stroke-width=".794" d="M2.479 2.597h3.508v.748H2.479z" />
|
||||
<path id="rect4970" fill="#fff" stroke-width=".794" d="M3.438 1.919h1.473v.865H3.438z" />
|
||||
<path id="rect4972" stroke-width=".684" d="M3.859 2.25h.631v.386h-.631z" />
|
||||
<path id="rect4974" stroke-width="1.442" d="M3.134 3.906h.468v2.315h-.468z" />
|
||||
<path id="rect4976" stroke-width="1.405" d="M-4.537 3.906h.444v2.315h-.444z" transform="scale(-1 1)" />
|
||||
<path id="rect4978" stroke-width="1.405" d="M4.958 3.906h.444v2.315h-.444z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="g5188" transform="translate(-96)">
|
||||
<path id="rect5180" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" />
|
||||
<g id="g5322" fill="#fff" transform="translate(7.066 7.065) scale(.04494)">
|
||||
<path id="path5267" stroke-width="445.017" d="M313.1 147.875c-6.057 0-11.962.654-17.661 1.871l-15.805-53.435 37.842-2.565a9.275 9.275 0 0 1 9.602 6.932 9.246 9.246 0 0 1-1.45 7.728 9.243 9.243 0 0 1-6.865 3.834l-9.126.652a6.5 6.5 0 0 0-6.02 6.947c.256 3.581 3.361 6.289 6.947 6.02l9.126-.652a22.21 22.21 0 0 0 16.493-9.21 22.21 22.21 0 0 0 3.484-18.566c-2.677-10.375-12.376-17.364-23.069-16.654l-45.936 3.114a6.5 6.5 0 0 0-5.793 8.329l6.405 21.656H145.477l-6.316-12H155.5a6.5 6.5 0 1 0 0-13h-47a6.5 6.5 0 1 0 0 13h16.086l9.542 18.349-18.836 33.485a84.026 84.026 0 0 0-30.792-5.834c-46.593 0-84.5 37.906-84.5 84.5s37.907 84.5 84.5 84.5c44.404 0 80.892-34.436 84.225-78h31.776a6.5 6.5 0 0 0 5.511-3.055l68.779-110.047 8.185 27.672c-31.758 12.162-54.376 42.945-54.376 78.93 0 46.594 37.907 84.5 84.5 84.5s84.5-37.906 84.5-84.5-37.907-84.501-84.5-84.501zm-228.6 156c-39.425 0-71.5-32.075-71.5-71.5s32.075-71.5 71.5-71.5c8.549 0 16.75 1.513 24.355 4.276l-31.482 55.968c-3.726 2.365-6.206 6.516-6.206 11.256 0 7.363 5.969 13.333 13.333 13.333 5.002 0 9.354-2.759 11.636-6.833h59.556c-3.297 36.388-33.959 65-71.192 65zm11.636-78a13.384 13.384 0 0 0-4.025-4.439l28.528-50.717c19.37 11.397 32.922 31.647 35.052 55.156zm72.589 0c-2.17-28.365-18.393-52.845-41.715-66.482l14.327-25.471 48.396 91.953zm32.258-6.538l-48.665-92.462h106.454zM313.1 303.875c-39.425 0-71.5-32.075-71.5-71.5 0-30.093 18.697-55.885 45.077-66.418l16.89 57.105a13.284 13.284 0 0 0-3.8 9.313c0 7.363 5.969 13.333 13.333 13.333s13.333-5.97 13.333-13.333c0-6.354-4.449-11.661-10.399-12.999l-16.895-57.123a71.624 71.624 0 0 1 13.962-1.378c39.425 0 71.5 32.075 71.5 71.5s-32.076 71.5-71.501 71.5z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-cycle">
|
||||
<path fill="black" d="M0 0h32v32H0z" />
|
||||
<g id="g5604" fill="#fff" transform="matrix(-.36615 0 0 .36615 24.543 7.457)">
|
||||
<g id="g5527">
|
||||
<g id="g5495">
|
||||
<path id="path5483" d="M37.687 24.66c-4.945 0-8.973 4.025-8.973 8.975 0 4.95 4.026 8.974 8.973 8.974 4.95 0 8.977-4.024 8.977-8.974s-4.027-8.975-8.977-8.975zm0 15.384a6.417 6.417 0 0 1-6.409-6.409 6.418 6.418 0 0 1 6.409-6.41 6.417 6.417 0 0 1 6.411 6.41 6.417 6.417 0 0 1-6.411 6.409z" />
|
||||
<path id="path5485" d="M23.588 15.501v.01h8.047c-2.375-2.4-5.273-5.251-5.99-5.578-1.257-.575-3.966 1.462-4.547 1.748-.532.283-.954.699-1.043 1.236l-2.112 5.927-6.167.069c-1.811.485-.465 2.065-.465 2.065l3.097.041-.295 1.405-1.803 2.989a8.914 8.914 0 0 0-3.333-.648C4.027 24.765 0 28.792 0 33.741c0 4.947 4.026 8.974 8.977 8.974 4.948 0 8.975-4.026 8.975-8.974a8.956 8.956 0 0 0-3.423-7.038l2.005-3.326.487-2.322 2.41.03c.479-.136.833-.538.912-1.029l2.603-4.074zM15.386 33.74a6.417 6.417 0 0 1-6.409 6.409c-3.534 0-6.411-2.876-6.411-6.409a6.419 6.419 0 0 1 6.411-6.411c.694 0 1.36.114 1.986.32l-3.579 5.939 2.197 1.323 3.607-5.989a6.386 6.386 0 0 1 2.198 4.818z" />
|
||||
<path id="path5487" d="M32.24 23.139s2.468-2.578 2.691-2.968c.225-.392.229-.872.007-1.265 0 0-.725-.76-1.771-1.832v.014h-8.949l4.281 3.716-6.367 9.947a1.264 1.264 0 0 0-.104 1.045l2.563 7.692 3.072-.17-1.741-7.787z" />
|
||||
<circle id="circle5489" cx="19.703" cy="7.384" r="3.435" />
|
||||
<path id="path5491" d="M32.845 5.919c-1.06-.744-2.366-.789-3.123-.034l-1.484 1.487c-.756.756-.711 2.062.03 3.124z" />
|
||||
<path id="path5493" d="M33.007 15.317c1.116 1.116 2.73 1.311 3.607.436l1.485-1.488c.877-.876.685-2.491-.434-3.606l-4.081-4.081-4.659 4.658z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-cycle-2">
|
||||
<path fill="black" d="M0 0h32v32H0z" />
|
||||
<g id="g5604" fill="#fff" transform="matrix(-.36615 0 0 .36615 24.543 7.457)">
|
||||
<g id="g5527">
|
||||
<g id="g5495">
|
||||
<path id="path5483" d="M37.687 24.66c-4.945 0-8.973 4.025-8.973 8.975 0 4.95 4.026 8.974 8.973 8.974 4.95 0 8.977-4.024 8.977-8.974s-4.027-8.975-8.977-8.975zm0 15.384a6.417 6.417 0 0 1-6.409-6.409 6.418 6.418 0 0 1 6.409-6.41 6.417 6.417 0 0 1 6.411 6.41 6.417 6.417 0 0 1-6.411 6.409z" />
|
||||
<path id="path5485" d="M23.588 15.501v.01h8.047c-2.375-2.4-5.273-5.251-5.99-5.578-1.257-.575-3.966 1.462-4.547 1.748-.532.283-.954.699-1.043 1.236l-2.112 5.927-6.167.069c-1.811.485-.465 2.065-.465 2.065l3.097.041-.295 1.405-1.803 2.989a8.914 8.914 0 0 0-3.333-.648C4.027 24.765 0 28.792 0 33.741c0 4.947 4.026 8.974 8.977 8.974 4.948 0 8.975-4.026 8.975-8.974a8.956 8.956 0 0 0-3.423-7.038l2.005-3.326.487-2.322 2.41.03c.479-.136.833-.538.912-1.029l2.603-4.074zM15.386 33.74a6.417 6.417 0 0 1-6.409 6.409c-3.534 0-6.411-2.876-6.411-6.409a6.419 6.419 0 0 1 6.411-6.411c.694 0 1.36.114 1.986.32l-3.579 5.939 2.197 1.323 3.607-5.989a6.386 6.386 0 0 1 2.198 4.818z" />
|
||||
<path id="path5487" d="M32.24 23.139s2.468-2.578 2.691-2.968c.225-.392.229-.872.007-1.265 0 0-.725-.76-1.771-1.832v.014h-8.949l4.281 3.716-6.367 9.947a1.264 1.264 0 0 0-.104 1.045l2.563 7.692 3.072-.17-1.741-7.787z" />
|
||||
<circle id="circle5489" cx="19.703" cy="7.384" r="3.435" />
|
||||
<path id="path5491" d="M32.845 5.919c-1.06-.744-2.366-.789-3.123-.034l-1.484 1.487c-.756.756-.711 2.062.03 3.124z" />
|
||||
<path id="path5493" d="M33.007 15.317c1.116 1.116 2.73 1.311 3.607.436l1.485-1.488c.877-.876.685-2.491-.434-3.606l-4.081-4.081-4.659 4.658z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-logo">
|
||||
<path fill="black" stroke="none" d="M0 0h32v32H0z" />
|
||||
<g transform="matrix(1 0 0 1 2 -1)">
|
||||
<path fill="none" stroke="#fff" stroke-width="2" d="M23.423 9.284c-6.673-4.127-12.713-4.175-19.269 0 .174 4.944-.354 13.294 9.635 17.855 10.323-4.9 9.545-13.305 9.634-17.855z" />
|
||||
<path fill="#fff" stroke="none" d="M13.435 5.925v21.037s10.695-3.27 9.988-17.678c0 0-3.447-2.828-9.988-3.359z" />
|
||||
<path fill="none" stroke-width="2" stroke="#fff" d="M5.773 15.118L15.7 6.19" />
|
||||
<path fill="none" stroke-width="2" stroke="#fff" d="M6.276 20.333l7.778-7.778" />
|
||||
<path fill="none" stroke-width="2" stroke="#fff" d="M8.574 23.691c.442-.53 5.612-5.612 5.612-5.612" />
|
||||
<path fill="none" stroke-width="2" stroke="#fff" d="M11.932 25.636l2.343-2.342" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-locate">
|
||||
<path id="rect5819" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<circle id="path5839" cx="16" cy="16" r="5.127" fill="none" stroke="#fff" stroke-width="2.5" />
|
||||
<path id="path5841" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width="2.5" d="M16.087 6.854v4.242" />
|
||||
<path id="path5843" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width="2.5" d="M16.087 20.686v4.243" />
|
||||
<path id="path5845" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width="2.5" d="M20.948 16.134h4.243" />
|
||||
<path id="path5847" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width="2.5" d="M6.63 16.134h4.242" />
|
||||
</g>
|
||||
<g id="icon-shot-2" stroke="none" transform="scale(1.1) translate(-2 -1)">
|
||||
<path id="rect5819" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<rect id="rect6617" width="21" height="13.89" x="6.25" y="9.833" fill="#fff" rx="2" ry="2" />
|
||||
<path id="rect6619" fill="#fff" stroke-width="1.979" d="M13.812 7.009h6.282c.45 0 .749.528 1.147 1.039l3.096 3.974c.398.51-3.793 6.989-4.243 6.989h-6.282c-.45 0-4.889-6.747-4.475-7.24l3.19-3.802c.415-.493.836-.96 1.285-.96z" />
|
||||
<circle id="path6621" cx="16.627" cy="16.311" r="6.158" fill="black" />
|
||||
<circle id="circle6628" cx="16.627" cy="16.311" r="3.533" fill="#fff" />
|
||||
</g>
|
||||
<g id="icon-trash-2" stroke="none">
|
||||
<g transform="translate(0 -3) scale(3.82445) scale(1.1) rotate(45 4 4)">
|
||||
<rect id="rect4966" width="3.125" height="2.893" x="2.671" y="3.751" fill="#fff" rx=".36" ry=".403" />
|
||||
<rect id="rect4968" width="3.663" height=".87" x="2.401" y="2.423" fill="#fff" rx=".435" ry=".435" />
|
||||
<rect id="rect4970" width="2.924" height="1.306" x="2.771" y="1.698" fill="#fff" rx=".653" ry=".653" />
|
||||
<rect id="rect4972" width="1.933" height=".523" x="3.266" y="2.114" fill="black" rx=".261" ry=".261" />
|
||||
<g id="g4918" stroke-width=".523" transform="matrix(1.3497 0 0 1.3497 -1.613 -1.674)">
|
||||
<rect id="rect4912" width="2.059" height=".425" x="5.622" y=".289" rx=".237" ry=".21" transform="rotate(45)" fill="black" />
|
||||
<rect id="rect4914" width="2.047" height=".389" x="-.491" y="-6.803" rx=".237" ry=".195" transform="rotate(135)" fill="black" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-logo-2" transform="scale(1.1) translate(-1 -2)">
|
||||
<path id="rect5611" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" stroke="none" />
|
||||
<g id="g5792" fill="none" stroke="#fff" stroke-width="2.37" transform="matrix(.84382 0 0 .84382 4.366 1.93)">
|
||||
<path id="path5774" fill-rule="evenodd" d="M22.399 10.08c-5.964-3.689-11.363-3.732-17.222 0 .155 4.418-.316 11.882 8.61 15.957 9.228-4.38 8.532-11.89 8.612-15.957z" />
|
||||
</g>
|
||||
<g id="g8284" fill="#fff" stroke-width="3.052" transform="matrix(.40236 0 0 .40236 10.45 8.978)">
|
||||
<path d="M22.399 10.08c-5.964-3.689-11.363-3.732-17.222 0 .155 4.418-.316 11.882 8.61 15.957 9.228-4.38 8.532-11.89 8.612-15.957z" stroke="none" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-sticker-2" transform="scale(1.1) translate(-3 -2)" stroke="none">
|
||||
<g id="g4867" transform="matrix(.94996 0 0 .94996 -.16 .083)">
|
||||
<path id="path4859" fill="#fff" fill-rule="evenodd" stroke-width=".767" d="M13.47 22.847l3.045 5.275 3.12-5.405z" />
|
||||
<circle id="path4863" cx="16.552" cy="14.98" r="8.712" fill="#fff" stroke-width="1.851" />
|
||||
<circle id="circle4869" cx="16.552" cy="14.98" r="5" fill="black" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-route-2" transform="scale(1.1) translate(32 -2)" stroke="none">
|
||||
<g fill="white">
|
||||
<circle id="path5642" cx="-24.357" cy="23.576" r="2.507" fill="#fff" stroke-width="4.363" />
|
||||
<path id="path5644" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width="2.621" d="M-24.289 21.588V10.61c0-4.22 6.62-4.194 6.62 0V21.42c0 4.011 6.828 4.027 6.828 0v-10.98" />
|
||||
<circle id="circle5646" cx="-10.93" cy="8.462" r="2.507" fill="#fff" stroke-width="4.363" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-poly-2" transform="scale(1.1) translate(-3 -2)" stroke="none">
|
||||
<g fill="white">
|
||||
<path id="path6375" fill="none" fill-rule="evenodd" stroke="#fff" stroke-linejoin="round" stroke-width="2.387" d="M7.924 12.991l8.569-4.989-6.55 14.587 13.762-7.974-3.958 9.12" />
|
||||
<circle id="path6363" cx="14.711" cy="-4.523" r="2.387" fill="#fff" stroke-width="5.304" transform="rotate(75)" />
|
||||
<circle id="circle6367" cx="12.17" cy="-13.883" r="2.387" fill="#fff" stroke-width="5.304" transform="rotate(75)" />
|
||||
<circle id="circle6371" cx="20.342" cy="-19.194" r="2.387" fill="#fff" stroke-width="5.304" transform="rotate(75)" />
|
||||
<circle id="circle6373" cx="24.138" cy="-3.701" r="2.387" fill="#fff" stroke-width="5.304" transform="rotate(75)" />
|
||||
<circle id="circle6774" cx="27.918" cy="-13.025" r="2.387" fill="#fff" stroke-width="5.304" transform="rotate(75)" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-save-2" transform="scale(1.1) translate(-1 -2)" stroke="none">
|
||||
<g fill="white">
|
||||
<path id="rect4766" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path id="rect4986" fill="#fff" stroke-opacity=".941" stroke-width="2.477" d="M6.894 8.502v15.455h18.212V11.402H14.042v-2.9z" />
|
||||
<path id="path7854" fill="none" fill-rule="evenodd" stroke="#000" stroke-width="2.735" d="M10.923 16.586l4.19 4.19 6.606-6.607" transform="scale(0.9) translate(2 2)" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-trash-3">
|
||||
<path id="rect4766" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" stroke="none" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z" fill="white" transform="translate(4 4)" stroke-width="1" />
|
||||
</g>
|
||||
<g id="icon-trash-4">
|
||||
<path id="rect4766" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" stroke="none" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M21 9l-9-7-2.59 2.02 7.87 7.87zm0 5.07l-1.63-1.27-.67.52 1.43 1.43zM3.41.86L2 2.27l4.22 4.22L3 9l9 7 2.1-1.63 1.42 1.42-3.53 2.75-7.37-5.73L3 14.07l9 7 4.95-3.85L20.73 21l1.41-1.41z" fill="white" transform="translate(4 4)" stroke-width="0" />
|
||||
</g>
|
||||
<g id="icon-logo-3" transform="scale(1.1) translate(-1 -2)">
|
||||
<path id="rect5611" stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" stroke="none" />
|
||||
<g id="g5792" fill="none" stroke="#fff" stroke-width="3" transform="matrix(.84382 0 0 .84382 5 2.53)">
|
||||
<path id="path5774" fill-rule="evenodd" d="M22.399 10.08c-5.964-3.689-11.363-3.732-17.222 0 .155 4.418-.316 11.882 8.61 15.957 9.228-4.38 8.532-11.89 8.612-15.957z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-shot-3" stroke="none" transform="">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M18 20H4V6h9V4H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-9h-2v9zm-7.79-3.17l-1.96-2.36L5.5 18h11l-3.54-4.71zM20 4V1h-2v3h-3c.01.01 0 2 0 2h3v2.99c.01.01 2 0 2 0V6h3V4h-3z" fill="white" transform="translate(4 4)" stroke-width="0" />
|
||||
</g>
|
||||
<g id="icon-reg-1" stroke="none" transform="scale(1.1) translate(-2 -1)">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M11 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0-6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zM5 18c.2-.63 2.57-1.68 4.96-1.94l2.04-2c-.39-.04-.68-.06-1-.06-2.67 0-8 1.34-8 4v2h9l-2-2H5zm15.6-5.5l-5.13 5.17-2.07-2.08L12 17l3.47 3.5L22 13.91z" fill="white" transform="translate(5 3)" stroke-width="1" />
|
||||
</g>
|
||||
<g id="icon-cancel-1" stroke="none">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" fill="white" stroke="white" transform="translate(4 4)" stroke-width="1" />
|
||||
</g>
|
||||
<g id="icon-check-1" stroke="none">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" fill="white" transform="translate(4 4)" stroke-width="1" stroke="white" />
|
||||
</g>
|
||||
<g id="icon-pin-1" stroke="none">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M13 16.12c3.47-.41 6.17-3.36 6.17-6.95 0-3.87-3.13-7-7-7s-7 3.13-7 7c0 3.47 2.52 6.34 5.83 6.89V20H5v2h14v-2h-6v-3.88z" fill="white" transform="translate(4 4)" stroke-width="0" stroke="white" />
|
||||
</g>
|
||||
<g id="icon-check-2" stroke="none" transform="scale(1.1) translate(-2 -1)">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M11 5v5.59H7.5l4.5 4.5 4.5-4.5H13V5h-2zm-5 9c0 3.31 2.69 6 6 6s6-2.69 6-6h-2c0 2.21-1.79 4-4 4s-4-1.79-4-4H6z" fill="white" transform="translate(4 4)" stroke-width="0" stroke="white" />
|
||||
</g>
|
||||
<g id="icon-map-1" stroke="none">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M20.5 3l-.16.03L15 5.1 9 3 3.36 4.9c-.21.07-.36.25-.36.48V20.5c0 .28.22.5.5.5l.16-.03L9 18.9l6 2.1 5.64-1.9c.21-.07.36-.25.36-.48V3.5c0-.28-.22-.5-.5-.5zM10 5.47l4 1.4v11.66l-4-1.4V5.47zm-5 .99l3-1.01v11.7l-3 1.16V6.46zm14 11.08l-3 1.01V6.86l3-1.16v11.84z" fill="white" transform="translate(4 4)" stroke-width="1" />
|
||||
</g>
|
||||
<g id="icon-sticker-3" stroke="none">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<g>
|
||||
<path fill="#fff" fill-rule="evenodd" stroke-width=".767" d="M12 24l4 6 3.5-6.405z" transform="rotate(45 16 16)" />
|
||||
<circle cx="16" cy="15" r="9.5" fill="#fff" stroke-width="1.851" />
|
||||
<circle cx="16" cy="15" r="6.5" fill="black" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-poly-3" transform="scale(1.1) translate(-3 -2)" stroke="none">
|
||||
<path stroke-opacity=".941" stroke-width=".265" d="M0 0h32v32H0z" fill="black" />
|
||||
<g fill="white" transform="translate(2 0.5)">
|
||||
<path stroke-width="2.3" fill="none" fill-rule="evenodd" stroke="#fff" stroke-linejoin="round" d="M7.924 12.991l8.569-4.989-6.55 14.587 13.762-7.974-3.958 9.12" />
|
||||
<circle cx="7.711" cy="13" r="1" fill="black" stroke-width="2.5" stroke="white" />
|
||||
<circle cx="16.5" cy="8" r="1" fill="black" stroke-width="2.5" stroke="white" />
|
||||
<circle cx="10" cy="22" r="1" fill="black" stroke-width="2.5" stroke="white" />
|
||||
<circle cx="19.7" cy="23.701" r="1" fill="black" stroke-width="2.5" stroke="white" />
|
||||
<circle cx="24" cy="14.5" r="1" fill="black" stroke-width="2.5" stroke="white" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-poly-4" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M23 8c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2zm0 0c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2z" fill="white" stroke="white" stroke-width="1" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-get-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z" fill="white" stroke="white" stroke-width="1" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-folder-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M9.17 6l2 2H20v10H4V6h5.17M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" fill="white" stroke="white" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-folder-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M9.17 6l2 2H20v10H4V6h5.17M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" fill="white" stroke="white" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-cycle-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M15.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM5 12c-2.8 0-5 2.2-5 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 8.5c-1.9 0-3.5-1.6-3.5-3.5s1.6-3.5 3.5-3.5 3.5 1.6 3.5 3.5-1.6 3.5-3.5 3.5zm5.8-10l2.4-2.4.8.8c1.3 1.3 3 2.1 5.1 2.1V9c-1.5 0-2.7-.6-3.6-1.5l-1.9-1.9c-.5-.4-1-.6-1.6-.6s-1.1.2-1.4.6L7.8 8.4c-.4.4-.6.9-.6 1.4 0 .6.2 1.1.6 1.4L11 14v5h2v-6.2l-2.2-2.3zM19 12c-2.8 0-5 2.2-5 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 8.5c-1.9 0-3.5-1.6-3.5-3.5s1.6-3.5 3.5-3.5 3.5 1.6 3.5 3.5-1.6 3.5-3.5 3.5z" fill="white" stroke="white" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-link-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z" fill="white" stroke="white" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-shot-4" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M14.12 4l1.83 2H20v12H4V6h4.05l1.83-2h4.24M15 2H9L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2zm-3 7c1.65 0 3 1.35 3 3s-1.35 3-3 3-3-1.35-3-3 1.35-3 3-3m0-2c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5z" fill="white" stroke="white" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-trash-5" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M14.12 10.47L12 12.59l-2.13-2.12-1.41 1.41L10.59 14l-2.12 2.12 1.41 1.41L12 15.41l2.12 2.12 1.41-1.41L13.41 14l2.12-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4zM6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-trash-6" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<g transform="scale(1.25) translate(1 1)">
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-copy-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M3 5H1v16c0 1.1.9 2 2 2h16v-2H3V5zm18-4H7c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V3c0-1.1-.9-2-2-2zm0 16H7V3h14v14z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-sync-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-block-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-edit-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M14.06 9.02l.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-eye-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M12 6c3.79 0 7.17 2.13 8.82 5.5-.59 1.22-1.42 2.27-2.41 3.12l1.41 1.41c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l1.65 1.65C10.66 6.09 11.32 6 12 6zm-1.07 1.14L13 9.21c.57.25 1.03.71 1.28 1.28l2.07 2.07c.08-.34.14-.7.14-1.07C16.5 9.01 14.48 7 12 7c-.37 0-.72.05-1.07.14zM2.01 3.87l2.68 2.68C3.06 7.83 1.77 9.53 1 11.5 2.73 15.89 7 19 12 19c1.52 0 2.98-.29 4.32-.82l3.42 3.42 1.41-1.41L3.42 2.45 2.01 3.87zm7.5 7.5l2.61 2.61c-.04.01-.08.02-.12.02-1.38 0-2.5-1.12-2.5-2.5 0-.05.01-.08.01-.13zm-3.4-3.4l1.75 1.75c-.23.55-.36 1.15-.36 1.78 0 2.48 2.02 4.5 4.5 4.5.63 0 1.23-.13 1.77-.36l.98.98c-.88.24-1.8.38-2.75.38-3.79 0-7.17-2.13-8.82-5.5.7-1.43 1.72-2.61 2.93-3.53z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-gpx-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M8 23h8v-2H8v2zm8-21.99L8 1c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM16 15H8V5h8v10z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-arrow-up-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<circle cx="16" cy="16" fill="white" r="4" />
|
||||
</g>
|
||||
<g id="icon-more-vert" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-star-blank" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-star-fill" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<path d="m12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27z" fill="white" stroke="none" stroke-width="0" transform="translate(4 4)" />
|
||||
</g>
|
||||
<g id="icon-cluster-1" stroke="none">
|
||||
<rect x="0" y="0" width="32" height="32" fill="black" stroke="none" />
|
||||
<circle cx="10" cy="21" fill="white" r="4" />
|
||||
<circle cx="22" cy="21" fill="white" r="4" />
|
||||
<circle cx="16" cy="11" fill="white" r="4" />
|
||||
</g>
|
||||
<g id="icon-chevron-down" stroke="none">
|
||||
<g transform="scale(2) translate(-4 -3)">
|
||||
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-sad-1" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<g transform="translate(4 4)">
|
||||
<circle cx="15.5" cy="9.5" r="1.5" fill="white" stroke="none" />
|
||||
<circle cx="8.5" cy="9.5" r="1.5" fill="white" stroke="none" />
|
||||
<path d="M12 14c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5zm-.01-12C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill="white" stroke="none" stroke-width="0" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-search" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<g transform="translate(4 4)">
|
||||
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-reverse" stroke="none">
|
||||
<path stroke="none" fill="black" />
|
||||
<g transform="translate(6 4)">
|
||||
<path d="M9.832 2.934c-.967 0-1.683-.018-1.683-.018L7.021 0l-3.1 6.51 5.843 2.368-.777-3.032s.351-.051.845-.052a6.974 6.974 0 016.973 6.972 6.973 6.973 0 01-.922 3.457l2.07 2.07a9.833 9.833 0 001.711-5.527c0-5.43-4.402-9.831-9.832-9.832zm-8.094 4.27A9.833 9.833 0 000 12.766c0 5.43 4.402 9.832 9.832 9.832 1.108 0 1.398-.153 1.398-.153l.902 2.4 3.734-5.378-6.43-1.959.914 2.2s.046.03-.518.031a6.974 6.974 0 01-6.973-6.973 6.974 6.974 0 01.948-3.494z" fill="#fff" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-drop-start" stroke="none">
|
||||
<g transform="translate(6 12)" fill="white">
|
||||
<path d="M23.528 6.662H11.979L9.953 4.98l2.026-1.887h11.549zM7.572 9.755l2.183-2.183-2.696-2.695 2.696-2.695L7.572 0 4.877 2.695 2.182 0 0 2.182l2.695 2.695L0 7.572l2.182 2.183 2.695-2.696z" fill="#fff" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-drop-end" stroke="none">
|
||||
<g transform="translate(6 12)" fill="white">
|
||||
<path d="M0 3.093h11.548l2.026 1.681-2.026 1.888H0zM15.955 0l-2.182 2.182 2.695 2.695-2.695 2.695 2.182 2.183 2.695-2.696 2.696 2.696 2.182-2.183-2.696-2.695 2.696-2.695L21.346 0 18.65 2.695z" fill="#fff" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-undo" stroke="none">
|
||||
<g transform="translate(8 8)" fill="white">
|
||||
<path d="M5.912 1.278c-.967 0-2.84.42-2.84.42L2.257 0 0 4.855 5.094 6.19l-.777-1.938s1.101-.113 1.595-.115a6.974 6.974 0 016.972 6.973 6.97 6.97 0 01-.921 3.457l2.07 2.07a9.833 9.833 0 001.71-5.527c0-5.43-4.401-9.832-9.831-9.832z" fill="#fff" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-redo" stroke="none">
|
||||
<g transform="translate(8 8)" fill="white">
|
||||
<path d="M9.832 1.278c.967 0 2.84.42 2.84.42L13.486 0l2.257 4.855L10.65 6.19l.777-1.938s-1.101-.113-1.595-.115a6.974 6.974 0 00-6.973 6.973 6.97 6.97 0 00.922 3.457l-2.07 2.07A9.833 9.833 0 010 11.11c0-5.43 4.402-9.832 9.832-9.832z" fill="#fff" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-draw-forward" stroke="none">
|
||||
<g transform="translate(4 11)" fill="white">
|
||||
<path d="M19.38 0l5.374 4.547-5.374 4.655V0z" />
|
||||
<path d="M0 2.872h19.91v3.569H0z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-draw-backward" stroke="none">
|
||||
<g transform="translate(4 11)" fill="white">
|
||||
<path d="M5.374 9.202L0 4.655 5.374 0v9.202z" />
|
||||
<path d="M24.754 6.33H4.844V2.762h19.91z" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="icon-to-poly" stroke="none">
|
||||
<path d="M0 0h32v32H0z" fill="black" />
|
||||
<path d="M7.924 12.991l8.569-4.989-6.55 14.587 13.762-7.974-3.958 9.12" fill="none" stroke="#fff" stroke-width="2.387" stroke-linejoin="round"/>
|
||||
<circle transform="rotate(75)" r="2.387" cy="-4.523" cx="14.711" fill="#fff"/>
|
||||
<circle transform="rotate(75)" r="2.387" cy="-13.883" cx="12.17" fill="#fff"/>
|
||||
<circle transform="rotate(75)" r="2.387" cy="-19.194" cx="20.342" fill="#fff"/>
|
||||
<circle transform="rotate(75)" cx="24.138" cy="-3.701" r="5.48" fill="black" />
|
||||
<circle transform="rotate(75)" cx="27.918" cy="-13.025" r="2.387" fill="#fff"/>
|
||||
|
||||
<path d="M8.224 18.144v3.132H2.357v3.891h5.867v3.012l5.86-5.077z"/>
|
||||
</g>
|
||||
|
||||
<g id="icon-start">
|
||||
<path d="M15.646 2.959a12.688 12.688 0 00-8 2.85v1.837h8zm0 4.687v8h8v-8zm8 0h1.838a12.688 12.688 0 00-1.838-1.835zm0 8v8h1.836a12.688 12.688 0 002.852-8zm0 8h-8v4.688a12.688 12.688 0 008-2.85zm-8 0v-8h-8v8zm-8 0H5.81a12.688 12.688 0 001.837 1.836zm0-8v-8H5.811a12.688 12.688 0 00-2.852 8z" fill="#fff"/>
|
||||
<path d="M15.646 0A15.646 15.646 0 000 15.646a15.646 15.646 0 0015.646 15.647 15.646 15.646 0 0015.647-15.647A15.646 15.646 0 0015.646 0zm0 3.24a12.407 12.407 0 0112.408 12.406 12.407 12.407 0 01-12.408 12.408A12.407 12.407 0 013.24 15.646 12.407 12.407 0 0115.646 3.24z" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
</defs>
|
||||
|
||||
<use xlink:href="#icon-trash-6" />
|
||||
</svg>
|
After Width: | Height: | Size: 36 KiB |
BIN
public/images/logos/jw.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
public/images/logos/lgo.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
public/images/logos/pedals.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
public/images/logos/pin-mix.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
public/images/logos/privet.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
public/images/logos/prokatimsya.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
public/images/logos/rider.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
public/images/logos/rider_evening.png
Normal file
After Width: | Height: | Size: 73 KiB |
9287
public/images/stickers-base.svg
Normal file
After Width: | Height: | Size: 612 KiB |
154
public/index.html
Normal file
|
@ -0,0 +1,154 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=0.95">
|
||||
<meta name="theme-color" content="#5A3D6D">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Rubik:300,400,500,700&subset=cyrillic" rel="stylesheet" />
|
||||
<title>Редактор маршрутов</title>
|
||||
|
||||
<!-- analytics, lol -->
|
||||
<script async src="https://anal.vault48.org/script.js" data-website-id="9f2c8b59-93ff-4bcc-9ab6-11541f68f198"></script>
|
||||
|
||||
<link rel="shortcut icon" href="/favicon.png" type="image/png">
|
||||
<meta property="og:image" content="/misc/vk_preview.png" />
|
||||
<meta content="/misc/vk_preview.png">
|
||||
<style>
|
||||
#loader {
|
||||
position: fixed;
|
||||
background: #333333;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
transition: opacity 1s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-family: sans-serif;
|
||||
font-size: 16px;
|
||||
flex-direction: column;
|
||||
font-weight: 500;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.9; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
#loader-current {
|
||||
text-transform: uppercase;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#loader-container {
|
||||
max-width: 320px;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#loader-progress {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
box-shadow: inset rgba(0,0,0,0.1) 0 0 0 1px;
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#loader-bar {
|
||||
/*background: linear-gradient(90deg, #845b9e, #54faff);*/
|
||||
background: linear-gradient(130deg,#46bff3, #7833ff);
|
||||
/* background: #7c5f9e; */
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
transition: width 500ms;
|
||||
animation: blink 0.5s infinite alternate linear;
|
||||
min-width: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#loader-bar.is_failed {
|
||||
background: linear-gradient(130deg, #f3572b, #a51519);
|
||||
}
|
||||
|
||||
#loader-bar.is_failed::after {
|
||||
content: 'перезагрузка';
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
canvas#renderer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#loader-error {
|
||||
color: white;
|
||||
/* background-color: white; */
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
opacity: 0;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
box-shadow: inset white 0 0 0 1px;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transform: translate(0, -16px);
|
||||
}
|
||||
|
||||
#loader-error h4 {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="renderer"></canvas>
|
||||
|
||||
<section id="canvas" style="position: absolute; width: 100%; height: 100%;"></section>
|
||||
|
||||
<section id="loader">
|
||||
<div id="loader-container">
|
||||
<div id="loader-current">ЗАГРУЗКА</div>
|
||||
|
||||
<div id="loader-error">
|
||||
<h4>Хранилище недоступно</h4>
|
||||
<div>Мы работаем над решением проблемы</div>
|
||||
</div>
|
||||
|
||||
<div id="loader-progress">
|
||||
<div id="loader-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="index"></section>
|
||||
</body>
|
|
@ -1,40 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { IModes, MODES } from '$constants/modes';
|
||||
import { IStickerPack, STICKERS } from '$constants/stickers';
|
||||
import { StickerIcon } from '$components/StickerIcon';
|
||||
|
||||
interface Props {
|
||||
mode: keyof IModes,
|
||||
sticker: string,
|
||||
set: keyof IStickerPack,
|
||||
}
|
||||
|
||||
export class Cursor extends React.PureComponent<Props, {}> {
|
||||
componentDidMount() {
|
||||
window.addEventListener('mousemove', this.moveCursor);
|
||||
}
|
||||
|
||||
moveCursor = e => {
|
||||
if (!e.clientX || !e.clientY) return;
|
||||
|
||||
const { clientX, clientY } = e;
|
||||
|
||||
this.cursor.style.transform = `translate3d(${clientX}px, ${clientY}px, 0)`;
|
||||
};
|
||||
|
||||
cursor: HTMLElement = null;
|
||||
|
||||
render() {
|
||||
const { mode, set, sticker } = this.props;
|
||||
const activeSticker = (sticker && set && STICKERS[set] && STICKERS[set].layers[sticker]);
|
||||
|
||||
return (
|
||||
<div className="cursor-tooltip desktop-only" ref={el => { this.cursor = el; }}>
|
||||
{ mode === MODES.ROUTER && <Icon icon="icon-router" />}
|
||||
{ mode === MODES.POLY && <Icon icon="icon-poly" />}
|
||||
{ mode === MODES.STICKERS && activeSticker && <StickerIcon sticker={sticker} set={set} /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export const Fills = () => (
|
||||
<svg>
|
||||
|
@ -43,6 +43,6 @@ export const Fills = () => (
|
|||
<path d="m 2.625,3.375 h 7.5 L 10.28125,1.609375 13.5,4.25 10.484375,6.921875 10.171875,5.15625 2.625,5.125 Z" fill="#ff3344" fillRule="evenodd" />
|
||||
</g>
|
||||
</defs>
|
||||
<image xlinkHref={require('$sprites/stickers/stickers-base.svg')} width={0} height={0} />
|
||||
<image xlinkHref="/images/stickers-base.svg" width={0} height={0} />
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { Scrollbars } from 'tt-react-custom-scrollbars';
|
||||
|
||||
export const Scroll = props => (
|
||||
|
|
|
@ -1,42 +1,48 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
onChange: (text: string) => void;
|
||||
onBlur: () => void;
|
||||
}
|
||||
|
||||
type State = {
|
||||
text: String;
|
||||
}
|
||||
|
||||
export class StickerDesc extends React.PureComponent<Props, State> {
|
||||
class StickerDesc extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
text: this.props.value,
|
||||
};
|
||||
|
||||
setText = e => {
|
||||
this.setState({ text: e.target.value });
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
blockMouse = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!this.input) {
|
||||
return
|
||||
}
|
||||
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
input: HTMLTextAreaElement;
|
||||
// todo: pass here locker for moving markers from Sticker.js
|
||||
input: HTMLTextAreaElement | null = null;
|
||||
|
||||
render() {
|
||||
const { text } = this.state;
|
||||
const { value: text } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('sticker-desc', { is_empty: !text.trim() })}
|
||||
onMouseDown={this.blockMouse}
|
||||
onMouseUp={this.blockMouse}
|
||||
onDragStart={this.blockMouse}
|
||||
onMouseDownCapture={this.blockMouse}
|
||||
onMouseUpCapture={this.blockMouse}
|
||||
onDragStartCapture={this.blockMouse}
|
||||
onTouchStartCapture={this.blockMouse}
|
||||
>
|
||||
<div className="sticker-desc-sizer">
|
||||
<span
|
||||
|
@ -50,9 +56,12 @@ export class StickerDesc extends React.PureComponent<Props, State> {
|
|||
onMouseDown={this.blockMouse}
|
||||
onDragStart={this.blockMouse}
|
||||
ref={el => { this.input = el; }}
|
||||
onBlur={this.props.onBlur}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { StickerDesc };
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { STICKERS } from '$constants/stickers';
|
||||
import React from 'react';
|
||||
import { STICKERS } from '~/constants/stickers';
|
||||
|
||||
type Props = {
|
||||
set: string,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -1,78 +1,36 @@
|
|||
import * as React from 'react';
|
||||
import { marker } from 'leaflet';
|
||||
import { DomMarker } from '$utils/DomMarker';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { editor } from '$modules/Editor';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IState } from '~/redux/store';
|
||||
import { selectUserLocation } from '~/redux/user/selectors';
|
||||
import { connect } from 'react-redux';
|
||||
import { Tooltip } from './panels/Tooltip';
|
||||
import { MainMap } from '~/constants/map';
|
||||
|
||||
interface Props {
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
location: selectUserLocation(state),
|
||||
});
|
||||
|
||||
}
|
||||
type Props = ReturnType<typeof mapStateToProps> & {};
|
||||
|
||||
export class UserLocation extends React.Component<Props, {}> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const UserLocationUnconnected: FC<Props> = ({ location }) => {
|
||||
const onClick = useCallback(() => {
|
||||
if (!location) return;
|
||||
|
||||
const element = document.createElement('div');
|
||||
this.icon = new DomMarker({ element, className: 'location-marker' });
|
||||
MainMap.setView(location, 17);
|
||||
}, [MainMap, location]);
|
||||
|
||||
this.map = editor.map.map;
|
||||
this.location = [];
|
||||
}
|
||||
return (
|
||||
<div className="status-bar location-bar pointer tooltip-container" onClick={onClick}>
|
||||
<Tooltip position="top">Где я?</Tooltip>
|
||||
|
||||
icon;
|
||||
mark = null;
|
||||
map;
|
||||
location;
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" style={{ opacity: location ? 1 : 0.5 }}>
|
||||
<g transform="translate(7 2)">
|
||||
<circle r="1.846" cy="1.846" cx="5.088" />
|
||||
<path d="M3.004 4.326h4l2-3 1 1-3 4v10h-1l-1-7-1 7h-1v-10s-3.125-4-3-4l1-1z" />
|
||||
<ellipse ry="1" rx="4" cy="16.326" cx="5.004" opacity=".262" fill="black" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getUserLocation(this.updateLocationMark);
|
||||
}
|
||||
|
||||
getUserLocation = callback => {
|
||||
// todo: TO SAGAS
|
||||
if (!window.navigator || !window.navigator.geolocation) return;
|
||||
|
||||
window.navigator.geolocation.getCurrentPosition(position => {
|
||||
if (!position || !position.coords || !position.coords.latitude || !position.coords.longitude) return;
|
||||
|
||||
const { latitude, longitude } = position.coords;
|
||||
|
||||
callback(latitude, longitude);
|
||||
});
|
||||
};
|
||||
|
||||
centerMapOnLocation = () => {
|
||||
if (this.location && this.location.length === 2) {
|
||||
this.panMapTo(this.location[0], this.location[1]);
|
||||
} else {
|
||||
this.getUserLocation(this.panMapTo);
|
||||
}
|
||||
|
||||
this.getUserLocation(this.updateLocationMark);
|
||||
};
|
||||
|
||||
panMapTo = (latitude, longitude) => {
|
||||
if (!latitude || !longitude) return;
|
||||
|
||||
this.map.panTo([latitude, longitude]);
|
||||
};
|
||||
|
||||
updateLocationMark = (latitude, longitude) => {
|
||||
if (!latitude || !longitude) return;
|
||||
|
||||
if (this.mark) this.map.removeLayer(this.mark);
|
||||
|
||||
this.location = [latitude, longitude];
|
||||
this.mark = marker(this.location, { icon: this.icon }).addTo(this.map);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="status-bar square pointer pointer">
|
||||
<div onClick={this.centerMapOnLocation}>
|
||||
<Icon icon="icon-locate" size={30} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const UserLocation = connect(mapStateToProps)(UserLocationUnconnected);
|
||||
|
|
|
@ -1,74 +1,62 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Scroll } from '$components/Scroll';
|
||||
import { APP_INFO } from '$constants/app_info';
|
||||
import React, { Fragment } from 'react';
|
||||
import { Scroll } from '~/components/Scroll';
|
||||
import { APP_INFO } from '~/constants/app_info';
|
||||
|
||||
export const AppInfoDialog = () => (
|
||||
<div className="dialog-content">
|
||||
<div className="dialog-head">
|
||||
<div className="dialog-head-title">
|
||||
Orchid Map
|
||||
</div>
|
||||
<div className="small gray">
|
||||
версия{' '}
|
||||
{(APP_INFO.VERSION || 1)}.
|
||||
{(APP_INFO.CHANGELOG[APP_INFO.VERSION].length || 0)}.
|
||||
{(APP_INFO.CHANGELOG[APP_INFO.VERSION][0].length - 1 || 0)}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="small app-info-list">
|
||||
<div>
|
||||
<div>Исходный код:</div>
|
||||
<a href="//github.com/muerwre/orchidMap" target="_blank">github.com/muerwre/orchidMap</a>
|
||||
<Fragment>
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
<div className="dialog-content">
|
||||
<div className="dialog-head">
|
||||
<div className="dialog-head-title">Orchid Map</div>
|
||||
<div className="small gray">
|
||||
версия {APP_INFO.VERSION || 1}.{APP_INFO.CHANGELOG[APP_INFO.VERSION].length || 0}.
|
||||
{APP_INFO.CHANGELOG[APP_INFO.VERSION][0].length - 1 || 0}
|
||||
</div>
|
||||
<div>
|
||||
<div>Frontend:</div>
|
||||
<a href="//reactjs.org/" target="_blank">ReactJS</a>,{' '}
|
||||
<a href="//leafletjs.com" target="_blank">Leaflet</a>,{' '}
|
||||
<a href="//www.liedman.net/leaflet-routing-machine/" target="_blank">Leaflet Routing Machine</a>{' '}
|
||||
</div>
|
||||
<div>
|
||||
<div>Backend:</div>
|
||||
<a href="//project-osrm.org/" target="_blank">OSRM</a>,{' '}
|
||||
<a href="//nodejs.org/" target="_blank">NodeJS</a>,{' '}
|
||||
<a href="//expressjs.com/" target="_blank">ExpressJS</a>,{' '}
|
||||
<a href="//mongodb.com/" target="_blank">MongoDB</a>
|
||||
<hr />
|
||||
<div className="small app-info-list">
|
||||
<div>
|
||||
<div>Исходный код:</div>
|
||||
<a href="//github.com/muerwre/orchid-front" target="_blank">
|
||||
github.com/muerwre/orchid-front
|
||||
</a>
|
||||
<br />
|
||||
<a href="//github.com/muerwre/orchid-backend" target="_blank">
|
||||
github.com/muerwre/orchid-backend
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div>Frontend:</div>
|
||||
<a href="//reactjs.org/" target="_blank">
|
||||
ReactJS
|
||||
</a>
|
||||
,{' '}
|
||||
<a href="//leafletjs.com" target="_blank">
|
||||
Leaflet
|
||||
</a>
|
||||
,{' '}
|
||||
<a href="//www.liedman.net/leaflet-routing-machine/" target="_blank">
|
||||
Leaflet Routing Machine
|
||||
</a>{' '}
|
||||
</div>
|
||||
<div>
|
||||
<div>Backend:</div>
|
||||
<a href="//project-osrm.org/" target="_blank">
|
||||
OSRM
|
||||
</a>
|
||||
,{' '}
|
||||
<a href="//golang.org/" target="_blank">
|
||||
Golang
|
||||
</a>
|
||||
,{' '}
|
||||
<a href="//nginx.org/" target="_blank">
|
||||
Nginx
|
||||
</a>
|
||||
,{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Scroll className="dialog-shader">
|
||||
<div>
|
||||
<div className="app-info-changelog">
|
||||
<h2>История изменений</h2>
|
||||
{
|
||||
[...Object.keys(APP_INFO.CHANGELOG)].reverse().map((version, i) => (
|
||||
<div className="app-info-changelog-item" key={version}>
|
||||
<div className="app-info-number">{version}.</div>
|
||||
<div className="app-info-version">
|
||||
{
|
||||
APP_INFO.CHANGELOG[version].map((release, y) => (
|
||||
<div className="app-info-release" key={release}>
|
||||
<div className="app-info-number">{APP_INFO.CHANGELOG[version].length - y}.</div>
|
||||
<div className="app-info-build">
|
||||
{
|
||||
APP_INFO.CHANGELOG[version][y].map((build, z) => (
|
||||
<div className="app-info-change" key={build}>
|
||||
<div className="app-info-number">{(z)}.</div>
|
||||
<span>{APP_INFO.CHANGELOG[version][y][z]}</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { MODES } from '$constants/modes';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { setMode, stopEditing } from "$redux/user/actions";
|
||||
import { MODES } from '~/constants/modes';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import { editorChangeMode, editorStopEditing } from '~/redux/editor/actions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
type Props = {
|
||||
stopEditing: typeof stopEditing,
|
||||
setMode: typeof setMode,
|
||||
width: number,
|
||||
const mapStateToProps = () => ({});
|
||||
const mapDispatchToProps = {
|
||||
editorChangeMode,
|
||||
editorStopEditing,
|
||||
};
|
||||
|
||||
export class CancelDialog extends React.Component<Props, void> {
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & { };
|
||||
|
||||
class CancelDialogUnconnected extends React.Component<Props, void> {
|
||||
cancel = () => {
|
||||
this.props.stopEditing();
|
||||
this.props.editorStopEditing();
|
||||
};
|
||||
|
||||
proceed = () => {
|
||||
this.props.setMode(MODES.NONE);
|
||||
this.props.editorChangeMode(MODES.NONE);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width } = this.props;
|
||||
|
||||
return (
|
||||
<div className="control-dialog" style={{ width }}>
|
||||
<div className="control-dialog control-dialog__medium">
|
||||
<div className="helper cancel-helper">
|
||||
<div className="helper__text danger">
|
||||
<Icon icon="icon-cancel-1" />
|
||||
|
@ -46,3 +47,5 @@ export class CancelDialog extends React.Component<Props, void> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CancelDialog = connect(mapStateToProps, mapDispatchToProps)(CancelDialogUnconnected)
|
16
src/components/dialogs/DialogLoader.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const DialogLoader: FC<IProps> = ({}) => {
|
||||
return (
|
||||
<div className="dialog-maplist-loader">
|
||||
<div className="dialog-maplist-icon spin">
|
||||
<Icon icon="icon-sync-1" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { DialogLoader };
|
177
src/components/dialogs/GpxDialog.tsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
import React, { FC, useCallback, ChangeEvent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { IState } from '~/redux/store';
|
||||
import { selectEditorGpx } from '~/redux/editor/selectors';
|
||||
import { GpxDialogRow } from '~/components/gpx/GpxDialogRow';
|
||||
import { GpxConfirm } from '~/components/gpx/GpxConfirm';
|
||||
import { MainMap } from '~/constants/map';
|
||||
import { latLngBounds } from 'leaflet';
|
||||
import { Switch } from '../Switch';
|
||||
import { selectMapRoute, selectMapTitle, selectMapAddress } from '~/redux/map/selectors';
|
||||
import classNames from 'classnames';
|
||||
import uuid from 'uuid';
|
||||
import { getUrlData } from '~/utils/history';
|
||||
import { getRandomColor } from '~/utils/dom';
|
||||
|
||||
import * as EDITOR_ACTIONS from '~/redux/editor/actions';
|
||||
import * as MAP_ACTIONS from '~/redux/map/actions';
|
||||
import { simplify } from '~/utils/simplify';
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
gpx: selectEditorGpx(state),
|
||||
route: selectMapRoute(state),
|
||||
title: selectMapTitle(state),
|
||||
address: selectMapAddress(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
editorDropGpx: EDITOR_ACTIONS.editorDropGpx,
|
||||
editorUploadGpx: EDITOR_ACTIONS.editorUploadGpx,
|
||||
editorSetGpx: EDITOR_ACTIONS.editorSetGpx,
|
||||
editorGetGPXTrack: EDITOR_ACTIONS.editorGetGPXTrack,
|
||||
mapSetRoute: MAP_ACTIONS.mapSetRoute,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const GpxDialogUnconnected: FC<Props> = ({
|
||||
title,
|
||||
address,
|
||||
gpx,
|
||||
route,
|
||||
editorGetGPXTrack,
|
||||
editorSetGpx,
|
||||
editorUploadGpx,
|
||||
mapSetRoute,
|
||||
}) => {
|
||||
const toggleGpx = useCallback(() => {
|
||||
editorSetGpx({ enabled: !gpx.enabled });
|
||||
}, [gpx, editorSetGpx]);
|
||||
|
||||
const onGpxUpload = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!event.target || !event.target.files || event.target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorUploadGpx(event.target.files[0]);
|
||||
},
|
||||
[editorUploadGpx]
|
||||
);
|
||||
|
||||
const onFocusRoute = useCallback(
|
||||
index => {
|
||||
if (!gpx.list[index] || !gpx.list[index].latlngs) return;
|
||||
|
||||
const bounds = latLngBounds(gpx.list[index].latlngs);
|
||||
MainMap.fitBounds(bounds);
|
||||
},
|
||||
[gpx, MainMap]
|
||||
);
|
||||
|
||||
const onRouteDrop = useCallback(
|
||||
index => {
|
||||
editorSetGpx({ list: gpx.list.filter((el, i) => i !== index) });
|
||||
},
|
||||
[gpx, editorSetGpx]
|
||||
);
|
||||
|
||||
const onRouteColor = useCallback(
|
||||
index => {
|
||||
if (!gpx.enabled) return;
|
||||
editorSetGpx({
|
||||
list: gpx.list.map((el, i) => (i !== index ? el : { ...el, color: getRandomColor() })),
|
||||
});
|
||||
},
|
||||
[gpx, editorSetGpx]
|
||||
);
|
||||
|
||||
const onRouteToggle = useCallback(
|
||||
index => {
|
||||
if (!gpx.enabled) return;
|
||||
|
||||
editorSetGpx({
|
||||
list: gpx.list.map((el, i) => (i !== index ? el : { ...el, enabled: !el.enabled })),
|
||||
});
|
||||
},
|
||||
[gpx, editorSetGpx]
|
||||
);
|
||||
|
||||
const addCurrent = useCallback(() => {
|
||||
if (!route.length) return;
|
||||
|
||||
const { path } = getUrlData();
|
||||
|
||||
editorSetGpx({
|
||||
list: [
|
||||
...gpx.list,
|
||||
{
|
||||
latlngs: route,
|
||||
enabled: false,
|
||||
name: title || address || path,
|
||||
id: uuid(),
|
||||
color: getRandomColor(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}, [route, gpx, editorSetGpx]);
|
||||
|
||||
const onRouteReplace = useCallback(
|
||||
(i: number) => {
|
||||
mapSetRoute(simplify(gpx.list[i].latlngs));
|
||||
|
||||
editorSetGpx({
|
||||
list: gpx.list.map((el, index) => (i !== index ? el : { ...el, enabled: false })),
|
||||
});
|
||||
},
|
||||
[gpx, mapSetRoute, editorSetGpx]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="control-dialog control-dialog__left control-dialog__small">
|
||||
<div className="gpx-title">
|
||||
<div className="flex_1 big white upper">Треки</div>
|
||||
<Switch active={gpx.enabled} onPress={toggleGpx} />
|
||||
</div>
|
||||
|
||||
{gpx.list.map((item, index) => (
|
||||
<GpxDialogRow
|
||||
item={item}
|
||||
key={item.id}
|
||||
index={index}
|
||||
enabled={gpx.enabled}
|
||||
onRouteDrop={onRouteDrop}
|
||||
onFocusRoute={onFocusRoute}
|
||||
onRouteToggle={onRouteToggle}
|
||||
onRouteColor={onRouteColor}
|
||||
onRouteReplace={onRouteReplace}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="gpx-buttons">
|
||||
<button className="button outline">
|
||||
<input type="file" onChange={onGpxUpload} />
|
||||
Загрузить GPX
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={classNames('button outline', { disabled: !route.length })}
|
||||
onClick={addCurrent}
|
||||
>
|
||||
Добавить текущий
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames('button success', { disabled: !route.length })}
|
||||
onClick={editorGetGPXTrack}
|
||||
>
|
||||
Скачать текущий
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GpxDialog = connect(mapStateToProps, mapDispatchToProps)(GpxDialogUnconnected);
|
||||
|
||||
export { GpxDialog };
|
|
@ -1,15 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { LOGOS } from '$constants/logos';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import React from 'react';
|
||||
import { LOGOS } from '~/constants/logos';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import classnames from 'classnames';
|
||||
import { setLogo as setLogoAction } from "$redux/user/actions";
|
||||
import { IRootState } from "$redux/user/reducer";
|
||||
import * as MAP_ACTIONS from "~/redux/map/actions"
|
||||
import { selectMapLogo } from '~/redux/map/selectors';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props extends IRootState {
|
||||
setLogo: typeof setLogoAction,
|
||||
}
|
||||
const mapStateToProps = state => ({
|
||||
logo: selectMapLogo(state),
|
||||
});
|
||||
|
||||
export const LogoDialog = ({ logo, setLogo }: Props) => (
|
||||
const mapDispatchToProps = {
|
||||
mapSetLogo: MAP_ACTIONS.mapSetLogo,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const LogoDialogUnconnected = ({ logo, mapSetLogo }: Props) => (
|
||||
<div className="control-dialog top">
|
||||
<div className="helper logo-helper">
|
||||
<div className="helper-back">
|
||||
|
@ -19,7 +26,7 @@ export const LogoDialog = ({ logo, setLogo }: Props) => (
|
|||
Object.keys(LOGOS).map(item => (
|
||||
<div
|
||||
className={classnames('helper-menu-item', { active: (item === logo) })}
|
||||
onMouseDown={() => setLogo(item)}
|
||||
onMouseDown={() => mapSetLogo(item)}
|
||||
key={item}
|
||||
>
|
||||
{LOGOS[item][0]}
|
||||
|
@ -29,3 +36,7 @@ export const LogoDialog = ({ logo, setLogo }: Props) => (
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LogoDialog = connect(mapStateToProps, mapDispatchToProps)(LogoDialogUnconnected);
|
||||
|
||||
export { LogoDialog };
|
|
@ -1,102 +1,123 @@
|
|||
import * as React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { RouteRowWrapper } from '$components/maps/RouteRowWrapper';
|
||||
import { Scroll } from '$components/Scroll';
|
||||
import { RouteRowWrapper } from '~/components/maps/RouteRowWrapper';
|
||||
import { Scroll } from '~/components/Scroll';
|
||||
import {
|
||||
searchSetDistance,
|
||||
searchSetTitle,
|
||||
searchSetTab,
|
||||
setDialogActive,
|
||||
mapsLoadMore,
|
||||
dropRoute,
|
||||
modifyRoute,
|
||||
} from '$redux/user/actions';
|
||||
import { isMobile } from '$utils/window';
|
||||
toggleRouteStarred,
|
||||
} from '~/redux/user/actions';
|
||||
|
||||
import { editorSetDialogActive } from '~/redux/editor/actions';
|
||||
|
||||
import { isMobile } from '~/utils/window';
|
||||
import classnames from 'classnames';
|
||||
|
||||
// import { Range } from 'rc-slider';
|
||||
import * as Range from 'rc-slider/lib/Range';
|
||||
import { TABS } from '$constants/dialogs';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { pushPath } from '$utils/history';
|
||||
import { IRootState, IRouteListItem } from '$redux/user/reducer';
|
||||
import { TABS, TABS_TITLES } from '~/constants/dialogs';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import { pushPath } from '~/utils/history';
|
||||
import { IRouteListItem } from '~/redux/user';
|
||||
import { ROLES } from '~/constants/auth';
|
||||
import { IState } from '~/redux/store';
|
||||
import { MapListDialogHead } from '~/components/search/MapListDialogHead';
|
||||
import { DialogLoader } from '~/components/dialogs/DialogLoader';
|
||||
|
||||
export interface IMapListDialogProps extends IRootState {
|
||||
marks: { [x: number]: string },
|
||||
routes_sorted: Array<IRouteListItem>,
|
||||
routes: IRootState['routes'],
|
||||
ready: IRootState['ready'],
|
||||
const mapStateToProps = ({
|
||||
editor: { editing },
|
||||
user: {
|
||||
routes,
|
||||
user: { role },
|
||||
},
|
||||
}: IState) => {
|
||||
return {
|
||||
role,
|
||||
routes,
|
||||
editing,
|
||||
ready: routes.filter.max < 9999,
|
||||
};
|
||||
};
|
||||
|
||||
mapsLoadMore: typeof mapsLoadMore,
|
||||
searchSetDistance: typeof searchSetDistance,
|
||||
searchSetTitle: typeof searchSetTitle,
|
||||
searchSetTab: typeof searchSetTab,
|
||||
setDialogActive: typeof setDialogActive,
|
||||
dropRoute: typeof dropRoute,
|
||||
modifyRoute: typeof modifyRoute,
|
||||
const mapDispatchToProps = {
|
||||
searchSetDistance,
|
||||
searchSetTitle,
|
||||
searchSetTab,
|
||||
editorSetDialogActive,
|
||||
mapsLoadMore,
|
||||
dropRoute,
|
||||
modifyRoute,
|
||||
toggleRouteStarred,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
export interface State {
|
||||
menu_target: IRouteListItem['address'];
|
||||
editor_target: IRouteListItem['address'];
|
||||
|
||||
is_editing: boolean;
|
||||
is_dropping: boolean;
|
||||
}
|
||||
|
||||
export interface IMapListDialogState {
|
||||
menu_target: IRouteListItem['_id'],
|
||||
editor_target: IRouteListItem['_id'],
|
||||
|
||||
is_editing: boolean,
|
||||
is_dropping: boolean,
|
||||
}
|
||||
|
||||
class Component extends React.Component<IMapListDialogProps, IMapListDialogState> {
|
||||
class MapListDialogUnconnected extends PureComponent<Props, State> {
|
||||
state = {
|
||||
menu_target: null,
|
||||
editor_target: null,
|
||||
|
||||
menu_target: '',
|
||||
editor_target: '',
|
||||
is_editing: false,
|
||||
is_dropping: false,
|
||||
};
|
||||
|
||||
startEditing = (editor_target: IRouteListItem['_id']): void => this.setState({
|
||||
editor_target,
|
||||
menu_target: null,
|
||||
is_editing: true,
|
||||
is_dropping: false,
|
||||
});
|
||||
startEditing = (editor_target: IRouteListItem['address']): void =>
|
||||
this.setState({
|
||||
editor_target,
|
||||
menu_target: '',
|
||||
is_editing: true,
|
||||
is_dropping: false,
|
||||
});
|
||||
|
||||
showMenu = (menu_target: IRouteListItem['_id']): void => this.setState({
|
||||
menu_target,
|
||||
});
|
||||
showMenu = (menu_target: IRouteListItem['address']): void =>
|
||||
this.setState({
|
||||
menu_target,
|
||||
});
|
||||
|
||||
hideMenu = (): void => this.setState({
|
||||
menu_target: null,
|
||||
});
|
||||
hideMenu = (): void =>
|
||||
this.setState({
|
||||
menu_target: '',
|
||||
});
|
||||
|
||||
showDropCard = (editor_target: IRouteListItem['_id']): void => this.setState({
|
||||
editor_target,
|
||||
menu_target: null,
|
||||
is_editing: false,
|
||||
is_dropping: true,
|
||||
});
|
||||
showDropCard = (editor_target: IRouteListItem['address']): void =>
|
||||
this.setState({
|
||||
editor_target,
|
||||
menu_target: '',
|
||||
is_editing: false,
|
||||
is_dropping: true,
|
||||
});
|
||||
|
||||
stopEditing = (): void => {
|
||||
this.setState({ editor_target: null });
|
||||
this.setState({ editor_target: '' });
|
||||
};
|
||||
|
||||
setTitle = ({ target: { value } }: { target: { value: string }}): void => {
|
||||
setTitle = ({ target: { value } }: { target: { value: string } }): void => {
|
||||
this.props.searchSetTitle(value);
|
||||
};
|
||||
|
||||
openRoute = (_id: string): void => {
|
||||
if (isMobile()) this.props.setDialogActive(false);
|
||||
if (isMobile()) this.props.editorSetDialogActive(false);
|
||||
|
||||
// pushPath(`/${_id}/${this.props.editing ? 'edit' : ''}`);
|
||||
this.stopEditing();
|
||||
|
||||
pushPath(`/${_id}`);
|
||||
|
||||
// pushPath(`/${_id}/${this.props.editing ? 'edit' : ''}`);
|
||||
};
|
||||
|
||||
onScroll = (e: { target: { scrollHeight: number, scrollTop: number, clientHeight: number }}): void => {
|
||||
const { target: { scrollHeight, scrollTop, clientHeight }} = e;
|
||||
onScroll = (e: {
|
||||
target: { scrollHeight: number; scrollTop: number; clientHeight: number };
|
||||
}): void => {
|
||||
const {
|
||||
target: { scrollHeight, scrollTop, clientHeight },
|
||||
} = e;
|
||||
const delta = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (
|
||||
|
@ -108,122 +129,105 @@ class Component extends React.Component<IMapListDialogProps, IMapListDialogState
|
|||
}
|
||||
};
|
||||
|
||||
dropRoute = (_id: string): void => {
|
||||
this.props.dropRoute(_id);
|
||||
dropRoute = (address: string): void => {
|
||||
this.props.dropRoute(address);
|
||||
};
|
||||
|
||||
modifyRoute = ({ _id, title, is_public }: { _id: string, title: string, is_public: boolean }): void => {
|
||||
this.props.modifyRoute(_id, { title, is_public });
|
||||
modifyRoute = ({
|
||||
address,
|
||||
title,
|
||||
is_public,
|
||||
}: {
|
||||
address: string;
|
||||
title: string;
|
||||
is_public: boolean;
|
||||
}): void => {
|
||||
this.props.modifyRoute(address, { title, is_public });
|
||||
this.stopEditing();
|
||||
};
|
||||
|
||||
toggleStarred = (id: string) => this.props.toggleRouteStarred(id);
|
||||
|
||||
render() {
|
||||
const {
|
||||
ready,
|
||||
role,
|
||||
routes: {
|
||||
list,
|
||||
loading,
|
||||
filter: {
|
||||
min,
|
||||
max,
|
||||
title,
|
||||
distance,
|
||||
tab,
|
||||
}
|
||||
filter: { min, max, title, distance, tab },
|
||||
},
|
||||
marks,
|
||||
}: IMapListDialogProps = this.props;
|
||||
}: // marks,
|
||||
Props = this.props;
|
||||
|
||||
const { editor_target, menu_target, is_editing, is_dropping } = this.state;
|
||||
|
||||
return (
|
||||
<div className="dialog-content">
|
||||
{ list.length === 0 && loading &&
|
||||
<div className="dialog-maplist-loader">
|
||||
<div className="dialog-maplist-icon spin">
|
||||
<Icon icon="icon-sync-1" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{ ready && !loading && list.length === 0 &&
|
||||
<div className="dialog-content full">
|
||||
{list.length === 0 && loading && (
|
||||
<DialogLoader />
|
||||
)}
|
||||
|
||||
{ready && !loading && list.length === 0 && (
|
||||
<div className="dialog-maplist-loader">
|
||||
<div className="dialog-maplist-icon">
|
||||
<Icon icon="icon-sad-1" />
|
||||
</div>
|
||||
ТУТ ПУСТО <br />
|
||||
И ОДИНОКО
|
||||
ТУТ ПУСТО <br />И ОДИНОКО
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<div className="dialog-tabs">
|
||||
{
|
||||
Object.keys(TABS).map(item => (
|
||||
<div
|
||||
className={classnames('dialog-tab', { active: tab === item })}
|
||||
onClick={() => this.props.searchSetTab(item)}
|
||||
key={item}
|
||||
>
|
||||
{TABS[item]}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="dialog-head">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск по названию"
|
||||
value={title}
|
||||
onChange={this.setTitle}
|
||||
/>
|
||||
<br />
|
||||
{
|
||||
ready
|
||||
?
|
||||
<Range
|
||||
min={min}
|
||||
max={max}
|
||||
marks={marks}
|
||||
step={25}
|
||||
onChange={this.props.searchSetDistance}
|
||||
defaultValue={[0, 10000]}
|
||||
value={distance}
|
||||
pushable={25}
|
||||
disabled={min >= max}
|
||||
/>
|
||||
: <div className="range-placeholder" />
|
||||
}
|
||||
|
||||
</div>
|
||||
{Object.values(TABS).map(
|
||||
item =>
|
||||
(role === ROLES.admin || item !== TABS.PENDING) && (
|
||||
<div
|
||||
className={classnames('dialog-tab', { active: tab === item })}
|
||||
onClick={() => this.props.searchSetTab(item)}
|
||||
key={item}
|
||||
>
|
||||
{TABS_TITLES[item]}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Scroll
|
||||
className="dialog-shader"
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<MapListDialogHead
|
||||
min={min}
|
||||
max={max}
|
||||
distance={distance}
|
||||
onDistanceChange={this.props.searchSetDistance}
|
||||
ready={ready}
|
||||
search={title}
|
||||
onSearchChange={this.setTitle}
|
||||
/>
|
||||
|
||||
<Scroll className="dialog-shader" onScroll={this.onScroll}>
|
||||
<div className="dialog-maplist">
|
||||
{
|
||||
list.map(route => (
|
||||
<RouteRowWrapper
|
||||
title={route.title}
|
||||
distance={route.distance}
|
||||
_id={route._id}
|
||||
is_public={route.is_public}
|
||||
tab={tab}
|
||||
is_editing_mode={is_dropping ? 'drop' : 'edit'}
|
||||
is_editing_target={editor_target === route._id}
|
||||
is_menu_target={menu_target === route._id}
|
||||
openRoute={this.openRoute}
|
||||
startEditing={this.startEditing}
|
||||
stopEditing={this.stopEditing}
|
||||
showMenu={this.showMenu}
|
||||
hideMenu={this.hideMenu}
|
||||
showDropCard={this.showDropCard}
|
||||
dropRoute={this.dropRoute}
|
||||
modifyRoute={this.modifyRoute}
|
||||
key={route._id}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{list.map(route => (
|
||||
<RouteRowWrapper
|
||||
title={route.title}
|
||||
distance={route.distance}
|
||||
address={route.address}
|
||||
is_public={route.is_public}
|
||||
is_published={route.is_published}
|
||||
tab={tab}
|
||||
is_editing_mode={is_dropping ? 'drop' : 'edit'}
|
||||
is_editing_target={editor_target === route.address}
|
||||
is_menu_target={menu_target === route.address}
|
||||
openRoute={this.openRoute}
|
||||
startEditing={this.startEditing}
|
||||
stopEditing={this.stopEditing}
|
||||
showMenu={this.showMenu}
|
||||
hideMenu={this.hideMenu}
|
||||
showDropCard={this.showDropCard}
|
||||
dropRoute={this.dropRoute}
|
||||
modifyRoute={this.modifyRoute}
|
||||
toggleStarred={this.toggleStarred}
|
||||
key={route.address}
|
||||
is_admin={role === ROLES.admin}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Scroll>
|
||||
|
||||
|
@ -233,33 +237,6 @@ class Component extends React.Component<IMapListDialogProps, IMapListDialogState
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ user: { editing, routes } }) => {
|
||||
if (routes.filter.max >= 9999) {
|
||||
return {
|
||||
routes, editing, marks: {}, ready: false,
|
||||
};
|
||||
}
|
||||
return ({
|
||||
routes,
|
||||
editing,
|
||||
ready: true,
|
||||
marks: [...new Array(Math.floor((routes.filter.max - routes.filter.min) / 25) + 1)].reduce((obj, el, i) => ({
|
||||
...obj,
|
||||
[routes.filter.min + (i * 25)]:
|
||||
` ${routes.filter.min + (i * 25)}${(routes.filter.min + (i * 25) >= 200) ? '+' : ''}
|
||||
`,
|
||||
}), {}),
|
||||
});
|
||||
};
|
||||
const MapListDialog = connect(mapStateToProps, mapDispatchToProps)(MapListDialogUnconnected);
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||
searchSetDistance,
|
||||
searchSetTitle,
|
||||
searchSetTab,
|
||||
setDialogActive,
|
||||
mapsLoadMore,
|
||||
dropRoute,
|
||||
modifyRoute,
|
||||
}, dispatch);
|
||||
|
||||
export const MapListDialog = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||
export { MapListDialog };
|
||||
|
|
46
src/components/dialogs/NominatimDialog.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React, { FC, Fragment, useCallback } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { IState } from '~/redux/store';
|
||||
import { selectEditorNominatim } from '~/redux/editor/selectors';
|
||||
import { DialogLoader } from './DialogLoader';
|
||||
import { NominatimListItem } from '~/components/nominatim/NominatimListItem';
|
||||
import { MainMap } from '~/constants/map';
|
||||
import { Scroll } from '../Scroll';
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
nominatim: selectEditorNominatim(state),
|
||||
});
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & {};
|
||||
|
||||
const NominatimDialogUnconnected: FC<Props> = ({ nominatim: { loading, list } }) => {
|
||||
const onItemClick = useCallback(
|
||||
(index: number) => {
|
||||
if (!list[index]) return;
|
||||
|
||||
MainMap.setView(list[index].latlng, 17);
|
||||
},
|
||||
[MainMap, list]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Scroll>
|
||||
<div className="dialog-flex-scroll">
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
<div className="dialog-content nominatim-dialog-content">
|
||||
{loading && <DialogLoader />}
|
||||
{list.map((item, i) => (
|
||||
<NominatimListItem item={item} key={item.id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const NominatimDialog = connect(mapStateToProps)(NominatimDialogUnconnected);
|
||||
|
||||
export { NominatimDialog };
|
37
src/components/dialogs/NominatimSearchPanel.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Icon } from '../panels/Icon';
|
||||
|
||||
interface IProps {
|
||||
active: boolean;
|
||||
onSearch: (search: string) => void;
|
||||
}
|
||||
|
||||
const NominatimSearchPanel: FC<IProps> = ({ active, onSearch }) => {
|
||||
const [search, setSearch] = useState('Колывань');
|
||||
|
||||
const setValue = useCallback(({ target: { value } }) => setSearch(value), [setSearch]);
|
||||
|
||||
const onSubmit = useCallback(event => {
|
||||
event.preventDefault();
|
||||
|
||||
if (search.length < 3) return;
|
||||
|
||||
onSearch(search);
|
||||
}, [search, onSearch]);
|
||||
|
||||
return (
|
||||
<form className="panel nominatim-panel active" onSubmit={onSubmit}>
|
||||
<div className="control-bar">
|
||||
<div className="nominatim-search-input">
|
||||
<input type="text" placeholder="Поиск на карте" value={search} onChange={setValue} />
|
||||
</div>
|
||||
|
||||
<button>
|
||||
<Icon icon="icon-search" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export { NominatimSearchPanel };
|
111
src/components/dialogs/PolylineDialog.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import { connect } from 'react-redux';
|
||||
import * as MAP_ACTIONS from '~/redux/map/actions';
|
||||
import { IState } from '~/redux/store';
|
||||
import { selectMapRoute } from '~/redux/map/selectors';
|
||||
import classNames from 'classnames';
|
||||
import { selectEditorDirection } from '~/redux/editor/selectors';
|
||||
import * as EDITOR_ACTIONS from '~/redux/editor/actions';
|
||||
import { DRAWING_DIRECTIONS } from '~/redux/editor/constants';
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
route: selectMapRoute(state),
|
||||
direction: selectEditorDirection(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
mapSetRoute: MAP_ACTIONS.mapSetRoute,
|
||||
editorSetDirection: EDITOR_ACTIONS.editorSetDirection,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const PolylineDialogUnconnected: FC<Props> = ({
|
||||
route,
|
||||
direction,
|
||||
editorSetDirection,
|
||||
mapSetRoute,
|
||||
}) => {
|
||||
const reverseRoute = useCallback(() => {
|
||||
if (route.length < 2) return;
|
||||
mapSetRoute([...route].reverse());
|
||||
}, [mapSetRoute, route]);
|
||||
|
||||
const curRouteStart = useCallback(() => {
|
||||
if (route.length < 1) return;
|
||||
|
||||
mapSetRoute(route.slice(1, route.length));
|
||||
}, [mapSetRoute, route]);
|
||||
|
||||
const curRouteEnd = useCallback(() => {
|
||||
if (route.length < 1) return;
|
||||
|
||||
mapSetRoute(route.slice(0, route.length - 1));
|
||||
}, [mapSetRoute, route]);
|
||||
|
||||
const continueBackward = useCallback(() => {
|
||||
editorSetDirection(DRAWING_DIRECTIONS.BACKWARDS);
|
||||
}, [editorSetDirection]);
|
||||
|
||||
const continueForward = useCallback(() => {
|
||||
editorSetDirection(DRAWING_DIRECTIONS.FORWARDS);
|
||||
}, [editorSetDirection]);
|
||||
|
||||
return (
|
||||
<div className="control-dialog control-dialog__medium">
|
||||
<div className="helper">
|
||||
<div className="helper__text">
|
||||
<button
|
||||
className={classNames('helper__icon_button', { inactive: route.length < 2 })}
|
||||
onClick={reverseRoute}
|
||||
>
|
||||
<Icon icon="icon-reverse" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={classNames('helper__icon_button', { inactive: route.length < 1 })}
|
||||
onClick={curRouteStart}
|
||||
>
|
||||
<Icon icon="icon-drop-start" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={classNames('helper__icon_button', { inactive: route.length < 1 })}
|
||||
onClick={curRouteEnd}
|
||||
>
|
||||
<Icon icon="icon-drop-end" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={classNames('helper__icon_button', {
|
||||
inactive: route.length < 2,
|
||||
active: direction === DRAWING_DIRECTIONS.BACKWARDS,
|
||||
})}
|
||||
onClick={continueBackward}
|
||||
>
|
||||
<Icon icon="icon-draw-backward" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={classNames('helper__icon_button', {
|
||||
inactive: route.length < 2,
|
||||
active: direction === DRAWING_DIRECTIONS.FORWARDS,
|
||||
})}
|
||||
onClick={continueForward}
|
||||
>
|
||||
<Icon icon="icon-draw-forward" />
|
||||
</button>
|
||||
|
||||
<div className="flex_1" />
|
||||
|
||||
<div className="big white upper">Ручной режим</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PolylineDialog = connect(mapStateToProps, mapDispatchToProps)(PolylineDialogUnconnected);
|
||||
|
||||
export { PolylineDialog };
|
|
@ -1,15 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { PROVIDERS, replaceProviderUrl } from '$constants/providers';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import React from 'react';
|
||||
import { PROVIDERS, replaceProviderUrl } from '~/constants/providers';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import classnames from 'classnames';
|
||||
import { changeProvider as changeProviderAction } from "$redux/user/actions";
|
||||
import { IRootState } from "$redux/user/reducer";
|
||||
import * as MAP_ACTIONS from "~/redux/map/actions";
|
||||
import { selectMapProvider } from '~/redux/map/selectors';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props extends IRootState {
|
||||
changeProvider: typeof changeProviderAction,
|
||||
}
|
||||
const mapStateToProps = state => ({
|
||||
provider: selectMapProvider(state),
|
||||
});
|
||||
|
||||
export const ProviderDialog = ({ provider, changeProvider }: Props) => (
|
||||
const mapDispatchToProps = {
|
||||
mapSetProvider: MAP_ACTIONS.mapSetProvider,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const ProviderDialogUnconnected = ({ provider, mapSetProvider }: Props) => (
|
||||
<div className="control-dialog top right control-dialog-provider">
|
||||
<div className="helper provider-helper">
|
||||
{
|
||||
|
@ -19,8 +26,8 @@ export const ProviderDialog = ({ provider, changeProvider }: Props) => (
|
|||
style={{
|
||||
backgroundImage: `url(${replaceProviderUrl(item, { x: 5980, y: 2589, zoom: 13 })})`,
|
||||
}}
|
||||
onMouseDown={() => changeProvider(item)}
|
||||
key={PROVIDERS[item].name}
|
||||
onMouseDown={() => mapSetProvider(item)}
|
||||
key={PROVIDERS[item]?.name}
|
||||
>
|
||||
{
|
||||
provider === item &&
|
||||
|
@ -34,3 +41,7 @@ export const ProviderDialog = ({ provider, changeProvider }: Props) => (
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ProviderDialog = connect(mapStateToProps, mapDispatchToProps)(ProviderDialogUnconnected)
|
||||
|
||||
export { ProviderDialog }
|
||||
|
|
|
@ -1,34 +1,30 @@
|
|||
import * as React from 'react';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import {
|
||||
routerCancel as routerCancelAction,
|
||||
routerSubmit as routerSubmitAction,
|
||||
} from "$redux/user/actions";
|
||||
import classnames from "classnames";
|
||||
import React, { FC } from 'react';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import * as EDITOR_ACTIONS from '~/redux/editor/actions';
|
||||
import classnames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectEditor, selectEditorRouter, selectEditorDirection } from '~/redux/editor/selectors';
|
||||
import pick from 'ramda/es/pick';
|
||||
import { IState } from '~/redux/store';
|
||||
|
||||
type Props = {
|
||||
routerPoints: number,
|
||||
width: number,
|
||||
is_routing: boolean,
|
||||
|
||||
routerCancel: typeof routerCancelAction,
|
||||
routerSubmit: typeof routerSubmitAction,
|
||||
}
|
||||
|
||||
const noPoints = ({ routerCancel }: { routerCancel: typeof routerCancelAction }) => (
|
||||
const noPoints = ({
|
||||
editorRouterCancel,
|
||||
}: {
|
||||
editorRouterCancel: typeof EDITOR_ACTIONS.editorRouterCancel;
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
<div className="helper router-helper">
|
||||
<div className="helper__text">
|
||||
<Icon icon="icon-pin-1" />
|
||||
<div className="big white upper">
|
||||
Укажите первую точку на карте
|
||||
</div>
|
||||
<div className="big white upper">Укажите первую точку на карте</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="helper router-helper">
|
||||
<div className="helper__buttons flex_1">
|
||||
<div className="flex_1" />
|
||||
<div className="button router-helper__button" onClick={routerCancel}>
|
||||
<div className="flex_1" />
|
||||
|
||||
<div className="helper__buttons">
|
||||
<div className="button router-helper__button" onClick={editorRouterCancel}>
|
||||
Отмена
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36,7 +32,11 @@ const noPoints = ({ routerCancel }: { routerCancel: typeof routerCancelAction })
|
|||
</React.Fragment>
|
||||
);
|
||||
|
||||
const firstPoint = ({ routerCancel }: { routerCancel: typeof routerCancelAction }) => (
|
||||
const firstPoint = ({
|
||||
editorRouterCancel,
|
||||
}: {
|
||||
editorRouterCancel: typeof EDITOR_ACTIONS.editorRouterCancel;
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
<div className="helper router-helper">
|
||||
<div className="helper__text">
|
||||
|
@ -45,9 +45,10 @@ const firstPoint = ({ routerCancel }: { routerCancel: typeof routerCancelAction
|
|||
</div>
|
||||
</div>
|
||||
<div className="helper router-helper">
|
||||
<div className="helper__buttons flex_1">
|
||||
<div className="flex_1" />
|
||||
<div className="button router-helper__button" onClick={routerCancel}>
|
||||
<div className="flex_1" />
|
||||
|
||||
<div className="helper__buttons">
|
||||
<div className="button router-helper__button" onClick={editorRouterCancel}>
|
||||
Отмена
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,10 +57,11 @@ const firstPoint = ({ routerCancel }: { routerCancel: typeof routerCancelAction
|
|||
);
|
||||
|
||||
const draggablePoints = ({
|
||||
routerCancel, routerSubmit
|
||||
editorRouterCancel,
|
||||
editorRouterSubmit,
|
||||
}: {
|
||||
routerCancel: typeof routerCancelAction,
|
||||
routerSubmit: typeof routerSubmitAction,
|
||||
editorRouterCancel: typeof EDITOR_ACTIONS.editorRouterCancel;
|
||||
editorRouterSubmit: typeof EDITOR_ACTIONS.editorRouterSubmit;
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
<div className="helper">
|
||||
|
@ -69,12 +71,13 @@ const draggablePoints = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="helper router-helper">
|
||||
<div className="helper__buttons button-group flex_1">
|
||||
<div className="flex_1" />
|
||||
<div className="button button_red router-helper__button" onClick={routerCancel}>
|
||||
<div className="flex_1" />
|
||||
|
||||
<div className="helper__buttons button-group">
|
||||
<div className="button button_red router-helper__button" onClick={editorRouterCancel}>
|
||||
Отмена
|
||||
</div>
|
||||
<div className="button primary router-helper__button" onClick={routerSubmit}>
|
||||
<div className="button primary router-helper__button" onClick={editorRouterSubmit}>
|
||||
Применить
|
||||
</div>
|
||||
</div>
|
||||
|
@ -82,14 +85,31 @@ const draggablePoints = ({
|
|||
</React.Fragment>
|
||||
);
|
||||
|
||||
export const RouterDialog = ({
|
||||
routerPoints, routerCancel, routerSubmit, width, is_routing,
|
||||
}: Props) => (
|
||||
<div className="control-dialog" style={{ width }}>
|
||||
<div className={classnames('save-loader', { active: is_routing })} />
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
router: selectEditorRouter(state),
|
||||
});
|
||||
|
||||
{!routerPoints && noPoints({ routerCancel })}
|
||||
{routerPoints === 1 && firstPoint({ routerCancel })}
|
||||
{routerPoints >= 2 && draggablePoints({ routerCancel, routerSubmit })}
|
||||
const mapDispatchToProps = {
|
||||
editorRouterCancel: EDITOR_ACTIONS.editorRouterCancel,
|
||||
editorRouterSubmit: EDITOR_ACTIONS.editorRouterSubmit,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const RouterDialogUnconnected: FC<Props> = ({
|
||||
router: { waypoints },
|
||||
editorRouterCancel,
|
||||
editorRouterSubmit,
|
||||
}) => (
|
||||
<div className="control-dialog control-dialog__medium">
|
||||
<div className={classnames('save-loader')} />
|
||||
|
||||
{!waypoints.length && noPoints({ editorRouterCancel })}
|
||||
{waypoints.length === 1 && firstPoint({ editorRouterCancel })}
|
||||
{waypoints.length >= 2 && draggablePoints({ editorRouterCancel, editorRouterSubmit })}
|
||||
</div>
|
||||
);
|
||||
|
||||
const RouterDialog = connect(mapStateToProps, mapDispatchToProps)(RouterDialogUnconnected);
|
||||
|
||||
export { RouterDialog };
|
||||
|
|
|
@ -1,40 +1,46 @@
|
|||
import * as React from 'react';
|
||||
import { copyToClipboard, getUrlData } from '$utils/history';
|
||||
import { toTranslit } from '$utils/format';
|
||||
import { TIPS } from '$constants/tips';
|
||||
import { MODES } from '$constants/modes';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { Switch } from '$components/Switch';
|
||||
import React from 'react';
|
||||
import { copyToClipboard, getUrlData } from '~/utils/history';
|
||||
import { toTranslit, parseDesc } from '~/utils/format';
|
||||
import { TIPS } from '~/constants/tips';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import { Switch } from '~/components/Switch';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { IRootState } from "$redux/user/reducer";
|
||||
import { sendSaveRequest, setMode } from "$redux/user/actions";
|
||||
import { connect } from 'react-redux';
|
||||
import { selectMap } from '~/redux/map/selectors';
|
||||
import * as EDITOR_ACTIONS from '~/redux/editor/actions';
|
||||
import { selectEditorSave } from '~/redux/editor/selectors';
|
||||
import { MODES } from '~/constants/modes';
|
||||
|
||||
interface Props extends IRootState {
|
||||
width: number,
|
||||
setMode: typeof setMode,
|
||||
sendSaveRequest: typeof sendSaveRequest,
|
||||
save_error: string,
|
||||
const mapStateToProps = state => ({
|
||||
map: selectMap(state),
|
||||
save: selectEditorSave(state),
|
||||
});
|
||||
|
||||
save_loading: boolean,
|
||||
save_finished: boolean,
|
||||
save_overwriting: boolean,
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
editorCancelSave: EDITOR_ACTIONS.editorCancelSave,
|
||||
editorChangeMode: EDITOR_ACTIONS.editorChangeMode,
|
||||
editorSendSaveRequest: EDITOR_ACTIONS.editorSendSaveRequest,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & { };
|
||||
|
||||
interface State {
|
||||
address: string,
|
||||
title: string,
|
||||
is_public: boolean,
|
||||
address: string;
|
||||
title: string;
|
||||
is_public: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class SaveDialog extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
class SaveDialogUnconnected extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
address: props.address || '',
|
||||
title: props.title || '',
|
||||
is_public: props.is_public || false,
|
||||
address: props.map.address || '',
|
||||
title: props.map.title || '',
|
||||
is_public: props.map.is_public || false,
|
||||
description: props.map.description || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -42,25 +48,38 @@ export class SaveDialog extends React.Component<Props, State> {
|
|||
const { path } = getUrlData();
|
||||
const { title, address } = this.state;
|
||||
|
||||
return toTranslit(address.trim()) || toTranslit(title.trim().toLowerCase()).substr(0, 32) || toTranslit(path.trim()).substr(0, 32);
|
||||
return (
|
||||
toTranslit(address.trim()) ||
|
||||
toTranslit(title.trim().toLowerCase()).substr(0, 32) ||
|
||||
toTranslit(path.trim()).substr(0, 32)
|
||||
);
|
||||
};
|
||||
|
||||
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)) || '' });
|
||||
|
||||
cancelSaving = () => this.props.setMode(MODES.NONE);
|
||||
setDescription = ({ target: { value } }) =>
|
||||
this.setState({ description: (value && value.substr(0, 256)) || '' });
|
||||
|
||||
sendSaveRequest = (e, force = false) => {
|
||||
const { title, is_public } = this.state;
|
||||
editorSendSaveRequest = (e, force = false) => {
|
||||
const { title, is_public, description } = this.state;
|
||||
const address = this.getAddress();
|
||||
|
||||
this.props.sendSaveRequest({
|
||||
title, address, force, is_public
|
||||
this.props.editorSendSaveRequest({
|
||||
title,
|
||||
address,
|
||||
force,
|
||||
is_public,
|
||||
description,
|
||||
});
|
||||
};
|
||||
|
||||
forceSaveRequest = e => this.sendSaveRequest(e, true);
|
||||
forceSaveRequest = e => this.editorSendSaveRequest(e, true);
|
||||
|
||||
cancelSaving = () => this.props.editorChangeMode(MODES.NONE);
|
||||
|
||||
onCopy = e => {
|
||||
e.preventDefault();
|
||||
|
@ -72,31 +91,45 @@ export class SaveDialog extends React.Component<Props, State> {
|
|||
this.setState({ is_public: !this.state.is_public });
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.props.editorCancelSave()
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, is_public } = this.state;
|
||||
const { save_error, save_finished, save_overwriting, width, save_loading } = this.props;
|
||||
const { title, is_public, description } = this.state;
|
||||
const {
|
||||
save: { error, finished, overwriting, loading },
|
||||
} = this.props;
|
||||
const { host, protocol } = getUrlData();
|
||||
|
||||
return (
|
||||
<div className="control-dialog control-dialog-medium" style={{ width }}>
|
||||
<div className="control-dialog control-dialog__medium">
|
||||
<div className="helper save-helper">
|
||||
<div className={classnames('save-loader', { active: save_loading })} />
|
||||
<div className={classnames('save-loader', { active: loading })} />
|
||||
|
||||
<div className="save-title">
|
||||
<div className="save-title-input">
|
||||
<div className="save-title-label">Название</div>
|
||||
<input type="text" value={title} onChange={this.setTitle} autoFocus readOnly={save_finished} />
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={this.setTitle}
|
||||
autoFocus
|
||||
readOnly={finished}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="save-description">
|
||||
<div className="save-address-input">
|
||||
<label className="save-address-label">{protocol}//{host}/</label>
|
||||
<label className="save-address-label">
|
||||
{protocol}//{host}/
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={this.getAddress()}
|
||||
onChange={this.setAddress}
|
||||
readOnly={save_finished}
|
||||
readOnly={finished}
|
||||
onCopy={this.onCopy}
|
||||
/>
|
||||
<div className="save-address-copy" onClick={this.onCopy}>
|
||||
|
@ -104,37 +137,49 @@ export class SaveDialog extends React.Component<Props, State> {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="save-text">
|
||||
{ save_error || TIPS.SAVE_INFO }
|
||||
<div className="save-textarea">
|
||||
<textarea
|
||||
placeholder="Описание маршрута"
|
||||
value={parseDesc(description)}
|
||||
onChange={this.setDescription}
|
||||
/>
|
||||
</div>
|
||||
<div className="save-text">{error || TIPS.SAVE_INFO}</div>
|
||||
|
||||
<div className="save-buttons">
|
||||
<div className={classnames('save-buttons-text pointer', { gray: !is_public })} onClick={this.togglePublic}>
|
||||
<div
|
||||
className={classnames('save-buttons-text pointer', { gray: !is_public })}
|
||||
onClick={this.togglePublic}
|
||||
>
|
||||
<Switch active={is_public} />
|
||||
{
|
||||
is_public
|
||||
? ' В каталоге карт'
|
||||
: ' Только по ссылке'
|
||||
}
|
||||
{is_public ? ' В каталоге карт' : ' Только по ссылке'}
|
||||
</div>
|
||||
<div>
|
||||
{ !save_finished &&
|
||||
<div className="button" onClick={this.cancelSaving}>Отмена</div>
|
||||
}
|
||||
{
|
||||
!save_finished && !save_overwriting &&
|
||||
<div className="button primary" onClick={this.sendSaveRequest}>Сохранить</div>
|
||||
}
|
||||
{
|
||||
save_overwriting &&
|
||||
<div className="button danger" onClick={this.forceSaveRequest}>Перезаписать</div>
|
||||
}
|
||||
{ save_finished &&
|
||||
<div className="button" onClick={this.onCopy}>Скопировать</div>
|
||||
}
|
||||
{ save_finished &&
|
||||
<div className="button success" onClick={this.cancelSaving}>Отлично!</div>
|
||||
}
|
||||
{!finished && (
|
||||
<div className="button" onClick={this.cancelSaving}>
|
||||
Отмена
|
||||
</div>
|
||||
)}
|
||||
{!finished && !overwriting && (
|
||||
<div className="button primary" onClick={this.editorSendSaveRequest}>
|
||||
Сохранить
|
||||
</div>
|
||||
)}
|
||||
{overwriting && (
|
||||
<div className="button danger" onClick={this.forceSaveRequest}>
|
||||
Перезаписать
|
||||
</div>
|
||||
)}
|
||||
{finished && (
|
||||
<div className="button" onClick={this.onCopy}>
|
||||
Скопировать
|
||||
</div>
|
||||
)}
|
||||
{finished && (
|
||||
<div className="button success" onClick={this.cancelSaving}>
|
||||
Отлично!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -143,3 +188,7 @@ export class SaveDialog extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SaveDialog = connect(mapStateToProps, mapDispatchToProps)(SaveDialogUnconnected);
|
||||
|
||||
export { SaveDialog };
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectEditorRenderer } from '~/redux/editor/selectors';
|
||||
|
||||
interface Props {
|
||||
renderer: {
|
||||
info: string,
|
||||
progress: number,
|
||||
}
|
||||
}
|
||||
const mapStateToProps = state => ({
|
||||
renderer: selectEditorRenderer(state),
|
||||
});
|
||||
|
||||
export const ShotPrefetchDialog = ({ renderer: { info, progress }}: Props) => (
|
||||
type Props = ReturnType<typeof mapStateToProps> & {};
|
||||
|
||||
const ShotPrefetchDialogUnconnected = ({ renderer: { info, progress }}: Props) => (
|
||||
<div className="control-dialog control-dialog-small left">
|
||||
<div className="helper helper-prefetch">
|
||||
<div className="dialog-prefetch-stage">{info}</div>
|
||||
|
@ -19,3 +20,7 @@ export const ShotPrefetchDialog = ({ renderer: { info, progress }}: Props) => (
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ShotPrefetchDialog = connect(mapStateToProps)(ShotPrefetchDialogUnconnected);
|
||||
|
||||
export { ShotPrefetchDialog }
|
|
@ -1,39 +1,44 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { STICKERS } from '$constants/stickers';
|
||||
import { setActiveSticker as setActiveStickerAction } from "$redux/user/actions";
|
||||
import { STICKERS } from '~/constants/stickers';
|
||||
import * as EDITOR_ACTIONS from '~/redux/editor/actions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
setActiveSticker: typeof setActiveStickerAction,
|
||||
width: number,
|
||||
}
|
||||
const mapStateToProps = () => ({});
|
||||
const mapDispatchToProps = {
|
||||
editorSetActiveSticker: EDITOR_ACTIONS.editorSetActiveSticker,
|
||||
};
|
||||
|
||||
export const StickersDialog = ({ setActiveSticker, width }: Props) => (
|
||||
<div className="control-dialog control-dialog-big" style={{ width }}>
|
||||
type Props = ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps & {
|
||||
width: number;
|
||||
};
|
||||
|
||||
const StickersDialogUnconnected = ({ editorSetActiveSticker, width }: Props) => (
|
||||
<div className="control-dialog control-dialog__medium" style={{ width }}>
|
||||
<div className="helper stickers-helper">
|
||||
{
|
||||
Object.keys(STICKERS).map(set => (
|
||||
<div key={set}>
|
||||
<div className="stickers-set-title">{STICKERS[set].title || null}</div>
|
||||
<div className="stickers-grid">
|
||||
{
|
||||
Object.keys(STICKERS[set].layers).map(sticker => (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(${STICKERS[set].url})`,
|
||||
backgroundPosition: `${-STICKERS[set].layers[sticker].off * 48}px 50%`,
|
||||
}}
|
||||
className="sticker-preview"
|
||||
key={`${set}-${sticker}`}
|
||||
onClick={() => setActiveSticker({ set, sticker })}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{Object.keys(STICKERS).map(set => (
|
||||
<div key={set}>
|
||||
<div className="stickers-set-title">{STICKERS[set].title || null}</div>
|
||||
<div className="stickers-grid">
|
||||
{Object.keys(STICKERS[set].layers).map(sticker => (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(${STICKERS[set].url})`,
|
||||
backgroundPosition: `${-STICKERS[set].layers[sticker].off * 48}px 50%`,
|
||||
}}
|
||||
className="sticker-preview"
|
||||
key={`${set}-${sticker}`}
|
||||
onClick={() => editorSetActiveSticker({ set, sticker })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const StickersDialog = connect(mapStateToProps, mapDispatchToProps)(StickersDialogUnconnected);
|
||||
|
||||
export { StickersDialog };
|
|
@ -1,50 +1,144 @@
|
|||
import * as React from 'react';
|
||||
import { bindActionCreators } from "redux";
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { getStyle } from '~/utils/dom';
|
||||
import { nearestInt } from '~/utils/geom';
|
||||
import { parseDesc } from '~/utils/format';
|
||||
import { selectMap } from '~/redux/map/selectors';
|
||||
import { selectEditor } from '~/redux/editor/selectors';
|
||||
|
||||
interface ITitleDialogProps {
|
||||
editing: boolean,
|
||||
title?: string,
|
||||
}
|
||||
const mapStateToProps = state => ({
|
||||
editor: selectEditor(state),
|
||||
map: selectMap(state),
|
||||
});
|
||||
|
||||
interface ITitleDialogState {
|
||||
type Props = ReturnType<typeof mapStateToProps> & {
|
||||
minLines?: number;
|
||||
maxLines?: number;
|
||||
};
|
||||
|
||||
interface State {
|
||||
raised: boolean;
|
||||
height: number;
|
||||
height_raised: number;
|
||||
}
|
||||
|
||||
export class Component extends React.PureComponent<ITitleDialogProps, ITitleDialogState> {
|
||||
export class TitleDialogUnconnected extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
raised: false,
|
||||
height: 0,
|
||||
height_raised: 0,
|
||||
};
|
||||
|
||||
onHover = () => this.setState({ raised: true });
|
||||
onLeave = () => this.setState({ raised: false });
|
||||
|
||||
componentDidMount() {
|
||||
this.setMaxHeight();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.setMaxHeight();
|
||||
}
|
||||
|
||||
setMaxHeight = () => {
|
||||
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 - height < 2 * text_line ? height_raised : height,
|
||||
height_raised,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { editing, title } = this.props;
|
||||
const {
|
||||
editor: { editing },
|
||||
map: { title, description },
|
||||
} = this.props;
|
||||
const { raised, height, height_raised } = this.state;
|
||||
|
||||
return (
|
||||
<div className="title-dialog-wrapper">
|
||||
<div className="title-dialog-sizer" ref={el => { this.sizer = el; }}>
|
||||
<div className={classnames('title-dialog', { active: title && !editing })}>
|
||||
<div className="title-dialog-pane title-dialog-name">
|
||||
<div
|
||||
className="title-dialog-sizer"
|
||||
ref={el => {
|
||||
this.ref_sizer = el;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
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>
|
||||
</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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
text;
|
||||
sizer;
|
||||
ref_sizer;
|
||||
ref_title;
|
||||
ref_text;
|
||||
ref_overflow;
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ user: { editing, title } }) => ({ editing, title });
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ }, dispatch);
|
||||
const TitleDialog = connect(mapStateToProps)(TitleDialogUnconnected);
|
||||
|
||||
export const TitleDialog = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||
export { TitleDialog };
|
||||
|
|
|
@ -1,43 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import React, { FC } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as EDITOR_ACTIONS from '~/redux/editor/actions';
|
||||
|
||||
type Props = {
|
||||
clearPoly: () => void,
|
||||
clearStickers: () => void,
|
||||
clearAll: () => void,
|
||||
clearCancel: () => void,
|
||||
const mapStateToProps = () => ({});
|
||||
const mapDispatchToProps = {
|
||||
editorClearPoly: EDITOR_ACTIONS.editorClearPoly,
|
||||
editorClearStickers: EDITOR_ACTIONS.editorClearStickers,
|
||||
editorClearAll: EDITOR_ACTIONS.editorClearAll,
|
||||
editorClearCancel: EDITOR_ACTIONS.editorClearCancel,
|
||||
};
|
||||
|
||||
width: number,
|
||||
}
|
||||
type Props = ReturnType<typeof mapStateToProps> &
|
||||
typeof mapDispatchToProps & {
|
||||
width: number;
|
||||
};
|
||||
|
||||
export const TrashDialog = ({
|
||||
clearPoly, clearStickers, clearAll, clearCancel, width,
|
||||
}: Props) => (
|
||||
<div className="control-dialog" style={{ width }}>
|
||||
<div className="helper trash-helper">
|
||||
const TrashDialogUnconnected: FC<Props> = ({
|
||||
editorClearPoly,
|
||||
editorClearStickers,
|
||||
editorClearAll,
|
||||
editorClearCancel,
|
||||
width,
|
||||
}) => (
|
||||
<div className="control-dialog control-dialog__medium" style={{ width }}>
|
||||
<div className="helper trash-helper desktop-only">
|
||||
<div className="helper__text danger">
|
||||
<Icon icon="icon-trash-4" />
|
||||
<div className="big upper desktop-only">Удалить:</div>
|
||||
<div className="big upper desktop-only">Все изменения будут удалены!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="helper trash-helper">
|
||||
<div className="helper__buttons flex_1 trash-buttons">
|
||||
<div className="button-group">
|
||||
<div className="button router-helper__button" onClick={clearPoly}>
|
||||
<div className="button router-helper__button" onClick={editorClearPoly}>
|
||||
Маршрут
|
||||
</div>
|
||||
<div className="button router-helper__button" onClick={clearStickers}>
|
||||
|
||||
<div className="button router-helper__button" onClick={editorClearStickers}>
|
||||
Стикеры
|
||||
</div>
|
||||
<div className="button router-helper__button" onClick={clearAll}>
|
||||
|
||||
<div className="button router-helper__button" onClick={editorClearAll}>
|
||||
ВСЕ
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex_1" />
|
||||
<div className="button primary router-helper__button" onClick={clearCancel}>
|
||||
|
||||
<div className="button primary router-helper__button" onClick={editorClearCancel}>
|
||||
Отмена
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TrashDialog = connect(mapStateToProps, mapDispatchToProps)(TrashDialogUnconnected);
|
||||
|
||||
export { TrashDialog };
|
||||
|
|
21
src/components/gpx/GpxConfirm.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const GpxConfirm: FC<IProps> = ({}) => {
|
||||
return (
|
||||
<div className="gpx-confirm">
|
||||
<div className="gpx-confirm__text">Маршрут уже нанесен. Что делаем?</div>
|
||||
|
||||
<div className="gpx-confirm__buttons">
|
||||
<div className="button success">Соединить</div>
|
||||
|
||||
<div className="button danger">Переписать</div>
|
||||
|
||||
<div className="button primary">Отмена</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { GpxConfirm };
|
60
src/components/gpx/GpxDialogRow.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import React, { FC, memo } from 'react';
|
||||
import { IGpxRoute } from '~/redux/editor';
|
||||
import { Switch } from '../Switch';
|
||||
import { Icon } from '../panels/Icon';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface IProps {
|
||||
item: IGpxRoute;
|
||||
index: number;
|
||||
enabled: boolean;
|
||||
|
||||
onFocusRoute: (i: number) => void;
|
||||
onRouteDrop: (i: number) => void;
|
||||
onRouteToggle: (i: number) => void;
|
||||
onRouteColor: (i: number) => void;
|
||||
onRouteReplace: (i: number) => void;
|
||||
}
|
||||
|
||||
const GpxDialogRow: FC<IProps> = memo(
|
||||
({
|
||||
item,
|
||||
index,
|
||||
enabled,
|
||||
onRouteToggle,
|
||||
onFocusRoute,
|
||||
onRouteDrop,
|
||||
onRouteColor,
|
||||
onRouteReplace,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classnames('gpx-row', { 'gpx-row_disabled': !enabled || !item.enabled })}>
|
||||
<div
|
||||
className="gpx-row__color"
|
||||
style={{ backgroundColor: item.color }}
|
||||
onClick={() => onRouteColor(index)}
|
||||
/>
|
||||
|
||||
<div className="gpx-row__title" onClick={() => onFocusRoute(index)}>
|
||||
{item.name}
|
||||
</div>
|
||||
|
||||
<div className="gpx-row__buttons">
|
||||
<div onClick={() => onRouteReplace(index)}>
|
||||
<Icon icon="icon-to-poly" size={24} />
|
||||
</div>
|
||||
|
||||
<div onClick={() => onRouteDrop(index)}>
|
||||
<Icon icon="icon-trash-6" size={24} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Switch active={item.enabled} onPress={() => onRouteToggle(index)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { GpxDialogRow };
|
|
@ -1,21 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import { LOGOS } from '$constants/logos';
|
||||
import React from 'react';
|
||||
import { LOGOS } from '~/constants/logos';
|
||||
import { connect } from 'react-redux';
|
||||
import { IRootState } from "$redux/user/reducer";
|
||||
import { IRootState } from '~/redux/user';
|
||||
import { selectMapLogo } from '~/redux/map/selectors';
|
||||
|
||||
interface Props extends IRootState {}
|
||||
const mapStateToProps = state => ({ logo: selectMapLogo(state) });
|
||||
type Props = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
const Component = ({ logo }: Props) => (
|
||||
const LogoPreviewUnconnected = React.memo(({ logo }: Props) => (
|
||||
<div
|
||||
className="logo-preview"
|
||||
style={{
|
||||
backgroundImage: logo
|
||||
? `url(${LOGOS[logo][1]})`
|
||||
: 'none'
|
||||
backgroundImage: logo ? `url(${LOGOS && LOGOS[logo] && LOGOS[logo][1]})` : 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
));
|
||||
|
||||
const mapStateToProps = ({ user: { logo } }) => ({ logo });
|
||||
const LogoPreview = connect(mapStateToProps)(LogoPreviewUnconnected);
|
||||
|
||||
export const LogoPreview = connect(mapStateToProps)(Component);
|
||||
export { LogoPreview };
|
||||
|
|
|
@ -1,29 +1,25 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { MapListDialog } from "$components/dialogs/MapListDialog";
|
||||
import { Tooltip } from "$components/panels/Tooltip";
|
||||
import { ReactElement } from "react";
|
||||
import React, { FC, memo } from 'react';
|
||||
import { MapListDialog } from '~/components/dialogs/MapListDialog';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface Props {
|
||||
_id: string,
|
||||
stopEditing: typeof MapListDialog.stopEditing,
|
||||
dropRoute: typeof MapListDialog.dropRoute,
|
||||
address: string;
|
||||
stopEditing: typeof MapListDialog.stopEditing;
|
||||
dropRoute: typeof MapListDialog.dropRoute;
|
||||
}
|
||||
|
||||
export const RouteRowDrop = ({
|
||||
_id, stopEditing, dropRoute,
|
||||
}: Props): ReactElement<Props, null> => (
|
||||
<div
|
||||
className="route-row-drop"
|
||||
>
|
||||
<div
|
||||
className="route-row"
|
||||
>
|
||||
export const RouteRowDrop: FC<Props> = memo(({ address, stopEditing, dropRoute }) => (
|
||||
<div className="route-row-drop">
|
||||
<div className="route-row">
|
||||
<div className="button-group">
|
||||
<div className="button" onClick={dropRoute.bind(null, _id)}>Удалить</div>
|
||||
<div className="button primary" onClick={stopEditing}>Отмена</div>
|
||||
<div className="button" onClick={dropRoute.bind(null, address)}>
|
||||
Удалить
|
||||
</div>
|
||||
<div className="button primary" onClick={stopEditing}>
|
||||
Отмена
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
));
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { Switch } from '$components/Switch';
|
||||
import { MapListDialog } from "$components/dialogs/MapListDialog";
|
||||
import React from 'react';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import { Switch } from '~/components/Switch';
|
||||
import { MapListDialog } from "~/components/dialogs/MapListDialog";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
_id: string;
|
||||
address: string;
|
||||
is_public: boolean,
|
||||
modifyRoute: typeof MapListDialog.modifyRoute,
|
||||
}
|
||||
|
@ -29,10 +29,10 @@ export class RouteRowEditor extends React.Component<Props, State> {
|
|||
stopEditing = () => {
|
||||
const {
|
||||
state: { title, is_public },
|
||||
props: { _id }
|
||||
props: { address }
|
||||
} = this;
|
||||
|
||||
this.props.modifyRoute({ _id, title, is_public })
|
||||
this.props.modifyRoute({ address, title, is_public })
|
||||
};
|
||||
|
||||
setPublic = () => this.setState({ is_public: !this.state.is_public });
|
||||
|
|
|
@ -1,72 +1,100 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { MapListDialog } from "$components/dialogs/MapListDialog";
|
||||
import { Tooltip } from "$components/panels/Tooltip";
|
||||
import React from "react";
|
||||
import { Icon } from "~/components/panels/Icon";
|
||||
import { MapListDialog } from "~/components/dialogs/MapListDialog";
|
||||
import { Tooltip } from "~/components/panels/Tooltip";
|
||||
import { ReactElement } from "react";
|
||||
import classnames from 'classnames';
|
||||
import classnames from "classnames";
|
||||
import { toggleRouteStarred } from "~/redux/user/actions";
|
||||
import { TABS } from "~/constants/dialogs";
|
||||
|
||||
interface Props {
|
||||
_id: string,
|
||||
tab: string,
|
||||
title: string,
|
||||
distance: number,
|
||||
is_public: boolean,
|
||||
tab: string;
|
||||
|
||||
openRoute: typeof MapListDialog.openRoute,
|
||||
startEditing: typeof MapListDialog.startEditing,
|
||||
stopEditing: typeof MapListDialog.stopEditing,
|
||||
showMenu: typeof MapListDialog.showMenu,
|
||||
hideMenu: typeof MapListDialog.hideMenu,
|
||||
showDropCard: typeof MapListDialog.showDropCard,
|
||||
address: string;
|
||||
title: string;
|
||||
distance: number;
|
||||
is_public: boolean;
|
||||
is_admin: boolean;
|
||||
is_published: boolean;
|
||||
|
||||
openRoute: typeof MapListDialog.openRoute;
|
||||
toggleStarred: typeof MapListDialog.toggleStarred;
|
||||
startEditing: typeof MapListDialog.startEditing;
|
||||
stopEditing: typeof MapListDialog.stopEditing;
|
||||
showMenu: typeof MapListDialog.showMenu;
|
||||
hideMenu: typeof MapListDialog.hideMenu;
|
||||
showDropCard: typeof MapListDialog.showDropCard;
|
||||
}
|
||||
|
||||
export const RouteRowView = ({
|
||||
title, distance, _id, openRoute, tab, startEditing, showMenu, showDropCard, hideMenu,
|
||||
}: Props): ReactElement<Props, null> => (
|
||||
<div
|
||||
className={classnames('route-row-view', { has_menu: (tab === 'mine') })}
|
||||
>
|
||||
<div
|
||||
className="route-row"
|
||||
onClick={() => openRoute(_id)}
|
||||
>
|
||||
title,
|
||||
distance,
|
||||
address,
|
||||
openRoute,
|
||||
tab,
|
||||
startEditing,
|
||||
showMenu,
|
||||
showDropCard,
|
||||
hideMenu,
|
||||
is_admin,
|
||||
is_published,
|
||||
toggleStarred
|
||||
}: Props): ReactElement<Props> => (
|
||||
<div className={classnames("route-row-view", { has_menu: tab === "my" })}>
|
||||
{(tab === TABS.PENDING || tab === TABS.STARRED) && is_admin && (
|
||||
<div className="route-row-fav" onClick={toggleStarred.bind(null, address)}>
|
||||
{is_published ? (
|
||||
<Icon icon="icon-star-fill" size={24} />
|
||||
) : (
|
||||
<Icon icon="icon-star-blank" size={24} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="route-row" onClick={() => openRoute(address)}>
|
||||
<div className="route-title">
|
||||
<span>{(title || _id)}</span>
|
||||
{(tab === "my" || !is_admin) && is_published && (
|
||||
<div className="route-row-corner">
|
||||
<Icon icon="icon-star-fill" size={18} />
|
||||
</div>
|
||||
)}
|
||||
<span>{title || address}</span>
|
||||
</div>
|
||||
|
||||
<div className="route-description">
|
||||
<span>
|
||||
<Icon icon="icon-link-1" />
|
||||
{_id}
|
||||
{address}
|
||||
</span>
|
||||
<span>
|
||||
<Icon icon="icon-cycle-1" />
|
||||
{(distance && `${distance} km`) || '0 km'}
|
||||
{(distance && `${distance} km`) || "0 km"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tab === 'mine' &&
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="route-row-edit-button pointer"
|
||||
onMouseOver={showMenu.bind(null, _id)}
|
||||
onMouseOut={hideMenu}
|
||||
>
|
||||
<Icon icon="icon-more-vert" />
|
||||
<div className="route-row-edit-menu pointer">
|
||||
<div onMouseDown={showDropCard.bind(null, _id)}>
|
||||
<Tooltip>Удалить</Tooltip>
|
||||
<Icon icon="icon-trash-3" size={32} />
|
||||
</div>
|
||||
<div onMouseDown={startEditing.bind(null, _id)} className="modify-button">
|
||||
<Tooltip>Редактировать</Tooltip>
|
||||
<Icon icon="icon-edit-1" size={32} />
|
||||
</div>
|
||||
{tab === "my" && (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="route-row-edit-button pointer"
|
||||
onMouseOver={showMenu.bind(null, address)}
|
||||
onMouseOut={hideMenu}
|
||||
>
|
||||
<Icon icon="icon-more-vert" />
|
||||
<div className="route-row-edit-menu pointer">
|
||||
<div onMouseDown={showDropCard.bind(null, address)}>
|
||||
<Tooltip>Удалить</Tooltip>
|
||||
<Icon icon="icon-trash-3" size={32} />
|
||||
</div>
|
||||
<div
|
||||
onMouseDown={startEditing.bind(null, address)}
|
||||
className="modify-button"
|
||||
>
|
||||
<Tooltip>Редактировать</Tooltip>
|
||||
<Icon icon="icon-edit-1" size={32} />
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,76 +1,93 @@
|
|||
import * as React from 'react';
|
||||
import React, { FC, memo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { MapListDialog } from "$components/dialogs/MapListDialog";
|
||||
import { RouteRowView } from "$components/maps/RouteRowView";
|
||||
import { RouteRowEditor } from "$components/maps/RouteRowEditor";
|
||||
import { RouteRowDrop } from "$components/maps/RouteRowDrop";
|
||||
import { ReactElement } from "react";
|
||||
import { MapListDialog } from '~/components/dialogs/MapListDialog';
|
||||
import { RouteRowView } from '~/components/maps/RouteRowView';
|
||||
import { RouteRowEditor } from '~/components/maps/RouteRowEditor';
|
||||
import { RouteRowDrop } from '~/components/maps/RouteRowDrop';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface Props {
|
||||
_id: string,
|
||||
tab: string,
|
||||
title: string,
|
||||
distance: number,
|
||||
is_public: boolean,
|
||||
address: string;
|
||||
tab: string;
|
||||
title: string;
|
||||
distance: number;
|
||||
is_public: boolean;
|
||||
is_published: boolean;
|
||||
|
||||
is_editing_target: boolean,
|
||||
is_menu_target: boolean,
|
||||
is_admin: boolean;
|
||||
is_editing_target: boolean;
|
||||
is_menu_target: boolean;
|
||||
|
||||
openRoute: typeof MapListDialog.openRoute,
|
||||
startEditing: typeof MapListDialog.startEditing,
|
||||
stopEditing: typeof MapListDialog.stopEditing,
|
||||
showMenu: typeof MapListDialog.showMenu,
|
||||
hideMenu: typeof MapListDialog.hideMenu,
|
||||
showDropCard: typeof MapListDialog.showDropCard,
|
||||
dropRoute: typeof MapListDialog.dropRoute,
|
||||
modifyRoute: typeof MapListDialog.modifyRoute,
|
||||
openRoute: typeof MapListDialog.openRoute;
|
||||
startEditing: typeof MapListDialog.startEditing;
|
||||
stopEditing: typeof MapListDialog.stopEditing;
|
||||
showMenu: typeof MapListDialog.showMenu;
|
||||
hideMenu: typeof MapListDialog.hideMenu;
|
||||
showDropCard: typeof MapListDialog.showDropCard;
|
||||
dropRoute: typeof MapListDialog.dropRoute;
|
||||
modifyRoute: typeof MapListDialog.modifyRoute;
|
||||
toggleStarred: typeof MapListDialog.toggleStarred;
|
||||
|
||||
is_editing_mode: 'edit' | 'drop',
|
||||
is_editing_mode: 'edit' | 'drop';
|
||||
}
|
||||
|
||||
export const RouteRowWrapper = ({
|
||||
title, distance, _id, openRoute, tab, startEditing, showMenu,
|
||||
showDropCard, is_public, is_editing_target, is_menu_target, is_editing_mode,
|
||||
dropRoute, stopEditing, modifyRoute, hideMenu,
|
||||
}: Props): ReactElement<Props, null> => (
|
||||
<div
|
||||
className={classnames('route-row-wrapper', {
|
||||
is_menu_target,
|
||||
is_editing_target,
|
||||
})}
|
||||
>
|
||||
{
|
||||
is_editing_target && is_editing_mode === 'edit' &&
|
||||
export const RouteRowWrapper: FC<Props> = memo(
|
||||
({
|
||||
title,
|
||||
distance,
|
||||
address,
|
||||
openRoute,
|
||||
tab,
|
||||
startEditing,
|
||||
showMenu,
|
||||
showDropCard,
|
||||
is_public,
|
||||
is_editing_target,
|
||||
is_menu_target,
|
||||
is_editing_mode,
|
||||
dropRoute,
|
||||
stopEditing,
|
||||
modifyRoute,
|
||||
hideMenu,
|
||||
is_admin,
|
||||
is_published,
|
||||
toggleStarred,
|
||||
}) => (
|
||||
<div
|
||||
className={classnames('route-row-wrapper', {
|
||||
is_menu_target,
|
||||
is_editing_target,
|
||||
})}
|
||||
>
|
||||
{is_editing_target && is_editing_mode === 'edit' && (
|
||||
<RouteRowEditor
|
||||
title={title}
|
||||
_id={_id}
|
||||
address={address}
|
||||
is_public={is_public}
|
||||
modifyRoute={modifyRoute}
|
||||
/>
|
||||
}
|
||||
{
|
||||
is_editing_target && is_editing_mode === 'drop' &&
|
||||
<RouteRowDrop
|
||||
_id={_id}
|
||||
dropRoute={dropRoute}
|
||||
stopEditing={stopEditing}
|
||||
/>
|
||||
}
|
||||
{
|
||||
!is_editing_target &&
|
||||
)}
|
||||
{is_editing_target && is_editing_mode === 'drop' && (
|
||||
<RouteRowDrop address={address} dropRoute={dropRoute} stopEditing={stopEditing} />
|
||||
)}
|
||||
{!is_editing_target && (
|
||||
<RouteRowView
|
||||
_id={_id}
|
||||
address={address}
|
||||
tab={tab}
|
||||
title={title}
|
||||
distance={distance}
|
||||
is_public={is_public}
|
||||
is_published={is_published}
|
||||
openRoute={openRoute}
|
||||
startEditing={startEditing}
|
||||
stopEditing={stopEditing}
|
||||
showMenu={showMenu}
|
||||
hideMenu={hideMenu}
|
||||
showDropCard={showDropCard}
|
||||
is_admin={is_admin}
|
||||
toggleStarred={toggleStarred}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
|
21
src/components/nominatim/NominatimListItem.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
import { INominatimResult } from '~/redux/types';
|
||||
import { MainMap } from '~/constants/map';
|
||||
|
||||
interface IProps {
|
||||
item: INominatimResult;
|
||||
}
|
||||
|
||||
const NominatimListItem: FC<IProps> = ({ item }) => {
|
||||
const onClick = useCallback(() => {
|
||||
MainMap.panTo(item.latlng);
|
||||
}, [MainMap]);
|
||||
|
||||
return (
|
||||
<div onClick={onClick} className="nominatim-list-item">
|
||||
<div className="title">{item.title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { NominatimListItem };
|
|
@ -1,25 +1,27 @@
|
|||
// flow
|
||||
import * as React from 'react';
|
||||
import { toHours } from '$utils/format';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import React from 'react';
|
||||
import { toHours } from '~/utils/format';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import { connect } from 'react-redux';
|
||||
// import Slider from 'rc-slider';
|
||||
import * as Slider from 'rc-slider/lib/Slider';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { setSpeed } from '$redux/user/actions';
|
||||
import { IRootState } from "$redux/user/reducer";
|
||||
import { Tooltip } from "$components/panels/Tooltip";
|
||||
import { isMobile } from "$utils/window";
|
||||
import Slider from 'rc-slider/lib/Slider';
|
||||
import { editorSetSpeed } from '~/redux/editor/actions';
|
||||
import { Tooltip } from '~/components/panels/Tooltip';
|
||||
import { isMobile } from '~/utils/window';
|
||||
import { IState } from '~/redux/store';
|
||||
import pick from 'ramda/es/pick';
|
||||
import { selectEditor } from '~/redux/editor/selectors';
|
||||
|
||||
interface Props extends IRootState {
|
||||
setSpeed: typeof setSpeed,
|
||||
}
|
||||
const mapStateToProps = (state: IState) =>
|
||||
pick(['distance', 'estimated', 'speed'], selectEditor(state));
|
||||
|
||||
const mapDispatchToProps = { editorSetSpeed };
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
interface State {
|
||||
dialogOpened: boolean,
|
||||
dialogOpened: boolean;
|
||||
}
|
||||
|
||||
class Component extends React.PureComponent<Props, State> {
|
||||
class DistanceBarUnconnected extends React.PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -31,10 +33,15 @@ class Component extends React.PureComponent<Props, State> {
|
|||
min: number = 5;
|
||||
max: number = 30;
|
||||
|
||||
marks: { [x: number]: string } = [...Array((Math.floor(this.max - this.min) / this.step) + 1)].reduce((obj, el, index) => ({
|
||||
...obj,
|
||||
[this.min + (index * this.step)]: String(this.min + (index * this.step)),
|
||||
}), { });
|
||||
marks: { [x: number]: string } = [
|
||||
...Array(Math.floor(this.max - this.min) / this.step + 1),
|
||||
].reduce(
|
||||
(obj, el, index) => ({
|
||||
...obj,
|
||||
[this.min + index * this.step]: String(this.min + index * this.step),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
toggleDialog = () => {
|
||||
if (isMobile()) return;
|
||||
|
@ -46,10 +53,12 @@ class Component extends React.PureComponent<Props, State> {
|
|||
const {
|
||||
props: { distance, estimated, speed },
|
||||
state: { dialogOpened },
|
||||
min, max, step, marks,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
marks,
|
||||
} = this;
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="status-bar padded pointer tooltip-container" onClick={this.toggleDialog}>
|
||||
|
@ -60,40 +69,24 @@ class Component extends React.PureComponent<Props, State> {
|
|||
</span>
|
||||
<div className="desktop-only">{toHours(estimated)}</div>
|
||||
</div>
|
||||
{
|
||||
dialogOpened &&
|
||||
{dialogOpened && (
|
||||
<div className="control-dialog top left" style={{ left: 0, top: 42 }}>
|
||||
<div className="helper speed-helper">
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onChange={this.props.setSpeed}
|
||||
onChange={this.props.editorSetSpeed}
|
||||
defaultValue={15}
|
||||
value={speed}
|
||||
marks={marks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {
|
||||
user: { distance, estimated, speed },
|
||||
} = state;
|
||||
|
||||
return { distance, estimated, speed };
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||
setSpeed,
|
||||
}, dispatch);
|
||||
|
||||
export const DistanceBar = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Component);
|
||||
export const DistanceBar = connect(mapStateToProps, mapDispatchToProps)(DistanceBarUnconnected);
|
||||
|
|
|
@ -1,38 +1,26 @@
|
|||
import * as React from 'react';
|
||||
import { IModes, MODES } from '$constants/modes';
|
||||
import React, { createElement } from 'react';
|
||||
import { MODES } from '~/constants/modes';
|
||||
|
||||
import { RouterDialog } from '$components/dialogs/RouterDialog';
|
||||
import { StickersDialog } from '$components/dialogs/StickersDialog';
|
||||
import { TrashDialog } from '$components/dialogs/TrashDialog';
|
||||
import { LogoDialog } from '$components/dialogs/LogoDialog';
|
||||
import { SaveDialog } from '$components/dialogs/SaveDialog';
|
||||
import { CancelDialog } from '$components/dialogs/CancelDialog';
|
||||
import { RouterDialog } from '~/components/dialogs/RouterDialog';
|
||||
import { PolylineDialog } from '~/components/dialogs/PolylineDialog';
|
||||
import { StickersDialog } from '~/components/dialogs/StickersDialog';
|
||||
import { TrashDialog } from '~/components/dialogs/TrashDialog';
|
||||
import { LogoDialog } from '~/components/dialogs/LogoDialog';
|
||||
import { SaveDialog } from '~/components/dialogs/SaveDialog';
|
||||
import { CancelDialog } from '~/components/dialogs/CancelDialog';
|
||||
import { GpxDialog } from '~/components/dialogs/GpxDialog';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
setMode,
|
||||
setLogo,
|
||||
routerCancel,
|
||||
routerSubmit,
|
||||
setActiveSticker,
|
||||
clearStickers,
|
||||
clearPoly,
|
||||
clearAll,
|
||||
clearCancel,
|
||||
stopEditing,
|
||||
setEditing,
|
||||
sendSaveRequest,
|
||||
changeProvider,
|
||||
} from '$redux/user/actions';
|
||||
import { ProviderDialog } from '$components/dialogs/ProviderDialog';
|
||||
import { ShotPrefetchDialog } from '$components/dialogs/ShotPrefetchDialog';
|
||||
import { IRootState } from "$redux/user/reducer";
|
||||
import { ProviderDialog } from '~/components/dialogs/ProviderDialog';
|
||||
import { ShotPrefetchDialog } from '~/components/dialogs/ShotPrefetchDialog';
|
||||
import { selectEditorMode } from '~/redux/editor/selectors';
|
||||
|
||||
interface Props extends IRootState {
|
||||
width: number,
|
||||
}
|
||||
const mapStateToProps = state => ({ mode: selectEditorMode(state) });
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & {
|
||||
width: number;
|
||||
};
|
||||
|
||||
const DIALOG_CONTENTS: { [x: string]: any } = {
|
||||
[MODES.ROUTER]: RouterDialog,
|
||||
|
@ -43,34 +31,13 @@ const DIALOG_CONTENTS: { [x: string]: any } = {
|
|||
[MODES.CONFIRM_CANCEL]: CancelDialog,
|
||||
[MODES.PROVIDER]: ProviderDialog,
|
||||
[MODES.SHOT_PREFETCH]: ShotPrefetchDialog,
|
||||
[MODES.POLY]: PolylineDialog,
|
||||
[MODES.GPX]: GpxDialog,
|
||||
};
|
||||
|
||||
export const Component = (props: Props) => (
|
||||
props.mode && DIALOG_CONTENTS[props.mode]
|
||||
? React.createElement(DIALOG_CONTENTS[props.mode], { ...props })
|
||||
: null
|
||||
);
|
||||
const EditorDialogUnconnected = (props: Props) =>
|
||||
props.mode && DIALOG_CONTENTS[props.mode] ? createElement(DIALOG_CONTENTS[props.mode]) : null;
|
||||
|
||||
const mapStateToProps = ({ user }) => ({ ...user });
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||
routerCancel,
|
||||
routerSubmit,
|
||||
setLogo,
|
||||
setActiveSticker,
|
||||
clearStickers,
|
||||
clearPoly,
|
||||
clearAll,
|
||||
clearCancel,
|
||||
stopEditing,
|
||||
setEditing,
|
||||
setMode,
|
||||
sendSaveRequest,
|
||||
changeProvider,
|
||||
}, dispatch);
|
||||
|
||||
export const EditorDialog = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Component);
|
||||
const EditorDialog = connect(mapStateToProps)(EditorDialogUnconnected);
|
||||
|
||||
export { EditorDialog };
|
||||
|
|
|
@ -1,26 +1,57 @@
|
|||
import * as React from 'react';
|
||||
import { MODES } from '$constants/modes';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { MODES } from '~/constants/modes';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import { EditorDialog } from '$components/panels/EditorDialog';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
import { EditorDialog } from '~/components/panels/EditorDialog';
|
||||
import { connect } from 'react-redux';
|
||||
import { setMode, startEditing, stopEditing, setLogo, takeAShot, keyPressed } from '$redux/user/actions';
|
||||
import { IRootState } from "$redux/user/reducer";
|
||||
import { Tooltip } from "$components/panels/Tooltip";
|
||||
import {
|
||||
editorChangeMode,
|
||||
editorKeyPressed,
|
||||
editorRedo,
|
||||
editorStartEditing,
|
||||
editorStopEditing,
|
||||
editorTakeAShot,
|
||||
editorUndo,
|
||||
} from '~/redux/editor/actions';
|
||||
import { Tooltip } from '~/components/panels/Tooltip';
|
||||
import { IState } from '~/redux/store';
|
||||
import { selectEditor } from '~/redux/editor/selectors';
|
||||
import { selectMap } from '~/redux/map/selectors';
|
||||
|
||||
interface Props extends IRootState {
|
||||
routing: IRootState['features']['routing'],
|
||||
setMode: typeof setMode,
|
||||
startEditing: typeof startEditing,
|
||||
stopEditing: typeof stopEditing,
|
||||
keyPressed: EventListenerOrEventListenerObject,
|
||||
}
|
||||
const mapStateToProps = (state: IState) => {
|
||||
const { mode, changed, editing, features, history } = selectEditor(state);
|
||||
const { route, stickers } = selectMap(state);
|
||||
return {
|
||||
mode,
|
||||
changed,
|
||||
editing,
|
||||
features,
|
||||
history,
|
||||
route,
|
||||
stickers,
|
||||
};
|
||||
};
|
||||
|
||||
class Component extends React.PureComponent<Props, void> {
|
||||
const mapDispatchToProps = {
|
||||
editorChangeMode,
|
||||
editorStartEditing,
|
||||
editorStopEditing,
|
||||
editorTakeAShot,
|
||||
editorKeyPressed,
|
||||
editorUndo,
|
||||
editorRedo,
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
class EditorPanelUnconnected extends PureComponent<Props, void> {
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.props.keyPressed);
|
||||
if (!this.panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', this.onKeyPress as any);
|
||||
|
||||
const obj = document.getElementById('control-dialog');
|
||||
const { width } = this.panel.getBoundingClientRect();
|
||||
|
@ -30,32 +61,82 @@ class Component extends React.PureComponent<Props, void> {
|
|||
obj.style.width = String(width);
|
||||
}
|
||||
|
||||
panel: HTMLElement = null;
|
||||
panel: HTMLDivElement | null = null;
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.props.keyPressed);
|
||||
window.removeEventListener('keydown', this.onKeyPress as any);
|
||||
}
|
||||
|
||||
startPolyMode = () => this.props.setMode(MODES.POLY);
|
||||
startStickerMode = () => this.props.setMode(MODES.STICKERS_SELECT);
|
||||
startRouterMode = () => this.props.setMode(MODES.ROUTER);
|
||||
startTrashMode = () => this.props.setMode(MODES.TRASH);
|
||||
onKeyPress = event => {
|
||||
if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') return;
|
||||
|
||||
this.props.editorKeyPressed(event);
|
||||
};
|
||||
|
||||
startPolyMode = () => this.props.editorChangeMode(MODES.POLY);
|
||||
startStickerMode = () => this.props.editorChangeMode(MODES.STICKERS_SELECT);
|
||||
startRouterMode = () => this.props.editorChangeMode(MODES.ROUTER);
|
||||
startTrashMode = () => this.props.editorChangeMode(MODES.TRASH);
|
||||
startSaveMode = () => {
|
||||
// if (!this.props.changed) return;
|
||||
this.props.setMode(MODES.SAVE);
|
||||
this.props.editorChangeMode(MODES.SAVE);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
mode, changed, editing, routing,
|
||||
mode,
|
||||
changed,
|
||||
editing,
|
||||
features: { routing },
|
||||
history: { records, position },
|
||||
route,
|
||||
stickers,
|
||||
} = this.props;
|
||||
|
||||
const can_undo = records.length > 0 && position > 0;
|
||||
const can_redo = records.length && records.length - 1 > position;
|
||||
const can_clear = route.length > 0 || stickers.length > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classnames('panel right', { active: editing })} ref={el => { this.panel = el; }}>
|
||||
<div
|
||||
className={classnames('panel right', { active: editing })}
|
||||
ref={el => {
|
||||
this.panel = el;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames('secondary-bar secondary-bar__undo', {
|
||||
active: can_undo || can_redo || can_clear,
|
||||
})}
|
||||
>
|
||||
<button className={classnames('undo-button', { inactive: !can_undo })} onClick={this.props.editorUndo}>
|
||||
<Tooltip>Отмена (z)</Tooltip>
|
||||
<Icon icon="icon-undo" size={24} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={classnames('undo-button', {
|
||||
inactive: !can_redo,
|
||||
})}
|
||||
onClick={this.props.editorRedo}
|
||||
>
|
||||
<Tooltip>Вернуть (x)</Tooltip>
|
||||
<Icon icon="icon-redo" size={24} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={classnames({
|
||||
inactive: !can_clear,
|
||||
})}
|
||||
onClick={this.startTrashMode}
|
||||
>
|
||||
<Tooltip>Очистить (c)</Tooltip>
|
||||
<Icon icon="icon-trash-4" size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="control-bar control-bar-padded">
|
||||
{
|
||||
routing &&
|
||||
{routing && (
|
||||
<button
|
||||
className={classnames({ active: mode === MODES.ROUTER })}
|
||||
onClick={this.startRouterMode}
|
||||
|
@ -63,8 +144,7 @@ class Component extends React.PureComponent<Props, void> {
|
|||
<Tooltip>Автоматический маршрут</Tooltip>
|
||||
<Icon icon="icon-route-2" />
|
||||
</button>
|
||||
}
|
||||
|
||||
)}
|
||||
|
||||
<button
|
||||
className={classnames({ active: mode === MODES.POLY })}
|
||||
|
@ -75,34 +155,20 @@ class Component extends React.PureComponent<Props, void> {
|
|||
</button>
|
||||
|
||||
<button
|
||||
className={classnames({ active: (mode === MODES.STICKERS || mode === MODES.STICKERS_SELECT) })}
|
||||
className={classnames({
|
||||
active: mode === MODES.STICKERS || mode === MODES.STICKERS_SELECT,
|
||||
})}
|
||||
onClick={this.startStickerMode}
|
||||
>
|
||||
<Tooltip>Точки маршрута</Tooltip>
|
||||
<Icon icon="icon-sticker-3" />
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="control-sep" />
|
||||
|
||||
<div className="control-bar control-bar-padded">
|
||||
<button
|
||||
className={classnames({ active: mode === MODES.TRASH })}
|
||||
onClick={this.startTrashMode}
|
||||
>
|
||||
<Tooltip>Удаление элементов</Tooltip>
|
||||
<Icon icon="icon-trash-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="control-sep" />
|
||||
|
||||
<div className="control-bar">
|
||||
<button
|
||||
className="highlighted cancel"
|
||||
onClick={this.props.stopEditing}
|
||||
>
|
||||
<button className="highlighted cancel" onClick={this.props.editorStopEditing}>
|
||||
<Icon icon="icon-cancel-1" />
|
||||
</button>
|
||||
|
||||
|
@ -114,59 +180,21 @@ class Component extends React.PureComponent<Props, void> {
|
|||
<Icon icon="icon-check-1" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={classnames('panel right', { active: !editing })}>
|
||||
<div className="control-bar">
|
||||
<button className="primary single" onClick={this.props.startEditing}>
|
||||
<button className="primary single" onClick={this.props.editorStartEditing}>
|
||||
<Icon icon="icon-route-2" />
|
||||
<span>
|
||||
РЕДАКТИРОВАТЬ
|
||||
</span>
|
||||
<span>РЕДАКТИРОВАТЬ</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditorDialog
|
||||
width={((this.panel && this.panel.getBoundingClientRect().width) || 0)}
|
||||
/>
|
||||
|
||||
<EditorDialog width={(this.panel && this.panel.getBoundingClientRect().width) || 0} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {
|
||||
user: {
|
||||
editing,
|
||||
mode,
|
||||
changed,
|
||||
features: {
|
||||
routing,
|
||||
}
|
||||
},
|
||||
} = state;
|
||||
|
||||
return {
|
||||
editing,
|
||||
mode,
|
||||
changed,
|
||||
routing,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||
setMode,
|
||||
setLogo,
|
||||
startEditing,
|
||||
stopEditing,
|
||||
takeAShot,
|
||||
keyPressed,
|
||||
}, dispatch);
|
||||
|
||||
export const EditorPanel = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Component);
|
||||
export const EditorPanel = connect(mapStateToProps, mapDispatchToProps)(EditorPanelUnconnected);
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
export const Icon = ({ icon, size = 32 }: { icon: string, size?: number }) => (
|
||||
export const Icon = memo(({ icon, size = 32 }: { icon: string; size?: number }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<mask id={`icon-mask-${icon}`}>
|
||||
<use xlinkHref={`${require('$sprites/icon.svg')}#${icon}`} x={0} y={0} />
|
||||
<use xlinkHref={`/images/icon.svg#${icon}`} x={0} y={0} />
|
||||
</mask>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="32" height="32" stroke="none" mask={`url(#icon-mask-${icon})`} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
import React from 'react';
|
||||
import { Icon } from '~/components/panels/Icon';
|
||||
|
||||
type Props = {
|
||||
onCancel: () => void,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export const Tooltip = ({ children, position = 'bottom' }: { children: string, position?: string }) => (
|
||||
|
|