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 (
+
+
+
+