Community Section - Search Commenters (#60)

* Table

* úsers api

* Ádding envs and version

* Community Table Added

* Ádding redux

* Adding loading data state

* Community Actions

* Adding Inmutable

* Fetching commenters

* Search commenters

* Linting and translations

* Ádding Sort

* sortBuilder

* pagr

* request per page, pager and mor

* package.json

* new Page actiokn

* éslint

* Changes and cleaner comps

* removed console.log
This commit is contained in:
Belén Curcio
2016-11-11 11:14:26 -05:00
committed by Dan Zajdband
parent 4a704fa85c
commit 5b74beabd6
28 changed files with 517 additions and 33 deletions
+1 -1
View File
@@ -1 +1 @@
dist
dist
+1 -1
View File
@@ -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 = (
@@ -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
});
-12
View File
@@ -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) => (
<Layout>
<Header>
{props.children}
</Header>
</Layout>
);
@@ -9,6 +9,9 @@ export default () => (
<Link className={styles.navLink} to="/admin">Moderate</Link>
<Link className={styles.navLink} to="/admin/community">Community</Link>
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
<span>
{`v${process.env.VERSION}`}
</span>
</Navigation>
</Header>
);
@@ -0,0 +1,4 @@
.layout {
max-width: 1170px;
margin: 0 auto;
}
@@ -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}) => (
<LayoutMDL fixedDrawer>
<Header />
<Drawer />
{children}
<div className={styles.layout} >
{children}
</div>
</LayoutMDL>
);
@@ -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';
@@ -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;
}
@@ -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 (
<Grid>
<Cell col={4}>
<form action="">
<div className={`mdl-textfield ${styles.searchBox}`}>
<label className="mdl-button mdl-js-button mdl-button--icon" htmlFor="commenters-search">
<i className="material-icons">search</i>
</label>
<div className="">
<input
id="commenters-search"
className={`mdl-textfield__input ${styles.searchInput}`}
type="text"
value={props.searchValue}
onKeyDown={props.onKeyDownHandler}
onChange={props.onChangeHandler}
/>
</div>
</div>
</form>
</Cell>
<Cell col={8}>
{ isFetching && <Loading /> }
{ !hasResults && <NoResults /> }
{ hasResults &&
<Table
headers={tableHeaders}
data={commenters}
onHeaderClickHandler={props.onHeaderClickHandler}
/>
}
<Pager
totalPages={props.totalPages}
page={props.page}
onNewPageHandler={props.onNewPageHandler}
/>
</Cell>
</Grid>
);
};
export default Community;
@@ -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 (
<Community
searchValue={searchValue}
commenters={community.get('commenters')}
isFetching={community.get('isFetching')}
error={community.get('error')}
totalPages={community.get('totalPages')}
page={community.get('page')}
{...this}
/>
);
}
}
export default connect(({community}) => ({community}))(CommunityContainer);
@@ -0,0 +1,7 @@
import React from 'react';
const Loading = () => (
<h1> Loading results </h1>
);
export default Loading;
@@ -0,0 +1,9 @@
import React from 'react';
const NoResults = () => (
<div>
No users found with that user name or email address
</div>
);
export default NoResults;
@@ -0,0 +1,10 @@
.li {
display: inline-block;
margin-right: 5px;
padding: 0;
min-width: 30px;
}
.current {
background: #e3edf3;
}
@@ -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) =>
<li className={`mdl-button mdl-js-button ${styles.li} ${curr === i ? styles.current : ''}`}
key={i} onClick={() => onClickHandler(i + 1)}>
{i + 1}
</li>
);
const Pager = ({totalPages, page, onNewPageHandler}) => (
<div className="pager">
<ul>
{
(totalPages > page) ?
<li
className={`mdl-button mdl-js-button ${styles.li}`}
onClick={() => onNewPageHandler(page - 1)}>
Prev
</li>
:
null
}
{Rows(page, totalPages, onNewPageHandler)}
{
(page < totalPages) ?
<li
className={`mdl-button mdl-js-button ${styles.li}`}
onClick={() => onNewPageHandler(page + 1)}>
Next
</li>
:
null
}
</ul>
</div>
);
Pager.propTypes = {
totalPages: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
};
export default Pager;
@@ -0,0 +1,34 @@
import React from 'react';
import styles from './Community.css';
const Table = ({headers, data, onHeaderClickHandler}) => (
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i)=> (
<tr key={i}>
<td className="mdl-data-table__cell--non-numeric">
{row.displayName}
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{row.created_at}
</td>
</tr>
))}
</tbody>
</table>
);
export default Table;
@@ -1,11 +0,0 @@
import React, {Component} from 'react';
export default class CommunityContainer extends Component {
render() {
return (
<div>
<h1>Community</h1>
</div>
);
}
}
@@ -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 <Layout { ...this.props } />;
return <Layout {...this.props} />;
}
}
LayoutContainer.propTypes = {};
const mapStateToProps = () => ({data: {}});
const mapStateToProps = () => ({});
const mapDispatchToProps = (dispatch) => ({dispatch});
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -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();
}
};
-1
View File
@@ -1,4 +1,3 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
@@ -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;
}
}
+4 -2
View File
@@ -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
});
+8
View File
@@ -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',
+21
View File
@@ -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.
+1
View File
@@ -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;
+63
View File
@@ -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;
+6
View File
@@ -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: {
+1 -1
View File
@@ -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({