mirror of
https://github.com/wassname/talk.git
synced 2026-07-23 13:10:20 +08:00
Merge branch 'master' of github.com:coralproject/talk into moderate-bio
Integrating graphql, updating tests, and preserving transition to ModerationList from CommentList.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
test
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
"no-template-curly-in-string": [1],
|
||||
"no-unsafe-negation": [1],
|
||||
"array-callback-return": [1],
|
||||
"eqeqeq": [2],
|
||||
"eqeqeq": [2, "smart"],
|
||||
"no-eval": [2],
|
||||
"no-global-assign": [2],
|
||||
"no-implied-eval": [2],
|
||||
|
||||
+2
-2
@@ -3,13 +3,13 @@ npm-debug.log*
|
||||
dist
|
||||
!dist/coral-admin
|
||||
dist/coral-admin/bundle.js
|
||||
tests/e2e/reports
|
||||
test/e2e/reports
|
||||
.DS_Store
|
||||
*.iml
|
||||
*.swp
|
||||
dump.rdb
|
||||
.env
|
||||
gaba.cfg
|
||||
*.cfg
|
||||
.idea/
|
||||
coverage/
|
||||
yarn.lock
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*"]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 4
|
||||
@@ -10,6 +10,8 @@ const RedisStore = require('connect-redis')(session);
|
||||
const redis = require('./services/redis');
|
||||
const csrf = require('csurf');
|
||||
const errors = require('./errors');
|
||||
const graph = require('./graph');
|
||||
const apollo = require('graphql-server-express');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -49,7 +51,7 @@ const session_opts = {
|
||||
name: 'talk.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 18000000, // 30 minutes for expiry.
|
||||
maxAge: 36000000, // 1 hour for expiry.
|
||||
},
|
||||
store: new RedisStore({
|
||||
ttl: 1800,
|
||||
@@ -77,6 +79,23 @@ app.use(session(session_opts));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
//==============================================================================
|
||||
// GraphQL Router
|
||||
//==============================================================================
|
||||
|
||||
// GraphQL endpoint.
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(graph.createGraphOptions));
|
||||
|
||||
// Only include the graphiql tool if we aren't in production mode.
|
||||
if (app.get('env') !== 'production') {
|
||||
|
||||
// Interactive graphiql interface.
|
||||
app.use('/api/v1/graph/iql', apollo.graphiqlExpress({
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// CSRF MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ const program = require('commander');
|
||||
const pkg = require('../package.json');
|
||||
const parseDuration = require('parse-duration');
|
||||
const Table = require('cli-table');
|
||||
const Asset = require('../models/asset');
|
||||
const AssetModel = require('../models/asset');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('../util');
|
||||
@@ -22,7 +22,7 @@ util.onshutdown([
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
function listAssets() {
|
||||
Asset
|
||||
AssetModel
|
||||
.find({})
|
||||
.sort({'created_at': 1})
|
||||
.then((asset) => {
|
||||
@@ -56,7 +56,7 @@ function refreshAssets(ageString) {
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
Asset.find({
|
||||
AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
|
||||
+10
-4
@@ -6,7 +6,7 @@
|
||||
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Setting = require('../models/setting');
|
||||
const SettingsService = require('../services/settings');
|
||||
const util = require('../util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
@@ -22,10 +22,16 @@ program
|
||||
.command('init')
|
||||
.description('initilizes the talk settings')
|
||||
.action(() => {
|
||||
const defaults = {id: '1', moderation: 'pre'};
|
||||
const defaults = {
|
||||
moderation: 'PRE',
|
||||
wordlist: {
|
||||
banned: [],
|
||||
suspect: []
|
||||
}
|
||||
};
|
||||
|
||||
Setting
|
||||
.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
|
||||
SettingsService
|
||||
.init(defaults)
|
||||
.then(() => {
|
||||
console.log('Created settings object.');
|
||||
util.shutdown();
|
||||
|
||||
+26
-19
@@ -7,7 +7,8 @@
|
||||
const program = require('commander');
|
||||
const pkg = require('../package.json');
|
||||
const prompt = require('prompt');
|
||||
const User = require('../models/user');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('../util');
|
||||
const Table = require('cli-table');
|
||||
@@ -76,16 +77,22 @@ function createUser(options) {
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
|
||||
return UsersService.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
if (result.role && result.role.length > 0) {
|
||||
return User
|
||||
.addRoleToUser(user.id, result.role.trim())
|
||||
let role = result.role ? result.role.trim() : null;
|
||||
|
||||
if (role && role.length > 0) {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
} else {
|
||||
util.shutdown();
|
||||
@@ -102,7 +109,7 @@ function createUser(options) {
|
||||
* Deletes a user.
|
||||
*/
|
||||
function deleteUser(userID) {
|
||||
User
|
||||
UserModel
|
||||
.findOneAndRemove({
|
||||
id: userID
|
||||
})
|
||||
@@ -148,7 +155,7 @@ function passwd(userID) {
|
||||
return;
|
||||
}
|
||||
|
||||
User
|
||||
UsersService
|
||||
.changePassword(userID, result.password)
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
@@ -168,7 +175,7 @@ function updateUser(userID, options) {
|
||||
const updates = [];
|
||||
|
||||
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
|
||||
let q = User.update({
|
||||
let q = UserModel.update({
|
||||
'id': userID,
|
||||
'profiles.provider': 'local'
|
||||
}, {
|
||||
@@ -181,7 +188,7 @@ function updateUser(userID, options) {
|
||||
}
|
||||
|
||||
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
|
||||
let q = User.update({
|
||||
let q = UserModel.update({
|
||||
'id': userID
|
||||
}, {
|
||||
$set: {
|
||||
@@ -208,7 +215,7 @@ function updateUser(userID, options) {
|
||||
* Lists all the users registered in the database.
|
||||
*/
|
||||
function listUsers() {
|
||||
User
|
||||
UsersService
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
@@ -248,7 +255,7 @@ function listUsers() {
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
*/
|
||||
function mergeUsers(dstUserID, srcUserID) {
|
||||
User
|
||||
UsersService
|
||||
.mergeUsers(dstUserID, srcUserID)
|
||||
.then(() => {
|
||||
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
|
||||
@@ -266,7 +273,7 @@ function mergeUsers(dstUserID, srcUserID) {
|
||||
* @param {String} role the role to add
|
||||
*/
|
||||
function addRole(userID, role) {
|
||||
User
|
||||
UsersService
|
||||
.addRoleToUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Added the ${role} role to User ${userID}.`);
|
||||
@@ -284,7 +291,7 @@ function addRole(userID, role) {
|
||||
* @param {String} role the role to remove
|
||||
*/
|
||||
function removeRole(userID, role) {
|
||||
User
|
||||
UsersService
|
||||
.removeRoleFromUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Removed the ${role} role from User ${userID}.`);
|
||||
@@ -301,8 +308,8 @@ function removeRole(userID, role) {
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
User
|
||||
.setStatus(userID, 'banned', '')
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
@@ -318,8 +325,8 @@ function ban(userID) {
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
User
|
||||
.setStatus(userID, 'active', '')
|
||||
UsersService
|
||||
.setStatus(userID, 'ACTIVE')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
@@ -335,7 +342,7 @@ function unban(userID) {
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
User
|
||||
UsersService
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
@@ -352,7 +359,7 @@ function disableUser(userID) {
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
User
|
||||
UsersService
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
|
||||
+15
-3
@@ -3,12 +3,24 @@ machine:
|
||||
version: 7
|
||||
services:
|
||||
- docker
|
||||
- redis
|
||||
|
||||
dependencies:
|
||||
post:
|
||||
# Lint the project here, before tests are ran.
|
||||
- npm run lint
|
||||
|
||||
database:
|
||||
post:
|
||||
# Initialize the settings in the database, this will create indicies for the
|
||||
# database.
|
||||
- ./bin/cli settings init
|
||||
- sleep 2
|
||||
|
||||
test:
|
||||
override:
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml ./node_modules/.bin/mocha tests --reporter mocha-junit-reporter
|
||||
- npm run lint
|
||||
- npm run build
|
||||
# Run the tests using the junit reporter.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml NPM_PACKAGE_CONFIG_MOCHA_REPORTER=mocha-junit-reporter npm run test
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -11,7 +11,7 @@ export const checkLogin = () => dispatch => {
|
||||
dispatch(checkLoginRequest());
|
||||
coralApi('/auth')
|
||||
.then(result => {
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
@@ -65,9 +65,9 @@ export const fetchFlaggedQueue = () => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/comments/flagged')
|
||||
.then(comments => {
|
||||
comments.forEach(comment => comment.flagged = true);
|
||||
return comments;
|
||||
.then(results => {
|
||||
results.comments.forEach(comment => comment.flagged = true);
|
||||
return results;
|
||||
})
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 280px;
|
||||
top: 10px;
|
||||
}
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 500px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 184px;
|
||||
padding: 20px;
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h2 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header h1, .separator h1{
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.formField {
|
||||
@@ -143,5 +151,14 @@ input.error{
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin: 10px 0;
|
||||
margin-right: 10px;
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.ban {
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@@ -19,23 +19,23 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => (
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h3>
|
||||
<h2>
|
||||
{lang.t('bandialog.ban_user')}
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h4>
|
||||
<h3>
|
||||
{lang.t('bandialog.are_you_sure', user.userName)}
|
||||
</h4>
|
||||
</h3>
|
||||
<i>
|
||||
{lang.t('bandialog.note')}
|
||||
</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} full>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} raised>
|
||||
{lang.t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" onClick={() => onClickBanUser('banned', user.userId, user.commentId)} full>
|
||||
<Button cStyle="black" className={styles.ban} onClick={() => onClickBanUser(user.userId, user.commentId)} raised>
|
||||
{lang.t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,8 @@ import styles from './ModerationList.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
import {Icon} from 'react-mdl';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import {FabButton, Button} from 'coral-ui';
|
||||
import {FabButton, Button, Icon} from 'coral-ui';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
@@ -20,22 +19,19 @@ const Comment = props => {
|
||||
const links = linkify.getMatches(comment.body);
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<li tabIndex={props.index} className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<i className={`material-icons ${styles.avatar}`}>person</i>
|
||||
<span>{author.displayName || lang.t('comment.anon')}</span>
|
||||
<span className={styles.created}>{timeago().format(comment.createdAt || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
|
||||
{comment.flagged ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.sideActions}>
|
||||
{links ?
|
||||
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{props.modActions.map((action, i) => getActionButton(action, i, props))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{authorStatus === 'banned' ?
|
||||
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
|
||||
</div>
|
||||
@@ -65,15 +61,19 @@ const getActionButton = (option, i, props) => {
|
||||
}
|
||||
if (option === 'ban') {
|
||||
return (
|
||||
<Button
|
||||
className='ban'
|
||||
cStyle='black'
|
||||
disabled={banned ? 'disabled' : ''}
|
||||
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
|
||||
key={i}
|
||||
>
|
||||
{lang.t('comment.ban_user')}
|
||||
</Button>
|
||||
<div className={styles.ban}>
|
||||
<Button
|
||||
className={`ban ${styles.banButton}`}
|
||||
cStyle='darkGrey'
|
||||
disabled={banned ? 'disabled' : ''}
|
||||
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
|
||||
key={i}
|
||||
raised
|
||||
>
|
||||
<Icon name='not_interested' className={styles.banIcon} />
|
||||
{lang.t('comment.ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const menuOption = menuOptionsMap[option];
|
||||
|
||||
@@ -36,13 +36,33 @@
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -72,25 +92,20 @@
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 10px;
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.7);
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
font-size: 1em;
|
||||
color: rgba(0,0,0,.54);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
@@ -150,3 +165,20 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
.header {
|
||||
background: #505050;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.header > div {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
width: 1170px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: #232323;
|
||||
background-color: #696969;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
min-width: 1280px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.rightPanel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 170px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 170px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rightPanel ul {
|
||||
list-style: none;
|
||||
line-height: 38px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.rightPanel li {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-left: 15px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.rightPanel .settings {
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #9e9e9e;
|
||||
line-height: 10px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.rightPanel .settings > div {
|
||||
@@ -45,3 +47,25 @@
|
||||
background: rgba(158, 158, 158, 0.69);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navLink {
|
||||
padding: 0 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
transition: background-color 200ms;
|
||||
|
||||
&.active {
|
||||
background-color: #232323;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow: hidden;
|
||||
height: 58px !important;
|
||||
}
|
||||
|
||||
.nav .navLink {
|
||||
padding: 0 20px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {Logo} from './Logo';
|
||||
export default ({handleLogout}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo />
|
||||
<Navigation>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink className={styles.navLink} to="/admin"
|
||||
activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
|
||||
<Link className={styles.navLink} to="/admin/community"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.layout {
|
||||
max-width: 1170px;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
overflow: hidden;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
.logo h1 {
|
||||
color: #272727;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
line-height: 60px;
|
||||
padding: 0 20px;
|
||||
color: #272727;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
line-height: 60px;
|
||||
padding-left: 13px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background: #E5E5E5;
|
||||
height: 100%;
|
||||
background: #E5E5E5;
|
||||
height: 100%;
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
|
||||
.base {
|
||||
stroke: #E5E5E5;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {CoralLogo} from 'coral-ui';
|
||||
export const Logo = () => (
|
||||
<div className={styles.logo}>
|
||||
<h1>
|
||||
<CoralLogo stroke="#E5E5E5" />
|
||||
<CoralLogo className={styles.base} />
|
||||
<span>Talk</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,96 @@
|
||||
.roleButton {
|
||||
display: block;
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
display: block;
|
||||
padding-left: 40px;
|
||||
width: auto;
|
||||
.leftColumn {
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.roleButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
background: white;
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
|
||||
i {
|
||||
color: #A1A1A1
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
padding: 0 2px 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.email {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dataTable {
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
|
||||
th {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
th:nth-child(2), th:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.selectField {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 36px;
|
||||
background: #2c2c2c;
|
||||
padding: 10px 15px;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.7px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
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 'coral-ui/components/Pager';
|
||||
import {Pager} from 'coral-ui';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -33,17 +32,17 @@ const tableHeaders = [
|
||||
const Community = ({isFetching, commenters, ...props}) => {
|
||||
const hasResults = !isFetching && !!commenters.length;
|
||||
return (
|
||||
<Grid>
|
||||
<Cell col={2}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<form action="">
|
||||
<div className={`mdl-textfield ${styles.searchBox}`}>
|
||||
<label className="mdl-button mdl-js-button mdl-button--icon" htmlFor="commenters-search">
|
||||
<div className={`${styles.searchBox}`}>
|
||||
<label htmlFor="commenters-search">
|
||||
<i className="material-icons">search</i>
|
||||
</label>
|
||||
<div className="">
|
||||
<input
|
||||
id="commenters-search"
|
||||
className={`mdl-textfield__input ${styles.searchInput}`}
|
||||
className={`${styles.searchInput}`}
|
||||
type="text"
|
||||
value={props.searchValue}
|
||||
onKeyDown={props.onKeyDownHandler}
|
||||
@@ -52,8 +51,8 @@ const Community = ({isFetching, commenters, ...props}) => {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Cell>
|
||||
<Cell col={6}>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ isFetching && <Loading /> }
|
||||
{ !hasResults && <NoResults /> }
|
||||
{ hasResults &&
|
||||
@@ -68,8 +67,8 @@ const Community = ({isFetching, commenters, ...props}) => {
|
||||
page={props.page}
|
||||
onNewPageHandler={props.onNewPageHandler}
|
||||
/>
|
||||
</Cell>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -52,19 +52,21 @@ class Table extends Component {
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.status || ''}
|
||||
className={styles.selectField}
|
||||
label={lang.t('community.status')}
|
||||
onChange={status => this.onCommenterStatusChange(row.id, status)}>
|
||||
<Option value={'active'}>{lang.t('community.active')}</Option>
|
||||
<Option value={'banned'}>{lang.t('community.banned')}</Option>
|
||||
<Option value={'ACTIVE'}>{lang.t('community.active')}</Option>
|
||||
<Option value={'BANNED'}>{lang.t('community.banned')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
className={styles.selectField}
|
||||
label={lang.t('community.role')}
|
||||
onChange={role => this.onRoleChange(row.id, role)}>
|
||||
<Option value={''}>.</Option>
|
||||
<Option value={'moderator'}>{lang.t('community.moderator')}</Option>
|
||||
<Option value={'admin'}>{lang.t('community.admin')}</Option>
|
||||
<Option value={'MODERATOR'}>{lang.t('community.moderator')}</Option>
|
||||
<Option value={'ADMIN'}>{lang.t('community.admin')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -3,15 +3,8 @@ import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemAction,
|
||||
Textfield,
|
||||
Checkbox,
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
import {Textfield, Checkbox} from 'react-mdl';
|
||||
import {Card, Icon, Spinner} from 'coral-ui';
|
||||
|
||||
const TIMESTAMPS = {
|
||||
weeks: 60 * 60 * 24 * 7,
|
||||
@@ -35,7 +28,7 @@ const updateCharCount = (updateSettings, settingsError) => (event) => {
|
||||
};
|
||||
|
||||
const updateModeration = (updateSettings, mod) => () => {
|
||||
const moderation = mod === 'pre' ? 'post' : 'pre';
|
||||
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
@@ -71,35 +64,32 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
|
||||
const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => {
|
||||
if (fetchingSettings) {
|
||||
|
||||
/* maybe a spinner here at some point */
|
||||
return <p>Loading settings...</p>;
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.commentSettingsSection}>
|
||||
<h3>{title}</h3>
|
||||
<List>
|
||||
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
|
||||
<p className={settings.moderation === 'pre' ? '' : styles.disabledSettingText}>
|
||||
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.enable-pre-moderation-text')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
|
||||
checked={settings.charCountEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
|
||||
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
|
||||
<span>{lang.t('configure.comment-count-text-pre')}</span>
|
||||
@@ -118,32 +108,44 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem threeLine className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
|
||||
checked={settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<div className={styles.content}>
|
||||
<Textfield
|
||||
onChange={updateInfoBoxContent(updateSettings)}
|
||||
value={settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={styles.configSettingInfoBox}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
<div>
|
||||
<Textfield
|
||||
onChange={updateInfoBoxContent(updateSettings)}
|
||||
value={settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
onChange={updateClosedMessage(updateSettings)}
|
||||
value={settings.closedMessage}
|
||||
label={lang.t('configure.closed-comments-label')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSettingInfoBox}`}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.close-after')}
|
||||
<br />
|
||||
<Textfield
|
||||
@@ -163,19 +165,8 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
|
||||
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
<Textfield
|
||||
onChange={updateClosedMessage(updateSettings)}
|
||||
value={settings.closedMessage}
|
||||
label={lang.t('configure.closed-comments-label')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
width: 300px;
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(70% - 300px)
|
||||
}
|
||||
|
||||
.settingOption {
|
||||
cursor: pointer;
|
||||
width: calc(100% - 300px);
|
||||
padding: 10px 14px;
|
||||
box-sizing: border-box;
|
||||
max-width: 718px;
|
||||
}
|
||||
|
||||
.configSetting {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
height: 95px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.settingsError {
|
||||
@@ -42,13 +45,13 @@
|
||||
}
|
||||
|
||||
.configSettingInfoBox {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
min-height: 100px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.configSettingInfoBox p {
|
||||
@@ -77,7 +80,7 @@
|
||||
}
|
||||
|
||||
.charCountTexfieldEnabled {
|
||||
border-color: #4caf50;
|
||||
border-color: #00796b;
|
||||
}
|
||||
|
||||
.charCountTexfield:focus {
|
||||
@@ -85,11 +88,12 @@
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color:#4caf50;
|
||||
background-color: #00796B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.copiedText {
|
||||
color: #008000;
|
||||
color: #00796b;
|
||||
float: right;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
@@ -97,6 +101,7 @@
|
||||
|
||||
.copyButton {
|
||||
float: right;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.embedInput {
|
||||
@@ -113,8 +118,12 @@
|
||||
}
|
||||
|
||||
#bannedWordlist, #suspectWordlist {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.wordlistHeader {
|
||||
@@ -124,7 +133,7 @@
|
||||
}
|
||||
|
||||
.enabledSetting {
|
||||
border-left-color: #4caf50;
|
||||
border-left-color: #00796b;
|
||||
border-left-style: solid;
|
||||
border-left-width: 7px;
|
||||
}
|
||||
@@ -136,3 +145,23 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.saveBox {
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.commentSettingsSection {
|
||||
padding-bottom: 200px;
|
||||
.action {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
fetchSettings,
|
||||
@@ -6,13 +6,8 @@ import {
|
||||
saveSettingsToServer,
|
||||
updateWordlist,
|
||||
} from '../../actions/settings';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
Button,
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
|
||||
import {Button, List, Item} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
@@ -21,7 +16,7 @@ import CommentSettings from './CommentSettings';
|
||||
import Wordlist from './Wordlist';
|
||||
import has from 'lodash/has';
|
||||
|
||||
class Configure extends React.Component {
|
||||
class Configure extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
@@ -30,6 +25,8 @@ class Configure extends React.Component {
|
||||
changed: false,
|
||||
errors: {}
|
||||
};
|
||||
|
||||
this.changeSection = this.changeSection.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
@@ -41,7 +38,7 @@ class Configure extends React.Component {
|
||||
this.setState({changed: false});
|
||||
}
|
||||
|
||||
changeSection = (activeSection) => () => {
|
||||
changeSection(activeSection) {
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
@@ -99,7 +96,8 @@ class Configure extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const section = this.getSection(this.state.activeSection);
|
||||
const {activeSection} = this.state;
|
||||
const section = this.getSection(activeSection);
|
||||
|
||||
const showSave = Object.keys(this.state.errors).reduce(
|
||||
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
|
||||
@@ -107,37 +105,40 @@ class Configure extends React.Component {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection('comments')}
|
||||
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection('embed')}
|
||||
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection('wordlist')}
|
||||
icon='settings'>{lang.t('configure.wordlist')}</ListItemContent>
|
||||
</ListItem>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<Item itemId='comments' icon="settings">
|
||||
{lang.t('configure.comment-settings')}
|
||||
</Item>
|
||||
<Item itemId='embed' icon='code'>
|
||||
{lang.t('configure.embed-comment-stream')}
|
||||
</Item>
|
||||
<Item itemId='wordlist' icon='settings'>
|
||||
{lang.t('configure.wordlist')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
{
|
||||
showSave ?
|
||||
<Button
|
||||
raised
|
||||
onClick={this.saveSettings}
|
||||
className={styles.changedSave}>
|
||||
<Icon name='check' /> {lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
: <Button
|
||||
raised
|
||||
disabled>
|
||||
{lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
<Button
|
||||
raised
|
||||
onClick={this.saveSettings}
|
||||
className={styles.changedSave}
|
||||
icon='check'
|
||||
full
|
||||
>
|
||||
{lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
:
|
||||
<Button
|
||||
raised
|
||||
disabled
|
||||
icon='check'
|
||||
full
|
||||
>
|
||||
{lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
|
||||
@@ -2,11 +2,7 @@ import React, {Component} from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Button
|
||||
} from 'react-mdl';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
|
||||
class EmbedLink extends Component {
|
||||
|
||||
@@ -34,16 +30,16 @@ class EmbedLink extends Component {
|
||||
return (
|
||||
<div>
|
||||
<h3>{this.props.title}</h3>
|
||||
<List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<div>
|
||||
<Card shadow="2">
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@ import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
|
||||
import styles from './Configure.css';
|
||||
|
||||
import {Card} from 'react-mdl';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
<div>
|
||||
<h3>{lang.t('configure.banned-words-title')}</h3>
|
||||
<Card id={styles.bannedWordlist} shadow={2}>
|
||||
<Card id={styles.bannedWordlist}>
|
||||
<p className={styles.wordlistHeader}>{lang.t('configure.banned-word-header')}</p>
|
||||
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
|
||||
<TagsInput
|
||||
@@ -18,10 +16,11 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
inputProps={{placeholder: 'word or phrase'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeWordlist('banned', tags)} />
|
||||
onChange={tags => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</Card>
|
||||
<h3>{lang.t('configure.suspect-words-title')}</h3>
|
||||
<Card id={styles.suspectWordlist} shadow={2}>
|
||||
<Card id={styles.suspectWordlist}>
|
||||
<p className={styles.wordlistHeader}>{lang.t('configure.suspect-word-header')}</p>
|
||||
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
|
||||
<TagsInput
|
||||
|
||||
@@ -75,11 +75,14 @@ class ModerationContainer extends React.Component {
|
||||
|
||||
render () {
|
||||
const {comments, actions} = this.props;
|
||||
|
||||
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'premod');
|
||||
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'rejected');
|
||||
const flaggedIds = comments.ids.filter(id => comments.byId[id].flagged === true);
|
||||
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'users');
|
||||
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD');
|
||||
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED');
|
||||
const flaggedIds = comments.ids.filter(id =>
|
||||
comments.byId[id].flagged === true &&
|
||||
comments.byId[id].status !== 'REJECTED' &&
|
||||
comments.byId[id].status !== 'ACCEPTED'
|
||||
);
|
||||
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
|
||||
|
||||
return (
|
||||
<ModerationQueue
|
||||
|
||||
@@ -6,12 +6,32 @@
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background: #9E9E9E;
|
||||
background: #262626;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 5px #F36451;
|
||||
}
|
||||
|
||||
.active > span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.showShortcuts {
|
||||
|
||||
@@ -12,114 +12,177 @@ const lang = new I18n(translations);
|
||||
|
||||
export default (props) => (
|
||||
<div>
|
||||
<div className='mdl-tabs mdl-js-tabs mdl-js-ripple-effect'>
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<a href='#all' onClick={() => props.onTabClick('all')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.all')}</a>
|
||||
<a href='#pending' onClick={() => props.onTabClick('pending')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.pending')}</a>
|
||||
<a href='#flagged' onClick={() => props.onTabClick('flagged')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
|
||||
<a href='#account' onClick={() => props.onTabClick('account')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.account')}</a>
|
||||
<a href='#rejected' onClick={() => props.onTabClick('rejected')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
|
||||
<a href='#pending'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onTabClick('pending');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'pending' ? styles.active : ''}`}
|
||||
>
|
||||
{lang.t('modqueue.pending')}
|
||||
</a>
|
||||
<a href='#account'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onTabClick('account');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
|
||||
{lang.t('modqueue.account')}
|
||||
</a>
|
||||
<a href='#rejected'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onTabClick('rejected');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
|
||||
>
|
||||
{lang.t('modqueue.rejected')}
|
||||
</a>
|
||||
<a href='#flagged'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onTabClick('flagged');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
|
||||
>
|
||||
{lang.t('modqueue.flagged')}
|
||||
</a>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='all'>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'all'}
|
||||
singleView={props.singleView}
|
||||
commentIds={[...props.premodIds, ...props.flaggedIds]}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
actionIds={props.userActionIds}
|
||||
actions={props.actions.byId}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
{
|
||||
props.activeTab === 'all' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'all'}
|
||||
singleView={props.singleView}
|
||||
commentIds={[...props.premodIds, ...props.flaggedIds]}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
actionIds={props.userActionIds}
|
||||
actions={props.actions.byId}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='pending'>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'pending'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.premodIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
actions={props.actions.byId}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'rejected'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.rejectedIds}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
modActions={['approve']}
|
||||
loading={props.comments.loading}
|
||||
/>
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
|
||||
{
|
||||
props.activeTab === 'pending' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'pending'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.premodIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
actions={props.actions.byId}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='account'>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'account'}
|
||||
singleView={props.singleView}
|
||||
users={props.users.byId}
|
||||
actionIds={props.userActionIds}
|
||||
actions={props.actions.byId}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
{
|
||||
props.activeTab === 'account' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'account'}
|
||||
singleView={props.singleView}
|
||||
users={props.users.byId}
|
||||
actionIds={props.userActionIds}
|
||||
actions={props.actions.byId}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'flagged'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.flaggedIds}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
modActions={['reject', 'approve']}
|
||||
loading={props.comments.loading}/>
|
||||
</div>
|
||||
{
|
||||
props.activeTab === 'flagged' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'flagged'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.flaggedIds}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
modActions={['reject', 'approve']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
{
|
||||
props.activeTab === 'rejected' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'rejected'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.rejectedIds}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
modActions={['approve']}
|
||||
loading={props.comments.loading}
|
||||
/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
width: 200px;
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(90% - 200px);
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
@@ -18,11 +21,12 @@
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
padding: 3px;
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
width: 90%;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.searchBoxInput {
|
||||
@@ -48,6 +52,16 @@
|
||||
|
||||
.streamsTable {
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
|
||||
th {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
th.status {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
@@ -55,18 +69,20 @@
|
||||
}
|
||||
|
||||
.statusMenu {
|
||||
border-radius: 3px;
|
||||
border-radius: 2px;
|
||||
width: 10em;
|
||||
text-align: center;
|
||||
float: right;
|
||||
border: 1px solid #ccc;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.7px;
|
||||
font-weight: 400;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
.statusMenuOpen {
|
||||
padding: 10px;
|
||||
background-color: #4caf50;
|
||||
background-color: #268D81;
|
||||
}
|
||||
|
||||
.statusMenuIcon {
|
||||
@@ -75,9 +91,17 @@
|
||||
|
||||
.statusMenuClosed {
|
||||
padding: 10px;
|
||||
background-color: #000;
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
margin-top: 5px;
|
||||
span {
|
||||
margin-bottom: 7px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,61 +103,63 @@ class Streams extends Component {
|
||||
render () {
|
||||
const {search, sort, filter} = this.state;
|
||||
const {assets} = this.props;
|
||||
|
||||
return <div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
className={styles.searchBoxInput}
|
||||
onChange={this.onSearchChange}
|
||||
placeholder={lang.t('streams.search')}/>
|
||||
</div>
|
||||
|
||||
<div className={styles.optionHeader}>{lang.t('streams.filter-streams')}</div>
|
||||
<div className={styles.optionDetail}>{lang.t('streams.stream-status')}</div>
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
className={styles.searchBoxInput}
|
||||
onChange={this.onSearchChange}
|
||||
placeholder={lang.t('streams.search')}/>
|
||||
</div>
|
||||
<div className={styles.optionHeader}>{lang.t('streams.filter-streams')}</div>
|
||||
<div className={styles.optionDetail}>{lang.t('streams.stream-status')}</div>
|
||||
<RadioGroup
|
||||
name='status filter'
|
||||
value={filter}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('filter')}>
|
||||
onChange={this.onSettingChange('filter')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='all'>{lang.t('streams.all')}</Radio>
|
||||
<Radio value='open'>{lang.t('streams.open')}</Radio>
|
||||
<Radio value='closed'>{lang.t('streams.closed')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.optionHeader}>{lang.t('streams.sort-by')}</div>
|
||||
<RadioGroup
|
||||
name='sort by'
|
||||
value={sort}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('sort')}>
|
||||
<Radio value='desc'>{lang.t('streams.newest')}</Radio>
|
||||
<Radio value='asc'>{lang.t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
<RadioGroup
|
||||
name='sort by'
|
||||
value={sort}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('sort')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='desc'>{lang.t('streams.newest')}</Radio>
|
||||
<Radio value='asc'>{lang.t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<DataTable
|
||||
className={styles.streamsTable}
|
||||
rows={assets.ids.map((id) => assets.byId[id])}>
|
||||
<TableHeader name="title">{lang.t('streams.article')}</TableHeader>
|
||||
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
|
||||
{lang.t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
|
||||
{lang.t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
<Pager
|
||||
totalPages={Math.ceil((assets.count || 0) / limit)}
|
||||
page={this.state.page}
|
||||
onNewPageHandler={this.onPageClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.mainContent}>
|
||||
<DataTable
|
||||
className={styles.streamsTable}
|
||||
rows={assets.ids.map((id) => assets.byId[id])}>
|
||||
<TableHeader name="title">{lang.t('streams.article')}</TableHeader>
|
||||
<TableHeader numeric name="publication_date" cellFormatter={this.renderDate}>
|
||||
{lang.t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader numeric name="closedAt" cellFormatter={this.renderStatus}>
|
||||
{lang.t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
<Pager
|
||||
totalPages={Math.ceil((assets.count || 0) / limit)}
|
||||
page={this.state.page}
|
||||
onNewPageHandler={this.onPageClick}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
"include-comment-stream": "Include Comment Stream Description for Readers.",
|
||||
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
|
||||
"include-text": "Include your text here.",
|
||||
"comment-settings": "Comment Settings",
|
||||
"embed-comment-stream": "Embed Comment Stream",
|
||||
"comment-settings": "Settings",
|
||||
"embed-comment-stream": "Embed Stream",
|
||||
"banned-word-header": "Write the banned words list",
|
||||
"suspect-word-header": "Write the suspect words list",
|
||||
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
|
||||
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
|
||||
"wordlist": "Banned & Suspect Words",
|
||||
"wordlist": "Banned Words",
|
||||
"banned-words-title": "Banned words list",
|
||||
"suspect-words-title": "Suspect words list",
|
||||
"save-changes": "Save Changes",
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {I18n} from '../../coral-framework';
|
||||
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config';
|
||||
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/asset';
|
||||
|
||||
import CloseCommentsInfo from '../components/CloseCommentsInfo';
|
||||
import ConfigureCommentStream from '../components/ConfigureCommentStream';
|
||||
@@ -13,8 +13,10 @@ class ConfigureStreamContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
console.log('moderation', props.asset.settings.moderation);
|
||||
|
||||
this.state = {
|
||||
premod: props.config.moderation === 'pre',
|
||||
premod: props.asset.settings.moderation === 'PRE',
|
||||
premodLinks: false
|
||||
};
|
||||
|
||||
@@ -26,7 +28,7 @@ class ConfigureStreamContainer extends Component {
|
||||
handleApply () {
|
||||
const {premod, changed} = this.state;
|
||||
const newConfig = {
|
||||
moderation: premod ? 'pre' : 'post'
|
||||
moderation: premod ? 'PRE' : 'POST'
|
||||
};
|
||||
if (changed) {
|
||||
this.props.updateConfiguration(newConfig);
|
||||
@@ -47,18 +49,17 @@ class ConfigureStreamContainer extends Component {
|
||||
}
|
||||
|
||||
toggleStatus () {
|
||||
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
|
||||
this.props.updateStatus(this.props.asset.closedAt === null ? 'closed' : 'open');
|
||||
}
|
||||
|
||||
getClosedIn () {
|
||||
const {closedTimeout} = this.props.config;
|
||||
const {closedTimeout} = this.props.asset.settings;
|
||||
const {created_at} = this.props.asset;
|
||||
return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {status} = this.props;
|
||||
|
||||
const status = this.props.asset.closedAt === null ? 'open' : 'closed';
|
||||
return (
|
||||
<div>
|
||||
<ConfigureCommentStream
|
||||
@@ -80,11 +81,7 @@ class ConfigureStreamContainer extends Component {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
config: state.config.toJS(),
|
||||
asset: state.items
|
||||
.get('assets')
|
||||
.first()
|
||||
.toJS()
|
||||
asset: state.asset.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// this component will
|
||||
// render its children
|
||||
// render a like button
|
||||
// render a permalink button
|
||||
// render a reply button
|
||||
// render a flag button
|
||||
// translate things?
|
||||
|
||||
import React, {PropTypes} from 'react';
|
||||
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
|
||||
|
||||
import AuthorName from 'coral-plugin-author-name/AuthorName';
|
||||
import Content from 'coral-plugin-commentcontent/CommentContent';
|
||||
import PubDate from 'coral-plugin-pubdate/PubDate';
|
||||
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
|
||||
import FlagComment from 'coral-plugin-flags/FlagComment';
|
||||
import LikeButton from 'coral-plugin-likes/LikeButton';
|
||||
|
||||
const getAction = (type, comment) => comment.actions.filter((a) => a.type === type)[0];
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {replyBoxVisible: false};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
refetch: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
postAction: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string
|
||||
}).isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired
|
||||
}),
|
||||
comment: PropTypes.shape({
|
||||
depth: PropTypes.number,
|
||||
actions: PropTypes.array.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
replies: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
body: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired
|
||||
})
|
||||
),
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
onReplyBoxClick = () => {
|
||||
if (this.props.currentUser) {
|
||||
this.setState({replyBoxVisible: !this.state.replyBoxVisible});
|
||||
} else {
|
||||
const offset = document.getElementById(`c_${this.props.comment.id}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comment,
|
||||
parentId,
|
||||
currentUser,
|
||||
asset,
|
||||
depth,
|
||||
postItem,
|
||||
refetch,
|
||||
addNotification,
|
||||
showSignInDialog,
|
||||
postAction,
|
||||
deleteAction
|
||||
} = this.props;
|
||||
|
||||
const like = getAction('LIKE', comment);
|
||||
const flag = getAction('FLAG', comment);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="comment"
|
||||
id={`c_${comment.id}`}
|
||||
style={{marginLeft: depth * 30}}>
|
||||
<hr aria-hidden={true} />
|
||||
<AuthorName
|
||||
author={comment.user}
|
||||
addNotification={this.props.addNotification}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postAction={this.props.postAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
deleteAction={this.props.deleteAction}
|
||||
currentUser={currentUser}/>
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<Content body={comment.body} />
|
||||
<div className="commentActionsLeft">
|
||||
<ReplyButton
|
||||
onClick={this.onReplyBoxClick}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
<LikeButton
|
||||
like={like}
|
||||
id={comment.id}
|
||||
postAction={postAction}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
<FlagComment
|
||||
flag={flag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postAction={postAction}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
|
||||
</div>
|
||||
{
|
||||
this.state.replyBoxVisible
|
||||
? <ReplyBox
|
||||
commentPostedHandler={() => {
|
||||
console.log('replyPostedHandler');
|
||||
this.setState({replyBoxVisible: false});
|
||||
refetch();
|
||||
}}
|
||||
parentId={parentId || comment.id}
|
||||
addNotification={addNotification}
|
||||
authorId={currentUser.id}
|
||||
postItem={postItem}
|
||||
assetId={asset.id} />
|
||||
: null
|
||||
}
|
||||
{
|
||||
comment.replies &&
|
||||
comment.replies.map(reply => {
|
||||
return <Comment
|
||||
refetch={refetch}
|
||||
addNotification={addNotification}
|
||||
parentId={comment.id}
|
||||
postItem={postItem}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={reply.id}
|
||||
comment={reply} />;
|
||||
})
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Comment;
|
||||
@@ -1,331 +0,0 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import Pym from 'pym.js';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {
|
||||
itemActions,
|
||||
Notification,
|
||||
notificationActions,
|
||||
authActions
|
||||
} from '../../coral-framework';
|
||||
|
||||
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
|
||||
import InfoBox from '../../coral-plugin-infobox/InfoBox';
|
||||
import Content from '../../coral-plugin-commentcontent/CommentContent';
|
||||
import PubDate from '../../coral-plugin-pubdate/PubDate';
|
||||
import Count from '../../coral-plugin-comment-count/CommentCount';
|
||||
import AuthorName from '../../coral-plugin-author-name/AuthorName';
|
||||
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
|
||||
import FlagComment from '../../coral-plugin-flags/FlagComment';
|
||||
import LikeButton from '../../coral-plugin-likes/LikeButton';
|
||||
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
import UserBox from '../../coral-sign-in/components/UserBox';
|
||||
|
||||
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
|
||||
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
|
||||
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
|
||||
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
|
||||
|
||||
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
|
||||
|
||||
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {logout, showSignInDialog} = authActions;
|
||||
|
||||
class CommentStream extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTab: 0
|
||||
};
|
||||
|
||||
this.changeTab = this.changeTab.bind(this);
|
||||
}
|
||||
|
||||
changeTab (tab) {
|
||||
this.setState({
|
||||
activeTab: tab
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.object.isRequired,
|
||||
addItem: PropTypes.func.isRequired,
|
||||
updateItem: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
this.pym = new Pym.Child({polling: 100});
|
||||
|
||||
let path = this.pym.parentUrl.split('#')[0];
|
||||
|
||||
if (!path) {
|
||||
path = window.location.href.split('#')[0];
|
||||
}
|
||||
|
||||
this.props.getStream(path || window.location);
|
||||
this.path = path;
|
||||
|
||||
this.pym.sendMessage('childReady');
|
||||
|
||||
this.pym.onMessage('DOMContentLoaded', hash => {
|
||||
const commentId = hash.replace('#', 'c_');
|
||||
let count = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (document.getElementById(commentId)) {
|
||||
window.clearInterval(interval);
|
||||
this.pym.scrollParentToChildEl(commentId);
|
||||
}
|
||||
|
||||
if (++count > 100) { // ~10 seconds
|
||||
// give up waiting for the comments to load.
|
||||
// it would be weird for the page to jump after that long.
|
||||
window.clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
|
||||
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
|
||||
const {actions, users, comments} = this.props.items;
|
||||
const {status, moderation, closedMessage, charCount, charCountEnable} = this.props.config;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
const {activeTab} = this.state;
|
||||
const banned = (this.props.userData.status === 'banned');
|
||||
|
||||
const expandForLogin = showSignInDialog ? {
|
||||
minHeight: document.body.scrollHeight + 150
|
||||
} : {};
|
||||
return <div style={expandForLogin}>
|
||||
{
|
||||
rootItem
|
||||
? <div className="commentStream">
|
||||
<TabBar onChange={this.changeTab} activeTab={activeTab}>
|
||||
<Tab><Count id={rootItemId} items={this.props.items}/></Tab>
|
||||
<Tab>Settings</Tab>
|
||||
<Tab restricted={!isAdmin}>Configure Stream</Tab>
|
||||
</TabBar>
|
||||
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
|
||||
<TabContent show={activeTab === 0}>
|
||||
{
|
||||
status === 'open'
|
||||
? <div id="commentBox">
|
||||
<InfoBox
|
||||
content={this.props.config.infoBoxContent}
|
||||
enable={this.props.config.infoBoxEnable}
|
||||
/>
|
||||
<RestrictedContent restricted={banned} restrictedComp={<SuspendedAccount />}>
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
premod={moderation}
|
||||
reply={false}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}
|
||||
author={user}
|
||||
charCount={charCountEnable && charCount}/>
|
||||
</RestrictedContent>
|
||||
</div>
|
||||
: <p>{closedMessage}</p>
|
||||
}
|
||||
{!loggedIn && <SignInContainer offset={signInOffset}/>}
|
||||
{
|
||||
rootItem.comments && rootItem.comments.map((commentId) => {
|
||||
const comment = comments[commentId];
|
||||
return <div className="comment" key={commentId} id={`c_${commentId}`}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName
|
||||
author={users[comment.author_id]}
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
author_id={comment.author_id}
|
||||
postAction={this.props.postAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content body={comment.body}/>
|
||||
<div className="commentActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={commentId}
|
||||
currentUser={this.props.auth.user}
|
||||
showReply={comment.showReply}
|
||||
banned={banned}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
like={actions[comment.like]}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}/>
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
<FlagComment
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
author_id={comment.author_id}
|
||||
flag={actions[comment.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
updateItem={this.props.updateItem}
|
||||
banned={banned}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
commentId={commentId}
|
||||
articleURL={this.path}/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
premod={moderation}
|
||||
currentUser={user}
|
||||
charCount={charCountEnable && charCount}
|
||||
showReply={comment.showReply}/>
|
||||
{
|
||||
comment.children &&
|
||||
comment.children.map((replyId) => {
|
||||
let reply = this.props.items.comments[replyId];
|
||||
return <div className="reply" key={replyId} id={`c_${replyId}`}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName author={users[reply.author_id]}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content body={reply.body}/>
|
||||
<div className="replyActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={replyId}
|
||||
banned={banned}
|
||||
currentUser={this.props.auth.user}
|
||||
showReply={reply.showReply}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
like={this.props.items.actions[reply.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={banned}/>
|
||||
</div>
|
||||
<div className="replyActionsRight">
|
||||
<FlagComment
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
author_id={comment.author_id}
|
||||
flag={actions[reply.flag]}
|
||||
postAction={this.props.postAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
banned={banned}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
commentId={reply.parent_id}
|
||||
articleURL={this.path}
|
||||
/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
child_id={replyId}
|
||||
premod={moderation}
|
||||
banned={banned}
|
||||
currentUser={user}
|
||||
charCount={charCountEnable && charCount}
|
||||
showReply={reply.showReply}/>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 1}>
|
||||
<SettingsContainer
|
||||
loggedIn={loggedIn}
|
||||
userData={this.props.userData}
|
||||
showSignInDialog={this.props.handleSignInDialog}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 2}>
|
||||
<RestrictedContent restricted={!loggedIn}>
|
||||
<ConfigureStreamContainer
|
||||
status={status}
|
||||
onClick={this.toggleStatus}
|
||||
/>
|
||||
</RestrictedContent>
|
||||
</TabContent>
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
<Spinner/>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
config: state.config.toJS(),
|
||||
items: state.items.toJS(),
|
||||
notification: state.notification.toJS(),
|
||||
auth: state.auth.toJS(),
|
||||
userData: state.user.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
addItem: (item, item_id) => dispatch(addItem(item, item_id)),
|
||||
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
|
||||
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
|
||||
getStream: (rootId) => dispatch(getStream(rootId)),
|
||||
addNotification: (type, text) => dispatch(addNotification(type, text)),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
postAction: (item, itemType, action) => dispatch(postAction(item, itemType, action)),
|
||||
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
|
||||
deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)),
|
||||
appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)),
|
||||
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
|
||||
logout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
|
||||
@@ -0,0 +1,205 @@
|
||||
import React, {Component} from 'react';
|
||||
import {compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {isEqual} from 'lodash';
|
||||
|
||||
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
|
||||
|
||||
const {logout, showSignInDialog} = authActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
|
||||
import {queryStream} from './graphql/queries';
|
||||
import {postComment, postAction, deleteAction} from './graphql/mutations';
|
||||
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
|
||||
|
||||
import Stream from './Stream';
|
||||
import InfoBox from 'coral-plugin-infobox/InfoBox';
|
||||
import Count from 'coral-plugin-comment-count/CommentCount';
|
||||
import CommentBox from 'coral-plugin-commentbox/CommentBox';
|
||||
import UserBox from '../../coral-sign-in/components/UserBox';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
|
||||
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
|
||||
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
|
||||
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
|
||||
|
||||
class Embed extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
showSignInDialog: false
|
||||
};
|
||||
|
||||
this.changeTab = this.changeTab.bind(this);
|
||||
}
|
||||
|
||||
changeTab (tab) {
|
||||
this.setState({
|
||||
activeTab: tab
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
data: React.PropTypes.shape({
|
||||
loading: React.PropTypes.bool,
|
||||
error: React.PropTypes.object
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
|
||||
// stream id, logged in user, settings
|
||||
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
|
||||
// this.props.getStream(path || window.location);
|
||||
// this.path = window.location.href.split('#')[0];
|
||||
//
|
||||
|
||||
pym.sendMessage('childReady');
|
||||
|
||||
pym.onMessage('DOMContentLoaded', hash => {
|
||||
const commentId = hash.replace('#', 'c_');
|
||||
let count = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (document.getElementById(commentId)) {
|
||||
window.clearInterval(interval);
|
||||
pym.scrollParentToChildEl(commentId);
|
||||
}
|
||||
|
||||
if (++count > 100) { // ~10 seconds
|
||||
// give up waiting for the comments to load.
|
||||
// it would be weird for the page to jump after that long.
|
||||
window.clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const {loadAsset} = this.props;
|
||||
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
|
||||
loadAsset(nextProps.data.asset);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {activeTab} = this.state;
|
||||
const {loading, asset, refetch} = this.props.data;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
|
||||
const expandForLogin = showSignInDialog ? {
|
||||
minHeight: document.body.scrollHeight + 200
|
||||
} : {};
|
||||
|
||||
return <div style={expandForLogin}>
|
||||
{
|
||||
loading ? <Spinner/>
|
||||
: <div className="commentStream">
|
||||
<TabBar onChange={this.changeTab} activeTab={activeTab}>
|
||||
<Tab><Count count={asset.comments.length}/></Tab>
|
||||
<Tab>Settings</Tab>
|
||||
<Tab restricted={!isAdmin}>Configure Stream</Tab>
|
||||
</TabBar>
|
||||
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
|
||||
<TabContent show={activeTab === 0}>
|
||||
{
|
||||
asset.closedAt === null
|
||||
? <div id="commentBox">
|
||||
<InfoBox
|
||||
content={asset.settings.infoBoxContent}
|
||||
enable={asset.settings.infoBoxEnable}
|
||||
/>
|
||||
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
|
||||
{
|
||||
user
|
||||
? <CommentBox
|
||||
commentPostedHandler={refetch}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
assetId={asset.id}
|
||||
premod={asset.settings.moderation}
|
||||
isReply={false}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={false}
|
||||
authorId={user.id}
|
||||
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
|
||||
: null
|
||||
}
|
||||
</RestrictedContent>
|
||||
</div>
|
||||
: <p>{asset.settings.closedMessage}</p>
|
||||
}
|
||||
{!loggedIn && <SignInContainer offset={signInOffset}/>}
|
||||
<Stream
|
||||
refetch={refetch}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={{text: null}}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 1}>
|
||||
<SettingsContainer
|
||||
loggedIn={loggedIn}
|
||||
userData={this.props.userData}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 2}>
|
||||
<RestrictedContent restricted={!loggedIn}>
|
||||
<ConfigureStreamContainer
|
||||
status={status}
|
||||
onClick={this.toggleStatus}
|
||||
/>
|
||||
</RestrictedContent>
|
||||
</TabContent>
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
items: state.items.toJS(),
|
||||
notification: state.notification.toJS(),
|
||||
auth: state.auth.toJS(),
|
||||
userData: state.user.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)),
|
||||
addNotification: (type, text) => dispatch(addNotification(type, text)),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
|
||||
logout: () => dispatch(logout()),
|
||||
dispatch: d => dispatch(d)
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
postComment,
|
||||
postAction,
|
||||
deleteAction,
|
||||
queryStream
|
||||
)(Embed);
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Comment from './Comment';
|
||||
|
||||
const Stream = ({
|
||||
comments,
|
||||
currentUser,
|
||||
asset,
|
||||
postItem,
|
||||
addNotification,
|
||||
postAction,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
comments.map(comment => {
|
||||
return <Comment
|
||||
refetch={refetch}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postItem={postItem}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
comment={comment} />;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Stream.propTypes = {
|
||||
refetch: PropTypes.func.isRequired,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
comments: PropTypes.array.isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
displayName: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
||||
export default Stream;
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation deleteAction ($id: ID!) {
|
||||
deleteAction(id:$id)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
import POST_COMMENT from './postComment.graphql';
|
||||
import POST_ACTION from './postAction.graphql';
|
||||
import DELETE_ACTION from './deleteAction.graphql';
|
||||
|
||||
export const postComment = graphql(POST_COMMENT, {
|
||||
props: ({mutate}) => ({
|
||||
postItem: ({asset_id, body, parent_id} /* , type */ ) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
asset_id,
|
||||
body,
|
||||
parent_id
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const postAction = graphql(POST_ACTION, {
|
||||
props: ({mutate}) => ({
|
||||
postAction: (action) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
action
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const deleteAction = graphql(DELETE_ACTION, {
|
||||
props: ({mutate}) => ({
|
||||
deleteAction: (id) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation CreateAction ($action: CreateActionInput!) {
|
||||
createAction(action:$action) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
status
|
||||
user {
|
||||
name: displayName
|
||||
}
|
||||
actions {
|
||||
type: action_type
|
||||
count
|
||||
current: current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
|
||||
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
import STREAM_QUERY from './streamQuery.graphql';
|
||||
import pym from 'coral-framework/PymConnection';
|
||||
|
||||
let url = pym.parentUrl.split('#')[0] || 'http://localhost:3000/';
|
||||
|
||||
export const queryStream = graphql(STREAM_QUERY, {
|
||||
options: {variables: {asset_url: url}}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
created_at
|
||||
user {
|
||||
id
|
||||
name: displayName
|
||||
settings {
|
||||
bio
|
||||
}
|
||||
}
|
||||
actions {
|
||||
type: action_type
|
||||
count
|
||||
current: current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query AssetQuery($asset_url: String!) {
|
||||
asset(url: $asset_url) {
|
||||
id
|
||||
title
|
||||
url
|
||||
closedAt
|
||||
created_at
|
||||
settings {
|
||||
moderation
|
||||
infoBoxEnable
|
||||
infoBoxContent
|
||||
closeTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
}
|
||||
comments {
|
||||
...commentView
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import CommentStream from './CommentStream';
|
||||
import {Provider} from 'react-redux';
|
||||
import {store} from '../../coral-framework';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {client} from 'coral-framework/client';
|
||||
import store from 'coral-framework/store';
|
||||
|
||||
import Embed from './Embed';
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<CommentStream />
|
||||
</Provider>
|
||||
, document.querySelector('#coralStream'));
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<Embed />
|
||||
</ApolloProvider>
|
||||
, document.querySelector('#coralStream')
|
||||
);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import Pym from '../../node_modules/pym.js';
|
||||
|
||||
const pym = new Pym.Child({polling: 100});
|
||||
export default pym;
|
||||
|
||||
export const link = (url) => (e) => {
|
||||
e.preventDefault();
|
||||
pym.sendMessage('navigate', url);
|
||||
};
|
||||
@@ -1,20 +1,21 @@
|
||||
import * as actions from '../constants/asset';
|
||||
import coralApi from '../helpers/response';
|
||||
import * as actions from '../constants/config';
|
||||
import {addNotification} from '../actions/notification';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export const fetchAssetRequest = () => ({type: actions.FETCH_ASSET_REQUEST});
|
||||
export const fetchAssetSuccess = asset => ({type: actions.FETCH_ASSET_SUCCESS, asset});
|
||||
export const fetchAssetFailure = error => ({type: actions.FETCH_ASSET_FAILURE, error});
|
||||
|
||||
const updateConfigRequest = () => ({type: actions.UPDATE_CONFIG_REQUEST});
|
||||
const updateConfigSuccess = config => ({type: actions.UPDATE_CONFIG_SUCCESS, config});
|
||||
const updateConfigFailure = () => ({type: actions.UPDATE_CONFIG_FAILURE});
|
||||
|
||||
export const updateConfiguration = newConfig => (dispatch, getState) => {
|
||||
const assetId = getState().items.get('assets')
|
||||
.keySeq()
|
||||
.toArray()[0];
|
||||
|
||||
const assetId = getState().asset.toJS().id;
|
||||
dispatch(updateConfigRequest());
|
||||
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
|
||||
.then(() => {
|
||||
@@ -25,12 +26,8 @@ export const updateConfiguration = newConfig => (dispatch, getState) => {
|
||||
};
|
||||
|
||||
export const updateOpenStream = closedBody => (dispatch, getState) => {
|
||||
const assetId = getState().items.get('assets')
|
||||
.keySeq()
|
||||
.toArray()[0];
|
||||
|
||||
const assetId = getState().asset.toJS().id;
|
||||
dispatch(updateConfigRequest());
|
||||
|
||||
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
|
||||
.then(() => {
|
||||
dispatch(addNotification('success', lang.t('successUpdateSettings')));
|
||||
@@ -27,7 +27,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
|
||||
dispatch(signInRequest());
|
||||
coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
|
||||
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(signInSuccess(user, isAdmin));
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(addItem(user, 'users'));
|
||||
@@ -132,7 +132,7 @@ export const checkLogin = () => dispatch => {
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
@@ -189,17 +189,34 @@ export function getItemsArray (ids) {
|
||||
* The newly put item to the item store
|
||||
*/
|
||||
|
||||
export function postItem (item, type, id) {
|
||||
return (dispatch) => {
|
||||
if (id) {
|
||||
item.id = id;
|
||||
export function postItem (item, type, id, mutate) {
|
||||
console.log(
|
||||
item,
|
||||
type,
|
||||
id,
|
||||
mutate
|
||||
);
|
||||
mutate({
|
||||
variables: {
|
||||
asset_id: id,
|
||||
body: item,
|
||||
parent_id: null
|
||||
}
|
||||
return coralApi(`/${type}`, {method: 'POST', body: item})
|
||||
.then((json) => {
|
||||
dispatch(addItem({...item, id:json.id}, type));
|
||||
return json;
|
||||
});
|
||||
};
|
||||
}).then(({data}) => {
|
||||
console.log('it workt');
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// return (dispatch) => {
|
||||
// if (id) {
|
||||
// item.id = id;
|
||||
// }
|
||||
// return coralApi(`/${type}`, {method: 'POST', body: item})
|
||||
// .then((json) => {
|
||||
// dispatch(addItem({...item, id:json.id}, type));
|
||||
// return json;
|
||||
// });
|
||||
// };
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -30,19 +30,27 @@ export const saveBio = (user_id, formData) => dispatch => {
|
||||
* @returns Promise
|
||||
*/
|
||||
export const fetchCommentsByUserId = userId => {
|
||||
return (dispatch) => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({type: actions.COMMENTS_BY_USER_REQUEST});
|
||||
return coralApi(`/comments?user_id=${userId}`)
|
||||
.then(({comments, assets}) => {
|
||||
const state = getState();
|
||||
comments.forEach(comment => dispatch(addItem(comment, 'comments')));
|
||||
assets.forEach(asset => {
|
||||
const prevAsset = state.items.getIn(['assets', asset.id]);
|
||||
|
||||
assets.forEach(asset => dispatch(addItem(asset, 'assets')));
|
||||
if (prevAsset) {
|
||||
|
||||
// Include data such as hydrated comments from assets already in the system.
|
||||
dispatch(addItem({...prevAsset.toJS(), ...asset}, 'assets'));
|
||||
} else {
|
||||
dispatch(addItem(asset, 'assets'));
|
||||
}
|
||||
});
|
||||
|
||||
dispatch({type: actions.COMMENTS_BY_USER_SUCCESS, comments: comments.map(comment => comment.id)});
|
||||
dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)});
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error});
|
||||
});
|
||||
.catch(error => dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error}));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface: getNetworkInterface()
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export const FETCH_ASSET_REQUEST = 'FETCH_ASSET_REQUEST';
|
||||
export const FETCH_ASSET_FAILURE = 'FETCH_ASSET_FAILURE';
|
||||
export const FETCH_ASSET_SUCCESS = 'FETCH_ASSET_SUCCESS';
|
||||
|
||||
export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST';
|
||||
export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS';
|
||||
export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE';
|
||||
|
||||
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
|
||||
|
||||
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
|
||||
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
|
||||
export const ADD_ITEM = 'ADD_ITEM';
|
||||
@@ -2,5 +2,5 @@ export default {
|
||||
email: email => (/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
|
||||
password: pass => (/^(?=.{8,}).*$/.test(pass)),
|
||||
confirmPassword: () => true,
|
||||
displayName: displayName => (/^[a-z0-9_]+$/.test(displayName))
|
||||
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName))
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@ import * as itemActions from './actions/items';
|
||||
import I18n from './modules/i18n/i18n';
|
||||
import * as notificationActions from './actions/notification';
|
||||
import * as authActions from './actions/auth';
|
||||
import * as configActions from './actions/config';
|
||||
import * as assetActions from './actions/asset';
|
||||
import pym from './PymConnection';
|
||||
|
||||
export {
|
||||
Notification,
|
||||
@@ -13,5 +14,6 @@ export {
|
||||
I18n,
|
||||
notificationActions,
|
||||
authActions,
|
||||
configActions
|
||||
assetActions,
|
||||
pym
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../constants/asset';
|
||||
|
||||
const initialState = Map({
|
||||
closedAt: null,
|
||||
settings: null,
|
||||
title: null,
|
||||
url: null,
|
||||
features: Map({}),
|
||||
status: 'open',
|
||||
moderation: null
|
||||
});
|
||||
|
||||
export default function asset (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSET_SUCCESS :
|
||||
return state
|
||||
.merge(action.asset);
|
||||
case actions.UPDATE_CONFIG:
|
||||
return state
|
||||
.merge(action.config);
|
||||
case actions.UPDATE_CONFIG_SUCCESS:
|
||||
return state
|
||||
.merge(action.config);
|
||||
case actions.OPEN_COMMENTS:
|
||||
return state
|
||||
.set('status', 'open')
|
||||
.set('closedAt', null);
|
||||
case actions.CLOSE_COMMENTS:
|
||||
return state
|
||||
.set('status', 'closed')
|
||||
.set('closedAt', Date.now());
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../constants/config';
|
||||
|
||||
const initialState = Map({
|
||||
features: Map({}),
|
||||
status: 'open',
|
||||
moderation: null
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch(action.type) {
|
||||
case actions.UPDATE_CONFIG:
|
||||
return state
|
||||
.merge(Map(action.config));
|
||||
case actions.UPDATE_CONFIG_SUCCESS:
|
||||
return state
|
||||
.merge(Map(action.config));
|
||||
case actions.OPEN_COMMENTS:
|
||||
return state
|
||||
.set('status', 'open');
|
||||
case actions.CLOSE_COMMENTS:
|
||||
return state
|
||||
.set('status', 'closed');
|
||||
case actions.ADD_ITEM:
|
||||
return action.item_type === 'assets' ? state.set('status', (action.item && action.item.closedAt && new Date(action.item.closedAt).getTime() <= new Date().getTime()) ? 'closed' : 'open') : state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -1,20 +1,13 @@
|
||||
/* @flow */
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import config from './config';
|
||||
import items from './items';
|
||||
import notification from './notification';
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
import asset from './asset';
|
||||
import items from './items';
|
||||
import notification from './notification';
|
||||
|
||||
/**
|
||||
* Expose the combined main reducer
|
||||
*/
|
||||
|
||||
export default combineReducers({
|
||||
config,
|
||||
items,
|
||||
notification,
|
||||
export default {
|
||||
auth,
|
||||
user
|
||||
});
|
||||
user,
|
||||
asset,
|
||||
items,
|
||||
notification
|
||||
};
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import {createStore, applyMiddleware} from 'redux';
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from './reducers';
|
||||
import {client} from './client';
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(client.middleware()),
|
||||
applyMiddleware(thunk)
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
export default createStore(
|
||||
mainReducer,
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk)
|
||||
combineReducers({
|
||||
...mainReducer,
|
||||
apollo: client.reducer()
|
||||
}),
|
||||
{},
|
||||
compose(...middlewares)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {print} from 'graphql-tag/printer';
|
||||
|
||||
// quick way to add the subscribe and unsubscribe functions to the network interface
|
||||
const addGraphQLSubscriptions = (networkInterface, wsClient) => Object.assign(networkInterface, {
|
||||
subscribe: (request, handler) => wsClient.subscribe({
|
||||
query: print(request.query),
|
||||
variables: request.variables,
|
||||
}, handler),
|
||||
unsubscribe: (id) => {
|
||||
wsClient.unsubscribe(id);
|
||||
},
|
||||
});
|
||||
|
||||
export default addGraphQLSubscriptions;
|
||||
@@ -14,6 +14,8 @@
|
||||
"PASSWORD_REQUIRED": "Must input a password",
|
||||
"PASSWORD_LENGTH": "Password is too short",
|
||||
"EMAIL_IN_USE": "Email address already in use",
|
||||
"EMAIL_DISPLAY_NAME_IN_USE": "Email address or display name already in use",
|
||||
"DISPLAYNAME_IN_USE": "Display name already in use",
|
||||
"DISPLAY_NAME_REQUIRED": "Must input a display name",
|
||||
"NO_SPECIAL_CHARACTERS": "Display names can contain letters, numbers and _ only",
|
||||
"PROFANITY_ERROR": "Display names must not contain profanity. Please contact the administrator if you believe this to be in error."
|
||||
@@ -34,6 +36,8 @@
|
||||
"PASSWORD_REQUIRED": "Debe ingresar una contraseña",
|
||||
"PASSWORD_LENGTH": "La contraseña es muy corta",
|
||||
"EMAIL_IN_USE": "La dirección de correo electrónico se encuentra en uso",
|
||||
"EMAIL_DISPLAY_NAME_IN_USE": "Correo o Nombre en uso.",
|
||||
"DISPLAYNAME_IN_USE": "Nombre en uso.",
|
||||
"DISPLAY_NAME_REQUIRED": "Debe ingresar un nombre",
|
||||
"NO_SPECIAL_CHARACTERS": "Los nombres pueden contener letras, números y _",
|
||||
"PROFANITY_ERROR": "Los nombres no pueden contener blasfemias. Por favor contacte al administrador si cree que esto es un error"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import {createNetworkInterface} from 'apollo-client';
|
||||
|
||||
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
|
||||
return new createNetworkInterface({
|
||||
uri: apiUrl,
|
||||
opts: {
|
||||
credentials: 'same-origin',
|
||||
headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export default class AuthorName extends Component {
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
{author && author.displayName}
|
||||
{ showTooltip && <Tooltip>
|
||||
{author && author.name}
|
||||
{ showTooltip && author.settings.bio && <Tooltip>
|
||||
<div className={`${packagename}-bio`}>
|
||||
{author.settings.bio}
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import has from 'lodash/has';
|
||||
import reduce from 'lodash/reduce';
|
||||
const name = 'coral-plugin-comment-count';
|
||||
|
||||
const CommentCount = ({items, id}) => {
|
||||
let count = 0;
|
||||
if (has(items, `assets.${id}.comments`)) {
|
||||
count += items.assets[id].comments.length;
|
||||
}
|
||||
|
||||
// lodash reduce works on {}
|
||||
count += reduce(items.comments, (accum, comment) => {
|
||||
if (comment.children) {
|
||||
accum += comment.children.length;
|
||||
}
|
||||
return accum;
|
||||
}, 0);
|
||||
|
||||
const CommentCount = ({count}) => {
|
||||
return <div className={`${name}-text`}>
|
||||
{`${count} ${count === 1 ? lang.t('comment') : lang.t('comment-plural')}`}
|
||||
</div>;
|
||||
};
|
||||
|
||||
CommentCount.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default CommentCount;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -8,11 +8,15 @@ const name = 'coral-plugin-commentbox';
|
||||
class CommentBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
postItem: PropTypes.func,
|
||||
updateItem: PropTypes.func,
|
||||
id: PropTypes.string,
|
||||
comments: PropTypes.array,
|
||||
reply: PropTypes.bool,
|
||||
|
||||
// updateItem: PropTypes.func,
|
||||
// comments: PropTypes.array,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
authorId: PropTypes.string.isRequired,
|
||||
isReply: PropTypes.bool.isRequired,
|
||||
canPost: PropTypes.bool,
|
||||
currentUser: PropTypes.object
|
||||
}
|
||||
@@ -24,73 +28,83 @@ class CommentBox extends Component {
|
||||
|
||||
postComment = () => {
|
||||
const {
|
||||
|
||||
// child_id,
|
||||
// updateItem,
|
||||
// appendItemArray,
|
||||
commentPostedHandler,
|
||||
postItem,
|
||||
updateItem,
|
||||
id,
|
||||
parent_id,
|
||||
child_id,
|
||||
assetId,
|
||||
parentId,
|
||||
addNotification,
|
||||
appendItemArray,
|
||||
premod,
|
||||
author
|
||||
authorId
|
||||
} = this.props;
|
||||
|
||||
let comment = {
|
||||
body: this.state.body,
|
||||
asset_id: id,
|
||||
author_id: author.id
|
||||
asset_id: assetId,
|
||||
author_id: authorId,
|
||||
parent_id: parentId
|
||||
};
|
||||
let related;
|
||||
let parent_type;
|
||||
if (parent_id) {
|
||||
comment.parent_id = parent_id;
|
||||
related = 'children';
|
||||
parent_type = 'comments';
|
||||
} else {
|
||||
related = 'comments';
|
||||
parent_type = 'assets';
|
||||
}
|
||||
if (child_id || parent_id) {
|
||||
updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
}
|
||||
|
||||
// let related;
|
||||
// let parent_type;
|
||||
// if (parent_id) {
|
||||
// comment.parent_id = parent_id;
|
||||
// related = 'children';
|
||||
// parent_type = 'comments';
|
||||
// } else {
|
||||
// related = 'comments';
|
||||
// parent_type = 'assets';
|
||||
// }
|
||||
// if (child_id || parent_id) {
|
||||
// updateItem(child_id || parent_id, 'showReply', false, 'comments');
|
||||
// }
|
||||
|
||||
if (this.props.charCount && this.state.body.length > this.props.charCount) {
|
||||
return;
|
||||
}
|
||||
postItem(comment, 'comments')
|
||||
.then((postedComment) => {
|
||||
const commentId = postedComment.id;
|
||||
if (postedComment.status === 'rejected') {
|
||||
.then(({data}) => {
|
||||
const postedComment = data.createComment;
|
||||
|
||||
// const commentId = postedComment.id;
|
||||
if (postedComment.status === 'REJECTED') {
|
||||
addNotification('error', lang.t('comment-post-banned-word'));
|
||||
} else if (premod === 'pre') {
|
||||
} else if (postedComment.status === 'PREMOD') {
|
||||
addNotification('success', lang.t('comment-post-notif-premod'));
|
||||
} else {
|
||||
appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type);
|
||||
|
||||
// appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type);
|
||||
addNotification('success', 'Your comment has been posted.');
|
||||
}
|
||||
|
||||
if (commentPostedHandler) {
|
||||
commentPostedHandler();
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
this.setState({body: ''});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {styles, reply, author, charCount} = this.props;
|
||||
const {styles, isReply, authorId, charCount} = this.props;
|
||||
const length = this.state.body.length;
|
||||
return <div>
|
||||
<div
|
||||
className={`${name}-container`}>
|
||||
<label
|
||||
htmlFor={ reply ? 'replyText' : 'commentText'}
|
||||
htmlFor={ isReply ? 'replyText' : 'commentText'}
|
||||
className="screen-reader-text"
|
||||
aria-hidden={true}>
|
||||
{reply ? lang.t('reply') : lang.t('comment')}
|
||||
{isReply ? lang.t('reply') : lang.t('comment')}
|
||||
</label>
|
||||
<textarea
|
||||
className={`${name}-textarea`}
|
||||
style={styles && styles.textarea}
|
||||
value={this.state.body}
|
||||
placeholder={lang.t('comment')}
|
||||
id={reply ? 'replyText' : 'commentText'}
|
||||
id={isReply ? 'replyText' : 'commentText'}
|
||||
onChange={(e) => this.setState({body: e.target.value})}
|
||||
rows={3}/>
|
||||
</div>
|
||||
@@ -101,7 +115,7 @@ class CommentBox extends Component {
|
||||
}
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
{ author && (
|
||||
{ authorId && (
|
||||
<Button
|
||||
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
|
||||
className={`${name}-button`}
|
||||
|
||||
@@ -9,7 +9,7 @@ const getPopupMenu = [
|
||||
() => {
|
||||
return {
|
||||
header: lang.t('step-2-header'),
|
||||
itemType: 'users',
|
||||
itemType: 'USERS',
|
||||
field: 'bio',
|
||||
options: [
|
||||
{val: 'This bio is offensive', text: lang.t('bio-offensive')},
|
||||
|
||||
@@ -14,22 +14,31 @@ class FlagButton extends Component {
|
||||
reason: '',
|
||||
note: '',
|
||||
step: 0,
|
||||
posted: false
|
||||
localPost: null,
|
||||
localDelete: false
|
||||
}
|
||||
|
||||
// When the "report" button is clicked expand the menu
|
||||
onReportClick = () => {
|
||||
if (!this.props.currentUser) {
|
||||
const {currentUser, flag, deleteAction} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current && !localDelete) || localPost;
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
return;
|
||||
}
|
||||
this.setState({showMenu: !this.state.showMenu});
|
||||
if (flagged) {
|
||||
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
|
||||
deleteAction(localPost || flag.current.id);
|
||||
} else {
|
||||
this.setState({showMenu: !this.state.showMenu});
|
||||
}
|
||||
}
|
||||
|
||||
onPopupContinue = () => {
|
||||
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
|
||||
const {itemType, field, reason, step, note, posted} = this.state;
|
||||
const {postAction, id, author_id} = this.props;
|
||||
const {itemType, reason, step, localPost} = this.state;
|
||||
|
||||
// Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.props.getPopupMenu.length) {
|
||||
@@ -39,33 +48,32 @@ class FlagButton extends Component {
|
||||
}
|
||||
|
||||
// If itemType and reason are both set, post the action
|
||||
if (itemType && reason && !posted) {
|
||||
if (itemType && reason && !localPost) {
|
||||
|
||||
// Set the text from the "other" field if it exists.
|
||||
let item_id;
|
||||
switch(itemType) {
|
||||
case 'comments':
|
||||
case 'COMMENTS':
|
||||
item_id = id;
|
||||
break;
|
||||
case 'users':
|
||||
case 'USERS':
|
||||
item_id = author_id;
|
||||
break;
|
||||
}
|
||||
const action = {
|
||||
action_type: `flag_${field}`,
|
||||
metadata: {
|
||||
field,
|
||||
reason,
|
||||
note
|
||||
|
||||
// Note: Action metadata has been temporarily removed.
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({localPost: 'temp'});
|
||||
}
|
||||
postAction({
|
||||
item_id,
|
||||
item_type: itemType,
|
||||
action_type: 'FLAG'
|
||||
}).then(({data}) => {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({localPost: data.createAction.id});
|
||||
}
|
||||
};
|
||||
postAction(item_id, itemType, action)
|
||||
.then((action) => {
|
||||
let id = `${action.action_type}_${action.item_id}`;
|
||||
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, id, action.item_type);
|
||||
this.setState({posted: true});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +106,8 @@ class FlagButton extends Component {
|
||||
|
||||
render () {
|
||||
const {flag, getPopupMenu} = this.props;
|
||||
const flagged = flag && flag.current_user;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const flagged = (flag && flag.current && !localDelete) || localPost;
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
@@ -106,7 +115,7 @@ class FlagButton extends Component {
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{lang.t('reported')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
|
||||
@@ -10,15 +10,15 @@ const getPopupMenu = [
|
||||
return {
|
||||
header: lang.t('step-1-header'),
|
||||
options: [
|
||||
{val: 'users', text: lang.t('flag-username')},
|
||||
{val: 'comments', text: lang.t('flag-comment')}
|
||||
{val: 'USERS', text: lang.t('flag-username')},
|
||||
{val: 'COMMENTS', text: lang.t('flag-comment')}
|
||||
],
|
||||
button: lang.t('continue'),
|
||||
sets: 'itemType'
|
||||
};
|
||||
},
|
||||
(itemType) => {
|
||||
const options = itemType === 'comments' ?
|
||||
const options = itemType === 'COMMENTS' ?
|
||||
[
|
||||
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
|
||||
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
import styles from './Comment.css';
|
||||
|
||||
const Comment = props => {
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
<p className="myCommentAsset">
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href={`${props.asset.url}#${props.comment.id}`}>{props.asset.url}</a>
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.url}</a>
|
||||
</p>
|
||||
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ const CommentHistory = props => {
|
||||
return <Comment
|
||||
key={i}
|
||||
comment={comment}
|
||||
link={props.link}
|
||||
asset={asset} />;
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,76 @@
|
||||
import React from 'react';
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-likes';
|
||||
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => {
|
||||
const liked = like && like.current_user;
|
||||
const onLikeClick = () => {
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
|
||||
showSignInDialog(offset);
|
||||
return;
|
||||
}
|
||||
if (banned) {
|
||||
return;
|
||||
}
|
||||
if (!liked) {
|
||||
const action = {
|
||||
action_type: 'like'
|
||||
};
|
||||
postAction(id, 'comments', action)
|
||||
.then((action) => {
|
||||
let id = `${action.action_type}_${action.item_id}`;
|
||||
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, id, 'comments');
|
||||
});
|
||||
} else {
|
||||
deleteAction(liked.id)
|
||||
.then(() => {
|
||||
updateItem(like.id, 'count', like.count - 1, 'actions');
|
||||
updateItem(like.id, 'current_user', false, 'actions');
|
||||
});
|
||||
}
|
||||
};
|
||||
class LikeButton extends Component {
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
|
||||
{
|
||||
liked
|
||||
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
|
||||
static propTypes = {
|
||||
like: PropTypes.shape({
|
||||
current: PropTypes.obect,
|
||||
count: PropTypes.number
|
||||
}),
|
||||
id: PropTypes.string,
|
||||
postAction: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
banned: PropTypes.boolean
|
||||
}),
|
||||
}
|
||||
|
||||
state = {
|
||||
localPost: null, // Set to the ID of an action if one is posted
|
||||
localDelete: false // Set to true is the user deletes an action, unless localPost is already set.
|
||||
}
|
||||
|
||||
render() {
|
||||
const {like, id, postAction, deleteAction, showSignInDialog, currentUser} = this.props;
|
||||
const {localPost, localDelete} = this.state;
|
||||
const liked = (like && like.current && !localDelete) || localPost;
|
||||
let count = like ? like.count : 0;
|
||||
if (localPost) {count += 1;}
|
||||
if (localDelete) {count -= 1;}
|
||||
|
||||
const onLikeClick = () => {
|
||||
if (!currentUser) {
|
||||
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
|
||||
showSignInDialog(offset);
|
||||
return;
|
||||
}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>thumb_up</i>
|
||||
<span className={`${name}-like-count`}>{like && like.count > 0 && like.count}</span>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
if (currentUser.banned) {
|
||||
return;
|
||||
}
|
||||
if (!liked) {
|
||||
this.setState({localPost: 'temp', localDelete: false});
|
||||
postAction({
|
||||
item_id: id,
|
||||
item_type: 'COMMENTS',
|
||||
action_type: 'LIKE'
|
||||
}).then(({data}) => {
|
||||
this.setState({localPost: data.createAction.id});
|
||||
});
|
||||
} else {
|
||||
this.setState((prev) => prev.localPost ? {...prev, localPost: null} : {...prev, localDelete: true});
|
||||
deleteAction(localPost || like.current.id);
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
|
||||
{
|
||||
liked
|
||||
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>thumb_up</i>
|
||||
<span className={`${name}-like-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default LikeButton;
|
||||
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import CommentBox from '../coral-plugin-commentbox/CommentBox';
|
||||
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyBox = (props) => <div
|
||||
className={`${name}-textarea`}
|
||||
style={props.styles && props.styles.container}>
|
||||
{
|
||||
props.showReply && <CommentBox
|
||||
{...props}
|
||||
comments = {props.child}
|
||||
reply = {true}/>
|
||||
}
|
||||
</div>;
|
||||
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler}) => (
|
||||
<div className={`${name}-textarea`} style={styles && styles.container}>
|
||||
<CommentBox
|
||||
commentPostedHandler={commentPostedHandler}
|
||||
parentId={parentId}
|
||||
addNotification={addNotification}
|
||||
authorId={authorId}
|
||||
assetId={assetId}
|
||||
postItem={postItem}
|
||||
isReply={true} />
|
||||
</div>
|
||||
);
|
||||
|
||||
ReplyBox.propTypes = {
|
||||
commentPostedHandler: PropTypes.func,
|
||||
parentId: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
authorId: PropTypes.string.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ReplyBox;
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyButton = (props) => <button
|
||||
className={`${name}-reply-button`}
|
||||
onClick={() => {
|
||||
if (props.banned) {
|
||||
return;
|
||||
}
|
||||
props.updateItem(props.id, 'showReply', !props.showReply, 'comments');
|
||||
}}>
|
||||
{lang.t('reply')}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>reply</i>
|
||||
</button>;
|
||||
const ReplyButton = ({banned, onClick}) => {
|
||||
return (
|
||||
<button
|
||||
className={`${name}-reply-button`}
|
||||
onClick={onClick}>
|
||||
{lang.t('reply')}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>{banned ? 'BANNED' : 'reply'}</i>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ReplyButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
banned: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ReplyButton;
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, {Component} from 'react';
|
||||
import {graphql} from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export class RileysAwesomeCommentBox extends Component {
|
||||
|
||||
postComment() {
|
||||
console.log(this.props);
|
||||
console.log('postComment', this.props.asset_id);
|
||||
this.props.mutate({
|
||||
variables: {
|
||||
asset_id: this.props.asset_id,
|
||||
body: this.textarea.value,
|
||||
parent_id: null
|
||||
}
|
||||
}).then(({data}) => {
|
||||
console.log('it workt');
|
||||
console.log(data);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<textarea ref={textarea => this.textarea = textarea}></textarea>
|
||||
<button onClick={this.postComment.bind(this)}>POST</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const postComment = gql`
|
||||
fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
user {
|
||||
name: displayName
|
||||
}
|
||||
actions {
|
||||
type: action_type
|
||||
count
|
||||
current: current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
|
||||
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RileysAwesomeCommentBoxWithData = graphql(
|
||||
postComment
|
||||
)(RileysAwesomeCommentBox);
|
||||
|
||||
export default RileysAwesomeCommentBoxWithData;
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, {Component} from 'react';
|
||||
import {graphql} from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import {fetchSignIn} from 'coral-framework/actions/auth';
|
||||
import RileysAwesomeCommentBox from 'coral-plugin-stream/RileysAwesomeCommentBox';
|
||||
|
||||
const assetID = '6187a94b-0b6d-4a96-ac6b-62b529cd8410';
|
||||
|
||||
// MyComponent is a "presentational" or apollo-unaware component,
|
||||
// It could be a simple React class:
|
||||
class Stream extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
logMeIn() {
|
||||
fetchSignIn({email: 'your@example.com', password: 'dfasidfaisdufoiausdfoiuaspdoifas'})(() => {});
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log(this.props);
|
||||
const {data} = this.props;
|
||||
return <div>
|
||||
<button onClick={this.logMeIn.bind(this)}>Login or whatever</button>
|
||||
{
|
||||
data.loading
|
||||
? 'loading!'
|
||||
: <div>
|
||||
<RileysAwesomeCommentBox asset_id={data.asset.id} />
|
||||
<p>Asset ID: {data.asset.id}</p>
|
||||
<ul>
|
||||
{
|
||||
data.asset.comments.map(comment => {
|
||||
return <li key={comment.id}>
|
||||
{comment.body} [{comment.id}]
|
||||
<ul>
|
||||
{
|
||||
comment.replies.map(reply => {
|
||||
return <li key={reply.id}>{reply.body}</li>;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</li>;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize GraphQL queries or mutations with the `gql` tag
|
||||
const StreamQuery = gql`fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
user {
|
||||
name: displayName
|
||||
}
|
||||
actions {
|
||||
type: action_type
|
||||
count
|
||||
current: current_user {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query AssetQuery($asset_id: ID!) {
|
||||
asset(id: $asset_id) {
|
||||
id
|
||||
title
|
||||
url
|
||||
comments {
|
||||
...commentView
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
// We then can use `graphql` to pass the query results returned by MyQuery
|
||||
// to MyComponent as a prop (and update them as the results change)
|
||||
const StreamWithData = graphql(
|
||||
StreamQuery, {
|
||||
options: {
|
||||
variables: {
|
||||
asset_id: assetID
|
||||
}
|
||||
}
|
||||
}
|
||||
)(Stream);
|
||||
|
||||
export default StreamWithData;
|
||||
@@ -1,10 +1,15 @@
|
||||
import React from 'react';
|
||||
import styles from './NotLoggedIn.css';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
|
||||
export default ({showSignInDialog}) => (
|
||||
<div className={styles.message}>
|
||||
<SignInContainer noButton={true}/>
|
||||
<div>
|
||||
<a onClick={showSignInDialog}>Sign In</a> to access Settings
|
||||
<a onClick={() => {
|
||||
console.log('Signin click');
|
||||
showSignInDialog();
|
||||
}}>Sign In</a> to access Settings
|
||||
</div>
|
||||
<div>
|
||||
From the Settings Page you can
|
||||
|
||||
@@ -4,7 +4,11 @@ import styles from './SettingsHeader.css';
|
||||
export default ({userData}) => (
|
||||
<div className={styles.header}>
|
||||
<h1>{userData.displayName}</h1>
|
||||
<h2>{userData.profiles.map(profile => profile.id)}</h2>
|
||||
|
||||
{
|
||||
|
||||
// Hiding display of users ID unless there's a use case for it.
|
||||
// <h2>{userData.profiles.map(profile => profile.id)}</h2>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {connect} from 'react-redux';
|
||||
|
||||
import {saveBio, fetchCommentsByUserId} from 'coral-framework/actions/user';
|
||||
|
||||
import {link} from 'coral-framework/PymConnection';
|
||||
import BioContainer from './BioContainer';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import {TabBar, Tab, TabContent} from '../../coral-ui';
|
||||
@@ -62,6 +63,7 @@ class SettingsContainer extends Component {
|
||||
user.myComments.length && user.myAssets.length
|
||||
? <CommentHistory
|
||||
comments={commentsMostRecentFirst}
|
||||
link={link}
|
||||
assets={user.myAssets.map(id => items.assets[id])} />
|
||||
: <p>{lang.t('user-no-comment')}</p>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Dialog} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
|
||||
import SignInContent from './SignInContent';
|
||||
import SingUpContent from './SignUpContent';
|
||||
import SignUpContent from './SignUpContent';
|
||||
import ForgotContent from './ForgotContent';
|
||||
|
||||
const SignDialog = ({open, view, handleClose, offset, ...props}) => (
|
||||
@@ -17,7 +17,7 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => (
|
||||
}}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
{view === 'SIGNIN' && <SignInContent {...props} />}
|
||||
{view === 'SIGNUP' && <SingUpContent {...props} />}
|
||||
{view === 'SIGNUP' && <SignUpContent {...props} />}
|
||||
{view === 'FORGOT' && <ForgotContent {...props} />}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ const SignUpContent = ({handleChange, formData, ...props}) => (
|
||||
onChange={handleChange}
|
||||
minLength="8"
|
||||
/>
|
||||
{ !props.errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
|
||||
{ props.errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
|
||||
<FormField
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
|
||||
@@ -19,6 +19,7 @@ export default {
|
||||
alreadyHaveAnAccount: 'Already have an account?',
|
||||
recoverPassword: 'Recover password',
|
||||
emailInUse: 'Email address already in use',
|
||||
emailORusernameInUse: 'Email address or Username already in use',
|
||||
requiredField: 'This field is required',
|
||||
passwordsDontMatch: 'Passwords don\'t match.',
|
||||
specialCharacters: 'Display names can contain letters, numbers and _ only',
|
||||
@@ -45,6 +46,7 @@ export default {
|
||||
alreadyHaveAnAccount: 'Ya tienes una cuenta?',
|
||||
recoverPassword: 'Recuperar contraseña',
|
||||
emailInUse: 'Este email se encuentra en uso',
|
||||
emailORusernameInUse: 'Este email ó nombre se encuentran en uso',
|
||||
requiredField: 'Este campo es requerido',
|
||||
passwordsDontMatch: 'Las contraseñas no coinciden',
|
||||
specialCharacters: 'Los nombres pueden contener letras, números y _',
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
display: inline-block;
|
||||
font-family: 'Roboto','Helvetica','Arial',sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
will-change: box-shadow,transform;
|
||||
-webkit-transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);
|
||||
@@ -24,6 +22,14 @@
|
||||
line-height: 28px;
|
||||
vertical-align: middle;
|
||||
margin: 2px;
|
||||
letter-spacing: 0.7px;
|
||||
font-weight: 400;
|
||||
|
||||
i {
|
||||
margin-right: 13px;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.type--black {
|
||||
@@ -69,7 +75,7 @@
|
||||
|
||||
.type--darkGrey {
|
||||
color: white;
|
||||
background: #696969;
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
.type--darkGrey:hover {
|
||||
@@ -104,6 +110,5 @@
|
||||
}
|
||||
|
||||
.raised {
|
||||
background: rgba(158,158,158,.2);
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react';
|
||||
import styles from './Button.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
const Button = ({cStyle = 'local', children, className, raised, full, ...props}) => (
|
||||
const Button = ({cStyle = 'local', children, className, raised = false, full = false, icon = '', ...props}) => (
|
||||
<button
|
||||
className={`
|
||||
${styles.button}
|
||||
${styles[`type--${cStyle}`]}
|
||||
${className}
|
||||
${full && styles.full}
|
||||
${raised && styles.button}
|
||||
${full ? styles.full : ''}
|
||||
${raised ? styles.raised : ''}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon name={icon} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
.base {
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
min-height: 200px;
|
||||
overflow: hidden;
|
||||
width: 330px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.shadow--4 {
|
||||
box-shadow: 0 4px 5px 0 rgba(0,0,0,.14), 0 1px 10px 0 rgba(0,0,0,.12), 0 2px 4px -1px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.shadow--2{
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
.shadow--3 {
|
||||
box-shadow: 0 3px 4px 0 rgba(0,0,0,.14), 0 3px 3px -2px rgba(0,0,0,.2), 0 1px 8px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import styles from './Card.css';
|
||||
|
||||
export default ({children, className, shadow = 2, ...props}) => (
|
||||
<div className={`${styles.base} ${className} ${styles[`shadow--${shadow}`]}`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
.base {
|
||||
height: 30px;
|
||||
width : 30px;
|
||||
}
|
||||
|
||||
.mark {
|
||||
stroke: #FFFFFF;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CoralLogo.css';
|
||||
|
||||
const CoralLogo = ({height = '30px', width = '30px', stroke = '#FFFFFF'}) => (
|
||||
<svg width={width} height={height} viewBox='0 0 381 391' version='1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink'>
|
||||
const CoralLogo = ({className = ''}) => (
|
||||
<svg className={`${styles.base} ${className}`} viewBox='0 0 381 391' version='1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink'>
|
||||
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
|
||||
<g id='Wordmark-Round' transform='translate(-1833.000000, -411.000000)' stroke={stroke} strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
|
||||
<g id='Wordmark-Round' className={styles.mark} transform='translate(-1833.000000, -411.000000)' strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
|
||||
<g id='coralProjectLogo-2-Copy-2' transform='translate(1842.000000, 421.000000)'>
|
||||
<g id='Layer_2' transform='translate(2.268750, 1.133903)'>
|
||||
<rect id='Rectangle-1' fill='#F47E6B' x='0' y='0' width='358.4625' height='368.518519' rx='40'>
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
.base {
|
||||
background: red;
|
||||
i {
|
||||
font-size: 30px;
|
||||
transform: translate(-14px,-12px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.type--approve {
|
||||
background: #00796b;
|
||||
color: rgba(255, 255, 255, 0.901961);
|
||||
background: #388E3C;
|
||||
color: rgba(255, 255, 255, 0.901961);
|
||||
}
|
||||
|
||||
.type--approve:hover {
|
||||
background: #40a244;
|
||||
}
|
||||
|
||||
.type--reject {
|
||||
background: #d32f2f ;
|
||||
color: rgba(255, 255, 255, 0.901961);
|
||||
background: #D32F2F ;
|
||||
color: rgba(255, 255, 255, 0.901961);
|
||||
}
|
||||
|
||||
.type--reject:hover {
|
||||
background: #e53333;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import styles from './FabButton.css';
|
||||
import {FABButton, Icon} from 'react-mdl';
|
||||
|
||||
const FabButton = ({cStyle = 'local', icon, className, ...props}) => (
|
||||
<FABButton className={`${styles[`type--${cStyle}`]} ${className ? className : ''}`} {...props}>
|
||||
<FABButton className={`${styles.base} ${styles[`type--${cStyle}`]} ${className ? className : ''}`} {...props}>
|
||||
<Icon name={icon} />
|
||||
</FABButton>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import {Icon as IconMDL} from 'react-mdl';
|
||||
|
||||
const Icon = ({className, name}) => (
|
||||
<IconMDL className={className} name={name} />
|
||||
);
|
||||
|
||||
export default Icon;
|
||||
@@ -0,0 +1,24 @@
|
||||
.base {
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
background-color: white;
|
||||
width: 208px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
list-style: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 13px;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './Item.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default ({children, itemId, active, onItemClick, className = '', icon}) => (
|
||||
<li
|
||||
className={`${styles.base} ${className} ${active ? styles.active : ''}`}
|
||||
onClick={() => onItemClick(itemId)}
|
||||
>
|
||||
{icon && <Icon name={icon} />}
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
.base {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './List.css';
|
||||
|
||||
export default class List extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickItem = this.handleClickItem.bind(this);
|
||||
}
|
||||
|
||||
handleClickItem(itemId) {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, activeItem, className = ''} = this.props;
|
||||
return (
|
||||
<ul className={`${styles.base} ${className}`}>
|
||||
{React.Children.toArray(children)
|
||||
.filter(child => !child.props.restricted)
|
||||
.map((child, i) =>
|
||||
React.cloneElement(child, {
|
||||
i,
|
||||
active: child.props.itemId === activeItem,
|
||||
onItemClick: this.handleClickItem,
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
.li {
|
||||
.pager {
|
||||
text-align: center;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
padding: 0;
|
||||
min-width: 30px;
|
||||
color: white;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.current {
|
||||
background: #e3edf3;
|
||||
}
|
||||
background: #696969;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
@@ -2,19 +2,18 @@ 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 : ''}`}
|
||||
<li className={curr === i ? styles.current : ''}
|
||||
key={i} onClick={() => onClickHandler(i + 1)}>
|
||||
{i + 1}
|
||||
</li>
|
||||
);
|
||||
|
||||
const Pager = ({totalPages, page, onNewPageHandler}) => (
|
||||
<div className="pager">
|
||||
<div className={styles.pager}>
|
||||
<ul>
|
||||
{
|
||||
(totalPages > page && totalPages > 1) ?
|
||||
<li
|
||||
className={`mdl-button mdl-js-button ${styles.li}`}
|
||||
onClick={() => onNewPageHandler(page - 1)}>
|
||||
Prev
|
||||
</li>
|
||||
@@ -25,7 +24,6 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
|
||||
{
|
||||
(page < totalPages && totalPages > 1) ?
|
||||
<li
|
||||
className={`mdl-button mdl-js-button ${styles.li}`}
|
||||
onClick={() => onNewPageHandler(page + 1)}>
|
||||
Next
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import styles from './TabBar.css';
|
||||
|
||||
export class TabBar extends React.Component {
|
||||
class TabBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickTab = this.handleClickTab.bind(this);
|
||||
|
||||
@@ -9,3 +9,8 @@ export {default as Spinner} from './components/Spinner';
|
||||
export {default as Tooltip} from './components/Tooltip';
|
||||
export {default as PopupMenu} from './components/PopupMenu';
|
||||
export {default as Checkbox} from './components/Checkbox';
|
||||
export {default as Icon} from './components/Icon';
|
||||
export {default as List} from './components/List';
|
||||
export {default as Item} from './components/Item';
|
||||
export {default as Card} from './components/Card';
|
||||
export {default as Pager} from './components/Pager';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user