mirror of
https://github.com/wassname/talk.git
synced 2026-07-22 13:00:29 +08:00
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:
committed by
Dan Zajdband
parent
4a704fa85c
commit
5b74beabd6
+1
-1
@@ -1 +1 @@
|
||||
dist
|
||||
dist
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user