diff --git a/backend/models/Route.js b/backend/models/Route.js index 9be11cf..32c3696 100644 --- a/backend/models/Route.js +++ b/backend/models/Route.js @@ -13,6 +13,7 @@ const RouteSchema = new Schema( owner: { type: Schema.Types.ObjectId, ref: 'User' }, distance: { type: Number, default: 0 }, is_public: { type: Boolean, default: false }, + is_starred: { type: Boolean, default: false }, is_deleted: { type: Boolean, default: false }, created_at: { type: Date, default: Date.now() }, updated_at: { type: Date, default: Date.now() }, diff --git a/backend/routes/route.js b/backend/routes/route.js index 65c49a3..c340d14 100644 --- a/backend/routes/route.js +++ b/backend/routes/route.js @@ -4,9 +4,11 @@ const get = require('./route/get'); const list = require('./route/list'); const drop = require('./route/drop'); const patch = require('./route/patch'); +const star = require('./route/star'); const router = express.Router(); +router.post('/star', star); router.post('/', post); router.get('/', get); router.patch('/', patch); diff --git a/backend/routes/route/list.js b/backend/routes/route/list.js index 12f559e..50198ab 100644 --- a/backend/routes/route/list.js +++ b/backend/routes/route/list.js @@ -3,10 +3,12 @@ const { Route, User } = require('../../models'); module.exports = async (req, res) => { const { query: { - id, token, title, distance, author, step = 20, shift = 0, + id, token, title, distance, author, step = 20, shift = 0, starred, } } = req; + const is_starred = parseInt(starred, 10) === 1; + const user = await User.findOne({ _id: id, token }); let criteria = { is_deleted: false }; @@ -14,13 +16,21 @@ module.exports = async (req, res) => { if (title) { criteria = { ...criteria, - $or: [ + $and: [ { title: new RegExp(title.trim(), 'ig') }, { _id: new RegExp(title.trim(), 'ig') }, ], }; } + console.log('is starred?', starred); + if (is_starred) { + criteria = { + ...criteria, + is_starred: true, + }; + } + if (!author || !user || (user._id !== author)) { criteria = { ...criteria, @@ -32,7 +42,7 @@ module.exports = async (req, res) => { { ...criteria, }, - '_id title distance owner updated_at is_public is_deleted', + '_id title distance owner updated_at is_public is_deleted is_starred', { limit: 9000, sort: { updated_at: -1 }, diff --git a/backend/routes/route/patch.js b/backend/routes/route/patch.js index 47eeecb..3abbdb1 100644 --- a/backend/routes/route/patch.js +++ b/backend/routes/route/patch.js @@ -17,7 +17,7 @@ module.exports = async (req, res) => { 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(); + await exists.set({ title, is_public }).save(); return res.send({ success: true, ...exists }); }; diff --git a/backend/routes/route/post.js b/backend/routes/route/post.js index cc92c0a..f2ac8af 100644 --- a/backend/routes/route/post.js +++ b/backend/routes/route/post.js @@ -27,7 +27,7 @@ module.exports = async (req, res) => { if (exists && !force) return res.send({ success: false, mode: 'overwriting' }); if (exists) { - exists.set({ + await exists.set({ title, route, stickers, logo, distance, updated_at: Date.now(), provider, is_public, }).save(); diff --git a/backend/routes/route/star.js b/backend/routes/route/star.js new file mode 100644 index 0000000..aa96fce --- /dev/null +++ b/backend/routes/route/star.js @@ -0,0 +1,19 @@ +const { User, Route } = require('../../models'); + +module.exports = async (req, res) => { + const { body, body: { id, token, address } } = req; + + const owner = await User.findOne({ _id: id, token }).populate('routes'); + + if (!owner || owner.role !== 'admin') return res.send({ success: false, reason: 'unauthorized' }); + + const is_starred = !!body.is_starred; + + const exists = await Route.findOne({ _id: address }).populate('owner', '_id'); + if (!exists) return res.send({ success: false, mode: 'not_exists' }); + + await exists.set({ is_starred }).save(); + + return res.send({ success: true, ...exists }); +}; + diff --git a/src/components/dialogs/MapListDialog.tsx b/src/components/dialogs/MapListDialog.tsx index c6b8b88..968a233 100644 --- a/src/components/dialogs/MapListDialog.tsx +++ b/src/components/dialogs/MapListDialog.tsx @@ -10,7 +10,7 @@ import { setDialogActive, mapsLoadMore, dropRoute, - modifyRoute, + modifyRoute, toggleRouteStarred, } from '$redux/user/actions'; import { isMobile } from '$utils/window'; import classnames from 'classnames'; @@ -21,12 +21,14 @@ import { TABS } from '$constants/dialogs'; import { Icon } from '$components/panels/Icon'; import { pushPath } from '$utils/history'; import { IRootState, IRouteListItem } from '$redux/user/reducer'; +import { ROLES } from "$constants/auth"; export interface IMapListDialogProps extends IRootState { marks: { [x: number]: string }, routes_sorted: Array, routes: IRootState['routes'], ready: IRootState['ready'], + role: IRootState['user']['role'], mapsLoadMore: typeof mapsLoadMore, searchSetDistance: typeof searchSetDistance, @@ -35,6 +37,7 @@ export interface IMapListDialogProps extends IRootState { setDialogActive: typeof setDialogActive, dropRoute: typeof dropRoute, modifyRoute: typeof modifyRoute, + toggleRouteStarred: typeof toggleRouteStarred, } export interface IMapListDialogState { @@ -117,9 +120,12 @@ class Component extends React.Component this.props.toggleRouteStarred(id); + render() { const { ready, + role, routes: { list, loading, @@ -136,6 +142,8 @@ class Component extends React.Component { list.length === 0 && loading && @@ -156,7 +164,7 @@ class Component extends React.Component { - Object.keys(TABS).map(item => ( + Object.keys(TABS).map(item => (role === ROLES.admin || item !== 'all') && (
this.props.searchSetTab(item)} @@ -208,6 +216,7 @@ class Component extends React.Component )) } @@ -233,13 +244,15 @@ class Component extends React.Component { +const mapStateToProps = ({ user: { editing, routes, user: { role } } }: { user: IRootState }) => { if (routes.filter.max >= 9999) { return { - routes, editing, marks: {}, ready: false, + routes, editing, marks: {}, ready: false, role, }; } + return ({ + role, routes, editing, ready: true, @@ -260,6 +273,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({ mapsLoadMore, dropRoute, modifyRoute, + toggleRouteStarred, }, dispatch); export const MapListDialog = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/maps/RouteRowView.tsx b/src/components/maps/RouteRowView.tsx index 703b720..8c00093 100644 --- a/src/components/maps/RouteRowView.tsx +++ b/src/components/maps/RouteRowView.tsx @@ -5,15 +5,20 @@ import { MapListDialog } from "$components/dialogs/MapListDialog"; import { Tooltip } from "$components/panels/Tooltip"; import { ReactElement } from "react"; import classnames from 'classnames'; +import { toggleRouteStarred } from "$redux/user/actions"; interface Props { - _id: string, tab: string, + + _id: string, title: string, distance: number, is_public: boolean, + is_admin: boolean, + is_starred: boolean, openRoute: typeof MapListDialog.openRoute, + toggleStarred: typeof MapListDialog.toggleStarred, startEditing: typeof MapListDialog.startEditing, stopEditing: typeof MapListDialog.stopEditing, showMenu: typeof MapListDialog.showMenu, @@ -22,11 +27,21 @@ interface Props { } export const RouteRowView = ({ - title, distance, _id, openRoute, tab, startEditing, showMenu, showDropCard, hideMenu, + title, distance, _id, openRoute, tab, startEditing, showMenu, showDropCard, hideMenu, is_admin, is_starred, toggleStarred, }: Props): ReactElement => (
+ { + (tab === 'all' || tab === 'starred') && is_admin && +
+ { + is_starred + ? + : + } +
+ }
openRoute(_id)} diff --git a/src/components/maps/RouteRowWrapper.tsx b/src/components/maps/RouteRowWrapper.tsx index a2cc981..a8379d4 100644 --- a/src/components/maps/RouteRowWrapper.tsx +++ b/src/components/maps/RouteRowWrapper.tsx @@ -12,7 +12,9 @@ interface Props { title: string, distance: number, is_public: boolean, + is_starred: boolean, + is_admin: boolean, is_editing_target: boolean, is_menu_target: boolean, @@ -24,6 +26,7 @@ interface Props { showDropCard: typeof MapListDialog.showDropCard, dropRoute: typeof MapListDialog.dropRoute, modifyRoute: typeof MapListDialog.modifyRoute, + toggleStarred: typeof MapListDialog.toggleStarred, is_editing_mode: 'edit' | 'drop', } @@ -31,7 +34,7 @@ interface Props { 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, + dropRoute, stopEditing, modifyRoute, hideMenu, is_admin, is_starred, toggleStarred, }: Props): ReactElement => (
}
diff --git a/src/constants/api.ts b/src/constants/api.ts index f8b8b05..f2aafdc 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -10,4 +10,5 @@ export const API: { [x: string]: string } = { DROP_ROUTE: `${CLIENT.API_ADDR}/route`, MODIFY_ROUTE: `${CLIENT.API_ADDR}/route/modify`, + SET_STARRED: `${CLIENT.API_ADDR}/route/star`, }; diff --git a/src/constants/auth.ts b/src/constants/auth.ts index 32fcb90..2b1e41e 100644 --- a/src/constants/auth.ts +++ b/src/constants/auth.ts @@ -1,6 +1,7 @@ export interface IRoles { guest: string, vk: string, + admin: string, } export interface IUser { @@ -24,6 +25,7 @@ export interface IUser { export const ROLES: IRoles = { guest: 'guest', vk: 'vk', + admin: 'admin', }; export const DEFAULT_USER: IUser = { diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 5c8fc6b..8f10cc8 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -7,6 +7,7 @@ export interface IDialogs { export interface IMapTabs { mine: string, all: string, + starred: string, } export const DIALOGS: IDialogs = ({ @@ -17,5 +18,6 @@ export const DIALOGS: IDialogs = ({ export const TABS: IMapTabs = ({ mine: 'Мои', - all: 'Общие', + starred: 'Общие', + all: 'ВСЕ', }); diff --git a/src/redux/user/actions.ts b/src/redux/user/actions.ts index 60ab5f2..9dd9a00 100644 --- a/src/redux/user/actions.ts +++ b/src/redux/user/actions.ts @@ -76,3 +76,5 @@ export const dropRoute = (_id: string) => ({ type: ACTIONS.DROP_ROUTE, _id }); export const modifyRoute = (_id: string, { title, is_public }: { title: string, is_public: boolean }) => ({ type: ACTIONS.MODIFY_ROUTE, _id, title, is_public }); +export const toggleRouteStarred = (_id: string) => ({ type: ACTIONS.TOGGLE_ROUTE_STARRED, _id }); +export const setRouteStarred = (_id: string, is_starred: boolean) => ({ type: ACTIONS.SET_ROUTE_STARRED, _id, is_starred }); diff --git a/src/redux/user/constants.ts b/src/redux/user/constants.ts index d5dfad9..2429a38 100644 --- a/src/redux/user/constants.ts +++ b/src/redux/user/constants.ts @@ -81,4 +81,6 @@ export const ACTIONS: IActions = { DROP_ROUTE: 'DROP_ROUTE', MODIFY_ROUTE: 'MODIFY_ROUTE', + SET_ROUTE_STARRED: 'SET_ROUTE_STARRED', + TOGGLE_ROUTE_STARRED: 'TOGGLE_ROUTE_STARRED', }; diff --git a/src/redux/user/reducer.ts b/src/redux/user/reducer.ts index 1c0deaf..137d49b 100644 --- a/src/redux/user/reducer.ts +++ b/src/redux/user/reducer.ts @@ -14,6 +14,7 @@ export interface IRouteListItem { title: string, distance: number, is_public: boolean, + is_starred: boolean, updated_at: string, } @@ -304,6 +305,18 @@ const setIsRouting: ActionHandler = (state, is_routing, }); +const setRouteStarred: ActionHandler = (state, { _id, is_starred }) => ({ + ...state, + routes: { + ...state.routes, + list: ( + state.routes.list + .map(el => el._id === _id ? { ...el, is_starred } : el) + .filter(el => state.routes.filter.tab !== 'starred' || el.is_starred) + ) + } +}); + const HANDLERS = ({ [ACTIONS.SET_USER]: setUser, [ACTIONS.SET_EDITING]: setEditing, @@ -348,6 +361,8 @@ const HANDLERS = ({ [ACTIONS.SET_FEATURE]: setFeature, [ACTIONS.SET_IS_ROUTING]: setIsRouting, + + [ACTIONS.SET_ROUTE_STARRED]: setRouteStarred, }); export const INITIAL_STATE: IRootReducer = { diff --git a/src/redux/user/sagas.ts b/src/redux/user/sagas.ts index d9cdfa6..90d1489 100644 --- a/src/redux/user/sagas.ts +++ b/src/redux/user/sagas.ts @@ -6,7 +6,7 @@ import { checkUserToken, dropRoute, getGuestToken, getRouteList, getStoredMap, modifyRoute, - postMap + postMap, sendRouteStarred } from '$utils/api'; import { hideRenderer, @@ -32,7 +32,7 @@ import { setProvider, changeProvider, setSaveLoading, - mapsSetShift, searchChangeDistance, clearAll, setFeature, searchSetTitle, + mapsSetShift, searchChangeDistance, clearAll, setFeature, searchSetTitle, setRouteStarred, } from '$redux/user/actions'; import { getUrlData, parseQuery, pushLoaderState, pushNetworkInitError, pushPath, replacePath } from '$utils/history'; import { editor } from '$modules/Editor'; @@ -51,7 +51,7 @@ import { } from '$utils/renderer'; import { ILogos, LOGOS } from '$constants/logos'; import { DEFAULT_PROVIDER } from '$constants/providers'; -import { DIALOGS } from '$constants/dialogs'; +import { DIALOGS, TABS } from '$constants/dialogs'; import * as ActionCreators from '$redux/user/actions'; import { IRootState } from "$redux/user/reducer"; @@ -501,7 +501,7 @@ function* searchGetRoutes() { const { routes: { step, shift, filter: { title, distance, tab } } } = yield select(getState); - const result = yield call(getRouteList, { + return yield call(getRouteList, { id, token, title, @@ -509,10 +509,8 @@ function* searchGetRoutes() { step, shift, author: tab === 'mine' ? id : '', - starred: tab === 'starred', + starred: tab === 'starred' ? 1 : 0, }); - - return result; } function* searchSetSagaWorker() { @@ -697,6 +695,17 @@ function* modifyRouteSaga({ _id, title, is_public }: ReturnType) { + const { routes: { list } } = yield select(getState); + const route = list.find(el => el._id === _id); + const { id, token } = yield select(getUser); + + yield put(setRouteStarred(_id, !route.is_starred)); + const result = yield sendRouteStarred({ id, token, _id, is_starred: !route.is_starred }); + + if (!result) return yield put(setRouteStarred(_id, route.is_starred)); +} + export function* userSaga() { yield takeLatest(REHYDRATE, authCheckSaga); yield takeEvery(ACTIONS.SET_MODE, setModeSaga); @@ -744,4 +753,5 @@ export function* userSaga() { yield takeLatest(ACTIONS.DROP_ROUTE, dropRouteSaga); yield takeLatest(ACTIONS.MODIFY_ROUTE, modifyRouteSaga); + yield takeLatest(ACTIONS.TOGGLE_ROUTE_STARRED, toggleRouteStarredSaga); } diff --git a/src/sprites/icon.svg b/src/sprites/icon.svg index 96b9fa0..41e267a 100644 --- a/src/sprites/icon.svg +++ b/src/sprites/icon.svg @@ -384,6 +384,16 @@ + + + + + + + + + + diff --git a/src/styles/dialogs.less b/src/styles/dialogs.less index 4bc4f51..66203a3 100644 --- a/src/styles/dialogs.less +++ b/src/styles/dialogs.less @@ -196,7 +196,7 @@ } &.is_menu_target { - .route-row { + .route-row, .route-row-fav { transform: translateX(-120px); } @@ -222,6 +222,7 @@ overflow: hidden; transition: height 500ms; position: relative; + display: flex; &.has_menu { padding-right: 32px; @@ -285,6 +286,21 @@ } } +.route-row-fav { + width: 32px; + display: flex; + align-items: center; + justify-content: center; + fill: fade(white, 30%); + background: fade(white, 5%); + cursor: pointer; + transition: background 250ms, transform 500ms; + + &:hover { + background: fade(white, 10%); + } +} + .route-row-edit-menu { width: 0; height: 100%; diff --git a/src/utils/api.ts b/src/utils/api.ts index 8f2f53f..66650c1 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -29,7 +29,7 @@ interface IGetRouteList { author: IRootState['routes']['filter']['author'], step: IRootState['routes']['step'], shift: IRootState['routes']['step'], - starred: IRootState['routes']['filter']['starred'], + starred: number, id: IRootState['user']['id'], token: IRootState['user']['token'], } @@ -112,3 +112,13 @@ export const modifyRoute = ( ): AxiosPromise => ( axios.patch(API.DROP_ROUTE, { address, id, token, title, is_public }) ); + +export const sendRouteStarred = ( + { id, token, _id, is_starred }: + { id: string, token: string, _id: string, is_starred: boolean } +): Promise => ( + axios.post(API.SET_STARRED, { id, token, address: _id, is_starred }) + .then(() => true) + .catch(() => true) +); +