mirror of
https://github.com/muerwre/orchidmap-front.git
synced 2025-04-25 02:56:41 +07:00
lazy loading maps
This commit is contained in:
parent
945878f2df
commit
559d6c8d07
11 changed files with 157 additions and 25 deletions
|
@ -3,7 +3,7 @@ const { Route, User } = require('../../models');
|
|||
module.exports = async (req, res) => {
|
||||
const {
|
||||
query: {
|
||||
id, token, title, distance, author, starred,
|
||||
id, token, title, distance, author, step = 20, shift = 0,
|
||||
}
|
||||
} = req;
|
||||
|
||||
|
@ -34,7 +34,7 @@ module.exports = async (req, res) => {
|
|||
},
|
||||
'_id title distance owner updated_at is_public',
|
||||
{
|
||||
limit: 500,
|
||||
limit: 9000,
|
||||
sort: { updated_at: -1 },
|
||||
}
|
||||
).populate('owner', '_id');
|
||||
|
@ -61,6 +61,12 @@ module.exports = async (req, res) => {
|
|||
));
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -74,6 +80,9 @@ module.exports = async (req, res) => {
|
|||
res.send({
|
||||
success: true,
|
||||
list,
|
||||
limit,
|
||||
step: parseInt(step, 10),
|
||||
shift: parseInt(shift, 10),
|
||||
...limits,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -141,7 +141,7 @@ const run = async () => {
|
|||
title: '',
|
||||
version: 1,
|
||||
distance: calcPolyDistance(route),
|
||||
is_public: false,
|
||||
is_public: true,
|
||||
};
|
||||
}));
|
||||
}));
|
||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -11461,6 +11461,11 @@
|
|||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||
"dev": true
|
||||
},
|
||||
"throttle-debounce": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
|
||||
"integrity": "sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg=="
|
||||
},
|
||||
"through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
"scrypt": "^6.0.3",
|
||||
"styled-components": "^3.2.6",
|
||||
"styled-theming": "^2.2.0",
|
||||
"throttle-debounce": "^2.1.0",
|
||||
"tt-react-custom-scrollbars": "^4.2.1-tt2",
|
||||
"typeface-pt-sans": "0.0.54",
|
||||
"typesafe-actions": "^3.0.0",
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
searchSetDistance,
|
||||
searchSetTitle,
|
||||
searchSetTab,
|
||||
setDialogActive,
|
||||
setDialogActive, mapsLoadMore,
|
||||
} from '$redux/user/actions';
|
||||
import { isMobile } from '$utils/window';
|
||||
import classnames from 'classnames';
|
||||
|
@ -21,7 +21,7 @@ import { IRootState } from '$redux/user/reducer';
|
|||
interface Props extends IRootState {
|
||||
marks: { [x: number]: string },
|
||||
routes_sorted: Array<string>,
|
||||
|
||||
mapsLoadMore: typeof mapsLoadMore,
|
||||
searchSetDistance: typeof searchSetDistance,
|
||||
searchSetTitle: typeof searchSetTitle,
|
||||
searchSetTab: typeof searchSetTab,
|
||||
|
@ -50,6 +50,19 @@ class Component extends React.Component<Props, State> {
|
|||
pushPath(`/${_id}/${this.props.editing ? 'edit' : ''}`);
|
||||
};
|
||||
|
||||
onScroll = (e: { target: { scrollHeight: number, scrollTop: number, clientHeight: number }}): void => {
|
||||
const { target: { scrollHeight, scrollTop, clientHeight }} = e;
|
||||
const delta = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (
|
||||
delta < 300 &&
|
||||
this.props.routes.list.length < this.props.routes.limit &&
|
||||
!this.props.routes.loading
|
||||
) {
|
||||
this.props.mapsLoadMore();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
ready,
|
||||
|
@ -130,7 +143,10 @@ class Component extends React.Component<Props, State> {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Scroll className="dialog-shader">
|
||||
<Scroll
|
||||
className="dialog-shader"
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<div className="dialog-maplist">
|
||||
{
|
||||
list.map(route => (
|
||||
|
@ -148,7 +164,7 @@ class Component extends React.Component<Props, State> {
|
|||
}
|
||||
</div>
|
||||
</Scroll>
|
||||
|
||||
<div className={classnames('dialog-maplist-pulse', { active: loading })} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -178,6 +194,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
|
|||
searchSetTitle,
|
||||
searchSetTab,
|
||||
setDialogActive,
|
||||
mapsLoadMore,
|
||||
}, dispatch);
|
||||
|
||||
export const MapListDialog = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||
|
|
|
@ -56,6 +56,7 @@ export const keyPressed = ({ key, target: { tagName } }) => ({ type: ACTIONS.KEY
|
|||
|
||||
export const searchSetTitle = title => ({ type: ACTIONS.SEARCH_SET_TITLE, title });
|
||||
export const searchSetDistance = distance => ({ type: ACTIONS.SEARCH_SET_DISTANCE, distance });
|
||||
export const searchChangeDistance = distance => ({ type: ACTIONS.SEARCH_CHANGE_DISTANCE, distance });
|
||||
export const searchSetTab = tab => ({ type: ACTIONS.SEARCH_SET_TAB, tab });
|
||||
export const searchSetLoading = loading => ({ type: ACTIONS.SEARCH_SET_LOADING, loading });
|
||||
|
||||
|
@ -64,3 +65,6 @@ export const searchPutRoutes = payload => ({ type: ACTIONS.SEARCH_PUT_ROUTES, ..
|
|||
export const setMarkersShown = markers_shown => ({ type: ACTIONS.SET_MARKERS_SHOWN, markers_shown });
|
||||
export const getGPXTrack = () => ({ type: ACTIONS.GET_GPX_TRACK });
|
||||
export const setIsEmpty = is_empty => ({ type: ACTIONS.SET_IS_EMPTY, is_empty });
|
||||
|
||||
export const mapsLoadMore = () => ({ type: ACTIONS.MAPS_LOAD_MORE });
|
||||
export const mapsSetShift = (shift: number) => ({ type: ACTIONS.MAPS_SET_SHIFT, shift });
|
||||
|
|
|
@ -59,6 +59,7 @@ export const ACTIONS: IActions = {
|
|||
|
||||
SEARCH_SET_TITLE: 'SEARCH_SET_TITLE',
|
||||
SEARCH_SET_DISTANCE: 'SEARCH_SET_DISTANCE',
|
||||
SEARCH_CHANGE_DISTANCE: 'SEARCH_CHANGE_DISTANCE',
|
||||
|
||||
SEARCH_SET_TAB: 'SEARCH_SET_TAB',
|
||||
SEARCH_PUT_ROUTES: 'SEARCH_PUT_ROUTES',
|
||||
|
@ -71,4 +72,7 @@ export const ACTIONS: IActions = {
|
|||
|
||||
GET_GPX_TRACK: 'GET_GPX_TRACK',
|
||||
SET_IS_EMPTY: 'SET_IS_EMPTY',
|
||||
|
||||
MAPS_LOAD_MORE: 'MAPS_LOAD_MORE',
|
||||
MAPS_SET_SHIFT: 'MAPS_SET_SHIFT',
|
||||
};
|
||||
|
|
|
@ -57,6 +57,8 @@ interface IRootReducer {
|
|||
limit: 0,
|
||||
loading: boolean,
|
||||
list: Array<IRoute>,
|
||||
step: number,
|
||||
shift: number,
|
||||
filter: {
|
||||
title: '',
|
||||
starred: boolean,
|
||||
|
@ -237,11 +239,14 @@ const searchSetTab: ActionHandler<typeof ActionCreators.searchSetTab> = (state,
|
|||
}
|
||||
});
|
||||
|
||||
const searchPutRoutes: ActionHandler<typeof ActionCreators.searchPutRoutes> = (state, { list = [], min, max }) => ({
|
||||
const searchPutRoutes: ActionHandler<typeof ActionCreators.searchPutRoutes> = (state, { list = [], min, max, limit, step, shift }) => ({
|
||||
...state,
|
||||
routes: {
|
||||
...state.routes,
|
||||
list,
|
||||
limit,
|
||||
step,
|
||||
shift,
|
||||
filter: {
|
||||
...state.routes.filter,
|
||||
distance: (state.routes.filter.min === state.routes.filter.max)
|
||||
|
@ -270,6 +275,13 @@ const setSpeed: ActionHandler<typeof ActionCreators.setSpeed> = (state, { speed
|
|||
|
||||
const setMarkersShown: ActionHandler<typeof ActionCreators.setMarkersShown> = (state, { markers_shown = true }) => ({ ...state, markers_shown });
|
||||
const setIsEmpty: ActionHandler<typeof ActionCreators.setIsEmpty> = (state, { is_empty = true }) => ({ ...state, is_empty });
|
||||
const mapsSetShift: ActionHandler<typeof ActionCreators.mapsSetShift> = (state, { shift = 0 }) => ({
|
||||
...state,
|
||||
routes: {
|
||||
...state.routes,
|
||||
shift,
|
||||
}
|
||||
});
|
||||
|
||||
const HANDLERS = ({
|
||||
[ACTIONS.SET_USER]: setUser,
|
||||
|
@ -302,6 +314,7 @@ const HANDLERS = ({
|
|||
|
||||
[ACTIONS.SEARCH_SET_TITLE]: searchSetTitle,
|
||||
[ACTIONS.SEARCH_SET_DISTANCE]: searchSetDistance,
|
||||
[ACTIONS.SEARCH_CHANGE_DISTANCE]: searchSetDistance,
|
||||
[ACTIONS.SEARCH_SET_TAB]: searchSetTab,
|
||||
[ACTIONS.SEARCH_PUT_ROUTES]: searchPutRoutes,
|
||||
[ACTIONS.SEARCH_SET_LOADING]: searchSetLoading,
|
||||
|
@ -310,6 +323,8 @@ const HANDLERS = ({
|
|||
|
||||
[ACTIONS.SET_MARKERS_SHOWN]: setMarkersShown,
|
||||
[ACTIONS.SET_IS_EMPTY]: setIsEmpty,
|
||||
[ACTIONS.MAPS_SET_SHIFT]: mapsSetShift,
|
||||
|
||||
});
|
||||
|
||||
export const INITIAL_STATE: IRootReducer = {
|
||||
|
@ -354,6 +369,8 @@ export const INITIAL_STATE: IRootReducer = {
|
|||
limit: 0,
|
||||
loading: false, // <-- maybe delete this
|
||||
list: [],
|
||||
step: 20,
|
||||
shift: 0,
|
||||
filter: {
|
||||
title: '',
|
||||
starred: false,
|
||||
|
|
|
@ -9,15 +9,32 @@ import {
|
|||
postMap
|
||||
} from '$utils/api';
|
||||
import {
|
||||
hideRenderer, searchPutRoutes, searchSetDistance, searchSetLoading,
|
||||
setActiveSticker, setAddress,
|
||||
setChanged, setDialogActive,
|
||||
hideRenderer,
|
||||
searchPutRoutes,
|
||||
searchSetDistance,
|
||||
searchSetLoading,
|
||||
setActiveSticker,
|
||||
setAddress,
|
||||
setChanged,
|
||||
setDialogActive,
|
||||
setEditing,
|
||||
setMode, setReady, setRenderer,
|
||||
setMode,
|
||||
setReady,
|
||||
setRenderer,
|
||||
setSaveError,
|
||||
setSaveOverwrite, setSaveSuccess, setTitle,
|
||||
setSaveOverwrite,
|
||||
setSaveSuccess,
|
||||
setTitle,
|
||||
searchSetTab,
|
||||
setUser, setDialog, setPublic, setAddressOrigin, setProvider, changeProvider, openMapDialog, setSaveLoading,
|
||||
setUser,
|
||||
setDialog,
|
||||
setPublic,
|
||||
setAddressOrigin,
|
||||
setProvider,
|
||||
changeProvider,
|
||||
openMapDialog,
|
||||
setSaveLoading,
|
||||
mapsSetShift, searchChangeDistance,
|
||||
} from '$redux/user/actions';
|
||||
import { getUrlData, parseQuery, pushLoaderState, pushNetworkInitError, pushPath, replacePath } from '$utils/history';
|
||||
import { editor } from '$modules/Editor';
|
||||
|
@ -456,28 +473,36 @@ function* keyPressedSaga({ key, target }: ReturnType<typeof ActionCreators.keyPr
|
|||
}
|
||||
}
|
||||
|
||||
function* searchSetSagaWorker() {
|
||||
function* searchGetRoutes() {
|
||||
const { id, token } = yield select(getUser);
|
||||
|
||||
const { routes: { filter, filter: { title, distance, tab } } } = yield select(getState);
|
||||
const { routes: { step, shift, filter: { title, distance, tab } } } = yield select(getState);
|
||||
|
||||
const { list, min, max } = yield call(getRouteList, {
|
||||
return yield call(getRouteList, {
|
||||
id,
|
||||
token,
|
||||
title,
|
||||
distance,
|
||||
step,
|
||||
shift,
|
||||
author: tab === 'mine' ? id : '',
|
||||
starred: tab === 'starred',
|
||||
});
|
||||
}
|
||||
|
||||
yield put(searchPutRoutes({ list, min, max }));
|
||||
function* searchSetSagaWorker() {
|
||||
const { routes: { filter } } = yield select(getState);
|
||||
|
||||
const { list, min, max, limit, shift, step } = yield call(searchGetRoutes);
|
||||
|
||||
yield put(searchPutRoutes({ list, min, max, limit, shift, step }));
|
||||
|
||||
// change distange range if needed and load additional data
|
||||
if (
|
||||
(filter.min > min && filter.distance[0] <= filter.min) ||
|
||||
(filter.max < max && filter.distance[1] >= filter.max)
|
||||
) {
|
||||
yield put(searchSetDistance([
|
||||
yield put(searchChangeDistance([
|
||||
(filter.min > min && filter.distance[0] <= filter.min)
|
||||
? min
|
||||
: filter.distance[0],
|
||||
|
@ -492,6 +517,7 @@ function* searchSetSagaWorker() {
|
|||
|
||||
function* searchSetSaga() {
|
||||
yield put(searchSetLoading(true));
|
||||
yield put(mapsSetShift(0));
|
||||
yield delay(500);
|
||||
yield call(searchSetSagaWorker);
|
||||
}
|
||||
|
@ -514,8 +540,8 @@ function* openMapDialogSaga({ tab }: ReturnType<typeof ActionCreators.openMapDia
|
|||
}
|
||||
|
||||
function* searchSetTabSaga() {
|
||||
yield put(searchSetDistance([0, 10000]));
|
||||
yield put(searchPutRoutes({ list: [], min: 0, max: 10000 }));
|
||||
yield put(searchChangeDistance([0, 10000]));
|
||||
yield put(searchPutRoutes({ list: [], min: 0, max: 10000, step: 20, shift: 0 }));
|
||||
|
||||
yield call(searchSetSaga);
|
||||
}
|
||||
|
@ -572,6 +598,36 @@ function* getGPXTrackSaga(): SagaIterator {
|
|||
return downloadGPXTrack({ track, title });
|
||||
}
|
||||
|
||||
function* mapsLoadMoreSaga() {
|
||||
const { routes: { limit, list, shift, step, loading, filter } } = yield select(getState);
|
||||
|
||||
if (loading || list.length >= limit || limit === 0) return;
|
||||
|
||||
yield delay(500);
|
||||
|
||||
yield put(searchSetLoading(true));
|
||||
yield put(mapsSetShift(shift + step));
|
||||
|
||||
const result = yield call(searchGetRoutes);
|
||||
|
||||
if (
|
||||
(filter.min > result.min && filter.distance[0] <= filter.min) ||
|
||||
(filter.max < result.max && filter.distance[1] >= filter.max)
|
||||
) {
|
||||
yield put(searchChangeDistance([
|
||||
(filter.min > result.min && filter.distance[0] <= filter.min)
|
||||
? result.min
|
||||
: filter.distance[0],
|
||||
(filter.max < result.max && filter.distance[1] >= filter.max)
|
||||
? result.max
|
||||
: filter.distance[1],
|
||||
]));
|
||||
}
|
||||
|
||||
yield put(searchPutRoutes({ ...result, list: [...list, ...result.list] }));
|
||||
yield put(searchSetLoading(false));
|
||||
}
|
||||
|
||||
export function* userSaga() {
|
||||
yield takeLatest(REHYDRATE, authCheckSaga);
|
||||
yield takeEvery(ACTIONS.SET_MODE, setModeSaga);
|
||||
|
@ -614,5 +670,6 @@ export function* userSaga() {
|
|||
yield takeLatest(ACTIONS.SEARCH_SET_TAB, searchSetTabSaga);
|
||||
yield takeLatest(ACTIONS.SET_USER, setUserSaga);
|
||||
|
||||
yield takeLatest(ACTIONS.GET_GPX_TRACK, getGPXTrackSaga)
|
||||
yield takeLatest(ACTIONS.GET_GPX_TRACK, getGPXTrackSaga);
|
||||
yield takeLatest(ACTIONS.MAPS_LOAD_MORE, mapsLoadMoreSaga);
|
||||
}
|
||||
|
|
|
@ -126,6 +126,24 @@
|
|||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.dialog-maplist-pulse {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
background: linear-gradient(fade(@red_secondary, 0%), @red_secondary);
|
||||
height: 48px;
|
||||
pointer-events: none;
|
||||
transition: opacity 250ms;
|
||||
opacity: 0;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
animation: pulse 500ms infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-maplist-loader {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -40,10 +40,10 @@ export const checkIframeToken = ({ viewer_id, auth_key }) => axios.get(API.IFRAM
|
|||
}).then(result => (result && result.data && result.data.success && result.data.user)).catch(() => (false));
|
||||
|
||||
export const getRouteList = ({
|
||||
title, distance, author, starred, id, token,
|
||||
title, distance, author, starred, id, token, step, shift,
|
||||
}) => axios.get(API.GET_ROUTE_LIST, {
|
||||
params: {
|
||||
title, distance, author, starred, id, token,
|
||||
title, distance, author, starred, id, token, step, shift
|
||||
}
|
||||
}).then(result => (result && result.data && result.data.success && result.data))
|
||||
.catch(() => ({ list: [], min: 0, max: 0 }));
|
||||
.catch(() => ({ list: [], min: 0, max: 0, limit: 0, step: 20, shift: 20 }));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue