mirror of
https://github.com/muerwre/orchidmap-front.git
synced 2025-04-25 02:56:41 +07:00
routes: spinners
This commit is contained in:
parent
a27db14c65
commit
b28803aff3
11 changed files with 216 additions and 76 deletions
|
@ -28,21 +28,28 @@ module.exports = async (req, res) => {
|
|||
};
|
||||
}
|
||||
|
||||
let list = await Route.find({
|
||||
...criteria,
|
||||
}, '_id title distance owner updated_at', { limit: 500 }).populate('owner');
|
||||
let list = await Route.find(
|
||||
{
|
||||
...criteria,
|
||||
},
|
||||
'_id title distance owner updated_at',
|
||||
{
|
||||
limit: 500,
|
||||
sort: { updated_at: -1 },
|
||||
}
|
||||
).populate('owner');
|
||||
|
||||
list = list.filter(item => (
|
||||
!author || item.owner._id === author
|
||||
));
|
||||
|
||||
let limits = list.reduce(({ min, max }, { distance: dist }) => ({
|
||||
min: Math.ceil(Math.min(dist, min) / 20) * 20,
|
||||
max: Math.ceil(Math.max(dist, max) / 20) * 20,
|
||||
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);
|
||||
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;
|
||||
|
@ -60,6 +67,8 @@ module.exports = async (req, res) => {
|
|||
limits = { min: 0, max: 0 };
|
||||
} else if (limits.min === limits.max) {
|
||||
limits = { min: limits.max - 20, max: limits.max };
|
||||
} else if (limits.max > 200) {
|
||||
limits = { min: limits.min, max: 200 };
|
||||
}
|
||||
|
||||
res.send({
|
||||
|
|
|
@ -7,12 +7,16 @@ import { Scroll } from '$components/Scroll';
|
|||
import {
|
||||
searchSetDistance,
|
||||
searchSetTitle,
|
||||
searchSetTab,
|
||||
} from '$redux/user/actions';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Range } from 'rc-slider';
|
||||
import { TABS } from '$constants/dialogs';
|
||||
import { Icon } from '$components/panels/Icon';
|
||||
|
||||
type Props = {
|
||||
ready: Boolean,
|
||||
routes: {
|
||||
limit: Number,
|
||||
loading: Boolean, // <-- maybe delete this
|
||||
|
@ -30,6 +34,7 @@ type Props = {
|
|||
searchSetAuthor: Function,
|
||||
searchSetDistance: Function,
|
||||
searchSetTitle: Function,
|
||||
searchSetTab: Function,
|
||||
};
|
||||
|
||||
class Component extends React.Component<Props> {
|
||||
|
@ -37,26 +42,12 @@ class Component extends React.Component<Props> {
|
|||
this.props.searchSetTitle(value);
|
||||
};
|
||||
|
||||
setDistanceMin = ({ target: { value } }) => {
|
||||
console.log('A');
|
||||
const parsed = parseInt(value > 0 ? value : 0, 10);
|
||||
const { distance } = this.props.routes.filter;
|
||||
|
||||
this.props.searchSetDistance([parsed, distance[1] > parsed ? distance[1] : parsed]);
|
||||
};
|
||||
|
||||
setDistanceMax = ({ target: { value } }) => {
|
||||
console.log('B');
|
||||
const parsed = parseInt(value > 0 ? value : 0, 10);
|
||||
const { distance } = this.props.routes.filter;
|
||||
|
||||
this.props.searchSetDistance([distance[0], parsed > distance[0] ? parsed : distance[0]]);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
ready,
|
||||
routes: {
|
||||
list,
|
||||
loading,
|
||||
filter: {
|
||||
min,
|
||||
max,
|
||||
|
@ -71,10 +62,18 @@ class Component extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<div className="dialog-content">
|
||||
<div className="dialog-head-tabs">
|
||||
<div className="dialog-head-tab active">Мои</div>
|
||||
<div className="dialog-head-tab">Общие</div>
|
||||
<div className="dialog-head-tab">Выбранные</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>
|
||||
|
@ -87,17 +86,22 @@ class Component extends React.Component<Props> {
|
|||
|
||||
<br />
|
||||
|
||||
<Range
|
||||
min={min}
|
||||
max={max}
|
||||
marks={marks}
|
||||
step={20}
|
||||
onChange={this.props.searchSetDistance}
|
||||
defaultValue={[0, 10000]}
|
||||
value={distance}
|
||||
pushable={20}
|
||||
disabled={min >= max}
|
||||
/>
|
||||
{
|
||||
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>
|
||||
</div>
|
||||
|
@ -112,6 +116,22 @@ class Component extends React.Component<Props> {
|
|||
/>
|
||||
))
|
||||
}
|
||||
{ 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-maplist-loader">
|
||||
<div className="dialog-maplist-icon">
|
||||
<Icon icon="icon-block-1" />
|
||||
</div>
|
||||
НИЧЕГО НЕ НАЙДЕНО
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
|
@ -119,18 +139,29 @@ class Component extends React.Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ user: { editing, routes } }) => ({
|
||||
routes,
|
||||
editing,
|
||||
marks: [...new Array((routes.filter.max - routes.filter.min) / 20 + 1)].reduce((obj, el, i) => ({
|
||||
...obj,
|
||||
[routes.filter.min + (i * 20)]: String(routes.filter.min + (i * 20)),
|
||||
}), {}),
|
||||
});
|
||||
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 mapDispatchToProps = dispatch => bindActionCreators({
|
||||
searchSetDistance,
|
||||
searchSetTitle,
|
||||
searchSetTab,
|
||||
}, dispatch);
|
||||
|
||||
export const MapListMoreDialog = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { GuestButton } from '$components/user/GuestButton';
|
|||
import { DEFAULT_USER, ROLES } from '$constants/auth';
|
||||
import { UserButton } from '$components/user/UserButton';
|
||||
import { UserMenu } from '$components/user/UserMenu';
|
||||
import { setUser, userLogout, takeAShot, setDialog, gotVkUser, setDialogActive } from '$redux/user/actions';
|
||||
import { setUser, userLogout, takeAShot, setDialog, gotVkUser, setDialogActive, openMapDialog } from '$redux/user/actions';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import type { UserType } from '$constants/types';
|
||||
|
@ -24,6 +24,7 @@ type Props = {
|
|||
setDialogActive: Function,
|
||||
gotVkUser: Function,
|
||||
takeAShot: Function,
|
||||
openMapDialog: Function,
|
||||
};
|
||||
|
||||
export class Component extends React.PureComponent<Props, void> {
|
||||
|
@ -61,8 +62,9 @@ export class Component extends React.PureComponent<Props, void> {
|
|||
|
||||
setMenuOpened = () => this.setState({ menuOpened: !this.state.menuOpened });
|
||||
openMapsDialog = () => {
|
||||
this.props.setDialog(DIALOGS.MAP_LIST);
|
||||
this.props.setDialogActive(this.props.dialog !== DIALOGS.MAP_LIST);
|
||||
// this.props.setDialog(DIALOGS.MAP_LIST);
|
||||
// this.props.setDialogActive(this.props.dialog !== DIALOGS.MAP_LIST);
|
||||
this.props.openMapDialog('mine');
|
||||
};
|
||||
|
||||
openAppInfoDialog = () => {
|
||||
|
@ -146,6 +148,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
|
|||
setDialog,
|
||||
gotVkUser,
|
||||
setDialogActive,
|
||||
openMapDialog,
|
||||
}, dispatch);
|
||||
|
||||
export const UserPanel = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||
|
|
|
@ -4,3 +4,9 @@ export const DIALOGS = ({
|
|||
MAP_LIST: 'MAP_LIST',
|
||||
APP_INFO: 'APP_INFO',
|
||||
}: { [key: String]: String });
|
||||
|
||||
export const TABS = ({
|
||||
mine: 'Мои',
|
||||
all: 'Общие',
|
||||
// starred: 'Выбранные',
|
||||
}: { [key: String]: String });
|
||||
|
|
|
@ -41,6 +41,7 @@ export const setProvider = provider => ({ type: ACTIONS.SET_PROVIDER, provider }
|
|||
|
||||
export const setDialog = dialog => ({ type: ACTIONS.SET_DIALOG, dialog });
|
||||
export const setDialogActive = dialog_active => ({ type: ACTIONS.SET_DIALOG_ACTIVE, dialog_active });
|
||||
export const openMapDialog = tab => ({ type: ACTIONS.OPEN_MAP_DIALOG, tab });
|
||||
|
||||
export const locationChanged = location => ({ type: ACTIONS.LOCATION_CHANGED, location });
|
||||
export const setReady = ready => ({ type: ACTIONS.SET_READY, ready });
|
||||
|
|
|
@ -57,4 +57,6 @@ export const ACTIONS = ({
|
|||
SEARCH_SET_TAB: 'SEARCH_SET_TAB',
|
||||
SEARCH_PUT_ROUTES: 'SEARCH_PUT_ROUTES',
|
||||
SEARCH_SET_LOADING: 'SEARCH_SET_LOADING',
|
||||
|
||||
OPEN_MAP_DIALOG: 'OPEN_MAP_DIALOG',
|
||||
}: { [key: String]: String });
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MODES } from '$constants/modes';
|
|||
import { DEFAULT_LOGO } from '$constants/logos';
|
||||
import { TIPS } from '$constants/tips';
|
||||
import { DEFAULT_PROVIDER } from '$constants/providers';
|
||||
import { DIALOGS } from '$constants/dialogs';
|
||||
import { DIALOGS, TABS } from '$constants/dialogs';
|
||||
|
||||
const getEstimated = distance => {
|
||||
const time = (distance && (distance / 15)) || 0;
|
||||
|
@ -117,28 +117,17 @@ const searchSetDistance = (state, { distance = [0, 9999] }) => ({
|
|||
}
|
||||
});
|
||||
|
||||
const searchSetTab = (state, { tab = 'mine' }) => ({
|
||||
const searchSetTab = (state, { tab = TABS[Object.keys(TABS)[0]] }) => ({
|
||||
...state,
|
||||
routes: {
|
||||
...state.routes,
|
||||
filter: {
|
||||
...state.routes.filter,
|
||||
tab: ['mine', 'all', 'star'].indexOf(tab) >= 0 ? tab : 'mine',
|
||||
tab: Object.keys(TABS).indexOf(tab) >= 0 ? tab : TABS[Object.keys(TABS)[0]],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const newDistCalc = ({
|
||||
distance, min, max, filter
|
||||
}) => {
|
||||
if (filter.min === filter.max) {
|
||||
// slider was disabled
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
// state.routes.filter.distance
|
||||
};
|
||||
|
||||
const searchPutRoutes = (state, { list = [], min, max }) => ({
|
||||
...state,
|
||||
routes: {
|
||||
|
@ -218,8 +207,8 @@ export const INITIAL_STATE = {
|
|||
save_overwriting: false,
|
||||
save_processing: false,
|
||||
|
||||
dialog: DIALOGS.MAP_LIST,
|
||||
dialog_active: true,
|
||||
dialog: DIALOGS.NONE,
|
||||
dialog_active: false,
|
||||
|
||||
renderer: {
|
||||
data: '',
|
||||
|
@ -237,11 +226,11 @@ export const INITIAL_STATE = {
|
|||
filter: {
|
||||
title: '',
|
||||
starred: false,
|
||||
distance: [0, 99999],
|
||||
distance: [0, 10000],
|
||||
author: '',
|
||||
tab: 'mine',
|
||||
min: 0,
|
||||
max: 0,
|
||||
max: 10000,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -16,7 +16,8 @@ import {
|
|||
setMode, setReady, setRenderer,
|
||||
setSaveError,
|
||||
setSaveOverwrite, setSaveSuccess, setTitle,
|
||||
setUser
|
||||
searchSetTab,
|
||||
setUser, setDialog,
|
||||
} from '$redux/user/actions';
|
||||
import { getUrlData, parseQuery, pushLoaderState, pushNetworkInitError, pushPath, replacePath } from '$utils/history';
|
||||
import { editor } from '$modules/Editor';
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
} from '$utils/renderer';
|
||||
import { LOGOS } from '$constants/logos';
|
||||
import { DEFAULT_PROVIDER } from '$constants/providers';
|
||||
import { DIALOGS } from '$constants/dialogs';
|
||||
|
||||
const getUser = state => (state.user.user);
|
||||
const getState = state => (state.user);
|
||||
|
@ -433,10 +435,9 @@ function* keyPressedSaga({ key }): void {
|
|||
}
|
||||
}
|
||||
|
||||
function* searchSetSaga() {
|
||||
function* searchSetSagaWorker() {
|
||||
const { id, token } = yield select(getUser);
|
||||
yield delay(1000);
|
||||
yield put(searchSetLoading(true));
|
||||
|
||||
const { routes: { filter, filter: { title, distance, tab } } } = yield select(getState);
|
||||
|
||||
const { list, min, max } = yield call(getRouteList, {
|
||||
|
@ -468,6 +469,33 @@ function* searchSetSaga() {
|
|||
return yield put(searchSetLoading(false));
|
||||
}
|
||||
|
||||
function* searchSetSaga() {
|
||||
yield put(searchSetLoading(true));
|
||||
yield delay(500);
|
||||
yield call(searchSetSagaWorker);
|
||||
}
|
||||
|
||||
function* openMapDialogSaga({ tab }) {
|
||||
const { dialog_active, routes: { filter: { tab: current } } } = yield select(getState);
|
||||
|
||||
if (dialog_active && tab === current) {
|
||||
return yield put(setDialogActive(false));
|
||||
}
|
||||
|
||||
yield put(searchSetTab(tab));
|
||||
yield put(setDialog(DIALOGS.MAP_LIST));
|
||||
yield put(setDialogActive(true));
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
function* searchSetTabSaga() {
|
||||
yield put(searchSetDistance([0, 10000]));
|
||||
yield put(searchPutRoutes({ list: [], min: 0, max: 10000 }));
|
||||
|
||||
yield call(searchSetSaga);
|
||||
}
|
||||
|
||||
export function* userSaga() {
|
||||
yield takeLatest(REHYDRATE, authCheckSaga);
|
||||
yield takeEvery(ACTIONS.SET_MODE, setModeSaga);
|
||||
|
@ -505,4 +533,7 @@ export function* userSaga() {
|
|||
ACTIONS.SEARCH_SET_TITLE,
|
||||
ACTIONS.SEARCH_SET_DISTANCE,
|
||||
], searchSetSaga);
|
||||
|
||||
yield takeLatest(ACTIONS.OPEN_MAP_DIALOG, openMapDialogSaga);
|
||||
yield takeLatest(ACTIONS.SEARCH_SET_TAB, searchSetTabSaga);
|
||||
}
|
||||
|
|
|
@ -348,8 +348,18 @@
|
|||
<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>
|
||||
</svg>
|
||||
</defs>
|
||||
|
||||
<use xlink:href="#icon-copy-1" />
|
||||
<use xlink:href="#icon-sync-1" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
@ -114,6 +114,44 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.dialog-maplist-loader {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
padding-left: 60px;
|
||||
|
||||
.spin {
|
||||
animation: spin infinite reverse 2s linear;
|
||||
}
|
||||
.dialog-maplist-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.route-row {
|
||||
margin-bottom: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
|
@ -168,20 +206,23 @@
|
|||
color: white;
|
||||
}
|
||||
|
||||
.dialog-head-tabs {
|
||||
.dialog-tabs {
|
||||
background: rgba(255, 255, 255, 0);
|
||||
border-radius: @panel_radius @panel_radius 0 0;
|
||||
height: 32px;
|
||||
user-select: none;
|
||||
flex-direction: row;
|
||||
|
||||
.dialog-head-tab {
|
||||
height: 32px;
|
||||
.dialog-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
padding: 0 10px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
border-radius: @panel_radius @panel_radius 0 0;
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
|
||||
&.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
|
|
@ -319,3 +319,20 @@
|
|||
border-top-color: @tooltip-arrow-color;
|
||||
}
|
||||
}
|
||||
|
||||
.range-placeholder {
|
||||
height: 40px;
|
||||
padding: 15px 0;
|
||||
box-sizing: border-box;
|
||||
margin: 8px;
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
height: 4px;
|
||||
border-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue