diff --git a/backend/routes/route/list.js b/backend/routes/route/list.js index 13c7fab..0a4f880 100644 --- a/backend/routes/route/list.js +++ b/backend/routes/route/list.js @@ -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, }); }; diff --git a/backend/tools/import.js b/backend/tools/import.js index ee19d5a..0481cc3 100755 --- a/backend/tools/import.js +++ b/backend/tools/import.js @@ -141,7 +141,7 @@ const run = async () => { title: '', version: 1, distance: calcPolyDistance(route), - is_public: false, + is_public: true, }; })); })); diff --git a/package-lock.json b/package-lock.json index 0b55d6c..3baed82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7b4b15e..25f6808 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/dialogs/MapListDialog.tsx b/src/components/dialogs/MapListDialog.tsx index 3a6665d..c13fb9e 100644 --- a/src/components/dialogs/MapListDialog.tsx +++ b/src/components/dialogs/MapListDialog.tsx @@ -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, - + mapsLoadMore: typeof mapsLoadMore, searchSetDistance: typeof searchSetDistance, searchSetTitle: typeof searchSetTitle, searchSetTab: typeof searchSetTab, @@ -50,6 +50,19 @@ class Component extends React.Component { 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 { - +
{ list.map(route => ( @@ -148,7 +164,7 @@ class Component extends React.Component { }
- +
); } @@ -178,6 +194,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({ searchSetTitle, searchSetTab, setDialogActive, + mapsLoadMore, }, dispatch); export const MapListDialog = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/redux/user/actions.ts b/src/redux/user/actions.ts index 73cdbe2..8a51feb 100644 --- a/src/redux/user/actions.ts +++ b/src/redux/user/actions.ts @@ -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 }); diff --git a/src/redux/user/constants.ts b/src/redux/user/constants.ts index 9500f93..363e066 100644 --- a/src/redux/user/constants.ts +++ b/src/redux/user/constants.ts @@ -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', }; diff --git a/src/redux/user/reducer.ts b/src/redux/user/reducer.ts index 0e0fa97..b7a8ab5 100644 --- a/src/redux/user/reducer.ts +++ b/src/redux/user/reducer.ts @@ -57,6 +57,8 @@ interface IRootReducer { limit: 0, loading: boolean, list: Array, + step: number, + shift: number, filter: { title: '', starred: boolean, @@ -237,11 +239,14 @@ const searchSetTab: ActionHandler = (state, } }); -const searchPutRoutes: ActionHandler = (state, { list = [], min, max }) => ({ +const searchPutRoutes: ActionHandler = (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 = (state, { speed const setMarkersShown: ActionHandler = (state, { markers_shown = true }) => ({ ...state, markers_shown }); const setIsEmpty: ActionHandler = (state, { is_empty = true }) => ({ ...state, is_empty }); +const mapsSetShift: ActionHandler = (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, diff --git a/src/redux/user/sagas.ts b/src/redux/user/sagas.ts index 512e5f8..4460342 100644 --- a/src/redux/user/sagas.ts +++ b/src/redux/user/sagas.ts @@ -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 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= 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); } diff --git a/src/styles/dialogs.less b/src/styles/dialogs.less index d982860..33d0f05 100644 --- a/src/styles/dialogs.less +++ b/src/styles/dialogs.less @@ -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; diff --git a/src/utils/api.js b/src/utils/api.js index 58cf1bb..6237cfb 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -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 }));