routes: spinners

This commit is contained in:
muerwre 2018-12-13 17:24:18 +07:00
parent a27db14c65
commit b28803aff3
11 changed files with 216 additions and 76 deletions

View file

@ -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({

View file

@ -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);

View file

@ -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);

View file

@ -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 });

View file

@ -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 });

View file

@ -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 });

View file

@ -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,
}
},
};

View file

@ -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);
}

View file

@ -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

Before After
Before After

View file

@ -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);

View file

@ -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;
}
}