diff --git a/.eslintignore b/.eslintignore index 53c37a166..a4865e1f6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -dist \ No newline at end of file +dist +client/lib diff --git a/app.js b/app.js index e84eb4b79..dd58e9883 100644 --- a/app.js +++ b/app.js @@ -22,7 +22,10 @@ if (app.get('env') !== 'test') { //============================================================================== app.set('trust proxy', 1); -app.use(helmet()); +// We disable frameward on helmet to allow crossdomain injection of the embed +app.use(helmet({ + frameguard: false +})); app.use(bodyParser.json()); app.use('/client', express.static(path.join(__dirname, 'dist'))); app.set('views', path.join(__dirname, 'views')); diff --git a/bin/cli-assets b/bin/cli-assets index 99965c6d9..9ab36685a 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -12,9 +12,11 @@ process.env.DEBUG = process.env.TALK_DEBUG; 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 mongoose = require('../mongoose'); +const scraper = require('../services/scraper'); const util = require('../util'); // Register the shutdown criteria. @@ -55,6 +57,37 @@ function listAssets() { }); } +function refreshAssets(ageString) { + const now = new Date().getTime(); + const ageMs = parseDuration(ageString); + const age = new Date(now - ageMs); + + Asset.find({ + $or: [ + { + scraped: { + $lte: age + } + }, + { + scraped: null + } + ] + }) + + // Queue all the assets for scraping. + .then((assets) => Promise.all(assets.map(scraper.create))) + + .then(() => { + console.log('Assets were queued to be scraped'); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); +} + //============================================================================== // Setting up the program command line arguments. //============================================================================== @@ -67,6 +100,11 @@ program .description('list all the assets in the database') .action(listAssets); +program + .command('refresh ') + .description('queues the assets that exceed the age requested') + .action(refreshAssets); + program.parse(process.argv); // If there is no command listed, output help. diff --git a/bin/cli-jobs b/bin/cli-jobs index f14276d38..bc320d14b 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -14,11 +14,15 @@ const program = require('commander'); const scraper = require('../services/scraper'); const util = require('../util'); const mongoose = require('../mongoose'); +const kue = require('../kue'); util.onshutdown([ () => mongoose.disconnect() ]); +/** + * Starts the job processor. + */ function processJobs() { // Start the processor. @@ -31,6 +35,65 @@ function processJobs() { ]); } +/** + * Removes a single job. + * @param {Object} job the job to be removed + * @return {Promise} + */ +function removeJob(job) { + return new Promise((resolve, reject) => job.remove((err) => { + if (err) { + return reject(err); + } + + return resolve(job); + })); +} + +/** + * Removes the jobs passed in and returns a promise. + * @param {Array} jobs array of jobs + * @return {Promise} + */ +function removeJobs(jobs) { + return Promise.all(jobs.map(removeJob)); +} + +/** + * Get the top n jobs with a specific state. + * @param {String} [state='complete'] state to list jobs by + * @param {Number} limit limit of jobs to load + * @return {Promise} + */ +function rangeJobsByState(state = 'complete', limit) { + return new Promise((resolve, reject) => { + kue.Job.rangeByState(state, 0, limit, 'asc', (err, jobs) => { + if (err) { + return reject(err); + } + + resolve(jobs); + }); + }); +} + +/** + * Cleans up the jobs that are in the queue. + */ +function cleanupJobs(options) { + const n = 100; + + Promise.all([ + rangeJobsByState('complete', n), + options.stuck ? rangeJobsByState('failed', n) : false + ]) + .then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs)) + .then(() => { + util.shutdown(); + console.log('Removed old jobs'); + }); +} + //============================================================================== // Setting up the program command line arguments. //============================================================================== @@ -40,6 +103,12 @@ program .description('starts job processing') .action(processJobs); +program + .command('cleanup') + .option('-s, --stuck', 'cleans up jobs that have been stuck', false) + .description('cleans up inactive jobs') + .action(cleanupJobs); + program.parse(process.argv); // If there is no command listed, output help. diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 2f8f1041e..54763259d 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,5 +1,5 @@ import * as actions from '../constants/auth'; -import {base, handleResp, getInit} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; // Check Login @@ -9,8 +9,7 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); - fetch(`${base}/auth`, getInit('GET')) - .then(handleResp) + coralApi('/auth') .then(user => { const isAdmin = !!user.roles.filter(i => i === 'admin').length; dispatch(checkLoginSuccess(user, isAdmin)); @@ -26,8 +25,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE}); export const logout = () => dispatch => { dispatch(logOutRequest()); - fetch(`${base}/auth`, getInit('DELETE')) - .then(handleResp) + coralApi('/auth', {method: 'DELETE'}) .then(() => dispatch(logOutSuccess())) .catch(error => dispatch(logOutFailure(error))); }; diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 5921573d1..8b8e883d8 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -9,12 +9,11 @@ import { SET_ROLE } from '../constants/community'; -import {base, getInit, handleResp} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; export const fetchCommenters = (query = {}) => dispatch => { dispatch(requestFetchCommenters()); - fetch(`${base}/user?${qs.stringify(query)}`, getInit('GET')) - .then(handleResp) + coralApi(`/user?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) => dispatch({ type: FETCH_COMMENTERS_SUCCESS, @@ -42,7 +41,7 @@ export const newPage = () => ({ }); export const setRole = (id, role) => dispatch => { - return fetch(`${base}/user/${id}/role`, getInit('POST', {role})) + return coralApi(`/user/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index 6a133ddb5..71106e1f7 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -1,4 +1,4 @@ -import {base, handleResp, getInit} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; export const SETTINGS_LOADING = 'SETTINGS_LOADING'; export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED'; @@ -12,8 +12,7 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; export const fetchSettings = () => dispatch => { dispatch({type: SETTINGS_LOADING}); - fetch(`${base}/settings`, getInit('GET')) - .then(handleResp) + coralApi('/settings') .then(settings => { dispatch({type: SETTINGS_RECEIVED, settings}); }) @@ -29,8 +28,7 @@ export const updateSettings = settings => { export const saveSettingsToServer = () => (dispatch, getState) => { const settings = getState().settings.toJS().settings; dispatch({type: SAVE_SETTINGS_LOADING}); - fetch(`${base}/settings`, getInit('PUT', settings)) - .then(handleResp) + coralApi('/settings', {method: 'PUT', body: settings}) .then(() => { dispatch({type: SAVE_SETTINGS_SUCCESS, settings}); }) diff --git a/client/coral-admin/src/helpers/response.js b/client/coral-admin/src/helpers/response.js deleted file mode 100644 index bccfc5a04..000000000 --- a/client/coral-admin/src/helpers/response.js +++ /dev/null @@ -1,30 +0,0 @@ -export const base = '/api/v1'; - -export const getInit = (method, body) => { - let init = { - method, - headers: new Headers({ - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }), - credentials: 'same-origin' - }; - - if (method.toLowerCase() !== 'get') { - init.body = JSON.stringify(body); - } - - return init; -}; - -export const handleResp = res => { - if (res.status === 401) { - throw new Error('Not Authorized to make this request'); - } else if (res.status > 399) { - throw new Error('Error! Status ', res.status); - } else if (res.status === 204) { - return res.text(); - } else { - return res.json(); - } -}; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 6b872d12d..15c9cf76a 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -1,4 +1,4 @@ -import {base, handleResp, getInit} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; /** * The adapter is a redux middleware that interecepts the actions that need @@ -33,14 +33,13 @@ export default store => next => action => { const fetchModerationQueueComments = store => Promise.all([ - fetch(`${base}/queue/comments/pending`, getInit('GET')), - fetch(`${base}/comments?status=rejected`, getInit('GET')), - fetch(`${base}/comments?action_type=flag`, getInit('GET')) + coralApi('/queue/comments/pending'), + coralApi('/comments?status=rejected'), + coralApi('/comments?action_type=flag') ]) -.then(res => Promise.all(res.map(handleResp))) -.then(res => { - res[2] = res[2].map(comment => { comment.flagged = true; return comment; }); - return res.reduce((prev, curr) => prev.concat(curr), []); +.then(([pending, rejected, flagged]) => { + flagged.forEach(comment => comment.flagged = true); + return [...pending, ...rejected, ...flagged]; }) .then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', comments: res})) @@ -49,8 +48,7 @@ Promise.all([ // Update a comment. Now to update a comment we need to send back the whole object const updateComment = (store, comment) => { - fetch(`${base}/comments/${comment.get('id')}/status`, getInit('PUT', {status: comment.get('status')})) - .then(handleResp) + coralApi(`/comments/${comment.get('id')}/status`, {method: 'PUT', body: {status: comment.get('status')}}) .then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res})) .catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error})); }; @@ -63,8 +61,7 @@ const createComment = (store, name, comment) => { name: name, createdAt: Date.now() }; - return fetch(`${base}/comments`, getInit('POST', body)) - .then(handleResp) + return coralApi('/comments', {method: 'POST', body}) .then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res})) .catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error})); }; diff --git a/client/coral-embed-stream/public/samplearticle.html b/client/coral-embed-stream/public/samplearticle.html index 64c3fd0f8..454cb249c 100644 --- a/client/coral-embed-stream/public/samplearticle.html +++ b/client/coral-embed-stream/public/samplearticle.html @@ -7,7 +7,7 @@

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut lobortis sollicitudin eros a ornare. Curabitur dignissim vestibulum massa non rhoncus. Cras laoreet ante vel nunc hendrerit, ac imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Pellentesque interdum nec elit sed tincidunt. Donec volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non enim nec neque congue faucibus porttitor sit amet dui.

Nunc pharetra orci id diam feugiat, vitae rutrum magna efficitur. Morbi porttitor blandit lorem, et facilisis tellus luctus at. Morbi tincidunt eget nisl id placerat. Nullam consectetur quam vel mauris lacinia, non consectetur est faucibus. Duis cursus auctor nulla nec sagittis. Aenean sem erat, ultrices a hendrerit consectetur, accumsan non lorem. Integer ac neque sed magna sodales vulputate at quis neque. Praesent eget ornare lacus. Donec ultricies, dolor eget commodo faucibus, arcu velit ullamcorper tellus, in cursus tellus elit sed urna. Suspendisse in consequat magna. Duis vel ullamcorper tortor, vel cursus libero. Proin et nisi luctus ligula faucibus luctus. Morbi pulvinar, justo ac feugiat elementum, libero tellus congue justo, pharetra ultrices felis felis id leo. Integer mattis quam tempus libero porta, ac pretium ligula elementum.

- + diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 40d249088..95a6f63f3 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -3,7 +3,7 @@ import { itemActions, Notification, notificationActions, - authActions + authActions, } from '../../coral-framework'; import {connect} from 'react-redux'; import CommentBox from '../../coral-plugin-commentbox/CommentBox'; @@ -19,6 +19,8 @@ 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} from '../../coral-ui'; +import SettingsContainer from '../../coral-settings/containers/SettingsContainer'; const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions; const {addNotification, clearNotification} = notificationActions; @@ -46,11 +48,27 @@ const mapDispatchToProps = (dispatch) => ({ }, appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)), - logout: () => dispatch(logout()) + logout: () => dispatch(logout()), }); 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, @@ -60,13 +78,32 @@ class CommentStream extends Component { componentDidMount () { // Set up messaging between embedded Iframe an parent component // Using recommended Pym init code which violates .eslint standards - const pym = new Pym.Child({polling: 100}); + this.pym = new Pym.Child({polling: 100}); - if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) { - this.props.getStream(pym.parentUrl); - } else { - this.props.getStream(window.location); - } + const path = this.pym.parentUrl.split('#')[0]; + + this.props.getStream(path || window.location); + this.path = path; + + this.pym.sendMessage('childReady'); + + this.pym.onMessage('DOMContentLoaded', hash => { + // the comment ids can start with numbers, which is invalid for DOM id attributes + 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 () { @@ -90,137 +127,148 @@ class CommentStream extends Component { const rootItem = this.props.items.assets && this.props.items.assets[rootItemId]; const {actions, users, comments} = this.props.items; const {loggedIn, user, showSignInDialog} = this.props.auth; + const {activeTab} = this.state; + return
{ rootItem - ?
-
- - - {loggedIn && } - - {!loggedIn && } -
- { - rootItem.comments && rootItem.comments.map((commentId) => { - const comment = comments[commentId]; - return
-
- - - -
- - -
-
- - -
- - { - comment.children && - comment.children.map((replyId) => { - let reply = this.props.items.comments[replyId]; - return
-
- - - -
- + + + Settings + + + +
+ + {loggedIn && } + + {!loggedIn && } +
+ { + rootItem.comments && rootItem.comments.map((commentId) => { + const comment = comments[commentId]; + return
+
+ + + +
+ + +
+
+ + +
+ + { + comment.children && + comment.children.map((replyId) => { + let reply = this.props.items.comments[replyId]; + return
+
+ + + +
+ + +
+
+ + +
+ - -
-
- - -
- -
; - }) - } -
; - }) - } - +
; + }) + } +
; + }) + } + + + + +
: 'Loading' } diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 174f3ef16..12bdfacae 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -71,6 +71,11 @@ hr { display: none; } +.commentStream { + /* prevent absolutely positioned final permalink popover from being clipped */ + padding-bottom: 50px; +} + /* Comment Box Styles */ .coral-plugin-commentbox-container { display: flex; @@ -106,6 +111,7 @@ hr { /* Comment styles */ .comment { margin-bottom: 10px; + position: relative; } .coral-plugin-commentcontent-text { @@ -139,9 +145,8 @@ hr { width: 50%; } -.commentActionsLeft .material-icons,.commentActionsRight .material-icons, -.replyActionsLeft .material-icons, .replyActionsRight .material-icons { - font-size: 12px; +.material-icons { + font-size: 12px !important; margin-left: 3px; vertical-align: middle; } @@ -154,12 +159,37 @@ hr { color: #F00; } -/* Comment count styles */ -.coral-plugin-comment-count-text { - margin-bottom: 15px; -} - .coral-plugin-pubdate-text { color: #CCC; display: inline-block; } + +.coral-plugin-permalinks-container { + /*position: relative;*/ + z-index: 2; +} + +.coral-plugin-permalinks-popover { + display: none; + background-color: white; + border: 1px solid black; + width: calc(100% - 15px); + position: absolute; + top: 70px; + right: 0; + padding: 5px; +} + +.coral-plugin-permalinks-popover.active { + display: block; +} + +.coral-plugin-permalinks-copy-field { + display: block; + width: calc(100% - 5px); +} + +.coral-plugin-permalinks-copied-text { + float: right; + margin: 8px; +} diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 1058edbbb..13a9c72bc 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -2,7 +2,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from './../translations'; const lang = new I18n(translations); import * as actions from '../constants/auth'; -import {base, handleResp, getInit} from '../helpers/response'; +import coralApi, {base} from '../helpers/response'; import {addItem} from './items'; // Dialog Actions @@ -25,8 +25,7 @@ const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); export const fetchSignIn = (formData) => dispatch => { dispatch(signInRequest()); - fetch(`${base}/auth/local`, getInit('POST', formData)) - .then(handleResp) + coralApi('/auth/local', {method: 'POST', body: formData}) .then(({user}) => { dispatch(hideSignInDialog()); dispatch(signInSuccess(user)); @@ -74,8 +73,7 @@ const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error}); export const fetchSignUp = formData => dispatch => { dispatch(signUpRequest()); - fetch(`${base}/user`, getInit('POST', formData)) - .then(handleResp) + coralApi('/user', {method: 'POST', body: formData}) .then(({user}) => { dispatch(signUpSuccess(user)); setTimeout(() =>{ @@ -93,8 +91,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU export const fetchForgotPassword = email => dispatch => { dispatch(forgotPassowordRequest(email)); - fetch(`${base}/user/request-password-reset`, getInit('POST', {email})) - .then(handleResp) + coralApi('/user/request-password-reset', {method: 'POST', body: {email}}) .then(() => dispatch(forgotPassowordSuccess())) .catch(error => dispatch(forgotPassowordFailure(error))); }; @@ -107,8 +104,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE}); export const logout = () => dispatch => { dispatch(logOutRequest()); - fetch(`${base}/auth`, getInit('DELETE')) - .then(handleResp) + coralApi('/auth', {method: 'DELETE'}) .then(() => dispatch(logOutSuccess())) .catch(error => dispatch(logOutFailure(error))); }; @@ -126,14 +122,13 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); - fetch(`${base}/auth`, getInit('GET')) - .then((res) => { - if (res.status !== 200) { + coralApi('/auth') + .then(user => { + if (!user) { throw new Error('not logged in'); } - return res.json(); + dispatch(checkLoginSuccess(user)); }) - .then(user => dispatch(checkLoginSuccess(user))) .catch(error => dispatch(checkLoginFailure(error))); }; diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index 4587b174b..2029714c7 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -1,4 +1,4 @@ -import {getInit, base, handleResp} from '../../coral-framework/helpers/response'; +import coralApi from '../helpers/response'; import {fromJS} from 'immutable'; /* Item Actions */ @@ -95,8 +95,7 @@ export const appendItemArray = (id, property, value, add_to_front, item_type) => */ export function getStream (assetUrl) { return (dispatch) => { - return fetch(`${base}/stream?asset_url=${encodeURIComponent(assetUrl)}`) - .then(handleResp) + return coralApi(`/stream?asset_url=${encodeURIComponent(assetUrl)}`) .then((json) => { /* Add items to the store */ @@ -166,8 +165,7 @@ export function getStream (assetUrl) { export function getItemsArray (ids) { return (dispatch) => { - return fetch(`${base}/item/${ids}`, getInit('GET')) - .then(handleResp) + return coralApi(`/item/${ids}`) .then((json) => { for (let i = 0; i < json.items.length; i++) { dispatch(addItem(json.items[i])); @@ -196,8 +194,7 @@ export function postItem (item, type, id) { if (id) { item.id = id; } - return fetch(`${base}/${type}`, getInit('POST', item)) - .then(handleResp) + return coralApi(`/${type}`, {method: 'POST', body: item}) .then((json) => { dispatch(addItem({...item, id:json.id}, type)); return json.id; @@ -227,8 +224,7 @@ export function postAction (item_id, action_type, user_id, item_type) { user_id }; - return fetch(`${base}/${item_type}/${item_id}/actions`, getInit('POST', action)) - .then(handleResp); + return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action}); }; } @@ -249,7 +245,6 @@ export function postAction (item_id, action_type, user_id, item_type) { export function deleteAction (action_id) { return () => { - return fetch(`${base}/actions/${action_id}`, {method: 'DELETE'}) - .then(handleResp); + return coralApi(`/actions/${action_id}`, {method: 'DELETE'}); }; } diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index 83f51e3ec..1b4340dc4 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -1,23 +1,25 @@ export const base = '/api/v1'; -export const getInit = (method, body) => { - let init = { - method, +const buildOptions = (inputOptions = {}) => { + + const defaultOptions = { + method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'same-origin' }; + const options = Object.assign({}, defaultOptions, inputOptions); - if (method.toLowerCase() !== 'get') { - init.body = JSON.stringify(body); + if (options.method.toLowerCase() !== 'get') { + options.body = JSON.stringify(options.body); } - return init; + return options; }; -export const handleResp = res => { +const handleResp = res => { if (res.status === 401) { throw new Error('Not Authorized to make this request'); } else if (res.status > 399) { @@ -28,3 +30,7 @@ export const handleResp = res => { return res.json(); } }; + +export default (url, options) => { + return fetch(`${base}${url}`, buildOptions(options)).then(handleResp); +}; diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js index 837c8956a..7b721badc 100644 --- a/client/coral-framework/index.js +++ b/client/coral-framework/index.js @@ -11,5 +11,5 @@ export { itemActions, I18n, notificationActions, - authActions + authActions, }; diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js index b38de61ae..90eaa7cb7 100644 --- a/client/coral-framework/reducers/index.js +++ b/client/coral-framework/reducers/index.js @@ -14,5 +14,5 @@ export default combineReducers({ config, items, notification, - auth + auth, }); diff --git a/client/coral-plugin-permalinks/PermalinkButton.js b/client/coral-plugin-permalinks/PermalinkButton.js index 2372e2035..782e709d7 100644 --- a/client/coral-plugin-permalinks/PermalinkButton.js +++ b/client/coral-plugin-permalinks/PermalinkButton.js @@ -9,8 +9,8 @@ const lang = new I18n(translations); class PermalinkButton extends React.Component { static propTypes = { - asset_id: PropTypes.string.isRequired, - comment_id: PropTypes.string.isRequired + articleURL: PropTypes.string.isRequired, + commentId: PropTypes.string.isRequired } constructor (props) { @@ -43,29 +43,27 @@ class PermalinkButton extends React.Component { } render () { - const publisherUrl = `${location.protocol}//${location.host}/`; - return ( -
+
+ className={`${name}-popover ${this.state.popoverOpen ? 'active' : ''}`}> this.permalinkInput = input} - value={`${publisherUrl}${this.props.asset_id}#${this.props.comment_id}`} + value={`${this.props.articleURL}#${this.props.commentId}`} onChange={() => {}} /> { - this.state.copySuccessful ?

copied to clipboard

: null + this.state.copySuccessful ?

copied to clipboard

: null } { this.state.copyFailure - ?

copying to clipboard not supported in this browser. Use Cmd + C.

+ ?

copying to clipboard not supported in this browser. Use Cmd + C.

: null }
@@ -75,20 +73,3 @@ class PermalinkButton extends React.Component { } export default onClickOutside(PermalinkButton); - -const styles = { - position: 'relative', - - popover: active => { - return { - display: active ? 'block' : 'none', - backgroundColor: 'white', - border: '1px solid black', - minWidth: 400, - position: 'absolute', - top: 30, - right: 0, - padding: 5 - }; - } -}; diff --git a/client/coral-settings/components/Bio.css b/client/coral-settings/components/Bio.css new file mode 100644 index 000000000..8c36b989d --- /dev/null +++ b/client/coral-settings/components/Bio.css @@ -0,0 +1,21 @@ +.bio textarea { + width: 100%; + box-sizing: border-box; + border-radius: 2px; + min-height: 100px; + margin: 10px 0; + border: solid 1px #d8d8d8; +} + +.bio h1 { + font-size: 16px; + margin: 3px 0; +} + +.bio p { + margin: 3px 0; +} + +.actions { + text-align: right; +} diff --git a/client/coral-settings/components/Bio.js b/client/coral-settings/components/Bio.js new file mode 100644 index 000000000..b82587322 --- /dev/null +++ b/client/coral-settings/components/Bio.js @@ -0,0 +1,16 @@ +import React from 'react'; +import styles from './Bio.css'; +import {Button} from '../../coral-ui'; + +export default () => ( +
+

Bio

+

Tell the community about yourself

+