Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a delete feature #55

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Please note that you must you React with addons, so choose the appropriate built
- **empty** (`string`): empty caption
- **locale** (`string`): application locale (from [momentJS](http://momentjs.com/docs/#/i18n/changing-locale/)),
- **currentUserId** (`number`): the current user id
- **canDelete** (`bool`): If the current user cand delete its comments
- **messageSentCallback** (`function`): a callback to know when a message has been sent
- **timeDisplay** (`string`): the display format, either `ago` or `dateTime`, `ago` by default
- **dateTimeFormat** (`string`): the dateTime format if **timeDisplay** is set to `dateTime`
Expand Down
27 changes: 14 additions & 13 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
"use strict";
'use strict';

const webpackConfig = require('./webpack.config');
const express = require('express');
const bodyParser = require('body-parser');
const faker = require('faker');
const serverLauncher = require('webpack-focus').serverLauncher;

const MOCKED_API_PORT = process.env.API_PORT;

/*****************************************
********* Webpack dev server *************
******************************************/

webpackConfig.externals = undefined; // Remove externals to make the app run in the dev server
serverLauncher(webpackConfig);
const MOCKED_API_PORT = process.env.API_PORT || 4444;

/*****************************************
************** Mocked API ****************
Expand Down Expand Up @@ -54,15 +45,16 @@ comments = comments.sort(function compare(a, b) {
const avatars = comments.reduce(function reduceComments(result, comment) {
result[comment.author] = faker.image.avatar();
return result;
}, {[myId]: faker.image.avatar()});
}, { [myId]: faker.image.avatar() });


// Middlewares

app.use(function corsMiddleware(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type');
res.header('Access-Control-Allow-Methods', 'POST,GET,OPTIONS,DELETE');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Content-Type', 'application/json');
next();
});
Expand Down Expand Up @@ -98,6 +90,15 @@ app.put(API_ROOT + '/api/comments/:uuid', function updateComment(req, res) {
res.end();
});

app.delete(API_ROOT + '/api/comments/:uuid', function deleteComment(req, res) {
const uuid = req.params.uuid;
comments = comments.filter(function deleteComment(comment) {
return comment.uuid !== uuid;
})

res.end();
});

app.get('/x/account/api/accounts/:authorId/photo', function getAvatar(req, res) {
const authorId = req.params.authorId;
res.redirect(avatars[authorId]);
Expand Down
100 changes: 87 additions & 13 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,53 @@ import fetch from 'isomorphic-fetch';
// Single comment actions
export const ADD_COMMENT = 'ADD_COMMENT';
export const UPDATE_COMMENT = 'UPDATE_COMMENT';
export const DELETE_COMMENT = 'DELETE_COMMENT';

/**
* Adding a comment.
* @param {string} message Message.
* @return {object} Action.
*/
const commentAdding = message => {
return {
type: ADD_COMMENT,
message
}
}

/**
* Updating a comment.
* @param {string} comment Comment.
* @return {object} Action.
*/
const commentUpdating = comment => {
return {
type: UPDATE_COMMENT,
comment
}
}

/**
* Deleting a comment.
* @param {string} commentId Comment's Id.
* @return {object} Action.
*/
const commentDeleting = commentId => {
return {
type: DELETE_COMMENT,
commentId
}
}

/**
* Add a commment.
* @param {string} concept Concept linked.
* @param {string} conceptId Concept's Id.
* @param {string} message Message
* @param {string} host Host URL.
* @param {Date} date Datetime.
* @return {func} Thunk.
*/
export const addComment = (concept, conceptId, message, host, date = new Date()) => {
return dispatch => {
dispatch(clearError());
Expand All @@ -35,15 +68,26 @@ export const addComment = (concept, conceptId, message, host, date = new Date())
body: JSON.stringify(comment),
credentials: 'include'
})
.then(({status}) => {
if (status >= 400) {
dispatch(setError('There was a problem adding your comment. The backend did not reply correctly.'));
} else {
dispatch(getComments(concept, conceptId, host, date));
}
});
.then(({ status }) => {
if (status >= 400) {
dispatch(setError('There was a problem adding your comment. The backend did not reply correctly.'));
} else {
dispatch(getComments(concept, conceptId, host, date));
}
});
}
}

/**
* Update a comment.
* @param {string} concept Concept linked.
* @param {string} conceptId Concept's Id.
* @param {object} comment Comment.
* @param {string} message Message.
* @param {string} host Host URL.
* @param {Date} date Datetime.
* @return {func} Thunk.
*/
export const updateComment = (concept, conceptId, comment, message, host, date = new Date()) => {
return dispatch => {
dispatch(clearError());
Expand All @@ -61,8 +105,7 @@ export const updateComment = (concept, conceptId, comment, message, host, date =
},
body: JSON.stringify(newComment),
credentials: 'include'
})
.then(({status}) => {
}).then(({ status }) => {
if (status >= 400) {
dispatch(setError('There was a problem updating your comment. The backend did not reply correctly.'));
} else {
Expand All @@ -72,6 +115,37 @@ export const updateComment = (concept, conceptId, comment, message, host, date =
}
}

/**
* Delete a comment.
* @param {string} concept Concept linked.
* @param {string} conceptId Concept's Id.
* @param {string} commentId Comment's Id.
* @param {string} host Host URL.
* @param {Date} date Datetime.
* @return {func} Thunk.
*/
export const deleteComment = (concept, conceptId, commentId, host, date = new Date()) => {
return dispatch => {
dispatch(clearError());
dispatch(commentDeleting(commentId));
return fetch(`${host}/api/comments/${commentId}`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include'
}).then(({ status }) => {
if (status >= 400) {
dispatch(setError('There was a problem deleting your comment. The backend did not reply correctly.'));
} else {
dispatch(getComments(concept, conceptId, host, date));
}
});
}
}


// Multiple comments actions
export const REQUEST_COMMENTS = 'REQUEST_COMMENTS';
export const RECEIVE_COMMENTS = 'RECEIVE_COMMENTS';
Expand All @@ -93,10 +167,10 @@ export const getComments = (concept, conceptId, host, date = new Date()) => {
return dispatch => {
dispatch(clearError());
dispatch(requestComments());
return fetch(`${host}/api/comments?concept=${concept}&id=${conceptId}`, {credentials: 'include'})
.then(response => response.json())
.then(comments => dispatch(receiveComments(comments, date)))
.catch(() => dispatch(setError('There was a problem fetching the comments. The backend did not reply correctly.')));
return fetch(`${host}/api/comments?concept=${concept}&id=${conceptId}`, { credentials: 'include' })
.then(response => response.json())
.then(comments => dispatch(receiveComments(comments, date)))
.catch(() => dispatch(setError('There was a problem fetching the comments. The backend did not reply correctly.')));
}
}
export const clearComments = () => ({
Expand Down
22 changes: 17 additions & 5 deletions src/component/comment/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import './style.scss';
import moment from 'moment';
import Input from '../input';
import { deleteComment } from '../../actions';

const propTypes = {
uuid: PropTypes.string.isRequired,
Expand Down Expand Up @@ -37,9 +38,12 @@ class Comment extends Component {
}

render() {
const { msg, author, authorDisplayName, creationDate, currentUserId, lastModified, userPictureResolver, texts, showAvatar, timeDisplay, dateTimeFormat, ...otherProps } = this.props;
const { msg, author, authorDisplayName, creationDate, currentUserId, lastModified, userPictureResolver, texts, showAvatar, timeDisplay, dateTimeFormat, canDelete, ...otherProps } = this.props;
const { dispatch, apiRootUrl, concept, conceptId, uuid } = otherProps;

const { isEditing } = this.state;
const isMine = currentUserId === author;

return (
<div data-focus='comment' data-editing={isEditing}>
{showAvatar &&
Expand All @@ -53,20 +57,28 @@ class Comment extends Component {
<div data-focus='name'>
<b>{authorDisplayName}</b>
</div>
{isMine &&
<div data-focus='edit'>
{isMine && (
<div data-focus='actions'>
<a data-focus='toggle' onClick={() => { this.setState({ isEditing: !this.state.isEditing }) }}>
{isEditing ? texts.cancel : texts.edit}
</a>
{canDelete && (
<a data-focus='toggle' onClick={() => dispatch(deleteComment(concept, conceptId, uuid, apiRootUrl))}>
{texts.delete}
</a>
)}
</div>
}
)}
<div data-focus='date'>
{timeDisplay === 'ago' && moment(creationDate).fromNow()}
{timeDisplay === 'dateTime' && moment(creationDate).format(dateTimeFormat)}
</div>
</div>
<div data-focus='body'>
{isMine && isEditing ? <Input inputType='update' texts={{ ...texts, placeholder: '' }} {...{ author, authorDisplayName, creationDate, ...otherProps }} ref='edit' value={msg} /> : <div dangerouslySetInnerHTML={{ __html: msg.replace(/\n/g, '<br>') }}></div>}
{isMine && isEditing
? (<Input inputType='update' texts={{ ...texts, placeholder: '' }} {...{ author, authorDisplayName, creationDate, ...otherProps }} ref='edit' value={msg} />)
: (<div dangerouslySetInnerHTML={{ __html: msg.replace(/\n/g, '<br>') }}></div>)
}
</div>
<div className='separator'></div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions src/component/comment/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}

&:hover {
[data-focus='content'] [data-focus='head'] [data-focus='edit'] {
[data-focus='content'] [data-focus='head'] [data-focus='actions'] {
display: inline-block;
}
}
Expand Down Expand Up @@ -56,13 +56,14 @@
font-size: 1.1em;
}

[data-focus='edit'] {
[data-focus='actions'] {
flex: 1;
margin-left: 10px;

a {
color: #3f51b5;
cursor: pointer;
margin-right: 10px;
}
display: none;
}
Expand Down
6 changes: 6 additions & 0 deletions src/component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ const propTypes = {
concept: PropTypes.string.isRequired,
conceptId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
userPictureResolver: PropTypes.func.isRequired,
currentUserId: PropTypes.number,
canDelete: PropTypes.bool,
texts: PropTypes.shape({
placeholder: PropTypes.string.isRequired,
send: PropTypes.string.isRequired,
edit: PropTypes.string.isRequired,
delete: PropTypes.string.isRequired,
cancel: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
singleComment: PropTypes.string.isRequired,
Expand All @@ -35,10 +38,13 @@ const propTypes = {

const defaultProps = {
userPictureResolver: userId => `./x/account/api/accounts/${userId}/photo`,
currentUserId: undefined,
canDelete: false,
texts: {
placeholder: 'Leave a comment...',
send: 'Send',
edit: 'Edit',
delete: 'Delete',
cancel: 'Cancel',
title: 'Comments',
singleComment: 'comment',
Expand Down
8 changes: 4 additions & 4 deletions src/component/list/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {Component, PropTypes} from 'react';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import CSSTransitionGroup from 'react-addons-css-transition-group';
import Comment from '../comment';
import {getComments} from '../../actions';
import { getComments } from '../../actions';
import 'animate.css/source/fading_entrances/fadeInRight.css';
import './style.scss';

Expand Down Expand Up @@ -32,11 +32,11 @@ class List extends Component {
}

render() {
const {comments, ...otherProps} = this.props;
const { comments, ...otherProps } = this.props;
return (
<div data-focus='comments-list' ref='list'>
<CSSTransitionGroup transitionName='comment' transitionEnterTimeout={TRANSITION_TIMEOUT} transitionLeaveTimeout={TRANSITION_TIMEOUT}>
{comments.map(comment => <Comment key={comment.uuid} {...comment} {...otherProps}/>)}
{comments.map(comment => <Comment key={comment.uuid} {...comment} {...otherProps} />)}
</CSSTransitionGroup>
</div>
);
Expand Down
Loading