Merge pull request #4 from coralproject/add-coral-embed-stream

Adding coral embed stream
This commit is contained in:
Riley Davis
2016-11-02 10:33:40 -06:00
committed by GitHub
38 changed files with 1702 additions and 3 deletions
+6 -2
View File
@@ -6,7 +6,11 @@
],
"plugins": [
["transform-decorators-legacy"],
["transform-react-jsx", { "pragma": "h" }],
["transform-object-assign"]
["transform-react-jsx"],
["transform-object-assign"],
["transform-class-properties"],
["transform-async-to-generator"],
["transform-object-rest-spread"],
["transform-class-properties"]
]
}
+41
View File
@@ -0,0 +1,41 @@
var path = require('path')
var express = require('express')
var http = require('http')
var webpack = require('webpack')
var config = require('./webpack.config.dev')
var Dashboard = require('webpack-dashboard')
var DashboardPlugin = require('webpack-dashboard/plugin')
var app = express()
var server = http.Server(app)
var compiler = webpack(config)
var dashboard = new Dashboard()
compiler.apply(new DashboardPlugin(dashboard.setData))
app.use(express.static('public'))
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
quiet: true,
publicPath: config.output.publicPath
}))
app.use(require('webpack-hot-middleware')(compiler, {log: () => {}}))
app.get('/default.css', function (req, res) {
res.sendFile(path.join(__dirname, '/style/default.css'))
})
app.get('*', function (req, res) {
res.sendFile(path.join(__dirname, 'index.html'))
})
server.listen(6182, 'localhost', function (err) {
if (err) {
console.log(err)
return
}
console.log('Listening at http://localhost:6182')
})
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="default.css">
<link href="https://fonts.googleapis.com/css?family=Lato|Open+Sans" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
</head>
<body>
<div id="coralStream"/>
<script src="./bundle.js"></script>
</body>
</html>
@@ -0,0 +1,180 @@
import React, {Component, PropTypes} from 'react'
import {
itemActions,
Notification,
notificationActions,
authActions
} from '../../coral-framework'
import {connect} from 'react-redux'
import CommentBox from '../../coral-plugin-commentbox/CommentBox'
import Content from '../../coral-plugin-commentcontent/CommentContent'
import PubDate from '../../coral-plugin-pubdate/PubDate'
import Count from '../../coral-plugin-comment-count/CommentCount'
import Flag from '../../coral-plugin-flags/FlagButton'
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'
import Pym from 'pym.js'
const {addItem, updateItem, postItem, getItemsQuery, postAction, appendItemRelated} = itemActions
const {addNotification, clearNotification} = notificationActions
const {setLoggedInUser} = authActions
@connect(
(state) => {
return {
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS()
}
},
(dispatch) => {
return {
addItem: (item) => {
return dispatch(addItem(item))
},
updateItem: (id, property, value) => {
return dispatch(updateItem(id, property, value))
},
postItem: (data, type, id) => {
return dispatch(postItem(data, type, id))
},
getItemsQuery: (rootId) => {
return dispatch(getItemsQuery(rootId))
},
addNotification: (type, text) => {
return dispatch(addNotification(type, text))
},
clearNotification: () => {
return dispatch(clearNotification())
},
setLoggedInUser: (user_id) => {
return dispatch(setLoggedInUser(user_id))
},
postAction: (item, action, user) => {
return dispatch(postAction(item, action, user))
},
appendItemRelated: (item, property, value) => {
return dispatch(appendItemRelated(item, property, value))
}
}
}
)
class CommentStream extends Component {
static propTypes = {
items: PropTypes.object.isRequired,
addItem: PropTypes.func.isRequired,
updateItem: PropTypes.func.isRequired
}
componentDidMount () {
// Set up messaging between embedded Iframe an parent component
// Using recommended Pym init code which violates .eslint standards
new Pym.Child({ polling: 500 })
this.props.getItemsQuery('assetTest')
}
render () {
if (Object.keys(this.props.items).length === 0) {
// Loading mock asset
this.props.postItem({
comments: [],
url: 'http://coralproject.net'
}, 'asset', 'assetTest')
// Loading mock user
this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
.then((id) => {
this.props.setLoggedInUser(id)
})
}
// TODO: Replace teststream id with id from params
const rootItemId = 'assetTest'
const rootItem = this.props.items[rootItemId]
console.log(this.props.items);
return <div>
{
rootItem ?
<div>
<div id="commentBox">
<Count
item_id={rootItemId}
items={this.props.items}/>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemRelated={this.props.appendItemRelated}
updateItem={this.props.updateItem}
item_id={rootItemId}/>
</div>
{
rootItem.related.comment.map((commentId) => {
const comment = this.props.items[commentId]
return <div className="comment">
<hr aria-hidden={true}/>
<PubDate created_at={comment.created_at}/>
<Content content={comment.data.content}/>
<div className="commentActions">
<Flag
addNotification={this.props.addNotification}
item_id={commentId}
flag={comment.flag}
postAction={this.props.postAction}
currentUser={this.props.auth.user}/>
<ReplyButton
updateItem={this.props.updateItem}
item_id={commentId}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemRelated={this.props.appendItemRelated}
updateItem={this.props.updateItem}
item_id={commentId}
showReply={comment.showReply}/>
{
comment.related &&
comment.related.child &&
comment.related.child.map((replyId) => {
let reply = this.props.items[replyId]
return <div className="reply">
<hr aria-hidden={true}/>
<PubDate created_at={reply.created_at}/>
<Content content={reply.data.content}/>
<div className="replyActions">
<Flag
addNotificiation={this.props.addNotification}
item_id={replyId}
flag={reply.flag}
postAction={this.props.postAction}
currentUser={this.props.auth.user}/>
<ReplyButton
updateItem={this.props.updateItem}
parent_id={reply.parent_id}/>
</div>
</div>
})
}
</div>
})
}
<Notification
notifLength={this.props.config.notifLength}
clearNotification={this.props.clearNotification}
notification={this.props.notification}/>
</div>
:'Loading'
}
</div>
}
}
export default CommentStream
+13
View File
@@ -0,0 +1,13 @@
import React from 'react'
import { render } from 'react-dom'
import CommentStream from './CommentStream'
import { Provider } from 'react-redux'
import { fetchConfig, store } from '../../coral-framework'
store.dispatch(fetchConfig())
render(
<Provider store={store}>
<CommentStream />
</Provider>
, document.querySelector('#coralStream'))
+113
View File
@@ -0,0 +1,113 @@
body {
font-family: 'Lato', sans-serif;
font-family: 'Open Sans', sans-serif;
width: 100%;
font-size: 12px;
margin: 0;
}
button {
padding: 5px 10px;
margin: 5px;
background: none;
border: none;
}
button:hover {
border-radius: 2px;
color: #FFF;
background-color: rgb(155, 155, 155);
}
button i {
margin-right: 3px;
}
hr {
border: 0;
height: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.screen-reader-text {
clip: rect(1px, 1px, 1px, 1px);
height: 1px;
width: 1px;
overflow: hidden;
position: absolute !important;
}
/* Notification styles */
#coral-notif {
position: fixed;
bottom: 0;
border: 0;
background: rgb(105,105,105);
color: white;
border-radius: 2px;
font-weight: bold;
}
/* Comment Box Styles */
.coral-plugin-commentbox-container {
display: flex;
}
.coral-plugin-commentbox-textarea {
flex: 1;
padding: 10px;
}
.coral-plugin-commentbox-button-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
#coralStream .coral-plugin-commentbox-button {
float: right;
margin-top: 10px;
padding: 5px 10px;
background: rgb(105, 105, 105);
color: #FFF;
border: none;
border-radius: 2px;
}
/* Comment styles */
.comment {
margin-bottom: 10px;
}
.coral-plugin-commentcontent-text {
margin-bottom: 10px;
}
/* Reply styles */
.comment .reply {
margin: 0px 0px 10px 20px;
}
/* Comment Action Styles */
.commentActions, .replyActions {
display: flex;
justify-content: flex-end;
}
.commentActions .material-icons, .replyActions .material-icons {
font-size: 12px;
vertical-align: middle;
}
/* Comment count styles */
.coral-plugin-comment-count-text {
margin-bottom: 15px;
}
.coral-plugin-pubdate-text {
color: #CCC;
}
@@ -0,0 +1,58 @@
var path = require('path')
var webpack = require('webpack')
module.exports = {
devtool: 'eval',
entry: [
'babel-polyfill',
'webpack-hot-middleware/client',
path.join(__dirname, 'src', 'app')
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/'
},
resolve: {
root: [
path.resolve(__dirname, 'src')
],
extensions: ['', '.js', '.jsx']
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('development')
}
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
}),
new webpack.NoErrorsPlugin()
],
module: {
loaders: [{
test: /\.(js|jsx)$/,
loaders: ['babel'],
exclude: /node_modules/,
include: path.join(__dirname, '../')
}, {
test: /\.css$/,
loader: 'style-loader!css-loader'
}, {
test: /\.png$/,
loader: 'url-loader?limit=100000'
}, {
test: /\.(jpg|png|gif|svg)$/,
loader: 'file-loader'
}, {
test: /\.json$/,
loader: 'json-loader'
}, {
test: /\.woff$/,
loader: 'url?limit=100000'
}]
}
}
@@ -0,0 +1,73 @@
const path = require('path')
const webpack = require('webpack')
const Copy = require('copy-webpack-plugin')
//Keeping this file for reference, it should move to a global webpack.
module.exports = {
devtool: 'source-map',
entry: [
'babel-polyfill',
'./src/app'
],
output: {
path: path.join(__dirname, '..', '..','dist', 'coral-embed-stream'),
filename: 'bundle.js',
publicPath: '/dist/'
},
resolve: {
root: [
path.resolve(__dirname, 'src')
],
extensions: ['', '.js', '.jsx']
},
plugins: [
new Copy([{
from: './index.html'
},
{
from: './style/default.css'
},
{
from: './public/',
to: './'
},
{
from: path.resolve(__dirname, '..', 'coral-framework', 'i18n', 'translations'),
to: './translations'
}]),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
}),
new webpack.ExtendedAPIPlugin()
],
module: {
loaders: [{
test: /\.(js|jsx)$/,
loaders: ['babel'],
exclude: /node_modules/,
include: path.join(__dirname, '../')
}, {
test: /\.css$/,
loader: 'style-loader!css-loader'
}, {
test: /\.png$/,
loader: 'url-loader?limit=100000'
}, {
test: /\.jpg$/,
loader: 'file-loader'
}, {
test: /\.json$/,
loader: 'json-loader'
}, {
test: /\.woff$/,
loader: 'url?limit=100000'
}]
}
}
@@ -0,0 +1,31 @@
import { Map } from 'immutable'
import {expect} from 'chai'
import authReducer from '../../store/reducers/auth'
import * as actions from '../../store/actions/auth'
describe ('authReducer', () => {
describe('SET_LOGGED_IN_USER', () => {
it('should set a logged in user', () => {
const action = {
type: actions.SET_LOGGED_IN_USER,
user_id: '123'
}
const store = new Map({})
const result = authReducer(store, action)
expect(result.get('user')).to.equal(action.user_id)
})
})
describe('LOG_OUT_USER', () => {
it('should clear the user store', () => {
const action = {
type: actions.LOG_OUT_USER
}
const store = new Map({
user: '123'
})
const result = authReducer(store, action)
expect(result.get('user')).to.equal(undefined)
})
})
})
@@ -0,0 +1,152 @@
import 'react'
import 'redux'
import {expect} from 'chai'
import fetchMock from 'fetch-mock'
import * as actions from '../../store/actions/items'
import {Map} from 'immutable'
import configureStore from 'redux-mock-store'
const mockStore = configureStore()
describe('itemActions', () => {
let store
const host = 'http://test.host'
beforeEach(() => {
store = mockStore(new Map({}))
fetchMock.restore()
})
describe('getItemsQuery', () => {
const query = 'all'
const rootId = '1234'
const view = 'testView'
const response = {results: [
{Docs: [
{type: 'comment', data: {content: 'stuff'}, item_id: '123'},
{type: 'comment', data: {content: 'morestuff'}, item_id: '456'}
]}
]}
it('should get an item from a query and send the appropriate dispatches', () => {
fetchMock.get('*', JSON.stringify(response))
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
.then((res) => {
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/exec/all/view/testView/1234')
expect(res).to.deep.equal(response.results[0].Docs)
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: response.results[0].Docs[0],
item_id: '123'
})
expect(store.getActions()[1]).to.deep.equal({
type: actions.ADD_ITEM,
item: response.results[0].Docs[1],
item_id: '456'
})
})
})
it('should handle an error', () => {
fetchMock.get('*', 404)
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
})
describe('getItemsArray', () => {
const response = {items: [{type: 'comment', item_id: '123'}, {type: 'comment', item_id: '456'}]}
const ids = [1, 2]
it('should get an item from an array of ids and send the appropriate dispatches', () => {
fetchMock.get('*', JSON.stringify(response))
return actions.getItemsArray(ids, host)(store.dispatch)
.then((res) => {
expect(res).to.deep.equal(response.items)
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
type: 'comment',
item_id: '123'
},
item_id: '123'
})
expect(store.getActions()[1]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
type: 'comment', item_id: '456'
},
item_id: '456'
})
})
})
it('should handle an error', () => {
fetchMock.get('*', 404)
return actions.getItemsArray(ids, host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
})
describe('postItem', () => {
const item = {
type: 'comment',
data:{content: 'stuff'}
}
it ('should post an item, return an id, then dispatch that item to the store', () => {
fetchMock.post('*', {item_id: '123', type: 'comment', data: {content: 'stuff'}})
return actions.postItem(item.data, item.type, undefined, host)(store.dispatch)
.then((id) => {
expect(fetchMock.calls().matched[0][1]).to.deep.equal(
{
method: 'POST',
body: JSON.stringify({...item, version: 1})
}
)
expect(id).to.equal('123')
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
type: 'comment',
data: {
content: 'stuff'
},
item_id: '123'
},
item_id: '123'
})
})
})
it('should handle an error', () => {
fetchMock.post('*', 404)
return actions.postItem(item, host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
})
describe('postAction', () => {
it ('should post an action', () => {
fetchMock.post('*', 200)
return actions.postAction('abc', 'flag', '123', host)(store.dispatch)
.then(response => {
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/action/flag/user/123/on/item/abc')
expect(response).to.equal('')
})
})
it('should handle an error', () => {
fetchMock.post('*', 404)
return actions.postItem('abc', 'flag', '123', host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
})
})
@@ -0,0 +1,149 @@
import { Map, fromJS } from 'immutable'
import {expect} from 'chai'
import itemsReducer from '../../store/reducers/items'
import * as actions from '../../store/actions/items'
describe ('itemsReducer', () => {
describe('ADD_ITEM', () => {
it('should add an item', () => {
const action = {
type: 'ADD_ITEM',
item: {
type: 'comment',
data: {
content: 'stuff'
},
item_id: '123'
},
item_id: '123'
}
const store = new Map({})
const result = itemsReducer(store, action)
expect(result.get('123').toJS()).to.deep.equal({
type: 'comment',
data: {
content: 'stuff'
},
item_id: '123'
})
})
})
describe ('UPDATE_ITEM', () => {
it ('should update an item', () => {
const action = {
type: 'UPDATE_ITEM',
property: 'stuff',
value: 'things',
item_id: '123'
}
const store = fromJS({
'123': {
item_id: '123',
data: {
stuff: 'morestuff'
}
}
})
const result = itemsReducer(store, action)
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
data: {
stuff: 'things'
}
})
})
})
describe('APPEND_ITEM_ARRAY', () => {
let action
let store
beforeEach (() => {
action = {
type: 'APPEND_ITEM_ARRAY',
property: 'stuff',
value: 'things',
item_id: '123'
}
store = fromJS({
'123': {
item_id: '123',
data: {
stuff: ['morestuff']
}
}
})
})
it ('should append to an existing array', () => {
const result = itemsReducer(store, action)
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
data: {
stuff: ['morestuff', 'things']
}
})
})
it ('should create a new array', () => {
store = fromJS({
'123': {
item_id: '123',
data: {}
}
})
const result = itemsReducer(store, action)
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
data: {
stuff: ['things']
}
})
})
})
describe('APPEND_ITEM_RELATED', () => {
let action
let store
beforeEach (() => {
action = {
type: 'APPEND_ITEM_RELATED',
property: 'stuff',
value: 'things',
item_id: '123'
}
store = fromJS({
'123': {
item_id: '123',
related: {
stuff: ['morestuff']
}
}
})
})
it ('should append to an existing array', () => {
const result = itemsReducer(store, action)
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
related: {
stuff: ['morestuff', 'things']
}
})
})
it ('should create a new array', () => {
store = fromJS({
'123': {
item_id: '123',
related: {}
}
})
const result = itemsReducer(store, action)
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
related: {
stuff: ['things']
}
})
})
})
})
@@ -0,0 +1,35 @@
import { Map } from 'immutable'
import {expect} from 'chai'
import notificationReducer from '../../store/reducers/notification'
import * as actions from '../../store/actions/notification'
describe ('notificationsReducer', () => {
describe('ADD_NOTIFICATION', () => {
it('should add a notification', () => {
const action = {
type: actions.ADD_NOTIFICATION,
text: 'Test notification',
notifType: 'test'
}
const store = new Map({})
const result = notificationReducer(store, action)
expect(result.get('text')).to.equal(action.text)
expect(result.get('type')).to.equal(action.notifType)
})
})
describe('CLEAR_NOTIFICATION', () => {
it('should clear a notification', () => {
const action = {
type: actions.CLEAR_NOTIFICATION
}
const store = new Map({
text: 'Test notification',
type: 'test'
})
const result = notificationReducer(store, action)
expect(result.get('text')).to.equal(undefined)
expect(result.get('type')).to.equal(undefined)
})
})
})
+74
View File
@@ -0,0 +1,74 @@
import timeago from 'timeago.js'
import esTA from 'timeago.js/locales/es'
/**
* Default locales, this should be overriden by config file
*/
class i18n {
constructor (translations) {
/**
* Register locales
*/
this.locales = {'en': 'en', 'es': 'es'}
timeago.register('es_ES', esTA)
this.timeagoInstance = new timeago()
/**
* Load translations
*/
let trans = translations || { en: {} }
try {
const locale = localStorage.getItem('locale') || navigator.language
localStorage.setItem('locale', locale)
const lang = this.locales[locale.split('-')[0]] || 'en'
this.translations = trans[lang]
} catch (err) {
this.translations = trans['en']
}
this.setLocale = (locale) => {
try {
localStorage.setItem('locale', locale)
} catch (err) {}
}
this.getLocale = () => (
localStorage.getItem('locale') || navigator.locale || 'en-US'
)
/**
* Expose the translation function
*
* it takes a string with the translation key and returns
* the translation value or the key itself if not found
* it works with nested translations (my.page.title)
*/
this.t = (key) => {
const arr = key.split('.')
let translation = this.translations
try {
for (var i = 0; i < arr.length; i++) translation = translation[arr[i]]
} catch (error) {
console.warn(`${key} language key not set`)
return key
}
const val = String(translation)
if (val) {
return val
} else {
console.warn(`${key} language key not set`)
return key
}
}
this.timeago = (time) => {
return this.timeagoInstance.format(time)
}
}
}
export default i18n
+17
View File
@@ -0,0 +1,17 @@
import Notification from './notification/Notification'
import store from './store/store'
import {fetchConfig} from './store/actions/config'
import * as itemActions from './store/actions/items'
import I18n from './i18n/i18n'
import * as notificationActions from './store/actions/notification'
import * as authActions from './store/actions/auth'
export {
Notification,
store,
fetchConfig,
itemActions,
I18n,
notificationActions,
authActions
}
+43
View File
@@ -0,0 +1,43 @@
{
"query": [
{
"item_id": "assetTest",
"type": "asset",
"data": {
"url": "http://coralproject.net"
},
"related": {
"comment": [
"abc",
"def"
]
}
},
{
"item_id": "abc",
"type": "comment",
"data": {
"content": "Sample comment"
},
"related":{
"child": [
"xyz"
]
}
},
{
"item_id": "xyz",
"type": "comment",
"data": {
"content": "Sample reply"
}
},
{
"item_id": "def",
"type": "comment",
"data": {
"content": "Another reply"
}
}
]
}
@@ -0,0 +1,19 @@
import React from 'react'
const Notification = (props) => {
if (props.notification.text) {
setTimeout(() => {
props.clearNotification()
}, props.notifLength)
}
return <div>
{
props.notification.text &&
<dialog open id='coral-notif' className={'coral-notif-' + props.notification.type}>
{props.notification.text}
</dialog>
}
</div>
}
export default Notification
@@ -0,0 +1,17 @@
/* Auth Actions */
export const SET_LOGGED_IN_USER = 'SET_LOGGED_IN_USER'
export const LOG_OUT_USER = 'LOG_OUT_USER'
export const setLoggedInUser = (user_id) => {
return {
type: SET_LOGGED_IN_USER,
user_id
}
}
export const LogOutUser = () => {
return {
type: LOG_OUT_USER
}
}
@@ -0,0 +1,28 @@
/* @flow */
import { fromJS } from 'immutable'
/**
* Action name constants
*/
export const FETCH_CONFIG_REQUEST = 'FETCH_CONFIG_REQUEST'
export const FETCH_CONFIG_FAILED = 'FETCH_CONFIG_FAILED'
export const FETCH_CONFIG_SUCCESS = 'FETCH_CONFIG_SUCCESS'
/**
* Action creators
*/
export const fetchConfig = () => async (dispatch) => {
dispatch({ type: FETCH_CONFIG_REQUEST })
try {
//TODO: Replace with fetching config from backend
// const response = await fetch(`./talk.config.json`)
// const json = await response.json()
dispatch({ type: FETCH_CONFIG_SUCCESS, config: fromJS({}) })
} catch (error) {
dispatch({ type: FETCH_CONFIG_FAILED })
}
}
@@ -0,0 +1,221 @@
/* Item Actions */
import { fromJS } from 'immutable'
import mocks from '../../mocks.json'
/**
* Action name constants
*/
export const ADD_ITEM = 'ADD_ITEM'
export const UPDATE_ITEM = 'UPDATE_ITEM'
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY'
export const APPEND_ITEM_RELATED = 'APPEND_ITEM_RELATED'
/**
* Action creators
*/
/*
* Adds an item to the local store without posting it to the server
* Useful for optimistic posting, etc.
*
* @params
* item - the item to be posted
*
*/
export const addItem = (item) => {
if (!item.item_id) {
console.warn('addItem called without an item id.')
}
return {
type: ADD_ITEM,
item: item,
item_id: item.item_id
}
}
/*
* Updates an item in the local store without posting it to the server
* Useful for item-level toggles, etc.
*
* @params
* item_id - the id of the item to be posted
* property - the property to be updated
* value - the value that the property should be set to
*
*/
export const updateItem = (item_id, property, value) => {
return {
type: UPDATE_ITEM,
item_id,
property,
value
}
}
export const appendItemArray = (item_id, property, value) => {
return {
type: APPEND_ITEM_ARRAY,
item_id,
property,
value
}
}
export const appendItemRelated = (item_id, property, value) => {
return {
type: APPEND_ITEM_RELATED,
item_id,
property,
value
}
}
/*
* Get Items from Query
* Gets a set of items from a predefined query
*
* @params
* Query - a predefiend query for retreiving items
*
* @returns
* A promise resolving to a set of items
*
* @dispatches
* A set of items to the item store
*/
export function getItemsQuery (rootId) {
return (dispatch) => {
// return fetch('/v1/exec/view/' + rootId)
// .then(
// response => {
// return response.ok ? response.json() : Promise.reject(response.status + ' ' + response.statusText)
// }
// )
// .then((json) => {
// let items = json.results[0].Docs
// for (var i = 0; i < items.length; i++) {
// dispatch(addItem(items[i]))
// }
// return (items)
// })
console.log('Loading mock data', mocks);
let mockData = mocks.query
for (var i=0; i < mockData.length; i++ ) {
dispatch(addItem(mockData[i]))
}
}
}
/*
* Get Items Array
* Gets a set of items from an array of item ids
*
* @params
* Query - a predefiend query for retreiving items
*
* @returns
* A promise resolving to a set of items
*
* @dispatches
* A set of items to the item store
*/
export function getItemsArray (ids) {
return (dispatch) => {
return fetch('/v1/item/' + ids)
.then(
response => {
return response.ok ? response.json()
: Promise.reject(response.status + ' ' + response.statusText)
}
)
.then((json) => {
for (var i = 0; i < json.items.length; i++) {
dispatch(addItem(json.items[i]))
}
return json.items
})
}
}
/*
* PutItem
* Puts an item
*
* @params
* Item - the item to be put
*
* @returns
* A promise resolving to an item is
*
* @dispatches
* The newly put item to the item store
*/
export function postItem (data, type, id) {
return (dispatch) => {
let item = {
type,
data,
version: 1
}
if (id) {
item.item_id = id
}
let options = {
method: 'POST',
body: JSON.stringify(item)
}
return fetch('/v1/item', options)
.then(
response => {
return response.ok ? response.json()
: Promise.reject(response.status + ' ' + response.statusText)
}
)
.then((json) => {
// Patch until ID is returned from backend
dispatch(addItem(json))
return json.item_id
})
}
}
//http://localhost:16180/v1/action/flag/user/user_89654/on/item/87e418c5-aafb-4eb7-9ce4-78f28793782a
/*
* PostAction
* Posts an action to an item
*
* @params
* item_id - the id of the item on which the action is taking place
* action - the name of the action
* user - the user performing the action
* host - the coral host
*
* @returns
* A promise resolving to null or an error
*
*/
export function postAction (item, action, user) {
return (dispatch) => {
let options = {
method: 'POST'
}
dispatch(appendItemArray(item, action, user))
return fetch('/v1/action/' + action + '/user/' + user + '/on/item/' + item, options)
.then(
response => {
return response.ok ? response.text()
: Promise.reject(response.status + ' ' + response.statusText)
}
)
}
}
@@ -0,0 +1,18 @@
/* Notification Actions */
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'
export const addNotification = (notifType, text) => {
return {
type: ADD_NOTIFICATION,
notifType,
text
}
}
export const clearNotification = () => {
return {
type: CLEAR_NOTIFICATION
}
}
@@ -0,0 +1,17 @@
/* Auth */
import * as actions from '../actions/auth'
import { fromJS } from 'immutable'
const initialState = fromJS({})
export default (state = initialState, action) => {
switch (action.type) {
case actions.SET_LOGGED_IN_USER:
return state.set('user', action.user_id)
case actions.LOG_OUT_USER:
return initialState
default:
return state
}
}
@@ -0,0 +1,25 @@
/* @flow */
import { Map, fromJS } from 'immutable'
import * as actions from '../actions/config'
const initialState = Map({
features: Map({})
})
export default (state = initialState, action) => {
switch(action.type) {
case actions.FETCH_CONFIG_REQUEST:
return state.set('loading', true)
case actions.FETCH_CONFIG_FAILED:
return state.set('loading', false)
// Override config if worked
case actions.FETCH_CONFIG_SUCCESS:
return action.config.set('loading', false)
default:
return state
}
}
@@ -0,0 +1,18 @@
/* @flow */
import { combineReducers } from 'redux'
import config from './config'
import items from './items'
import notification from './notification'
import auth from './auth'
/**
* Expose the combined main reducer
*/
export default combineReducers({
config,
items,
notification,
auth
})
@@ -0,0 +1,29 @@
/* Items Reducer */
import { Map, fromJS } from 'immutable'
import * as actions from '../actions/items'
const initialState = fromJS({})
export default (state = initialState, action) => {
switch (action.type) {
case actions.ADD_ITEM:
return state.set(action.item_id, fromJS(action.item))
case actions.UPDATE_ITEM:
return state.updateIn([action.item_id, 'data', action.property], () =>
fromJS(action.value)
)
case actions.APPEND_ITEM_ARRAY:
return state.updateIn([action.item_id, 'data', action.property], (prop) => {
return prop ? prop.push(action.value) : fromJS([action.value])
}
)
case actions.APPEND_ITEM_RELATED:
return state.updateIn([action.item_id, 'related', action.property], (prop) => {
return prop ? prop.push(action.value) : fromJS([action.value])
}
)
default:
return state
}
}
@@ -0,0 +1,17 @@
/* Items Notifications */
import * as actions from '../actions/notification'
import { fromJS } from 'immutable'
const initialState = fromJS({})
export default (state = initialState, action) => {
switch (action.type) {
case actions.ADD_NOTIFICATION:
return state.set('text', action.text).set('type', action.notifType)
case actions.CLEAR_NOTIFICATION:
return initialState
default:
return state
}
}
+10
View File
@@ -0,0 +1,10 @@
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import mainReducer from './reducers'
export default createStore(
mainReducer,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk)
)
@@ -0,0 +1,23 @@
import React from 'react'
const name = 'coral-plugin-comment-count'
const CommentCount = ({items, item_id}) => {
let count = 0
if (items[item_id]) {
count += items[item_id].related.comment.length
}
const itemKeys = Object.keys(items)
for (var i=0; i < itemKeys.length; i++) {
const item = items[itemKeys[i]]
if (item.type === 'comment' && item.related && item.related.child) {
count += item.related.child.length
}
}
return <div className={name + '-text'}>
{count + ' ' + (count === 1 ? 'Comment':'Comments')}
</div>
}
export default CommentCount
@@ -0,0 +1,84 @@
import React, {Component, PropTypes} from 'react'
import {I18n} from '../coral-framework'
const name='coral-plugin-commentbox'
class CommentBox extends Component {
static propTypes = {
postItem: PropTypes.func,
updateItem: PropTypes.func,
item_id: PropTypes.string,
comments: PropTypes.array,
reply: PropTypes.bool
}
state = {
content: ''
}
postComment = () => {
const {postItem, updateItem, item_id, reply, addNotification, appendItemRelated} = this.props
let comment = {
content: this.state.content
}
let related
if (reply) {
comment.parent_id = item_id
related = 'child'
} else {
comment.asset_id = item_id
related = 'comment'
}
updateItem(item_id, 'showReply', false)
postItem(comment, 'comment')
.then((id) => {
appendItemRelated(item_id, related, id)
addNotification('success', 'Your comment has been posted.')
}).catch((err) => console.error(err))
this.setState({content: ''})
}
render () {
const {styles, reply} = this.props
// How to handle language in plugins? Should we have a dependency on our central translation file?
return <div>
<div
className={name + '-container'}
style={styles && styles.container}>
<label
htmlFor={ reply ? 'replyText' : 'commentText'}
className="screen-reader-text"
aria-hidden={true}>
{reply ? 'Reply': 'Comment'}
</label>
<textarea
className={name + '-textarea'}
style={styles && styles.textarea}
value={this.state.content}
id={reply ? 'replyText' : 'commentText'}
onChange={(e) => this.setState({content: e.target.value})}
rows={3}/>
</div>
<div className={name + '-button-container'}>
<button
className={name + '-button'}
style={styles && styles.button}
onClick={this.postComment}>
{lang.t('post')}
</button>
</div>
</div>
}
}
export default CommentBox
const lang = new I18n({
en: {
post: 'Post'
},
es: {
post: 'Publicar'
}
})
@@ -0,0 +1,26 @@
import React from 'react'
import {shallow, mount} from 'enzyme'
import {expect} from 'chai'
import CommentBox from '../CommentBox'
describe('CommentBox', () => {
let comment
let render
beforeEach(() => {
comment = {}
const postItem = (item) => {
comment.posted=item
return Promise.resolve(4)
}
render = shallow(<CommentBox
postItem={postItem}
updateItem={(e) => comment.text=e.target.value}
item_id={'1'}
comments={['1', '2', '3']}/>)
})
it('should render the CommentBox appropriately', () => {
expect(render.contains('<div class="CommentBox"')).to.be.truthy
expect(render.contains('<button class="postCommentButton"')).to.be.truthy
})
})
@@ -0,0 +1,10 @@
import React from 'react'
const name = 'coral-plugin-replies'
const Content = (props) => <div
className={name + '-text'}
style={props.styles && props.styles.text}>
{props.content}
</div>
export default Content
@@ -0,0 +1,11 @@
import React from 'react'
import {shallow, mount} from 'enzyme'
import {expect} from 'chai'
import CommentContent from '../CommentContent'
describe('CommentContent', () => {
it('should render content', () => {
const render = shallow(<CommentContent content="test"/>)
expect(render.contains('test')).to.be.truthy
})
})
+35
View File
@@ -0,0 +1,35 @@
import React from 'react'
const name='coral-plugin-flags'
const FlagButton = ({flag, item_id, postAction, currentUser, addNotification}) => {
const flagged = flag && flag.includes(currentUser)
const onFlagClick = () => {
postAction(item_id, 'flag', currentUser)
addNotification('success', 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.')
}
return <div className={name + '-container'}>
<button onClick={onFlagClick} className={name + '-button'}>
<i className={name + '-icon material-icons'}
style={flagged ? styles.flaggedIcon : styles.unflaggedIcon}
aria-hidden={true}>flag</i>
{
flagged
? <span className={name + '-button-text'}>Flagged</span>
: <span className={name + '-button-text'}>Flag</span>
}
</button>
</div>
}
export default FlagButton
const styles = {
flaggedIcon: {
color: '#F00'
},
unflaggedIcon: {
color: 'inherit'
}
}
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import {I18n} from '../coral-framework'
const lang = new I18n()
const name = 'coral-plugin-pubdate'
const PubDate = ({created_at}) => <div className={name + '-text'}>
{lang.timeago(created_at)}
</div>
export default PubDate
+21
View File
@@ -0,0 +1,21 @@
import React 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
item_id = {props.item_id}
postItem = {props.postItem}
addNotification = {props.addNotification}
appendItemRelated = {props.appendItemRelated}
updateItem = {props.updateItem}
comments = {props.child}
reply = {true}/>
}
</div>
export default ReplyBox
@@ -0,0 +1,23 @@
import React from 'react'
import {I18n} from '../coral-framework'
const name = 'coral-plugin-replies'
const ReplyButton = (props) => <button
className={name + '-reply-button'}
onClick={(e) => props.updateItem(props.item_id || props.parent_id, 'showReply', true)}>
<i className={name + '-icon material-icons'}
aria-hidden={true}>reply</i>
{lang.t('reply')}
</button>
export default ReplyButton
const lang = new I18n({
en: {
'reply': 'Reply'
},
es: {
'reply': '¡traduceme!'
}
})
+7
View File
@@ -0,0 +1,7 @@
import ReplyBox from './ReplyBox'
import ReplyButton from './ReplyButton'
export {
ReplyBox,
ReplyButton
}
+34 -1
View File
@@ -5,7 +5,8 @@
"main": "app.js",
"scripts": {
"lint": "eslint .",
"start": "./bin/www"
"start": "./bin/www",
"embed-start": "node client/coral-embed-stream/dev-server.js"
},
"repository": {
"type": "git",
@@ -27,5 +28,37 @@
"debug": "^2.2.0",
"express": "^4.14.0",
"mongoose": "^4.6.5"
},
"devDependencies": {
"babel-core": "6.14.0",
"babel-jest": "^15.0.0",
"babel-loader": "6.2.5",
"babel-plugin-transform-async-to-generator": "^6.8.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-object-assign": "^6.8.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "6.13.0",
"babel-preset-es2015-minimal": "^2.1.0",
"babel-preset-stage-0": "^6.16.0",
"copy-webpack-plugin": "^3.0.1",
"exports-loader": "^0.6.3",
"immutable": "^3.8.1",
"imports-loader": "^0.6.5",
"json-loader": "^0.5.4",
"pym.js": "^1.1.1",
"react": "15.3.2",
"react-dom": "15.3.2",
"react-redux": "^4.4.5",
"redux": "^3.6.0",
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
"timeago.js": "^2.0.3",
"webpack": "^1.13.2",
"webpack-dashboard": "^0.2.0",
"webpack-dev-middleware": "^1.8.3",
"webpack-hot-middleware": "^2.12.2",
"webpack-module-hot-accept": "^1.0.4",
"whatwg-fetch": "^1.0.0"
}
}