diff --git a/.eslintignore b/.eslintignore
index 1521c8b76..53c37a166 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1 @@
-dist
+dist
\ No newline at end of file
diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js
index d1b25d0f1..5d21fc819 100644
--- a/client/coral-admin/src/AppRouter.js
+++ b/client/coral-admin/src/AppRouter.js
@@ -4,7 +4,7 @@ import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import ModerationQueue from 'containers/ModerationQueue';
import CommentStream from 'containers/CommentStream';
import Configure from 'containers/Configure';
-import CommunityContainer from 'containers/CommunityContainer';
+import CommunityContainer from 'containers/Community/CommunityContainer';
import LayoutContainer from 'containers/LayoutContainer';
const routes = (
diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js
new file mode 100644
index 000000000..06c8ab0f6
--- /dev/null
+++ b/client/coral-admin/src/actions/community.js
@@ -0,0 +1,42 @@
+import qs from 'qs';
+
+import {
+ FETCH_COMMENTERS_REQUEST,
+ FETCH_COMMENTERS_SUCCESS,
+ FETCH_COMMENTERS_FAILURE,
+ SORT_UPDATE,
+ COMMENTERS_NEW_PAGE
+} from '../constants/community';
+
+import {base, getInit, handleResp} from '../helpers/response';
+
+export const fetchCommenters = (query = {}) => dispatch => {
+ dispatch(requestFetchCommenters());
+ fetch(`${base}/user?${qs.stringify(query)}`, getInit('GET'))
+ .then(handleResp)
+ .then(({result, page, count, limit, totalPages}) =>
+ dispatch({
+ type: FETCH_COMMENTERS_SUCCESS,
+ commenters: result,
+ page,
+ count,
+ limit,
+ totalPages
+ })
+ )
+ .catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
+};
+
+const requestFetchCommenters = () => ({
+ type: FETCH_COMMENTERS_REQUEST
+});
+
+export const updateSorting = sort => ({
+ type: SORT_UPDATE,
+ sort
+});
+
+export const newPage = () => ({
+ type: COMMENTERS_NEW_PAGE
+});
+
diff --git a/client/coral-admin/src/components/Page.js b/client/coral-admin/src/components/Page.js
deleted file mode 100644
index 845c10cb8..000000000
--- a/client/coral-admin/src/components/Page.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import {Layout} from 'react-mdl';
-import 'material-design-lite';
-import Header from 'components/Header';
-
-export default (props) => (
-
-
-
-);
diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js
index b7c654bd4..86e622672 100644
--- a/client/coral-admin/src/components/ui/Header.js
+++ b/client/coral-admin/src/components/ui/Header.js
@@ -9,6 +9,9 @@ export default () => (
Moderate
Community
Configure
+
+ {`v${process.env.VERSION}`}
+
);
diff --git a/client/coral-admin/src/components/ui/Layout.css b/client/coral-admin/src/components/ui/Layout.css
new file mode 100644
index 000000000..779dec8b9
--- /dev/null
+++ b/client/coral-admin/src/components/ui/Layout.css
@@ -0,0 +1,4 @@
+.layout {
+ max-width: 1170px;
+ margin: 0 auto;
+}
\ No newline at end of file
diff --git a/client/coral-admin/src/components/ui/Layout.js b/client/coral-admin/src/components/ui/Layout.js
index e80d55c30..3e1b9cf2d 100644
--- a/client/coral-admin/src/components/ui/Layout.js
+++ b/client/coral-admin/src/components/ui/Layout.js
@@ -2,11 +2,14 @@ import React from 'react';
import {Layout as LayoutMDL} from 'react-mdl';
import Header from './Header';
import Drawer from './Drawer';
+import styles from './Layout.css';
export const Layout = ({children}) => (
- {children}
+
+ {children}
+
);
diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js
new file mode 100644
index 000000000..e628a14d6
--- /dev/null
+++ b/client/coral-admin/src/constants/community.js
@@ -0,0 +1,5 @@
+export const FETCH_COMMENTERS_REQUEST = 'FETCH_COMMENTERS_REQUEST';
+export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS';
+export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
+export const SORT_UPDATE = 'SORT_UPDATE';
+export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
diff --git a/client/coral-admin/src/containers/Community/Community.css b/client/coral-admin/src/containers/Community/Community.css
new file mode 100644
index 000000000..63148da7e
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/Community.css
@@ -0,0 +1,22 @@
+.dataTable {
+ width: 100%;
+}
+
+.roleButton {
+ display: block;
+}
+
+.searchInput {
+ display: block;
+ padding-left: 40px;
+ /*border: none;*/
+}
+
+.searchBox {
+ /*border: 1px solid rgba(0,0,0,.12);*/
+ background: white;
+}
+
+.email {
+ display: block;
+}
\ No newline at end of file
diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js
new file mode 100644
index 000000000..8e0b955a4
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/Community.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import I18n from 'coral-framework/i18n/i18n';
+import translations from '../../translations';
+import {Grid, Cell} from 'react-mdl';
+
+import styles from './Community.css';
+import Table from './Table';
+import Loading from './Loading';
+import NoResults from './NoResults';
+import Pager from './Pager';
+
+const lang = new I18n(translations);
+
+const tableHeaders = [
+ {
+ title: lang.t('community.username_and_email'),
+ field: 'displayName'
+ },
+ {
+ title: lang.t('community.account_creation_date'),
+ field: 'created_at'
+ }
+];
+
+const Community = ({isFetching, commenters, ...props}) => {
+ const hasResults = !isFetching && !!commenters.length;
+ return (
+
+
+
+ |
+
+ { isFetching && }
+ { !hasResults && }
+ { hasResults &&
+
+ }
+
+ |
+
+ );
+};
+
+export default Community;
diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js
new file mode 100644
index 000000000..cf67dba4d
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/CommunityContainer.js
@@ -0,0 +1,80 @@
+import React, {Component} from 'react';
+import {connect} from 'react-redux';
+import {
+ fetchCommenters,
+ updateSorting,
+ newPage,
+} from '../../actions/community';
+
+import Community from './Community';
+
+class CommunityContainer extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ searchValue: ''
+ };
+
+ this.onKeyDownHandler = this.onKeyDownHandler.bind(this);
+ this.onChangeHandler = this.onChangeHandler.bind(this);
+ this.onHeaderClickHandler = this.onHeaderClickHandler.bind(this);
+ this.onNewPageHandler = this.onNewPageHandler.bind(this);
+ }
+
+ onKeyDownHandler(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ this.search();
+ }
+ }
+
+ onChangeHandler(e) {
+ this.setState({
+ searchValue: e.target.value
+ });
+ }
+
+ search(query = {}) {
+ const {community} = this.props;
+
+ this.props.dispatch(fetchCommenters({
+ value: this.state.searchValue,
+ field: community.get('field'),
+ asc: community.get('asc'),
+ ...query
+ }));
+ }
+
+ componentDidMount() {
+ this.search();
+ }
+
+ onHeaderClickHandler(sort) {
+ this.props.dispatch(updateSorting(sort));
+ this.search();
+ }
+
+ onNewPageHandler(page) {
+ this.props.dispatch(newPage(page));
+ this.search({page});
+ }
+
+ render() {
+ const {searchValue} = this.state;
+ const {community} = this.props;
+ return (
+
+ );
+ }
+}
+
+export default connect(({community}) => ({community}))(CommunityContainer);
diff --git a/client/coral-admin/src/containers/Community/Loading.js b/client/coral-admin/src/containers/Community/Loading.js
new file mode 100644
index 000000000..beda271eb
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/Loading.js
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const Loading = () => (
+
Loading results
+);
+
+export default Loading;
diff --git a/client/coral-admin/src/containers/Community/NoResults.js b/client/coral-admin/src/containers/Community/NoResults.js
new file mode 100644
index 000000000..9e0663960
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/NoResults.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const NoResults = () => (
+
+ No users found with that user name or email address
+
+);
+
+export default NoResults;
diff --git a/client/coral-admin/src/containers/Community/Pager.css b/client/coral-admin/src/containers/Community/Pager.css
new file mode 100644
index 000000000..82e96f727
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/Pager.css
@@ -0,0 +1,10 @@
+.li {
+ display: inline-block;
+ margin-right: 5px;
+ padding: 0;
+ min-width: 30px;
+}
+
+.current {
+ background: #e3edf3;
+}
\ No newline at end of file
diff --git a/client/coral-admin/src/containers/Community/Pager.js b/client/coral-admin/src/containers/Community/Pager.js
new file mode 100644
index 000000000..3184cfca0
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/Pager.js
@@ -0,0 +1,45 @@
+import React, {PropTypes} from 'react';
+import styles from './Pager.css';
+
+const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) =>
+ onClickHandler(i + 1)}>
+ {i + 1}
+
+);
+
+const Pager = ({totalPages, page, onNewPageHandler}) => (
+
+
+ {
+ (totalPages > page) ?
+ - onNewPageHandler(page - 1)}>
+ Prev
+
+ :
+ null
+ }
+ {Rows(page, totalPages, onNewPageHandler)}
+ {
+ (page < totalPages) ?
+ - onNewPageHandler(page + 1)}>
+ Next
+
+ :
+ null
+ }
+
+
+);
+
+Pager.propTypes = {
+ totalPages: PropTypes.number.isRequired,
+ page: PropTypes.number.isRequired,
+};
+
+export default Pager;
+
diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js
new file mode 100644
index 000000000..a15a88723
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/Table.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import styles from './Community.css';
+
+const Table = ({headers, data, onHeaderClickHandler}) => (
+
+
+
+ {headers.map((header, i) =>(
+ | onHeaderClickHandler({field: header.field})}>
+ {header.title}
+ |
+ ))}
+
+
+
+ {data.map((row, i)=> (
+
+ |
+ {row.displayName}
+ {row.profiles.map(({id}) => id)}
+ |
+
+ {row.created_at}
+ |
+
+ ))}
+
+
+);
+
+export default Table;
diff --git a/client/coral-admin/src/containers/CommunityContainer.js b/client/coral-admin/src/containers/CommunityContainer.js
deleted file mode 100644
index b936edcec..000000000
--- a/client/coral-admin/src/containers/CommunityContainer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React, {Component} from 'react';
-
-export default class CommunityContainer extends Component {
- render() {
- return (
-
-
Community
-
- );
- }
-}
diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js
index 50f54a631..921af251d 100644
--- a/client/coral-admin/src/containers/LayoutContainer.js
+++ b/client/coral-admin/src/containers/LayoutContainer.js
@@ -1,18 +1,19 @@
-import React, {Component}from 'react';
+import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Layout} from '../components/ui/Layout';
class LayoutContainer extends Component {
render () {
- return ;
+ return ;
}
}
LayoutContainer.propTypes = {};
-const mapStateToProps = () => ({data: {}});
+const mapStateToProps = () => ({});
const mapDispatchToProps = (dispatch) => ({dispatch});
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
+
diff --git a/client/coral-admin/src/helpers/response.js b/client/coral-admin/src/helpers/response.js
new file mode 100644
index 000000000..439ba5a8a
--- /dev/null
+++ b/client/coral-admin/src/helpers/response.js
@@ -0,0 +1,27 @@
+export const base = '/api/v1';
+
+export const getInit = (method, body) => {
+ const headers = new Headers({
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ });
+
+ const init = {method, headers};
+ if (method.toLowerCase() !== 'get') {
+ init.body = JSON.stringify(body);
+ }
+
+ return init;
+};
+
+export const handleResp = res => {
+ if (res.status === 401) {
+ throw new Error('Not Authorized to make this request');
+ } else if (res.status > 399) {
+ throw new Error('Error! Status ', res.status);
+ } else if (res.status === 204) {
+ return res.text();
+ } else {
+ return res.json();
+ }
+};
diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js
index aed9d5748..84d1b84ad 100644
--- a/client/coral-admin/src/index.js
+++ b/client/coral-admin/src/index.js
@@ -1,4 +1,3 @@
-
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js
new file mode 100644
index 000000000..8d5dfd2c3
--- /dev/null
+++ b/client/coral-admin/src/reducers/community.js
@@ -0,0 +1,47 @@
+import {Map} from 'immutable';
+
+import {
+ FETCH_COMMENTERS_REQUEST,
+ FETCH_COMMENTERS_FAILURE,
+ FETCH_COMMENTERS_SUCCESS,
+ SORT_UPDATE
+} from '../constants/community';
+
+const initialState = Map({
+ community: Map(),
+ isFetching: false,
+ error: '',
+ commenters: [],
+ field: 'created_at',
+ asc: false,
+ totalPages: 0,
+ page: 0
+});
+
+export default function community (state = initialState, action) {
+ switch (action.type) {
+ case FETCH_COMMENTERS_REQUEST :
+ return state
+ .set('isFetching', true);
+ case FETCH_COMMENTERS_FAILURE :
+ return state
+ .set('isFetching', false)
+ .set('error', action.error);
+ case FETCH_COMMENTERS_SUCCESS : {
+ const {commenters, type, ...rest} = action; // eslint-disable-line
+ return state
+ .merge({
+ isFetching: false,
+ error: '',
+ ...rest
+ })
+ .set('commenters', commenters); // Sets to normal array
+ }
+ case SORT_UPDATE :
+ return state
+ .set('field', action.sort.field)
+ .set('asc', !state.get('asc'));
+ default :
+ return state;
+ }
+}
diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js
index 63176ca3b..1b29ced69 100644
--- a/client/coral-admin/src/reducers/index.js
+++ b/client/coral-admin/src/reducers/index.js
@@ -1,10 +1,12 @@
-
import {combineReducers} from 'redux';
import comments from 'reducers/comments';
import settings from 'reducers/settings';
+import community from 'reducers/community';
// Combine all reducers into a main one
export default combineReducers({
settings,
- comments
+ comments,
+ community
});
+
diff --git a/client/coral-admin/src/translations.js b/client/coral-admin/src/translations.js
index 5302c1221..67a8142fd 100644
--- a/client/coral-admin/src/translations.js
+++ b/client/coral-admin/src/translations.js
@@ -1,5 +1,9 @@
export default {
en: {
+ 'community': {
+ username_and_email: 'Username and Email',
+ account_creation_date: 'Account Creation Date'
+ },
'modqueue': {
'pending': 'pending',
'rejected': 'rejected',
@@ -24,6 +28,10 @@ export default {
}
},
es: {
+ 'community': {
+ username_and_email: 'Usuario y E-mail',
+ account_creation_date: 'Fecha de creación de la cuenta'
+ },
'modqueue': {
'pending': 'pendiente',
'rejected': 'rechazado',
diff --git a/models/user.js b/models/user.js
index ac642c910..c0a846f1a 100644
--- a/models/user.js
+++ b/models/user.js
@@ -25,6 +25,11 @@ const UserSchema = new mongoose.Schema({
}
}],
roles: [String]
+}, {
+ timestamps: {
+ createdAt: 'created_at',
+ updatedAt: 'updated_at'
+ }
});
// Add the indixies on the user profile data.
@@ -52,6 +57,22 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => {
return ret;
};
+/**
+ * toObject overrides to remove the password field from the toObject
+ * output.
+ */
+UserSchema.options.toObject = {};
+UserSchema.options.toObject.hide = 'password';
+UserSchema.options.toObject.transform = (doc, ret, options) => {
+ if (options.hide) {
+ options.hide.split(' ').forEach((prop) => {
+ delete ret[prop];
+ });
+ }
+
+ return ret;
+};
+
/**
* Finds a user given their email address that we have for them in the system
* and ensures that the retuned user matches the password passed in as well.
diff --git a/routes/api/index.js b/routes/api/index.js
index d161cf622..2fb489b88 100644
--- a/routes/api/index.js
+++ b/routes/api/index.js
@@ -6,5 +6,6 @@ router.use('/asset', require('./asset'));
router.use('/comments', require('./comments'));
router.use('/settings', require('./settings'));
router.use('/stream', require('./stream'));
+router.use('/user', require('./user'));
module.exports = router;
diff --git a/routes/api/user/index.js b/routes/api/user/index.js
new file mode 100644
index 000000000..18872eab7
--- /dev/null
+++ b/routes/api/user/index.js
@@ -0,0 +1,63 @@
+const express = require('express');
+const router = express.Router();
+const User = require('../../../models/user');
+
+router.get('/', (req, res, next) => {
+ const {
+ value = '',
+ field = 'created_at',
+ page = 1,
+ asc = 'false',
+ limit = 50 // Total Per Page
+ } = req.query;
+
+ let q = {
+ $or: [
+ {
+ 'displayName': {
+ $regex: new RegExp(`^${value}`),
+ $options: 'i'
+ },
+ 'profiles': {
+ $elemMatch: {
+ id: {
+ $regex: new RegExp(`^${value}`),
+ $options: 'i'
+ },
+ provider: 'local'
+ }
+ }
+ }
+ ]
+ };
+
+ Promise.all([
+ User.find(q)
+ .sort({[field]: (asc === 'true') ? 1 : -1})
+ .skip((page - 1) * limit)
+ .limit(limit),
+ User.count()
+ ])
+ .then(([data, count]) => {
+ const users = data.map((user) => {
+ const {displayName, created_at} = user;
+ return {
+ displayName,
+ created_at,
+ profiles: user.toObject().profiles
+ };
+ });
+
+ res.json({
+ result: users,
+ limit: Number(limit),
+ count,
+ page: Number(page),
+ totalPages: Math.ceil(count / limit)
+ });
+
+ })
+ .catch(next);
+});
+
+module.exports = router;
diff --git a/webpack.config.dev.js b/webpack.config.dev.js
index e2c92b396..8e2f253e5 100644
--- a/webpack.config.dev.js
+++ b/webpack.config.dev.js
@@ -82,6 +82,12 @@ module.exports = {
precss,
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
+ }),
+ new webpack.DefinePlugin({
+ 'process.env': {
+ 'NODE_ENV': `"${'development'}"`,
+ 'VERSION': `"${require('./package.json').version}"`
+ }
})
],
resolve: {
diff --git a/webpack.config.js b/webpack.config.js
index 434da160d..bf602733f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -8,7 +8,7 @@ devConfig.plugins = devConfig.plugins.concat([
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': `"${'production'}"`,
- 'VERSION': `"${require('./package.json')}"`
+ 'VERSION': `"${require('./package.json').version}"`
}
}),
new webpack.optimize.UglifyJsPlugin({