app/components/Post.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { push } from 'react-router-redux';
import moment from 'moment';
import { createAction as action } from 'redux-actions';
import Linkify from 'react-linkify';
// - Material UI
import { CardActions, CardHeader, CardMedia, CardText } from 'material-ui/Card';
import Snackbar from 'material-ui/Snackbar';
import SvgLink from 'material-ui/svg-icons/content/link';
import SvgFavorite from 'material-ui/svg-icons/action/favorite';
import SvgFavoriteBorder from 'material-ui/svg-icons/action/favorite-border';
import Checkbox from 'material-ui/Checkbox';
import Paper from 'material-ui/Paper';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import TextField from 'material-ui/TextField';
import IconMenu from 'material-ui/IconMenu';
import reactStringReplace from 'react-string-replace';
// - Import app components
import CommentGroup from 'CommentGroup';
import PostWrite from 'PostWrite';
import Img from 'Img';
import IconButtonElement from 'IconButtonElement';
import UserAvatar from 'UserAvatar';
// - Import actions
import * as voteActions from 'voteActions';
import * as postActions from 'postActions';
import * as globalActions from 'globalActions';
export class Post extends Component {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props);
this.state = {
// Post text
text: this.props.body,
// It's true if whole the text post is visible
readMoreState: false,
// Handle open comment from parent component
openComments: false,
// If it's true, share dialog will be open
shareOpen: false,
// If it's true comment will be disabled on post
disableComments: this.props.disableComments,
// If it's true share will be disabled on post
disableSharing: this.props.disableSharing,
// Title of share post
shareTitle: 'Share On',
// If it's true, post link will be visible in share post dialog
openCopyLink: false,
// If it's true, post write will be open
openPostWrite: false
};
}
/**
* Toggle on show/hide comment
* @param {event} evt passed by clicking on comment slide show
*/
handleOpenComments = (evt) => {
this.setState({ openComments: !this.state.openComments });
}
/**
* Open post write
*
* @memberof Blog
*/
handleOpenPostWrite = () => {
this.setState({ openPostWrite: true });
}
/**
* Close post write
*
* @memberof Blog
*/
handleClosePostWrite = () => {
this.setState({ openPostWrite: false });
}
/**
* Delete a post
*
* @memberof Post
*/
handleDelete = () => {
this.props.delete(this.props.id);
}
/**
* Show copy link
*
* @memberof Post
*/
handleCopyLink = () => {
this.setState({
openCopyLink: true,
shareTitle: 'Copy Link'
});
}
/**
* Open share post
*
* @memberof Post
*/
handleOpenShare = (event) => {
this.setState({ shareOpen: true });
const text = `${location.origin}/${this.props.ownerUserId}/posts/${this.props.id}`;
if (window.clipboardData && window.clipboardData.setData) {
// IE specific code path to prevent textarea being shown while dialog is visible.
return clipboardData.setData("Text", text);
} else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
var textarea = document.createElement("textarea");
textarea.textContent = text;
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge.
document.body.appendChild(textarea);
textarea.select();
try {
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
} catch (ex) {
console.warn("Copy to clipboard failed.", ex);
return false;
} finally {
document.body.removeChild(textarea);
}
}
}
/**
* Close share post
*
* @memberof Post
*/
handleCloseShare = () => {
this.setState({
shareOpen: false,
shareTitle: 'Share On',
openCopyLink: false
});
}
/**
* Handle vote on a post
*
* @memberof Post
*/
handleVote = () => {
if (this.props.userVoteStatus) {
this.props.unvote();
}
else {
this.props.vote();
}
}
/**
* Set open comment group function on state which passed by CommentGroup component
* @param {function} open the function to open comment list
*/
getOpenCommentGroup = (open) => {
this.setState({ openCommentGroup: open });
}
/**
* Handle read more event
* @param {event} evt is the event passed by click on read more
*/
handleReadMore(evt) {
this.setState({ readMoreState: !this.state.readMoreState });
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
/**
* DOM styles
*
* @memberof Post
*/
const styles = {
counter: {
lineHeight: '36px',
color: '#757575',
fontSize: '12px'
},
postBody: {
wordWrap: "break-word"
},
dialog: {
width: '',
maxWidth: '530px',
borderRadius: "4px"
},
rightIconMenu: {
position: 'absolute',
right: 18,
top: 8
}
};
const RightIconMenu = () => (
<IconMenu iconButtonElement={IconButtonElement} style={{ display: "block", position: "absolute", top: "0px", right: "4px", transform: 'rotate(90deg)' }}>
<MenuItem primaryText="Edit" onClick={this.handleOpenPostWrite} />
<MenuItem primaryText="Delete" onClick={this.handleDelete} />
<MenuItem primaryText={this.props.disableComments ? "Enable comments" : "Disable comments"} onClick={() => this.props.toggleDisableComments(!this.props.disableComments)} />
<MenuItem primaryText={this.props.disableSharing ? "Enable sharing" : "Disable sharing"} onClick={() => this.props.toggleSharingComments(!this.props.disableSharing)} />
</IconMenu>
);
const { ownerUserId, setHomeTitle, goTo, ownerDisplayName, creationDate, avatar, fullName, isPostOwner, image, body } = this.props;
return (
<div style={{ backgroundColor: '#fff', border: '1px solid #dddfe2', borderRadius: '7px' }}>
<CardHeader
title={<NavLink to={`/${ownerUserId}`}>{ownerDisplayName}</NavLink>}
subtitle={moment.unix(creationDate).fromNow()}
avatar={<NavLink to={`/${ownerUserId}`}><UserAvatar fullName={fullName} fileName={avatar} size={36} /></NavLink>}
>
{isPostOwner ? (<div style={styles.rightIconMenu}><RightIconMenu /></div>) : ''}
</CardHeader>
{body ?
<CardText style={styles.postBody}>
<Linkify properties={{ target: '_blank', style: { color: 'blue' } }}>
{reactStringReplace(body, /#(\w+)/g, (match, i) => (
<NavLink
style={{ color: 'green' }}
key={match + i}
to={`/tag/${match}`}
onClick={evt => {
evt.preventDefault()
goTo(`/tag/${match}`)
setHomeTitle(`#${match}`)
}}
>
#{match}
</NavLink>
))}
</Linkify>
</CardText> : ''
}
{image ? (
<CardMedia>
<Img fileName={image} />
</CardMedia>) : ''}
<CardActions>
<div style={{ margin: "16px 8px", display: 'flex', justifyContent: 'space-between' }}>
<div style={{ display: 'flex' }}>
<div className='g__circle' onClick={this.handleVote}>
<Checkbox
checkedIcon={<SvgFavorite style={{ fill: '#EC425C' }} />}
uncheckedIcon={<SvgFavoriteBorder style={{ fill: '#757575' }} />}
defaultChecked={this.props.userVoteStatus}
style={{ transform: 'translate(6px, 6px)' }}
/>
</div>
<div style={styles.counter}> {this.props.voteCount > 0 ? this.props.voteCount : ''} </div>
</div>
<div style={{ display: 'flex' }}>
{!this.props.disableComments ? (<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={styles.counter}>
{this.props.commentCount > 1 ? this.props.commentCount + " comments" :
this.props.commentCount === 1 ?
this.props.commentCount + " comment" : ''}
</div>
<span className='g__circle' style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0' }}>
<svg onClick={this.handleOpenComments} style={{ marginTop: '2px' }} width="20" height="21" xmlns="http://www.w3.org/2000/svg"><path d="M11.87 16l-7.435 4.415A.288.288 0 0 1 4 20.168V16h-.493c-1.22 0-1.661-.127-2.107-.365A2.486 2.486 0 0 1 .365 14.6C.127 14.154 0 13.712 0 12.493V3.507C0 2.287.127 1.846.365 1.4A2.486 2.486 0 0 1 1.4.365C1.846.127 2.288 0 3.507 0h12.986c1.22 0 1.661.127 2.107.365.446.239.796.589 1.035 1.035.238.446.365.888.365 2.107v8.986c0 1.22-.127 1.661-.365 2.107a2.486 2.486 0 0 1-1.035 1.035c-.446.238-.888.365-2.107.365h-4.624zM3.753 2c-.61 0-.831.063-1.054.183-.223.119-.398.294-.517.517-.12.223-.183.444-.183 1.054v8.492c0 .61.063.831.183 1.054.119.223.294.398.517.517.223.12.444.183 1.054.183h12.492c.61 0 .831-.063 1.054-.183.223-.119.398-.294.517-.517.12-.223.183-.444.183-1.054V3.754c0-.61-.063-.831-.183-1.054a1.243 1.243 0 0 0-.517-.517c-.223-.12-.444-.183-1.054-.183H3.754zm6.97 12H6v3.104L10.724 14z" fill={(this.props.commentCount > 0) ? '#4E7FF7' : '#757575'} />
<rect x="4" y="4" width="12" height="2" fill={(this.props.commentCount > 0) ? '#4E7FF7' : '#fff'}></rect>
<rect x="4" y="7" width="12" height="2" fill={(this.props.commentCount > 0) ? '#4E7FF7' : '#fff'}></rect>
<rect x="4" y="10" width="12" height="2" fill={(this.props.commentCount > 0) ? '#4E7FF7' : '#fff'}></rect>
</svg>
</span>
</div>) : ''}
{!this.props.disableSharing ?
<div className='g__circle' style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg onClick={this.handleOpenShare} style={{marginBottom: '1px'}} width="20" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M19.7 6.2l-6.6-6c-.5-.5-1.1 0-1.1.8v3C7.3 4 3.3 6.9 1.4 10.8.7 12.1.3 13.5 0 14.9c-.2 1 1.3 1.5 1.9.6C4.1 12 7.8 9.7 12 9.7V13c0 .8.6 1.3 1.1.8l6.6-6c.4-.4.4-1.2 0-1.6z" fill="#757575"/></svg>
</div>
: ''}
</div>
</div>
</CardActions>
<CommentGroup open={this.state.openComments} ownerPostUserId={this.props.ownerUserId} onToggleRequest={this.handleOpenComments} isPostOwner={this.props.isPostOwner} disableComments={this.props.disableComments} postId={this.props.id} />
<Snackbar
open={this.state.shareOpen}
message={"Link to Post copied!"}
autoHideDuration={1000}
style={{ left: '1%', transform: 'none' }}
/>
<PostWrite
open={this.state.openPostWrite}
onRequestClose={this.handleClosePostWrite}
edit={true}
text={this.props.body}
image={this.props.image ? this.props.image : ''}
id={this.props.id}
disableComments={this.props.disableComments}
disableSharing={this.props.disableSharing}
/>
</div>
);
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
vote: () => dispatch(voteActions.dbAddVote(ownProps.id, ownProps.ownerUserId)),
unvote: () => dispatch(voteActions.dbDeleteVote(ownProps.id)),
delete: (id) => dispatch(postActions.dbDeletePost(id)),
toggleDisableComments: (status) => dispatch(postActions.dbUpdatePost({ id: ownProps.id, disableComments: status }, _ => _)),
toggleSharingComments: (status) => dispatch(postActions.dbUpdatePost({ id: ownProps.id, disableSharing: status }, _ => _)),
goTo: (url) => dispatch(push(url)),
setHomeTitle: (title) => dispatch(globalActions.setHeaderTitle(title || ''))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const { uid } = state.authorize
let votes = state.vote.postVotes[ownProps.id]
const post = (state.post.userPosts[uid] ? Object.keys(state.post.userPosts[uid]).filter((key) => { return ownProps.id === key }).length : 0)
return {
avatar: state.user.info && state.user.info[ownProps.ownerUserId] ? state.user.info[ownProps.ownerUserId].avatar || '' : '',
fullName: state.user.info && state.user.info[ownProps.ownerUserId] ? state.user.info[ownProps.ownerUserId].fullName || '' : '',
commentCount: state.comment.postComments[ownProps.id] ? Object.keys(state.comment.postComments[ownProps.id]).length : 0,
voteCount: state.vote.postVotes[ownProps.id] ? Object.keys(state.vote.postVotes[ownProps.id]).length : 0,
userVoteStatus: votes && Object.keys(votes).filter((key) => votes[key].userId === state.authorize.uid)[0] ? true : false,
isPostOwner: post > 0
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Post)