diff --git a/backend/api/v1/admin/index.js b/backend/api/v1/admin/index.js new file mode 100644 index 00000000..e0a91d88 --- /dev/null +++ b/backend/api/v1/admin/index.js @@ -0,0 +1,24 @@ +import express from 'express' + +module.exports = cache => { + const router = express.Router() + + router.use((req, res, next) => { + const isAdmin = req.isAuthenticated() && req.user.type === 'admin' + return next() + + // prevent unauthorized access + if (isAdmin) { + return next() + } + + res.status(401).send({ + success: false, + error: `You have not access to execute this action` + }) + }) + + router.use('/tags', require('./tags')(cache)) + + return router +} diff --git a/backend/api/v1/admin/tags.js b/backend/api/v1/admin/tags.js new file mode 100644 index 00000000..0b522686 --- /dev/null +++ b/backend/api/v1/admin/tags.js @@ -0,0 +1,93 @@ +import express from 'express' +import axios from 'axios' + +const env = process.env.NODE_ENV === 'dev' ? 'dev' : 'prod' +const c = require('../../../../config/config.json') +const base_url = c[env]['base_api'] + env + +const getTagsList = () => + axios.get(`${base_url}/drip/tag/list`).then(res => res.data) + +const addTag = tag => + axios + .post(`${base_url}/drip/tag/add`, { + name: tag.tag_name, + description: '' + }) + .then(res => res.data) + +const getUserTags = email => + axios + .get(`${base_url}/drip/user/tag/list?email=${email}`) + .then(res => res.data) + +const addUserTag = (tag_id, email) => + axios + .post(`${base_url}/drip/user/tag/add`, { tag_id, email }) + .then(res => res.data) + +const filterTags = (items, query) => { + if (query === '') { + return items + } + + return items.filter( + item => + item.tag_name.toLowerCase().indexOf(query) > -1 || + item.descrip.toLowerCase().indexOf(query) > -1 + ) +} + +module.exports = cache => { + const router = express.Router() + + router.get('/', (req, res) => { + let { tag = '' } = req.query + tag = tag.toLowerCase() + + getTagsList() + .then(data => filterTags(data, tag)) + .then(data => res.send(data)) + .catch(err => + res.status(500).end({ + error: true, + message: err.message + }) + ) + }) + + router.post('/', (req, res) => { + const tag = req.body + console.dir(tag) + + addTag(tag) + .then(data => res.send(data)) + .catch(err => + res.status(500).end({ + error: true, + message: err.message + }) + ) + }) + + router.get('/user', (req, res) => { + const { email } = req.query + + getUserTags(email) + .then(data => res.send(data)) + .catch(err => + res.status(500).end({ + error: true, + message: err.message + }) + ) + }) + + router.post('/user', (req, res) => { + res.send({ + success: true + }) + }) + + return router +} diff --git a/backend/api/v1/index.js b/backend/api/v1/index.js index 0a5c1536..f0bad193 100644 --- a/backend/api/v1/index.js +++ b/backend/api/v1/index.js @@ -21,10 +21,10 @@ module.exports = cache => { router.use('/files', require('./files')(cache)) router.use('/chat', require('./chat')(cache)) - // lets map to the start of url request router.use('/tse', require('./tse')(cache)) router.use('/jobs', require('./jobs')(cache)) router.use('/careers', require('./careers')(cache)) + router.use('/admin', require('./admin')(cache)) return router } diff --git a/shared/Forms/Common/ClearableInput.js b/shared/Forms/Common/ClearableInput.js new file mode 100644 index 00000000..8e2f1ff3 --- /dev/null +++ b/shared/Forms/Common/ClearableInput.js @@ -0,0 +1,124 @@ +import React, { Component } from 'react' +import styled from 'styled-components' + +export default class ClearableInput extends Component { + static defaultProps = { + error: false, + placeholder: '', + immediate: true, + onBlur: () => {}, + onChange: () => {} + } + state = { + value: '' + } + initRef = ref => (this.input = ref) + + handleChange = e => { + const { immediate, onChange } = this.props + const value = e.target ? e.target.value : e + this.setState({ value }) + + if (immediate) { + onChange(value) + } + } + + handleKeyDown = e => { + const { onChange } = this.props + if (e.keyCode === 13) { + onChange(this.state.value) + } + } + clear = () => { + const { onChange } = this.props + const value = '' + this.setState({ value }) + + onChange(value) + } + blur = () => { + this.props.onBlur(this.state.value) + } + + onFocus() { + this.input.focus() + } + + componentWillReceiveProps(nextProps) { + if (this.props.value !== nextProps.value) { + this.setState({ value: nextProps.value }) + } + } + + componentDidMount() { + if (this.props.autoFocus) { + this.props.autoFocus && this.onFocus() + } + + this.state.value = this.props.value + } + + isEmpty = () => this.state.value.trim() === '' + + render() { + const { error, placeholder } = this.props + const { value } = this.state + return ( + + + {!this.isEmpty() && ×} + + ) + } +} + +export const Wrapper = styled.div` + width: 100%; + position: relative; + background: #fff; + border: 1px solid #d7d9d9; + + border-radius: 4px; + overflow: hidden; + width: 100%; + display: flex; + + ${props => + props.error && + ` + color: indianred; + `}; +` + +export const Input = styled.input` + flex: 1; + padding: 8px 26px 8px 16px; + border: none; +` + +const Close = styled.button` + position: absolute; + right: 10px; + top: 7px; + width: 16px; + border: none; + color: #999; + text-align: center; + padding: 0px; + font-size: 18px; + + text-align: center; + vertical-align: top; + + &:hover { + color: #d0021b; + } +` diff --git a/shared/components/admin/AdminMenu.js b/shared/components/admin/AdminMenu.js index 4844e496..86261242 100644 --- a/shared/components/admin/AdminMenu.js +++ b/shared/components/admin/AdminMenu.js @@ -58,6 +58,9 @@ class AdminMenu extends Component {
  • Send
  • +
  • + Tag Users +
  • diff --git a/shared/components/admin/AdminTagUsers.js b/shared/components/admin/AdminTagUsers.js new file mode 100644 index 00000000..e4f40a38 --- /dev/null +++ b/shared/components/admin/AdminTagUsers.js @@ -0,0 +1,29 @@ +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import AdminLayout from './AdminLayout' +import TagUsers from './TagUsers' +import { changePageTitle } from '../../Layout/Actions/LayoutActions' + +class AdminTagUsers extends Component { + static getPageMeta = () => ({ + title: 'Tag Users' + }) + + componentDidMount() { + const { title } = AdminTagUsers.getPageMeta() + this.props.dispatch(changePageTitle(title)) + } + + render() { + const { history } = this.props + + return ( + +

    Tag Users

    + + +
    + ) + } +} +export default connect(state => ({}))(AdminTagUsers) diff --git a/shared/components/admin/TagUsers.js b/shared/components/admin/TagUsers.js new file mode 100644 index 00000000..c6815f53 --- /dev/null +++ b/shared/components/admin/TagUsers.js @@ -0,0 +1,92 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import styled from 'styled-components' +import UsersSearch from './tags/UsersSearch' +import TagsSelect from './tags/TagsSelect' +import { + loadUserTags, + selectTagsUser, + clearUserTags +} from '../../reducers/AdminReducer' + +const Box = ({ title, children, ...rest }) => ( + + {title && {title}} + {children} + +) + +class TagUsers extends Component { + defaultProps = { + selectedUser: {} + } + + selectUser = (user, valid) => { + this.props.dispatch(selectTagsUser(user)) + + if (valid) { + this.loadUserTags(user) + } else { + this.props.dispatch(clearUserTags()) + } + } + + loadUserTags = user => { + this.props.dispatch(loadUserTags(user)) + } + + update = () => alert('in dev..') + + render() { + const { selectedUser, userTags } = this.props + return ( + + Manage Tags + + + + + {JSON.stringify(userTags)} + + {selectedUser && } + + + + + Update + + + + ) + } +} + +export default connect(state => ({ + selectedUser: state.admin.getIn(['tags', 'user']), + userTags: state.admin.getIn(['tags', 'list']) +}))(TagUsers) + +const Container = styled.section`` + +const Heading = styled.h4`` + +const BoxWrapper = styled.div` + margin-top: 2em; +` + +const BoxTitle = styled.div`` + +const BoxContent = styled.div`` + +const Buttons = styled.div` + margin-top: 2em; +` + +const UpdateButton = styled.button` + height: 40px; + background: #f0d943; + font-size: 16px; + color: #333333; + border: none; + border-radius: 5px; +` diff --git a/shared/components/admin/tags/TagsSelect.js b/shared/components/admin/tags/TagsSelect.js new file mode 100644 index 00000000..f855eaba --- /dev/null +++ b/shared/components/admin/tags/TagsSelect.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react' +import axios from 'axios' +import { AsyncCreatable } from 'react-select' + +const getUserTags = tag => + axios.get(`/api/v1/admin/tags?tag=${tag}`).then(res => res.data) + +const addTag = tag => + axios.post(`/api/v1/admin/tags`, tag).then(res => res.data) + +export default class TagsSelect extends Component { + state = { + selectedOptions: this.props.value || [] + } + getTags = search => { + if (!search) { + return Promise.resolve({ options: [] }) + } + + return getUserTags(search).then(options => ({ options })) + } + createOption = data => { + addTag(data).then(result => { + if (result.error) { + console.error(result.error) + } else { + const option = { + ...data, + tag_id: result.tag_id + } + + this.setState(prevState => ({ + selectedOptions: [...prevState.selectedOptions, option] + })) + } + }) + } + update = selectedOptions => this.setState({ selectedOptions }) + + componentWillReceiveProps(nextProps) { + if (this.props.value !== nextProps.value) { + this.setState({ + selectedOptions: nextProps.value + }) + } + } + + render() { + const { selectedOptions } = this.state + + return ( + + ) + } +} diff --git a/shared/components/admin/tags/UsersSearch.js b/shared/components/admin/tags/UsersSearch.js new file mode 100644 index 00000000..da5a2342 --- /dev/null +++ b/shared/components/admin/tags/UsersSearch.js @@ -0,0 +1,53 @@ +import React, { Component } from 'react' +import styled from 'styled-components' +import ClearableInput from '../../../Forms/Common/ClearableInput' + +export default class UsersSearch extends Component { + state = { + error: null + } + + defaultProps = { + onChange: () => {} + } + + setError = error => this.setState({ error }) + + validate = val => { + const valid = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(val) + + if (!valid) { + this.setError('Invalid email format') + } else { + this.setError(null) + } + + return valid + } + + onChange = value => { + const valid = this.validate(value) + this.props.onChange(value, valid) + } + + render() { + const { error } = this.state + + return ( + + + {error && {error}} + + ) + } +} + +const Wrapper = styled.div`` + +const Error = styled.div` + color: indianred; +` diff --git a/shared/reducers/AdminReducer.js b/shared/reducers/AdminReducer.js index 20226737..9f6f4f5e 100644 --- a/shared/reducers/AdminReducer.js +++ b/shared/reducers/AdminReducer.js @@ -4,7 +4,7 @@ import querystring from 'querystring' import axios from 'axios' import snserror from '../SnsUtil' import PrintfulClient from '../printful/printfulclient' -import Request from "../Request"; +import Request from '../Request' var env = process.env.NODE_ENV === 'dev' ? 'dev' : 'prod' @@ -18,6 +18,13 @@ export const ADD_JOB = 'ADD_JOB' export const ADD_JOB_SUCCESS = 'ADD_JOB_SUCCESS' export const ADD_JOB_FAIL = 'ADD_JOB_FAIL' +export const SELECT_TAGS_USER = 'SELECT_TAGS_USER' + +export const CLEAR_USER_TAGS = 'CLEAR_USER_TAGS' +export const LOAD_USER_TAGS = 'LOAD_USER_TAGS' +export const LOAD_USER_TAGS_SUCCESS = 'LOAD_USER_TAGS_SUCCESS' +export const LOAD_USER_TAGS_FAIL = 'LOAD_USER_TAGS_FAIL' + const init = { email_send_msg: '', pending_blogs: [], @@ -72,6 +79,10 @@ const init = { processing: false, success: false, error: null + }, + tags: { + user: null, + list: [] } } @@ -335,7 +346,7 @@ export default function adminReducer(state = defaultState, action) { snserror('RELATED_CONTENT_DELETE', errorMsg) }) break - + case ADD_JOB: nstate.jobs.processing = true nstate.jobs.success = false @@ -351,12 +362,24 @@ export default function adminReducer(state = defaultState, action) { nstate.jobs.success = false nstate.jobs.error = action.payload.error break + + case SELECT_TAGS_USER: + nstate.tags.user = action.payload.user + break + + case LOAD_USER_TAGS_SUCCESS: + nstate.tags.list = action.payload.result + break + + case LOAD_USER_TAGS: + case LOAD_USER_TAGS_FAIL: + nstate.tags.list = [] + break } return Immutable.fromJS(nstate) } - -export const addJob = (job) => { +export const addJob = job => { return dispatch => { dispatch(addJobRequest(job)) @@ -382,3 +405,40 @@ export const addJobFail = error => ({ type: ADD_JOB_FAIL, payload: { error } }) + +export const selectTagsUser = user => ({ + type: SELECT_TAGS_USER, + payload: { user } +}) + +export const clearUserTags = user => ({ + type: CLEAR_USER_TAGS, + payload: { user } +}) + +export const loadUserTags = user => { + return dispatch => { + dispatch(loadUserTagsRequest(user)) + + Request.get(`/api/v1/admin/tags/user?email=${user}`) + .then(result => dispatch(loadUserTagsSuccess(result.data))) + .catch(err => dispatch(loadUserTagsFail(err.data.error))) + } +} + +export const loadUserTagsRequest = user => ({ + type: LOAD_USER_TAGS, + payload: { + ...user + } +}) + +export const loadUserTagsSuccess = result => ({ + type: LOAD_USER_TAGS_SUCCESS, + payload: { result } +}) + +export const loadUserTagsFail = error => ({ + type: LOAD_USER_TAGS_FAIL, + payload: { error } +}) diff --git a/shared/reducers/SiteReducer.js b/shared/reducers/SiteReducer.js index c901401f..6bdf304b 100644 --- a/shared/reducers/SiteReducer.js +++ b/shared/reducers/SiteReducer.js @@ -7,7 +7,7 @@ import { clone } from 'lodash' * Whenever some reducer initial state have been changed update current schema version * Unique version number is total count of commits in `master` */ -export const SCHEMA_VER = 'v1954' +export const SCHEMA_VER = 'v1955' const randomSessionId = () => v4() diff --git a/shared/routes.jsx b/shared/routes.jsx index 6ee0e4db..1039870e 100644 --- a/shared/routes.jsx +++ b/shared/routes.jsx @@ -58,6 +58,7 @@ import AdminCmsRecent from 'components/admin/AdminCmsRecent' import AdminCmsFeature from 'components/admin/AdminCmsFeature' import AdminCmsRecentRelated from 'components/admin/AdminCmsRecentRelated' import AdminEmailsSend from 'components/admin/AdminEmailsSend' +import AdminTagUsers from 'components/admin/AdminTagUsers' import AdminOrdersNew from 'components/admin/AdminOrdersNew' import AdminOrdersProcessing from 'components/admin/AdminOrdersProcessing' import AdminAddJob from 'components/admin/AdminAddJob' @@ -284,6 +285,10 @@ export default ( + + + +