Initial git
This commit is contained in:
271
app/components/Blog.jsx
Normal file
271
app/components/Blog.jsx
Normal file
@@ -0,0 +1,271 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Card, CardActions, CardHeader, CardMedia, CardTitle, CardText } from 'material-ui/Card'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import { grey400, grey800, darkBlack, lightBlack } from 'material-ui/styles/colors'
|
||||
import SvgCamera from 'material-ui/svg-icons/image/photo-camera'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import { List, ListItem } from 'material-ui/List'
|
||||
|
||||
|
||||
|
||||
// - Import app components
|
||||
import Post from 'Post'
|
||||
import PostWrite from 'PostWrite'
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
// - Import API
|
||||
import * as AuthAPI from 'AuthAPI'
|
||||
import * as PostAPI from 'PostAPI'
|
||||
|
||||
// - Import actions
|
||||
import * as globalActions from 'globalActions'
|
||||
|
||||
|
||||
// - Create Blog component class
|
||||
export class Blog extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* If it's true , writing post block will be visible
|
||||
*/
|
||||
displayWriting: PropTypes.bool.isRequired,
|
||||
/**
|
||||
* A list of post
|
||||
*/
|
||||
posts: PropTypes.object,
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
/**
|
||||
* It's true if we want to have two column of posts
|
||||
*/
|
||||
divided: 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,
|
||||
/**
|
||||
* If it's true, post write will be open
|
||||
*/
|
||||
openPostWrite: false,
|
||||
/**
|
||||
* The title of home header
|
||||
*/
|
||||
homeTitle: PropTypes.string
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.postLoad = this.postLoad.bind(this)
|
||||
this.handleOpenPostWrite = this.handleOpenPostWrite.bind(this)
|
||||
this.handleClosePostWrite = this.handleClosePostWrite.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Open post write
|
||||
*
|
||||
*
|
||||
* @memberof Blog
|
||||
*/
|
||||
handleOpenPostWrite = () => {
|
||||
this.setState({
|
||||
openPostWrite: true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Close post write
|
||||
*
|
||||
*
|
||||
* @memberof Blog
|
||||
*/
|
||||
handleClosePostWrite = () => {
|
||||
this.setState({
|
||||
openPostWrite: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a list of posts
|
||||
* @return {DOM} posts
|
||||
*/
|
||||
postLoad = () => {
|
||||
|
||||
let { posts,match } = this.props
|
||||
let {tag} = match.params
|
||||
if (posts === undefined || !Object.keys(posts).length > 0) {
|
||||
|
||||
return (
|
||||
|
||||
<h1>
|
||||
'Nothing has shared.'
|
||||
</h1>
|
||||
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
var postBack = { oddPostList: [], evenPostList: [] }
|
||||
var parsedPosts = []
|
||||
Object.keys(posts).forEach((postId) => {
|
||||
if(tag){
|
||||
let regex = new RegExp("#" + tag,'g')
|
||||
let postMatch = posts[postId].body.match(regex)
|
||||
if(postMatch !==null)
|
||||
parsedPosts.push({ ...posts[postId]})
|
||||
}else{
|
||||
parsedPosts.push({ ...posts[postId]})
|
||||
|
||||
}
|
||||
})
|
||||
const sortedPosts = PostAPI.sortObjectsDate(parsedPosts)
|
||||
if (sortedPosts.length > 6) {
|
||||
postBack.divided = true
|
||||
|
||||
} else {
|
||||
postBack.divided = false
|
||||
}
|
||||
sortedPosts.forEach((post, index) => {
|
||||
|
||||
var newPost = (
|
||||
<div key={post.id}>
|
||||
|
||||
{index > 1 || (!postBack.divided && index > 0) ? <div style={{ height: "16px" }}></div> : ''}
|
||||
<Post
|
||||
body={post.body}
|
||||
commentCounter={post.commentCounter}
|
||||
creationDate={post.creationDate}
|
||||
id={post.id}
|
||||
image={post.image}
|
||||
lastEditDate={post.lastEditDate}
|
||||
ownerDisplayName={post.ownerDisplayName}
|
||||
ownerUserId={post.ownerUserId}
|
||||
ownerAvatar={post.ownerAvatar}
|
||||
postTypeId={post.postTypeId}
|
||||
score={post.score}
|
||||
tags={post.tags}
|
||||
video={post.video}
|
||||
disableComments={post.disableComments}
|
||||
disableSharing={post.disableSharing}
|
||||
viewCount={posts.viewCount}
|
||||
pictureState={true} />
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
if ((index % 2) === 1 && postBack.divided) {
|
||||
postBack.oddPostList.push(newPost)
|
||||
}
|
||||
else {
|
||||
postBack.evenPostList.push(newPost)
|
||||
}
|
||||
})
|
||||
return postBack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {setHomeTitle} = this.props
|
||||
setHomeTitle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
let postList = this.postLoad()
|
||||
|
||||
const {tag, displayWriting, } = this.props
|
||||
|
||||
return (
|
||||
<div >
|
||||
|
||||
|
||||
<div className='grid grid__gutters grid__1of2 grid__space-around animate-top'>
|
||||
|
||||
<div className='grid-cell animate-top' style={{maxWidth: '530px', minWidth: '280px'}}>
|
||||
{displayWriting && !tag
|
||||
? (<PostWrite open={this.state.openPostWrite} onRequestClose={this.handleClosePostWrite} edit={false} >
|
||||
<Paper zDepth={2} style={{ height: "68px", width: "100%" }}>
|
||||
|
||||
<ListItem
|
||||
primaryText={<span style={{ color: grey400, cursor: "text" }}> What's new with you? </span>}
|
||||
leftAvatar={<UserAvatar fileName={this.props.avatar} size={36} />}
|
||||
rightIcon={<SvgCamera />}
|
||||
style={{ padding: "7px 0px", fontWeight: "200" }}
|
||||
onTouchTap={this.handleOpenPostWrite}
|
||||
/>
|
||||
|
||||
</Paper>
|
||||
<div style={{ height: "16px" }}></div>
|
||||
</PostWrite>)
|
||||
: ''}
|
||||
|
||||
{postList.evenPostList}
|
||||
<div style={{ height: "16px" }}></div>
|
||||
</div>
|
||||
{postList.divided
|
||||
? (<div className='grid-cell animate-top' style={{maxWidth: '530px', minWidth: '280px'}}>
|
||||
<div className="blog__right-list">
|
||||
{postList.oddPostList}
|
||||
<div style={{ height: "16px" }}></div>
|
||||
</div>
|
||||
</div>)
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
|
||||
</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 {
|
||||
setHomeTitle: () => dispatch(globalActions.setHeaderTitle(ownProps.homeTitle || '')),
|
||||
showTopLoading: () => dispatch(globalActions.showTopLoading()),
|
||||
hideTopLoading: () => dispatch(globalActions.hideTopLoading())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : '',
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Blog))
|
||||
266
app/components/Circle.jsx
Normal file
266
app/components/Circle.jsx
Normal file
@@ -0,0 +1,266 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {push} from 'react-router-redux'
|
||||
import {grey400, darkBlack, lightBlack} from 'material-ui/styles/colors'
|
||||
import { List, ListItem } from 'material-ui/List'
|
||||
import SvgGroup from 'material-ui/svg-icons/action/group-work'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
|
||||
import IconMenu from 'material-ui/IconMenu'
|
||||
import TextField from 'material-ui/TextField'
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import IconButtonElement from 'IconButtonElement'
|
||||
import Dialog from 'material-ui/Dialog'
|
||||
import Divider from 'material-ui/Divider'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
import SvgClose from 'material-ui/svg-icons/navigation/close'
|
||||
import AppBar from 'material-ui/AppBar'
|
||||
|
||||
// - Import app components
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as circleActions from 'circleActions'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class Circle extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* Circle object
|
||||
*/
|
||||
circle: PropTypes.object.isRequired,
|
||||
/**
|
||||
* Circle identifier
|
||||
*/
|
||||
id: PropTypes.string.isRequired
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
/**
|
||||
* If is true circle is open to show users in circle list
|
||||
*/
|
||||
open:false,
|
||||
/**
|
||||
* Circle name on change
|
||||
*/
|
||||
circleName: this.props.circle.name,
|
||||
/**
|
||||
* Save operation will be disable if user doesn't meet requirement
|
||||
*/
|
||||
disabledSave: false
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleToggleCircle = this.handleToggleCircle.bind(this)
|
||||
this.handleDeleteCircle = this.handleDeleteCircle.bind(this)
|
||||
this.handleUpdateCircle = this.handleUpdateCircle.bind(this)
|
||||
this.handleChangeCircleName = this.handleChangeCircleName.bind(this)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle chage circle name
|
||||
*
|
||||
*
|
||||
* @memberof Circle
|
||||
*/
|
||||
handleChangeCircleName = (evt) => {
|
||||
const {value} = evt.target
|
||||
this.setState({
|
||||
circleName: value,
|
||||
disabledSave: (!value || value.trim() === '')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user's circle
|
||||
*
|
||||
*
|
||||
* @memberof Circle
|
||||
*/
|
||||
handleUpdateCircle = () => {
|
||||
const {circleName} = this.state
|
||||
if(circleName && circleName.trim() !== ''){
|
||||
this.props.updateCircle({name:circleName,id: this.props.id})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delete circle
|
||||
*
|
||||
*
|
||||
* @memberof Circle
|
||||
*/
|
||||
handleDeleteCircle = () => {
|
||||
this.props.deleteCircle(this.props.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle circle to close/open
|
||||
*
|
||||
*
|
||||
* @memberof Circle
|
||||
*/
|
||||
handleToggleCircle = () => {
|
||||
this.setState({
|
||||
open: !this.state.open
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
userList = () => {
|
||||
const {users} = this.props.circle
|
||||
const {userInfo} = this.props
|
||||
let usersParsed =[]
|
||||
|
||||
if(users){
|
||||
Object.keys(users).forEach((key, index) => {
|
||||
const { fullName} = users[key]
|
||||
let avatar = userInfo && userInfo[key] ? userInfo[key].avatar || '' : ''
|
||||
usersParsed.push(<ListItem
|
||||
key={`${this.props.id}.${key}`}
|
||||
style={{backgroundColor: '#e2e2e2'}}
|
||||
value={2}
|
||||
primaryText={fullName}
|
||||
leftAvatar={<UserAvatar fileName={avatar}/>}
|
||||
onClick={()=> this.props.goTo(`/${key}`)}
|
||||
/>)
|
||||
|
||||
})
|
||||
return usersParsed
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Right icon menue of circle
|
||||
*
|
||||
*
|
||||
* @memberof Circle
|
||||
*/
|
||||
rightIconMenu =(
|
||||
<IconMenu iconButtonElement={IconButtonElement} style={{ display: "block", position: "absolute", top: "0px", right: "12px" }}>
|
||||
<MenuItem primaryText="Delete circle" onClick={this.handleDeleteCircle} />
|
||||
<MenuItem primaryText="Circle settings" onClick={this.props.openCircleSettings} />
|
||||
</IconMenu>
|
||||
)
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const circleTitle =(
|
||||
<div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: "space-between" }}>
|
||||
<div style={{paddingRight: '10px'}}>
|
||||
<SvgClose onClick={this.props.closeCircleSettings} hoverColor={grey400} style={{cursor: 'pointer'}} />
|
||||
</div>
|
||||
<div style={{
|
||||
color: 'rgba(0,0,0,0.87)',
|
||||
flex: '1 1',
|
||||
font: '500 20px Roboto,RobotoDraft,Helvetica,Arial,sans-serif'
|
||||
}}>
|
||||
Circle settings
|
||||
</div>
|
||||
<div style={{marginTop: '-9px'}}>
|
||||
<FlatButton label="SAVE" primary={true} disabled={this.state.disabledSave} onClick={this.handleUpdateCircle} />
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<ListItem
|
||||
key={this.props.id}
|
||||
style={{backgroundColor: '#fff', borderBottom: '1px solid rgba(0,0,0,0.12)',height: '72px',padding: '12px 0' }}
|
||||
primaryText={<span style={{color:'rgba(0,0,0,0.87)',fontSize:'16px',marginRight:'8px',whiteSpace:'nowrap',textOverflow:'ellipsis',overflow:'hidden'}}>{this.props.circle.name}</span>}
|
||||
leftIcon={<SvgGroup style={{width:'40px',height:'40px',transform: 'translate(0px, -9px)',fill:'#bdbdbd'}} />}
|
||||
rightIconButton={this.rightIconMenu}
|
||||
initiallyOpen={false}
|
||||
onClick={this.handleToggleCircle}
|
||||
open={this.state.open}
|
||||
nestedItems={this.userList()}
|
||||
>
|
||||
<Dialog
|
||||
id={this.props.id}
|
||||
title={circleTitle}
|
||||
modal={false}
|
||||
open={this.props.openSetting}
|
||||
onRequestClose={this.props.closeCircleSettings}
|
||||
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
|
||||
contentStyle={{maxWidth: '400px'}}
|
||||
>
|
||||
<div>
|
||||
<TextField
|
||||
hintText="Circle name"
|
||||
floatingLabelText="Circle name"
|
||||
onChange={this.handleChangeCircleName}
|
||||
value={this.state.circleName}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</ListItem>
|
||||
</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) => {
|
||||
let {uid} = ownProps
|
||||
return {
|
||||
deleteCircle: (id) => dispatch(circleActions.dbDeleteCircle(id)),
|
||||
updateCircle: (circle) => dispatch(circleActions.dbUpdateCircle(circle)),
|
||||
closeCircleSettings: () => dispatch(circleActions.closeCircleSettings(uid,ownProps.id)),
|
||||
openCircleSettings: () => dispatch(circleActions.openCircleSettings(uid,ownProps.id)),
|
||||
goTo: (url)=> dispatch(push(url))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
let {uid} = state.authorize
|
||||
return {
|
||||
openSetting: state.circle ? (state.circle.userCircles[uid] ? (state.circle.userCircles[uid][ownProps.id].openCircleSettings || false) : false ) : false,
|
||||
userInfo: state.user.info
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Circle)
|
||||
314
app/components/Comment.jsx
Normal file
314
app/components/Comment.jsx
Normal file
@@ -0,0 +1,314 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { NavLink} from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { createAction as action } from 'redux-actions'
|
||||
import moment from 'moment'
|
||||
import { List, ListItem } from 'material-ui/List'
|
||||
import Divider from 'material-ui/Divider'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
|
||||
import IconMenu from 'material-ui/IconMenu'
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import TextField from 'material-ui/TextField'
|
||||
|
||||
// - Import app components
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
// - Import action types
|
||||
import * as types from 'actionTypes'
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as commentActions from 'commentActions'
|
||||
import * as userActions from 'userActions'
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class Comment extends Component {
|
||||
|
||||
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* Comment object
|
||||
*/
|
||||
comment: PropTypes.object,
|
||||
/**
|
||||
* If it's true the post owner is the logged in user which this post be long to the comment
|
||||
*/
|
||||
isPostOwner: PropTypes.bool,
|
||||
/**
|
||||
* If it's true the comment is disable to write
|
||||
*/
|
||||
disableComments: PropTypes.bool,
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.textareaRef = i => { this.inputText = i }
|
||||
this.divCommentRef = i => { this.divComment = i }
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
/**
|
||||
* Comment text
|
||||
*/
|
||||
text: this.props.comment.text,
|
||||
/**
|
||||
* Comment text to match edit with new comment that is edited
|
||||
*/
|
||||
initialText: this.props.comment.text,
|
||||
/**
|
||||
* If comment text dosn't take any change it will be true
|
||||
*/
|
||||
editDisabled: true,
|
||||
/**
|
||||
* If it's true the post owner is the logged in user which this post be long to the comment
|
||||
*/
|
||||
isPostOwner: PropTypes.bool
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleDelete = this.handleDelete.bind(this)
|
||||
this.handleUpdateComment = this.handleUpdateComment.bind(this)
|
||||
this.handleOnChange = this.handleOnChange.bind(this)
|
||||
this.handleCancelEdit = this.handleCancelEdit.bind(this)
|
||||
this.handleEditComment = this.handleEditComment.bind(this)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle show edit comment
|
||||
* @param {event} evt is an event passed by clicking on edit button
|
||||
*/
|
||||
handleEditComment = (evt) => {
|
||||
this.inputText.style.height = this.divComment.clientHeight + 'px'
|
||||
this.props.openEditor()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle cancel edit
|
||||
* @param {event} evt is an event passed by clicking on cancel button
|
||||
*/
|
||||
handleCancelEdit = (evt) => {
|
||||
|
||||
this.setState({
|
||||
text: this.state.initialText
|
||||
})
|
||||
this.props.closeEditor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle edit comment
|
||||
* @param {event} evt is an event passed by clicking on post button
|
||||
*/
|
||||
handleUpdateComment = (evt) => {
|
||||
|
||||
this.props.update(this.props.comment.id, this.props.comment.postId, this.state.text)
|
||||
this.setState({
|
||||
initialText: this.state.text
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* When comment text changed
|
||||
* @param {event} evt is an event passed by change comment text callback funciton
|
||||
* @param {string} data is the comment text which user writes
|
||||
*/
|
||||
handleOnChange = (evt) => {
|
||||
const data = evt.target.value
|
||||
this.inputText.style.height = evt.target.scrollHeight + 'px'
|
||||
if (data.length === 0 || data.trim() === '' || data.trim() === this.state.initialText) {
|
||||
this.setState({
|
||||
text: data,
|
||||
editDisabled: true
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
text: data,
|
||||
editDisabled: false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a comment
|
||||
* @param {event} evt an event passed by click on delete comment
|
||||
* @param {string} id comment identifire
|
||||
* @param {string} postId post identifier which comment belong to
|
||||
*/
|
||||
handleDelete = (evt, id, postId) => {
|
||||
this.props.delete(id, postId)
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {userId} = this.props.comment
|
||||
if (!this.props.isCommentOwner && !this.props.info[userId]) {
|
||||
this.props.getUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
/**
|
||||
* DOM styles
|
||||
*
|
||||
*
|
||||
* @memberof Comment
|
||||
*/
|
||||
const styles = {
|
||||
comment: {
|
||||
marginBottom: '12px'
|
||||
},
|
||||
iconButton: {
|
||||
width: 16,
|
||||
height: 16
|
||||
|
||||
},
|
||||
author:{
|
||||
fontSize: "13px",
|
||||
paddingRight: "10px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(0,0,0,0.87)",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden"
|
||||
|
||||
},
|
||||
commentBody:{
|
||||
fontSize: "13px",
|
||||
lineHeight: "20px",
|
||||
color: "rgba(0,0,0,0.87)",
|
||||
fontWeight: 300,
|
||||
height: "",
|
||||
display: "block"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment object from props
|
||||
*/
|
||||
const {comment} = this.props
|
||||
|
||||
const iconButtonElement = (
|
||||
<IconButton style={styles.iconButton} iconStyle={styles.iconButton}
|
||||
touch={true}
|
||||
>
|
||||
<MoreVertIcon color={grey400} viewBox='9 0 24 24' />
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
|
||||
const RightIconMenu = () => (
|
||||
<IconMenu iconButtonElement={iconButtonElement} style={{ display: "block", position: "absolute", top: "0px", right: "4px" }}>
|
||||
<MenuItem style={{ fontSize: "14px" }}>Reply</MenuItem>
|
||||
{this.props.isCommentOwner ? (<MenuItem style={{ fontSize: "14px" }} onClick={this.handleEditComment}>Edit</MenuItem>): ''}
|
||||
{(this.props.isCommentOwner || this.props.isPostOwner) ? ( <MenuItem style={{ fontSize: "14px" }} onClick={(evt) => this.handleDelete(evt, comment.id, comment.postId)}>Delete</MenuItem>): ''}
|
||||
</IconMenu>
|
||||
)
|
||||
|
||||
const Author = () => (
|
||||
<div style={{ marginTop: "-11px" }}>
|
||||
<span style={styles.author}>{comment.userDisplayName}</span><span style={{
|
||||
fontWeight: 100,
|
||||
fontSize: "10px"
|
||||
}}>{moment.unix(comment.creationDate).fromNow()}</span>
|
||||
</div>
|
||||
)
|
||||
const commentBody = (
|
||||
<p style={styles.commentBody}>{comment.text}</p>
|
||||
)
|
||||
|
||||
const {userId} = comment
|
||||
|
||||
return (
|
||||
<div className="animate-top" style={styles.comment} key={comment.id}>
|
||||
<Paper zDepth={0} className="animate2-top10" style={{ position: "relative", padding: "", display: (!this.state.display ? "block" : "none") }}>
|
||||
<div style={{ marginLeft: "0px", padding: "16px 56px 0px 72px", position: "relative" }}>
|
||||
<NavLink to={`/${userId}`}><UserAvatar fileName={this.props.avatar} style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", position: "absolute", top: "8px", left: "16px" }} size={36} /></NavLink>
|
||||
<NavLink to={`/${userId}`}> <Author /></NavLink>
|
||||
{(!this.props.isCommentOwner && !this.props.isPostOwner && this.props.disableComments )? '' : (<RightIconMenu />)}
|
||||
<div style={{ outline: "none", marginLeft: "16px", flex: "auto", flexGrow: 1 }}>
|
||||
<textarea ref={this.textareaRef} className="animate2-top10" style={{ fontWeight: 100, fontSize: "14px", border: "none", width: "100%", outline: "none", resize: "none", display: (this.props.comment.editorStatus ? 'block' : 'none') }} onChange={this.handleOnChange} value={this.state.text}></textarea>
|
||||
<div ref={this.divCommentRef} className="animate2-top10" style={{ fontWeight: 100, fontSize: "14px", height: "100%", border: "none", width: "100%", outline: "none", resize: "none", display: (!this.props.comment.editorStatus ? 'block' : 'none') }}>{this.state.text}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: (this.props.comment.editorStatus ? "flex" : "none"), flexDirection: "row-reverse" }}>
|
||||
<FlatButton primary={true} disabled={this.state.editDisabled} label="Update" style={{ float: "right", clear: "both", zIndex: 5, margin: "0px 5px 5px 0px", fontWeight: 400 }} onClick={this.handleUpdateComment} />
|
||||
<FlatButton primary={true} label="Cancel" style={{ float: "right", clear: "both", zIndex: 5, margin: "0px 5px 5px 0px", fontWeight: 400 }} onClick={this.handleCancelEdit} />
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
</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 {
|
||||
delete: (id, postId) => dispatch(commentActions.dbDeleteComment(id, postId)),
|
||||
update: (id, postId, comment) => dispatch(commentActions.dbUpdateComment(id, postId, comment)),
|
||||
openEditor: () => dispatch(commentActions.openCommentEditor({ id: ownProps.comment.id, postId: ownProps.comment.postId })),
|
||||
closeEditor: () => dispatch(commentActions.closeCommentEditor({ id: ownProps.comment.id, postId: ownProps.comment.postId })),
|
||||
getUserInfo: () => dispatch(userActions.dbGetUserInfoByUserId(ownProps.comment.userId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const avatar = state.user.info && state.user.info[ownProps.comment.userId] ? state.user.info[ownProps.comment.userId].avatar || '' : ''
|
||||
return {
|
||||
uid: uid,
|
||||
isCommentOwner: (uid === ownProps.comment.userId),
|
||||
info: state.user.info,
|
||||
avatar
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Comment)
|
||||
270
app/components/CommentGroup.jsx
Normal file
270
app/components/CommentGroup.jsx
Normal file
@@ -0,0 +1,270 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import TextField from 'material-ui/TextField'
|
||||
import Divider from 'material-ui/Divider'
|
||||
import { ListItem } from 'material-ui/List'
|
||||
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
|
||||
|
||||
// - Import actions
|
||||
import * as commentActions from 'commentActions'
|
||||
|
||||
// - Import app components
|
||||
import CommentList from 'CommentList'
|
||||
import CommentWrite from 'CommentWrite'
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class CommentGroup extends Component {
|
||||
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* If it's true comment box will be open
|
||||
*/
|
||||
open: PropTypes.bool,
|
||||
/**
|
||||
* If it's true the comment is disable to write
|
||||
*/
|
||||
disableComments: PropTypes.bool,
|
||||
/**
|
||||
* The post identifier which comment belong to
|
||||
*/
|
||||
postId: PropTypes.string,
|
||||
/**
|
||||
* If it's true the post owner is the logged in user which this post be long to the comment
|
||||
*/
|
||||
isPostOwner: PropTypes.bool,
|
||||
/**
|
||||
* Toggle on show/hide comment by passing callback from parent component
|
||||
*/
|
||||
onToggleRequest: PropTypes.func,
|
||||
/**
|
||||
* The user identifier of the post owner which comment belong to
|
||||
*/
|
||||
ownerPostUserId:PropTypes.string
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
/**
|
||||
* Defaul state
|
||||
*/
|
||||
this.state = {
|
||||
commentText: "",
|
||||
postDisable: true
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.commentList = this.commentList.bind(this)
|
||||
this.handlePostComment = this.handlePostComment.bind(this)
|
||||
this.clearCommentWrite = this.clearCommentWrite.bind(this)
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear comment text field
|
||||
*/
|
||||
clearCommentWrite = () => {
|
||||
this.setState({
|
||||
commentText: '',
|
||||
postDisable: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Post comment
|
||||
*/
|
||||
handlePostComment = () => {
|
||||
this.props.send(this.state.commentText, this.props.postId, this.clearCommentWrite)
|
||||
}
|
||||
|
||||
/**
|
||||
* When comment text changed
|
||||
* @param {event} evt is an event passed by change comment text callback funciton
|
||||
* @param {string} data is the comment text which user writes
|
||||
*/
|
||||
handleOnChange = (evt, data) => {
|
||||
this.setState({ commentText: data })
|
||||
if (data.length === 0 || data.trim() === '') {
|
||||
this.setState({
|
||||
commentText: '',
|
||||
postDisable: true
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
commentText: data,
|
||||
postDisable: false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comments' DOM
|
||||
* @return {DOM} list of comments' DOM
|
||||
*/
|
||||
commentList = () => {
|
||||
var comments = this.props.comments
|
||||
if (comments) {
|
||||
|
||||
|
||||
var parsedComments = [];
|
||||
Object.keys(comments).slice(0, 3).forEach((commentId) => {
|
||||
parsedComments.push({
|
||||
id: commentId,
|
||||
...comments[commentId]
|
||||
});
|
||||
});
|
||||
if (parsedComments.length === 2) {
|
||||
parsedComments.push(parsedComments[0])
|
||||
}
|
||||
else if (parsedComments.length === 1) {
|
||||
parsedComments.push(parsedComments[0])
|
||||
parsedComments.push(parsedComments[0])
|
||||
}
|
||||
return parsedComments.map((comment, index) => {
|
||||
const {userInfo} = this.props
|
||||
|
||||
const commentAvatar = userInfo && userInfo[comment.userId] ? userInfo[comment.userId].avatar || '' : ''
|
||||
|
||||
return (<ListItem key={index} style={{ height: "60px", position: "", zIndex: "" }} innerDivStyle={{ padding: "6px 16px 16px 72px" }}
|
||||
leftAvatar={<UserAvatar fileName={commentAvatar} style={{ top: "8px" }} size={36} />}
|
||||
secondaryText={<div style={{ height: "" }}>
|
||||
<span style={{
|
||||
fontSize: "13px",
|
||||
paddingRight: "10px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(0,0,0,0.87)",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{comment.userDisplayName}:
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: "13px",
|
||||
lineHeight: "20px",
|
||||
color: "rgba(0,0,0,0.87)",
|
||||
fontWeight: 300,
|
||||
whiteSpace: "pre-wrap"
|
||||
}}>{comment.text}</span>
|
||||
|
||||
</div>}
|
||||
secondaryTextLines={2}
|
||||
/>
|
||||
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={this.props.comments && Object.keys(this.props.comments).length > 0 ? { display: "block" } : { display: "none" }}>
|
||||
<Divider />
|
||||
<Paper zDepth={0} className="animate-top" style={!this.props.open ? { display: "block" } : { display: "none" }}>
|
||||
|
||||
<div style={{ position: "relative", height: "60px" }} >
|
||||
<FlatButton label=" " style={{ height: "60px", zIndex: 5 }} fullWidth={true} onClick={this.props.onToggleRequest} />
|
||||
|
||||
<div className="comment__list-show">
|
||||
{this.commentList()}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
{(this.props.comments && Object.keys(this.props.comments).length > 0)
|
||||
? (<Paper zDepth={0} style={this.props.open ? { display: "block", padding: "0px 0px" } : { display: "none", padding: "12px 16px" }}>
|
||||
<CommentList comments={this.props.comments} isPostOwner={this.props.isPostOwner} disableComments={this.props.disableComments}/>
|
||||
|
||||
</Paper>) : ''}
|
||||
|
||||
</div>
|
||||
{!this.props.disableComments ? (<div>
|
||||
<Divider />
|
||||
<Paper zDepth={0} className="animate2-top10" style={{ position: "relative", overflowY: "auto", padding: "12px 16px", display: (this.props.open ? "block" : "none") }}>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
<UserAvatar fileName={this.props.avatar} style={{ flex: "none", margin: "4px 0px" }} size={36} />
|
||||
<div style={{ outline: "none", marginLeft: "16px", flex: "auto", flexGrow: 1 }}>
|
||||
<TextField
|
||||
value={this.state.commentText}
|
||||
onChange={this.handleOnChange}
|
||||
hintText="Add a comment..."
|
||||
underlineShow={false}
|
||||
multiLine={true}
|
||||
rows={1}
|
||||
hintStyle={{ fontWeight: 100, fontSize: "14px" }}
|
||||
rowsMax={4}
|
||||
textareaStyle={{ fontWeight: 100, fontSize: "14px" }}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FlatButton primary={true} disabled={this.state.postDisable} label="Post" style={{ float: "right", clear: "both", zIndex: 5, margin: "0px 5px 5px 0px", fontWeight: 400 }} onClick={this.handlePostComment} />
|
||||
</Paper>
|
||||
</div>): ''}
|
||||
</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 {
|
||||
send: (text, postId, callBack) => {
|
||||
dispatch(commentActions.dbAddComment({
|
||||
postId: postId,
|
||||
text: text,
|
||||
ownerPostUserId:ownProps.ownerPostUserId
|
||||
}, callBack))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
comments: state.comment.postComments[ownProps.postId],
|
||||
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar || '' : '',
|
||||
userInfo: state.user.info
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CommentGroup)
|
||||
133
app/components/CommentList.jsx
Normal file
133
app/components/CommentList.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types'
|
||||
import { List, ListItem } from 'material-ui/List'
|
||||
|
||||
|
||||
|
||||
|
||||
// - Import app components
|
||||
import Comment from 'Comment'
|
||||
import * as PostAPI from 'PostAPI'
|
||||
|
||||
// - Import actions
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class CommentList extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* If it's true the post owner is the logged in user which this post be long to the comment
|
||||
*/
|
||||
isPostOwner: PropTypes.bool,
|
||||
/**
|
||||
* If it's true the comment is disable to write
|
||||
*/
|
||||
disableComments: PropTypes.bool,
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
/**
|
||||
* Default state
|
||||
*/
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get comments' DOM
|
||||
* @return {DOM} list of comments' DOM
|
||||
*/
|
||||
commentList = () => {
|
||||
var comments = this.props.comments
|
||||
if (comments) {
|
||||
|
||||
|
||||
var parsedComments = [];
|
||||
Object.keys(comments).forEach((commentId) => {
|
||||
parsedComments.push({
|
||||
id: commentId,
|
||||
...comments[commentId]
|
||||
});
|
||||
});
|
||||
let sortedComments = PostAPI.sortObjectsDate(parsedComments)
|
||||
|
||||
return sortedComments.map((comment, index, array) => {
|
||||
|
||||
return <Comment key={comment.id} comment={comment} isPostOwner={this.props.isPostOwner} disableComments={this.props.disableComments}/>
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const styles = {
|
||||
list: {
|
||||
width: "100%",
|
||||
maxHeight: 450,
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
|
||||
<List style={styles.list}>
|
||||
|
||||
{this.commentList()}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CommentList)
|
||||
84
app/components/CommentWrite.jsx
Normal file
84
app/components/CommentWrite.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import Faker from 'faker'
|
||||
|
||||
// - Import app components
|
||||
|
||||
// - Import actions
|
||||
import * as commentActions from 'commentActions'
|
||||
|
||||
// - Define variable
|
||||
const buttonStyle = {
|
||||
marginTop: '5px'
|
||||
};
|
||||
|
||||
// - Create CommentWrite component class
|
||||
export class CommentWrite extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inputValue:''
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleRef = this.handleRef.bind(this);
|
||||
this.focus = this.focus.bind(this);
|
||||
this.handleAddComment = this.handleAddComment.bind(this);
|
||||
this.handleOnChange = this.handleOnChange.bind(this)
|
||||
|
||||
}
|
||||
|
||||
handleOnChange = (evt) =>{
|
||||
this.setState({inputValue:evt.target.value})
|
||||
|
||||
}
|
||||
|
||||
handleRef = c => {
|
||||
this.inputRef = c
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
this.inputRef.focus()
|
||||
}
|
||||
handleAddComment = (evt) => {
|
||||
this.props.send(this.state.inputValue,this.props.postId,this.props.close)
|
||||
|
||||
}
|
||||
|
||||
// Render DOM
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<textarea autoFocus defaultValue={this.props.commentText} onChange={this.handleOnChange}/>
|
||||
|
||||
<Button basic style={buttonStyle} onClick={this.handleAddComment} color='teal'>Add Comment</Button>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// - Map dispatch to props
|
||||
const mapDispatchToProps = (dispatch,ownProps) => {
|
||||
return{
|
||||
send: (text,postId,callBack) => {
|
||||
dispatch(commentActions.dbAddComment({
|
||||
postId: postId,
|
||||
text: text
|
||||
},callBack))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// - Map state to props
|
||||
const mapStateToProps = (state) => {
|
||||
return{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to store
|
||||
export default connect(mapStateToProps,mapDispatchToProps)(CommentWrite)
|
||||
458
app/components/EditProfile.jsx
Normal file
458
app/components/EditProfile.jsx
Normal file
@@ -0,0 +1,458 @@
|
||||
|
||||
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
|
||||
import SvgCamera from 'material-ui/svg-icons/image/photo-camera'
|
||||
import IconMenu from 'material-ui/IconMenu'
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
import EventListener, { withOptions } from 'react-event-listener'
|
||||
import Dialog from 'material-ui/Dialog'
|
||||
import Divider from 'material-ui/Divider'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import TextField from 'material-ui/TextField'
|
||||
|
||||
|
||||
// - Import app components
|
||||
import ImgCover from 'ImgCover'
|
||||
import DialogTitle from 'DialogTitle'
|
||||
import ImageGallery from 'ImageGallery'
|
||||
import FileAPI from 'FileAPI'
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as userActions from 'userActions'
|
||||
import * as globalActions from 'globalActions'
|
||||
import * as imageGalleryActions from 'imageGalleryActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class EditProfile extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* User avatar address
|
||||
*/
|
||||
avatar: PropTypes.string,
|
||||
/**
|
||||
* User avatar address
|
||||
*/
|
||||
banner: PropTypes.string,
|
||||
/**
|
||||
* User full name
|
||||
*/
|
||||
fullName: PropTypes.string.isRequired
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
//Defaul state
|
||||
this.state = {
|
||||
/**
|
||||
* If it's true the winow is in small size
|
||||
*/
|
||||
isSmall: false,
|
||||
/**
|
||||
* User tag line input value
|
||||
*/
|
||||
tagLineInput: props.info.tagLine || '',
|
||||
/**
|
||||
* User full name input value
|
||||
*/
|
||||
fullNameInput: props.info.fullName || '',
|
||||
/**
|
||||
* Error message of full name input
|
||||
*/
|
||||
fullNameInputError: '',
|
||||
/**
|
||||
* User banner address
|
||||
*/
|
||||
banner: this.props.banner || '',
|
||||
/**
|
||||
* User avatar address
|
||||
*/
|
||||
avatar: this.props.avatar || '',
|
||||
/**
|
||||
* It's true if the image galley for banner is open
|
||||
*/
|
||||
openBanner: false,
|
||||
/**
|
||||
* It's true if the image gallery for avatar is open
|
||||
*/
|
||||
openAvatar: false
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleChangeDate = this.handleChangeDate.bind(this)
|
||||
this.handleUpdate = this.handleUpdate.bind(this)
|
||||
this.handleRequestSetAvatar = this.handleRequestSetAvatar.bind(this)
|
||||
this.handleRequestSetBanner = this.handleRequestSetBanner.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Close image gallery of banner
|
||||
*/
|
||||
handleCloseBannerGallery = () => {
|
||||
this.setState({
|
||||
openBanner: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open image gallery of banner
|
||||
*/
|
||||
handleOpenBannerGallery = () => {
|
||||
this.setState({
|
||||
openBanner: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Close image gallery of avatar
|
||||
*/
|
||||
handleCloseAvatarGallery = () => {
|
||||
this.setState({
|
||||
openAvatar: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open image gallery of avatar
|
||||
*/
|
||||
handleOpenAvatarGallery = () => {
|
||||
this.setState({
|
||||
openAvatar: true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set banner image url
|
||||
*/
|
||||
handleRequestSetBanner = (url) => {
|
||||
this.setState({
|
||||
banner: url
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set avatar image url
|
||||
*/
|
||||
handleRequestSetAvatar = (fileName) => {
|
||||
this.setState({
|
||||
avatar: fileName
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update profile on the server
|
||||
*
|
||||
*
|
||||
* @memberof EditProfile
|
||||
*/
|
||||
handleUpdate = () => {
|
||||
const {fullNameInput, tagLineInput, avatar, banner} = this.state
|
||||
|
||||
if (this.state.fullNameInput.trim() === '') {
|
||||
this.setState({
|
||||
fullNameInputError: 'This field is required'
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
fullNameInputError: ''
|
||||
})
|
||||
|
||||
this.props.update({
|
||||
fullName: fullNameInput,
|
||||
tagLine: tagLineInput,
|
||||
avatar: avatar,
|
||||
banner: banner
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data on input change
|
||||
* @param {event} evt is an event of inputs of element on change
|
||||
*/
|
||||
handleInputChange = (evt) => {
|
||||
const target = evt.target
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value
|
||||
const name = target.name
|
||||
this.setState({
|
||||
[name]: value
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Handle change date
|
||||
*/
|
||||
handleChangeDate = (evt, date) => {
|
||||
this.setState({
|
||||
birthdayInput: date,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resize event for window to change sidebar status
|
||||
* @param {event} evt is the event is passed by winodw resize event
|
||||
*/
|
||||
handleResize = (evt) => {
|
||||
|
||||
// Set initial state
|
||||
var width = window.innerWidth
|
||||
|
||||
if (width > 900) {
|
||||
this.setState({
|
||||
isSmall: false
|
||||
})
|
||||
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
isSmall: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount = () => {
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const styles = {
|
||||
avatar: {
|
||||
border: '2px solid rgb(255, 255, 255)'
|
||||
},
|
||||
paper: {
|
||||
width: '90%',
|
||||
height: '100%',
|
||||
margin: '0 auto',
|
||||
display: 'block',
|
||||
},
|
||||
title: {
|
||||
padding: '24px 24px 20px 24px',
|
||||
font: '500 20px Roboto,RobotoDraft,Helvetica,Arial,sans-serif',
|
||||
display: 'flex',
|
||||
wordWrap: 'break-word',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
flexGrow: 1
|
||||
},
|
||||
actions: {
|
||||
display: 'flex',
|
||||
justifyContent: "flex-end",
|
||||
padding: '24px 24px 20px'
|
||||
},
|
||||
updateButton: {
|
||||
marginLeft: '10px'
|
||||
},
|
||||
box: {
|
||||
padding: '0px 24px 20px 24px',
|
||||
display: 'flex'
|
||||
|
||||
},
|
||||
dialogGallery: {
|
||||
width: '',
|
||||
maxWidth: '530px',
|
||||
borderRadius: "4px"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const iconButtonElement = (
|
||||
<IconButton style={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton} iconStyle={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton}
|
||||
touch={true}
|
||||
>
|
||||
<MoreVertIcon color={grey400} viewBox='10 0 24 24' />
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
|
||||
const RightIconMenu = () => (
|
||||
<IconMenu iconButtonElement={iconButtonElement}>
|
||||
<MenuItem style={{ fontSize: "14px" }}>Reply</MenuItem>
|
||||
<MenuItem style={{ fontSize: "14px" }}>Edit</MenuItem>
|
||||
<MenuItem style={{ fontSize: "14px" }}>Delete</MenuItem>
|
||||
</IconMenu>
|
||||
)
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div>
|
||||
{/* Edit profile dialog */}
|
||||
<Dialog
|
||||
id='Edit-Profile'
|
||||
modal={false}
|
||||
open={this.props.open}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
autoScrollBodyContent={true}
|
||||
bodyStyle={{ backgroundColor: "none", padding: 'none', borderTop: 'none', borderBottom: 'none' }}
|
||||
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
|
||||
contentStyle={{ backgroundColor: "none", maxWidth: '450px', maxHeight: 'none', height: '100%' }}
|
||||
style={{ backgroundColor: "none", maxHeight: 'none', height: '100%' }}
|
||||
>
|
||||
{/* Banner */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<ImgCover width='100%' height='250px' borderRadius='2px' fileName={this.state.banner} />
|
||||
<div className='g__circle-black' onClick={this.handleOpenBannerGallery} style={{ position: 'absolute', right: '10px', top: '10px' }}>
|
||||
<SvgCamera style={{ fill: 'rgba(255, 255, 255, 0.88)', transform: 'translate(6px, 6px)' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='profile__edit'>
|
||||
<EventListener
|
||||
target="window"
|
||||
onResize={this.handleResize}
|
||||
/>
|
||||
<div className='left'>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{/* Avatar */}
|
||||
<div className='g__circle-black' onClick={this.handleOpenAvatarGallery} style={{ position: 'absolute', left: '50%', display: 'inline-block', top: '52px', margin: '-18px' }}>
|
||||
<SvgCamera style={{ fill: 'rgba(255, 255, 255, 0.88)', transform: 'translate(6px, 6px)' }} />
|
||||
|
||||
</div>
|
||||
<UserAvatar fileName={this.state.avatar} size={90} style={styles.avatar} />
|
||||
</div>
|
||||
<div className='info'>
|
||||
<div className='fullName'>
|
||||
{this.props.fullName}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Edit user information box*/}
|
||||
<Paper style={styles.paper} zDepth={1}>
|
||||
<div style={styles.title}>Personal Information</div>
|
||||
<div style={styles.box}>
|
||||
<TextField
|
||||
floatingLabelText="Full name"
|
||||
onChange={this.handleInputChange}
|
||||
name='fullNameInput'
|
||||
errorText={this.state.fullNameInputError}
|
||||
value={this.state.fullNameInput}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div style={styles.box}>
|
||||
<TextField
|
||||
floatingLabelText="Tag Line"
|
||||
onChange={this.handleInputChange}
|
||||
name='tagLineInput'
|
||||
value={this.state.tagLineInput}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div style={styles.actions}>
|
||||
<FlatButton label="CANCEL" onClick={this.props.onRequestClose} />
|
||||
<RaisedButton label="UPDATE" primary={true} onClick={this.handleUpdate} style={styles.updateButton} />
|
||||
</div>
|
||||
</Paper>
|
||||
<div style={{ height: '16px' }}></div>
|
||||
|
||||
|
||||
</Dialog>
|
||||
|
||||
{/* Image gallery for banner*/}
|
||||
<Dialog
|
||||
title={<DialogTitle title='Choose an banner image' onRequestClose={this.handleCloseBannerGallery} />}
|
||||
modal={false}
|
||||
open={this.state.openBanner}
|
||||
contentStyle={styles.dialogGallery}
|
||||
onRequestClose={this.handleCloseBannerGallery}
|
||||
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
|
||||
autoDetectWindowHeight={false}
|
||||
|
||||
>
|
||||
<ImageGallery set={this.handleRequestSetBanner} close={this.handleCloseBannerGallery} />
|
||||
</Dialog>
|
||||
|
||||
{/* Image gallery for avatar */}
|
||||
<Dialog
|
||||
title={<DialogTitle title='Choose an avatar image' onRequestClose={this.handleCloseAvatarGallery} />}
|
||||
modal={false}
|
||||
open={this.state.openAvatar}
|
||||
contentStyle={styles.dialogGallery}
|
||||
onRequestClose={this.handleCloseAvatarGallery}
|
||||
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
|
||||
autoDetectWindowHeight={false}
|
||||
|
||||
>
|
||||
<ImageGallery set={this.handleRequestSetAvatar} close={this.handleCloseAvatarGallery} />
|
||||
</Dialog>
|
||||
|
||||
</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 {
|
||||
update: (info) => dispatch(userActions.dbUpdateUserInfo(info)),
|
||||
onRequestClose: () => dispatch(userActions.closeEditProfile()),
|
||||
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
open: state.user.openEditProfile,
|
||||
info: state.user.info[state.authorize.uid],
|
||||
avatarURL: state.imageGallery.imageURLList
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditProfile)
|
||||
110
app/components/FindPeople.jsx
Normal file
110
app/components/FindPeople.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import Paper from 'material-ui/Paper'
|
||||
|
||||
|
||||
// - Import app components
|
||||
import UserBoxList from 'UserBoxList'
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as userActions from 'userActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class FindPeople extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.loadPeople()
|
||||
}
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const styles = {
|
||||
paper: {
|
||||
height: 254,
|
||||
width: 243,
|
||||
margin: 10,
|
||||
textAlign: 'center',
|
||||
maxWidth: '257px'
|
||||
},
|
||||
followButton:{
|
||||
position: 'absolute',
|
||||
bottom: '8px',
|
||||
left: 0,
|
||||
right: 0
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.props.peopleInfo && Object.keys(this.props.peopleInfo).length !== 0 ? (<div>
|
||||
<div className='profile__title'>
|
||||
Suggestions for you
|
||||
</div>
|
||||
<UserBoxList users={this.props.peopleInfo}/>
|
||||
<div style={{ height: '24px' }}></div>
|
||||
</div>) : (<div className='g__title-center'>
|
||||
Nothing to show! :(
|
||||
</div>)}
|
||||
</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 {
|
||||
loadPeople: () => dispatch(userActions.dbGetPeopleInfo())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
peopleInfo: state.user.info
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FindPeople)
|
||||
89
app/components/Followers.jsx
Normal file
89
app/components/Followers.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// - Import app components
|
||||
import UserBoxList from 'UserBoxList'
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class Followers extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{(this.props.followers && Object.keys(this.props.followers).length !==0) ? (<div>
|
||||
<div className='profile__title'>
|
||||
Followers
|
||||
</div>
|
||||
<UserBoxList users={this.props.followers} />
|
||||
<div style={{ height: '24px' }}></div>
|
||||
</div>)
|
||||
: (<div className='g__title-center'>
|
||||
No followers!
|
||||
</div>)}
|
||||
</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{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const circles = state.circle ? state.circle.userCircles[uid] : {}
|
||||
return{
|
||||
followers: circles ? (circles['-Followers'] ? circles['-Followers'].users || {} : {}) : {}
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps,mapDispatchToProps)(Followers)
|
||||
95
app/components/Following.jsx
Normal file
95
app/components/Following.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// - Import app components
|
||||
import UserBoxList from 'UserBoxList'
|
||||
|
||||
// - Import API
|
||||
import CircleAPI from 'CircleAPI'
|
||||
|
||||
|
||||
// - Import actions
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class Following extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(this.props.followingUsers && Object.keys(this.props.followingUsers).length !== 0 )? (<div>
|
||||
<div className='profile__title'>
|
||||
Following
|
||||
</div>
|
||||
<UserBoxList users={this.props.followingUsers} />
|
||||
<div style={{ height: '24px' }}></div>
|
||||
|
||||
</div>) : (<div className='g__title-center'>
|
||||
No following user!
|
||||
</div>)}
|
||||
</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{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const circles = state.circle ? state.circle.userCircles[uid] : {}
|
||||
const followingUsers = CircleAPI.getFollowingUsers(circles)
|
||||
return {
|
||||
uid,
|
||||
circles,
|
||||
followingUsers,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps,mapDispatchToProps)(Following)
|
||||
202
app/components/Home.jsx
Normal file
202
app/components/Home.jsx
Normal file
@@ -0,0 +1,202 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import _ from 'lodash'
|
||||
import { Route, Switch, withRouter, Redirect, NavLink } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { push } from 'react-router-redux'
|
||||
import Menu from 'material-ui/Menu'
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import Divider from 'material-ui/Divider'
|
||||
import SvgArrowLeft from 'material-ui/svg-icons/hardware/keyboard-arrow-left'
|
||||
import SvgHome from 'material-ui/svg-icons/action/home'
|
||||
import SvgFeedback from 'material-ui/svg-icons/action/feedback'
|
||||
import SvgSettings from 'material-ui/svg-icons/action/settings'
|
||||
import SvgAccountCircle from 'material-ui/svg-icons/action/account-circle'
|
||||
import SvgPeople from 'material-ui/svg-icons/social/people'
|
||||
|
||||
// - Import app components
|
||||
import Sidebar from 'Sidebar'
|
||||
import Blog from 'Blog'
|
||||
import HomeHeader from 'HomeHeader'
|
||||
import SidebarContent from 'SidebarContent'
|
||||
import SidebarMain from 'SidebarMain'
|
||||
import Profile from 'Profile'
|
||||
import PostPage from 'PostPage'
|
||||
import People from 'People'
|
||||
|
||||
// - Import API
|
||||
import CircleAPI from 'CircleAPI'
|
||||
|
||||
// - Import actions
|
||||
import * as globalActions from 'globalActions'
|
||||
|
||||
|
||||
// - Create Home component class
|
||||
export class Home extends Component {
|
||||
|
||||
// Constructor
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
// Default state
|
||||
this.state = {
|
||||
sidebarOpen: () => _,
|
||||
sidebarStatus: true,
|
||||
sidebaOverlay: false
|
||||
}
|
||||
|
||||
// Binding function to `this`
|
||||
this.sidebar = this.sidebar.bind(this)
|
||||
this.sidebarStatus = this.sidebarStatus.bind(this)
|
||||
this.sidebarOverlay = this.sidebarOverlay.bind(this)
|
||||
this.handleCloseSidebar = this.handleCloseSidebar.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* handle close sidebar
|
||||
*/
|
||||
handleCloseSidebar = () => {
|
||||
this.state.sidebarOpen(false, 'overlay')
|
||||
}
|
||||
|
||||
/**
|
||||
* Change sidebar overlay status
|
||||
* @param {boolean} status if is true, the sidebar is on overlay status
|
||||
*/
|
||||
sidebarOverlay = (status) => {
|
||||
this.setState({
|
||||
sidebarOverlay: status
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pass function to change sidebar status
|
||||
* @param {boolean} open is a function callback to change sidebar status out of sidebar component
|
||||
*/
|
||||
sidebar = (open) => {
|
||||
|
||||
this.setState({
|
||||
sidebarOpen: open
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change sidebar status if is open or not
|
||||
* @param {boolean} status is true, if the sidebar is open
|
||||
*/
|
||||
sidebarStatus = (status) => {
|
||||
this.setState({
|
||||
sidebarStatus: status
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Render DOM component
|
||||
*
|
||||
* @returns DOM
|
||||
*
|
||||
* @memberof Home
|
||||
*/
|
||||
render() {
|
||||
|
||||
|
||||
return (
|
||||
<div id="home">
|
||||
<HomeHeader sidebar={this.state.sidebarOpen} sidebarStatus={this.state.sidebarStatus} />
|
||||
<Sidebar overlay={this.sidebarOverlay} open={this.sidebar} status={this.sidebarStatus}>
|
||||
<SidebarContent>
|
||||
<Menu style={{ color: "rgb(117, 117, 117)", width: '210px' }}>
|
||||
{this.state.sidebarOverlay
|
||||
? <div><MenuItem onClick={this.handleCloseSidebar} primaryText={<span style={{ color: "rgb(117, 117, 117)" }} className="sidebar__title">Green</span>} rightIcon={<SvgArrowLeft viewBox="0 3 24 24" style={{ color: "#fff", marginLeft: "15px", width: "32px", height: "32px", cursor: "pointer" }} />} /><Divider /></div>
|
||||
: ""
|
||||
}
|
||||
|
||||
<NavLink to='/'><MenuItem primaryText="Home" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgHome />} /></NavLink>
|
||||
<NavLink to={`/${this.props.uid}`}><MenuItem primaryText="Profile" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgAccountCircle />} /></NavLink>
|
||||
<NavLink to='/people'><MenuItem primaryText="People" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgPeople />} /></NavLink>
|
||||
<Divider />
|
||||
<NavLink to='/settings'><MenuItem primaryText="Settings" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgSettings />} /></NavLink>
|
||||
<NavLink to='#'><MenuItem primaryText="Send feedback" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgFeedback />} /></NavLink>
|
||||
</Menu>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarMain>
|
||||
<Switch>
|
||||
<Route path="/people/:tab?" render={() => {
|
||||
return (
|
||||
this.props.authed
|
||||
? <People />
|
||||
: <Redirect to="/login" />
|
||||
)
|
||||
}} />
|
||||
<Route path="/tag/:tag" render={({match}) => {
|
||||
|
||||
return (
|
||||
this.props.authed
|
||||
? <div className="blog"><Blog displayWriting={false} homeTitle={`#${match.params.tag}`} posts={this.props.mergedPosts} /></div>
|
||||
: <Redirect to="/login" />
|
||||
)
|
||||
}} />
|
||||
<Route path="/:userId/posts/:postId/:tag?" component={PostPage} />
|
||||
<Route path="/:userId" component={Profile} />
|
||||
|
||||
<Route path="/" render={() => {
|
||||
|
||||
return (
|
||||
this.props.authed
|
||||
? <div className="blog"><Blog homeTitle='Home' posts={this.props.mergedPosts} displayWriting={true} /></div>
|
||||
: <Redirect to="/login" />
|
||||
)
|
||||
}} />
|
||||
</Switch>
|
||||
</SidebarMain>
|
||||
</Sidebar>
|
||||
|
||||
</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 {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 mergedPosts = {}
|
||||
const circles = state.circle ? (state.circle.userCircles[uid] || {}) : {}
|
||||
const followingUsers = CircleAPI.getFollowingUsers(circles)
|
||||
const posts = state.post.userPosts ? state.post.userPosts[state.authorize.uid] : {}
|
||||
Object.keys(followingUsers).forEach((userId)=>{
|
||||
let newPosts = state.post.userPosts ? state.post.userPosts[userId] : {}
|
||||
_.merge(mergedPosts,newPosts)
|
||||
})
|
||||
_.merge(mergedPosts,posts)
|
||||
return {
|
||||
authed: state.authorize.authed,
|
||||
mainStyle: state.global.sidebarMainStyle,
|
||||
mergedPosts
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Home))
|
||||
285
app/components/HomeHeader.jsx
Normal file
285
app/components/HomeHeader.jsx
Normal file
@@ -0,0 +1,285 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import SvgDehaze from 'material-ui/svg-icons/image/dehaze'
|
||||
import { green700, grey400, blue500 } from 'material-ui/styles/colors'
|
||||
import { Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle } from 'material-ui/Toolbar'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover'
|
||||
import Menu from 'material-ui/Menu'
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import NotificationsIcon from 'material-ui/svg-icons/social/notifications'
|
||||
import EventListener, { withOptions } from 'react-event-listener'
|
||||
|
||||
|
||||
// - Import components
|
||||
import UserAvatar from 'UserAvatar'
|
||||
import Notify from 'Notify'
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as globalActions from 'globalActions'
|
||||
import * as authorizeActions from 'authorizeActions'
|
||||
|
||||
// - Create HomeHeader component class
|
||||
export class HomeHeader extends Component {
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
// Default state
|
||||
this.state = {
|
||||
/**
|
||||
* User avatar popover is open if true
|
||||
*/
|
||||
openAvatarMenu: false,
|
||||
/**
|
||||
* Show header title or not (true/false)
|
||||
*/
|
||||
showTitle: true,
|
||||
/**
|
||||
* If true notification menu will be open
|
||||
*/
|
||||
openNotifyMenu: false
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.onToggleSidebar = this.onToggleSidebar.bind(this)
|
||||
this.handleCloseNotify = this.handleCloseNotify.bind(this)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle close notification menu
|
||||
*
|
||||
*
|
||||
* @memberof HomeHeader
|
||||
*/
|
||||
handleCloseNotify = () => {
|
||||
this.setState({
|
||||
openNotifyMenu: false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// On click toggle sidebar
|
||||
onToggleSidebar = () => {
|
||||
if (this.props.sidebarStatus) {
|
||||
this.props.sidebar(false)
|
||||
|
||||
} else {
|
||||
this.props.sidebar(true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle notification touch
|
||||
*
|
||||
*
|
||||
* @memberof HomeHeader
|
||||
*/
|
||||
handleNotifyTouchTap = (event) => {
|
||||
// This prevents ghost click.
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
openNotifyMenu: true,
|
||||
anchorEl: event.currentTarget,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch on user avatar for popover
|
||||
*
|
||||
*
|
||||
* @memberof HomeHeader
|
||||
*/
|
||||
handleAvatarTouchTap = (event) => {
|
||||
// This prevents ghost click.
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
openAvatarMenu: true,
|
||||
anchorEl: event.currentTarget,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout user
|
||||
*
|
||||
*
|
||||
* @memberof HomeHeader
|
||||
*/
|
||||
handleLogout = () => {
|
||||
this.props.logout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle close popover
|
||||
*
|
||||
*
|
||||
* @memberof HomeHeader
|
||||
*/
|
||||
handleRequestClose = () => {
|
||||
this.setState({
|
||||
openAvatarMenu: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle resize event for window to manipulate home header status
|
||||
* @param {event} evt is the event is passed by winodw resize event
|
||||
*/
|
||||
handleResize = (evt) => {
|
||||
|
||||
// Set initial state
|
||||
var width = window.innerWidth
|
||||
|
||||
if (width >= 600 && !this.state.showTitle) {
|
||||
this.setState({
|
||||
showTitle: true
|
||||
})
|
||||
|
||||
}
|
||||
else if (width < 600 && this.state.showTitle) {
|
||||
|
||||
this.setState({
|
||||
showTitle: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
|
||||
// Render app DOM component
|
||||
render() {
|
||||
|
||||
/**
|
||||
* Styles
|
||||
*/
|
||||
var styles = {
|
||||
toolbarStyle: {
|
||||
backgroundColor: "",
|
||||
transition: "all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms",
|
||||
boxSizing: "border-box",
|
||||
fontFamily: "Roboto, sans-serif",
|
||||
position: "fixed",
|
||||
zIndex: "1101",
|
||||
width: "100%",
|
||||
top: "0px",
|
||||
boxShadow: '0 1px 8px rgba(0,0,0,.3)'
|
||||
},
|
||||
avatarStyle: {
|
||||
margin: 5,
|
||||
cursor: 'pointer'
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Toolbar style={styles.toolbarStyle} className="g__greenBox">
|
||||
<EventListener
|
||||
target="window"
|
||||
onResize={this.handleResize}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
/>
|
||||
{/* Left side */}
|
||||
<ToolbarGroup firstChild={true}>
|
||||
|
||||
<IconButton iconStyle={{ color: "#fff" }} onClick={this.onToggleSidebar} >
|
||||
<SvgDehaze style={{ color: "#fff", marginLeft: "15px", cursor: "pointer" }} />
|
||||
</IconButton>
|
||||
{/* Header title */}
|
||||
<ToolbarTitle style={{ color: "#fff", marginLeft: "15px" }} text="Green" />
|
||||
{this.state.showTitle ? <div className="homeHeader__page">{this.props.title}</div> : ''}
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
|
||||
</ToolbarGroup>
|
||||
|
||||
{/* Notification */}
|
||||
<ToolbarGroup lastChild={true}>
|
||||
<div className="homeHeader__right">
|
||||
{this.props.notifyCount > 0 ? (<IconButton tooltip="Notifications" onTouchTap={this.handleNotifyTouchTap}>
|
||||
<div className="homeHeader__notify">
|
||||
<div className='title'>{this.props.notifyCount}</div>
|
||||
</div>
|
||||
</IconButton>)
|
||||
|
||||
: (<IconButton tooltip="Notifications" onTouchTap={this.handleNotifyTouchTap}>
|
||||
<NotificationsIcon color='rgba(255, 255, 255, 0.87)' />
|
||||
</IconButton>)}
|
||||
<Notify open={this.state.openNotifyMenu} anchorEl={this.state.anchorEl} onRequestClose={this.handleCloseNotify}/>
|
||||
|
||||
|
||||
{/* User avatar*/}
|
||||
<UserAvatar
|
||||
onTouchTap={this.handleAvatarTouchTap}
|
||||
fileName={this.props.avatar}
|
||||
size={32}
|
||||
style={styles.avatarStyle}
|
||||
/>
|
||||
<Popover
|
||||
open={this.state.openAvatarMenu}
|
||||
anchorEl={this.state.anchorEl}
|
||||
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: 'left', vertical: 'top' }}
|
||||
onRequestClose={this.handleRequestClose}
|
||||
>
|
||||
<Menu>
|
||||
<MenuItem style={{ backgroundColor: 'white', color: blue500, fontSize: '14px' }} primaryText="MY ACCOUNT" />
|
||||
<MenuItem primaryText="LOGOUT" style={{ fontSize: '14px' }} onClick={this.handleLogout.bind(this)} />
|
||||
|
||||
</Menu>
|
||||
</Popover>
|
||||
</div>
|
||||
</ToolbarGroup>
|
||||
|
||||
</Toolbar>
|
||||
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// - Map dispatch to props
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
return {
|
||||
logout: () => dispatch(authorizeActions.dbLogout())
|
||||
}
|
||||
}
|
||||
|
||||
// - Map state to props
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
let notifyCount = state.notify.userNotifies
|
||||
? Object
|
||||
.keys(state.notify.userNotifies)
|
||||
.filter((key)=> !state.notify.userNotifies[key].isSeen).length
|
||||
: 0
|
||||
return {
|
||||
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : '',
|
||||
title: state.global.headerTitle,
|
||||
notifyCount
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HomeHeader)
|
||||
246
app/components/ImageGallery.jsx
Normal file
246
app/components/ImageGallery.jsx
Normal file
@@ -0,0 +1,246 @@
|
||||
// - Impoer react components
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { GridList, GridTile } from 'material-ui/GridList'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import Subheader from 'material-ui/Subheader'
|
||||
import StarBorder from 'material-ui/svg-icons/toggle/star-border'
|
||||
import FloatingActionButton from 'material-ui/FloatingActionButton';
|
||||
import SvgUpload from 'material-ui/svg-icons/file/cloud-upload'
|
||||
import SvgAddImage from 'material-ui/svg-icons/image/add-a-photo'
|
||||
import SvgDelete from 'material-ui/svg-icons/action/delete'
|
||||
import { grey200, grey600 } from 'material-ui/styles/colors'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import uuid from 'uuid'
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as imageGalleryActions from 'imageGalleryActions'
|
||||
import * as fileActions from 'fileActions'
|
||||
import * as globalActions from 'globalActions'
|
||||
|
||||
// - Import app components
|
||||
import Img from 'Img'
|
||||
|
||||
// - Import API
|
||||
import FileAPI from 'FileAPI'
|
||||
|
||||
/**
|
||||
* Create ImageGallery component class
|
||||
*/
|
||||
export class ImageGallery extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* Callback function to ser image url on parent component
|
||||
*/
|
||||
open: PropTypes.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Binding function to `this`
|
||||
this.close = this.close.bind(this)
|
||||
this.onFileChange = this.onFileChange.bind(this)
|
||||
this.handleSetImage = this.handleSetImage.bind(this)
|
||||
this.handleDeleteImage = this.handleDeleteImage.bind(this)
|
||||
this.imageList = this.imageList.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle set image
|
||||
* @param {event} evt passed by on click event on add image
|
||||
* @param {string} name is the name of the image
|
||||
*/
|
||||
handleSetImage = (evt, name) => {
|
||||
this.props.set(name)
|
||||
this.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delete image
|
||||
* @param {event} evt passed by on click event on delete image
|
||||
* @param {integer} id is the image identifier which selected to delete
|
||||
*/
|
||||
handleDeleteImage = (evt, id) => {
|
||||
this.props.deleteImage(id)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("onSendResizedImage", this.handleSendResizedImage)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("onSendResizedImage", this.handleSendResizedImage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle send image resize event that pass the resized image
|
||||
*
|
||||
*
|
||||
* @memberof ImageGallery
|
||||
*/
|
||||
handleSendResizedImage = (event) => {
|
||||
const { resizedImage, fileName } = event.detail
|
||||
this.props.saveImage(resizedImage, fileName)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle on change file upload
|
||||
*/
|
||||
onFileChange = (evt) => {
|
||||
|
||||
const extension = FileAPI.getExtension(evt.target.files[0].name)
|
||||
var fileName = (`${uuid()}.${extension}`)
|
||||
let image = FileAPI.constraintImage(evt.target.files[0], fileName)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide image gallery
|
||||
*/
|
||||
close = () => {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
imageList = () => {
|
||||
|
||||
console.log('====================================');
|
||||
console.log(this.props.images);
|
||||
console.log('====================================');
|
||||
return this.props.images.map((image, index) => {
|
||||
|
||||
return (<GridTile
|
||||
key={image.id}
|
||||
title={<SvgDelete hoverColor={grey200} color="white" color="white" style={{ marginLeft: "5px", cursor: "pointer" }} onClick={evt => this.handleDeleteImage(evt, image.id)} />}
|
||||
subtitle={<span></span>}
|
||||
actionIcon={<SvgAddImage hoverColor={grey200} color="white" style={{ marginRight: "5px", cursor: "pointer" }} onClick={evt => this.handleSetImage(evt, image.name)} />}
|
||||
>
|
||||
<div>
|
||||
<div style={{ overflowY: "hidden", overflowX: "auto" }}>
|
||||
<ul style={{ whiteSpace: "nowrap", padding: "0 6px", margin: "8px 0 0 0", verticalAlign: "bottom", flexShrink: 0, listStyleType: "none" }}>
|
||||
<div style={{ display: "block" }}>
|
||||
<div style={{ display: "block", marginRight: "8px", transition: "transform .25s" }}>
|
||||
<li style={{ width: "100%", margin: 0, verticalAlign: "bottom", position: "static", display: "inline-block" }}>
|
||||
<Img fileName={image.name} style={{ width: "100%", height: "auto" }} />
|
||||
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</GridTile>)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* When the post text changed
|
||||
* @param {event} evt is an event passed by change post text callback funciton
|
||||
* @param {string} data is the post content which user writes
|
||||
*/
|
||||
render() {
|
||||
|
||||
|
||||
/**
|
||||
* Component styles
|
||||
* @type {Object}
|
||||
*/
|
||||
const styles = {
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
gridList: {
|
||||
width: 500,
|
||||
height: 450,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
uploadButton: {
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
uploadInput: {
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
opacity: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.root}>
|
||||
<GridList
|
||||
cellHeight={180}
|
||||
style={styles.gridList}
|
||||
>
|
||||
<GridTile >
|
||||
|
||||
<div style={{ display: "flex", backgroundColor: "rgba(222, 222, 222, 0.52)", flexDirection: "column", justifyContent: "center", alignItems: "center", height: "100%" }}>
|
||||
|
||||
<FlatButton
|
||||
label="Upload Photo"
|
||||
labelStyle={{ fontWeight: 100 }}
|
||||
labelPosition="before"
|
||||
style={styles.uploadButton}
|
||||
containerElement="label"
|
||||
>
|
||||
<input type="file" onChange={this.onFileChange} accept="image/*" style={styles.uploadInput} />
|
||||
</FlatButton>
|
||||
</div>
|
||||
</GridTile>
|
||||
{this.imageList()}
|
||||
</GridList>
|
||||
</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 {
|
||||
saveImage: (file, fileName) => {
|
||||
dispatch(imageGalleryActions.dbUploadImage(file, fileName))
|
||||
},
|
||||
deleteImage: (id) => {
|
||||
dispatch(imageGalleryActions.dbDeleteImage(id))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
images: state.imageGallery.images,
|
||||
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : ''
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ImageGallery)
|
||||
158
app/components/Img.jsx
Normal file
158
app/components/Img.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import SvgImage from 'material-ui/svg-icons/image/image'
|
||||
|
||||
|
||||
// - Import app components
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
// - Import actions
|
||||
import * as imageGalleryActions from 'imageGalleryActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class Img extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* Use for getting url address from server
|
||||
*/
|
||||
fileName: PropTypes.string,
|
||||
/**
|
||||
* Avatar style
|
||||
*/
|
||||
style: PropTypes.object
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
isImageLoaded: false
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.getImageURL = this.getImageURL.bind(this)
|
||||
this.handleLoadImage = this.handleLoadImage.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called on loading image
|
||||
*
|
||||
* @memberof Img
|
||||
*/
|
||||
handleLoadImage = () => {
|
||||
this.setState({
|
||||
isImageLoaded: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image url if it is not exist on redux store
|
||||
*
|
||||
* @memberof Img
|
||||
*/
|
||||
getImageURL = () => {
|
||||
let { fileName } = this.props
|
||||
if (fileName && fileName !== '') {
|
||||
if (this.props.imageRequests.indexOf(fileName) > -1)
|
||||
return
|
||||
this.props.getImage(fileName)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
let { fileName } = this.props
|
||||
|
||||
if (this.props.imageRequests.indexOf(fileName) > -1)
|
||||
return
|
||||
this.getImageURL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
const styles = {
|
||||
loding: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100px',
|
||||
position: 'relative',
|
||||
color: '#cacecd',
|
||||
fontWeight: 100
|
||||
},
|
||||
loadingContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
},
|
||||
loadingImage: {
|
||||
fill: 'aliceblue',
|
||||
width: '50px',
|
||||
height: '50px'
|
||||
}
|
||||
}
|
||||
|
||||
let { fileName, style } = this.props
|
||||
let { isImageLoaded } = this.state
|
||||
return (
|
||||
<div>
|
||||
<img onLoad={this.handleLoadImage} src={this.props.avatarURL[fileName] || ''} style={isImageLoaded ? style : { display: 'none' }} />
|
||||
<div style={{ backgroundColor: 'blue' }} style={isImageLoaded ? { display: 'none' } : styles.loding}>
|
||||
<div style={styles.loadingContent}>
|
||||
<SvgImage style={styles.loadingImage} />
|
||||
<div>Image has not loaded</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 {
|
||||
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
avatarURL: state.imageGallery.imageURLList,
|
||||
imageRequests: state.imageGallery.imageRequests
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Img)
|
||||
191
app/components/ImgCover.jsx
Normal file
191
app/components/ImgCover.jsx
Normal file
@@ -0,0 +1,191 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import SvgImage from 'material-ui/svg-icons/image/image'
|
||||
|
||||
// - Import app components
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
// - Import actions
|
||||
import * as imageGalleryActions from 'imageGalleryActions'
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class ImgCover extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* Use for getting url address from server
|
||||
*/
|
||||
fileName: PropTypes.string,
|
||||
/**
|
||||
* Image width
|
||||
*/
|
||||
width: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
]).isRequired,
|
||||
/**
|
||||
* Image height
|
||||
*/
|
||||
height: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
]).isRequired,
|
||||
/**
|
||||
* Image border radius
|
||||
*/
|
||||
borderRadius: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
isImageLoaded: false
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.getImageURL = this.getImageURL.bind(this)
|
||||
this.handleLoadImage = this.handleLoadImage.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called on loading image
|
||||
*
|
||||
* @memberof Img
|
||||
*/
|
||||
handleLoadImage = () => {
|
||||
this.setState({
|
||||
isImageLoaded: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image url if it is not exist on redux store
|
||||
*
|
||||
* @memberof Img
|
||||
*/
|
||||
getImageURL = () => {
|
||||
let { fileName } = this.props
|
||||
if (fileName && fileName !== '') {
|
||||
if (this.props.imageRequests.indexOf(fileName) > -1)
|
||||
return
|
||||
this.props.getImage(fileName)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
let { fileName } = this.props
|
||||
|
||||
if (this.props.imageRequests.indexOf(fileName) > -1)
|
||||
return
|
||||
this.getImageURL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
let { fileName, style } = this.props
|
||||
let { isImageLoaded } = this.state
|
||||
|
||||
/**
|
||||
* Styles
|
||||
*/
|
||||
const styles = {
|
||||
cover: {
|
||||
backgroundImage: 'url(' + (this.props.avatarURL[fileName] || '#') + ')',
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
borderRadius: this.props.borderRadius
|
||||
|
||||
},
|
||||
loding: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100px',
|
||||
position: 'relative',
|
||||
color: '#cacecd',
|
||||
fontWeight: 100
|
||||
},
|
||||
loadingContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
},
|
||||
loadingImage: {
|
||||
fill: 'aliceblue',
|
||||
width: '50px',
|
||||
height: '50px'
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div style={Object.assign({},styles.cover,style)}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
<div style={{ backgroundColor: 'blue' }} style={isImageLoaded ? { display: 'none' } : styles.loding}>
|
||||
<div style={styles.loadingContent}>
|
||||
<SvgImage style={styles.loadingImage} />
|
||||
<div>Image has not loaded</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 {
|
||||
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
avatarURL: state.imageGallery.imageURLList,
|
||||
imageRequests: state.imageGallery.imageRequests
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ImgCover)
|
||||
212
app/components/Login.jsx
Normal file
212
app/components/Login.jsx
Normal file
@@ -0,0 +1,212 @@
|
||||
// - Import external components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { NavLink, withRouter } from 'react-router-dom'
|
||||
import { push } from 'react-router-redux'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import TextField from 'material-ui/TextField'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
|
||||
// - Import actions
|
||||
import * as authorizeActions from 'authorizeActions'
|
||||
|
||||
|
||||
// - Create Login component class
|
||||
export class Login extends Component {
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
emailInput: '',
|
||||
emailInputError: '',
|
||||
passwordInput: '',
|
||||
passwordInputError: '',
|
||||
|
||||
|
||||
}
|
||||
// Binding function to `this`
|
||||
this.handleForm = this.handleForm.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data on input change
|
||||
* @param {event} evt is an event of inputs of element on change
|
||||
*/
|
||||
handleInputChange = (evt) => {
|
||||
const target = evt.target;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
this.setState({
|
||||
[name]: value
|
||||
});
|
||||
|
||||
|
||||
switch (name) {
|
||||
case 'emailInput':
|
||||
this.setState({
|
||||
emailInputError: ''
|
||||
})
|
||||
break
|
||||
case 'passwordInput':
|
||||
this.setState({
|
||||
confirmInputError: '',
|
||||
passwordInputError: ''
|
||||
})
|
||||
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle register form
|
||||
*/
|
||||
handleForm = () => {
|
||||
|
||||
var error = false
|
||||
if (this.state.emailInput === '') {
|
||||
this.setState({
|
||||
emailInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
if (this.state.passwordInput === '') {
|
||||
this.setState({
|
||||
passwordInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
this.props.login(
|
||||
this.state.emailInput,
|
||||
this.state.passwordInput
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const paperStyle = {
|
||||
minHeight: 370,
|
||||
width: 450,
|
||||
margin: 20,
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
margin: "auto"
|
||||
};
|
||||
return (
|
||||
<form>
|
||||
|
||||
<h1 style={{
|
||||
textAlign: "center",
|
||||
padding: "20px",
|
||||
fontSize: "30px",
|
||||
fontWeight: 500,
|
||||
lineHeight: "32px",
|
||||
margin: "auto",
|
||||
color: "rgba(138, 148, 138, 0.2)"
|
||||
}}>Green</h1>
|
||||
|
||||
<div className="animate-bottom">
|
||||
<Paper style={paperStyle} zDepth={1} rounded={false} >
|
||||
<div style={{ padding: "48px 40px 36px" }}>
|
||||
<div style={{
|
||||
paddingLeft: "40px",
|
||||
paddingRight: "40px"
|
||||
}}>
|
||||
|
||||
<h2 style={{
|
||||
textAlign: "left",
|
||||
paddingTop: "16px",
|
||||
fontSize: "24px",
|
||||
fontWeight: 400,
|
||||
lineHeight: "32px",
|
||||
margin: 0
|
||||
}}>Sign in</h2>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.emailInputError}
|
||||
name="emailInput"
|
||||
floatingLabelStyle={{ fontSize: "15px" }}
|
||||
floatingLabelText="Email"
|
||||
type="email"
|
||||
tabIndex={1}
|
||||
/><br />
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.passwordInputError}
|
||||
name="passwordInput"
|
||||
floatingLabelStyle={{ fontSize: "15px" }}
|
||||
floatingLabelText="Password"
|
||||
type="password"
|
||||
tabIndex={2}
|
||||
/><br />
|
||||
<br />
|
||||
<br />
|
||||
<div className="login__button-box">
|
||||
<div>
|
||||
<FlatButton label="Create an account" onClick={this.props.signupPage} tabIndex={4} />
|
||||
</div>
|
||||
<div >
|
||||
<RaisedButton label="Login" primary={true} onClick={this.handleForm} tabIndex={3} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
login: (username, password) => {
|
||||
dispatch(authorizeActions.dbLogin(username, password))
|
||||
},
|
||||
signupPage: () => {
|
||||
dispatch(push("/signup"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Login))
|
||||
216
app/components/Master.jsx
Normal file
216
app/components/Master.jsx
Normal file
@@ -0,0 +1,216 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Route, Switch, NavLink, withRouter, Redirect } from 'react-router-dom'
|
||||
import { firebaseAuth, firebaseRef } from 'app/firebase/'
|
||||
import { push } from 'react-router-redux'
|
||||
import Snackbar from 'material-ui/Snackbar';
|
||||
import LinearProgress from 'material-ui/LinearProgress'
|
||||
|
||||
|
||||
// - Import components
|
||||
import Home from 'Home'
|
||||
import Signup from 'Signup'
|
||||
import Login from 'Login'
|
||||
import Settings from 'Settings'
|
||||
|
||||
import MasterLoading from 'MasterLoading'
|
||||
|
||||
|
||||
// - Import API
|
||||
import { PrivateRoute, PublicRoute } from 'AuthRouterAPI'
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as authorizeActions from 'authorizeActions'
|
||||
import * as imageGalleryActions from 'imageGalleryActions'
|
||||
import * as postActions from 'postActions'
|
||||
import * as commentActions from 'commentActions'
|
||||
import * as voteActions from 'voteActions'
|
||||
import * as userActions from 'userActions'
|
||||
import * as globalActions from 'globalActions'
|
||||
import * as circleActions from 'circleActions'
|
||||
import * as notifyActions from 'notifyActions'
|
||||
|
||||
|
||||
/* ------------------------------------ */
|
||||
|
||||
|
||||
// - Create Master component class
|
||||
export class Master extends Component {
|
||||
|
||||
static isPrivate = true
|
||||
// Constructor
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
authed: false,
|
||||
dataLoaded: false
|
||||
};
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleLoading = this.handleLoading.bind(this)
|
||||
this.handleMessage = this.handleMessage.bind(this)
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Handle click on message
|
||||
handleMessage = (evt) => {
|
||||
this.props.closeMessage()
|
||||
}
|
||||
|
||||
// Handle loading
|
||||
handleLoading = (status) => {
|
||||
this.setState({
|
||||
loading: status,
|
||||
authed: false
|
||||
})
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
|
||||
firebaseAuth().onAuthStateChanged((user) => {
|
||||
|
||||
if (user) {
|
||||
this.props.login(user)
|
||||
this.setState({
|
||||
loading: false
|
||||
})
|
||||
|
||||
if (!this.props.global.defaultLoadDataStatus) {
|
||||
this.props.clearData()
|
||||
this.props.loadData()
|
||||
this.props.defaultDataEnable()
|
||||
}
|
||||
} else {
|
||||
this.props.logout()
|
||||
this.setState({
|
||||
loading: false
|
||||
})
|
||||
if (this.props.global.defaultLoadDataStatus) {
|
||||
this.props.defaultDataDisable()
|
||||
this.props.clearData()
|
||||
}
|
||||
this.props.loadDataGuest()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Render app DOM component
|
||||
*
|
||||
* @returns
|
||||
*
|
||||
* @memberof Master
|
||||
*/
|
||||
render() {
|
||||
|
||||
const {progress, global} = this.props
|
||||
|
||||
return (
|
||||
<div id="master">
|
||||
|
||||
<div className='master__progress' style={{display: (progress.visible ? 'block' : 'none' )}}>
|
||||
<LinearProgress mode="determinate" value={progress.percent} />
|
||||
</div>
|
||||
<div className='master__loading animate-fading2' style={{display: ( global.showTopLoading ? 'flex' : 'none' )}}>
|
||||
<div className='title'> Loading ... </div>
|
||||
</div>
|
||||
<MasterLoading activeLoading={this.state.loading || !(this.props.loaded || this.props.guest)} handleLoading={this.handleLoading} />
|
||||
|
||||
{(!this.state.loading && (this.props.loaded || this.props.guest))
|
||||
?(<Switch>
|
||||
<Route path="/signup" component={Signup} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/login" render={() => {
|
||||
console.log('this.props.authed: ', this.props.authed, "this.props: ", this.props)
|
||||
return (
|
||||
this.props.authed
|
||||
? <Redirect to="/" />
|
||||
: <Login />
|
||||
)
|
||||
}
|
||||
} />
|
||||
<Route render={() =><Home uid={this.props.uid}/>} />
|
||||
|
||||
</Switch>) : ''
|
||||
}
|
||||
<Snackbar
|
||||
open={this.props.global.messageOpen}
|
||||
message={this.props.global.message}
|
||||
autoHideDuration={4000}
|
||||
style={{left: '1%', transform: 'none'}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// - Map dispatch to props
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
|
||||
return {
|
||||
loadData: () => {
|
||||
dispatch(commentActions.dbGetComments())
|
||||
dispatch(imageGalleryActions.downloadForImageGallery())
|
||||
dispatch(postActions.dbGetPosts())
|
||||
dispatch(userActions.dbGetUserInfo())
|
||||
dispatch(voteActions.dbGetVotes())
|
||||
dispatch(notifyActions.dbGetNotifies())
|
||||
dispatch(circleActions.dbGetCircles())
|
||||
|
||||
},
|
||||
clearData: () => {
|
||||
dispatch(imageGalleryActions.clearAllData())
|
||||
dispatch(postActions.clearAllData())
|
||||
dispatch(userActions.clearAllData())
|
||||
dispatch(commentActions.clearAllData())
|
||||
dispatch(voteActions.clearAllvotes())
|
||||
dispatch(notifyActions.clearAllNotifications())
|
||||
dispatch(circleActions.clearAllCircles())
|
||||
|
||||
},
|
||||
login: (user) => {
|
||||
dispatch(authorizeActions.login(user.uid))
|
||||
},
|
||||
logout: () => {
|
||||
dispatch(authorizeActions.logout())
|
||||
},
|
||||
defaultDataDisable: () => {
|
||||
dispatch(globalActions.defaultDataDisable())
|
||||
},
|
||||
defaultDataEnable: () => {
|
||||
dispatch(globalActions.defaultDataEnable())
|
||||
},
|
||||
closeMessage: () => {
|
||||
dispatch(globalActions.hideMessage())
|
||||
},
|
||||
loadDataGuest: () => {
|
||||
dispatch(globalActions.loadDataGuest())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// - Map state to props
|
||||
const mapStateToProps = ({authorize, global, user, post, comment, imageGallery , vote, notify,circle }) => {
|
||||
|
||||
return {
|
||||
guest: authorize.guest,
|
||||
uid: authorize.uid,
|
||||
authed: authorize.authed,
|
||||
progress: global.progress,
|
||||
global: global,
|
||||
loaded: user.loaded && post.loaded && comment.loaded && imageGallery.loaded && vote.loaded && notify.loaded && circle.loaded
|
||||
}
|
||||
|
||||
}
|
||||
// - Connect commponent to redux store
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Master))
|
||||
53
app/components/MasterLoading.jsx
Normal file
53
app/components/MasterLoading.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
import CircularProgress from 'material-ui/CircularProgress'
|
||||
import Dialog from 'material-ui/Dialog'
|
||||
|
||||
// - Import app components
|
||||
|
||||
// - Create MasterLoading component class
|
||||
export default class MasterLoading extends Component {
|
||||
|
||||
// Constructor
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Render app DOM component
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
modal={true}
|
||||
open={this.props.activeLoading}
|
||||
autoDetectWindowHeight={false}
|
||||
overlayStyle={{backgroundColor: "white"}}
|
||||
contentClassName="mLoading__content"
|
||||
bodyStyle={{backgroundColor: ""}}
|
||||
bodyClassName="mLoading__body"
|
||||
>
|
||||
|
||||
<div>
|
||||
<div className="mLoading__context">
|
||||
|
||||
<CircularProgress color="white" size={80} thickness={7} />
|
||||
<h1 style={{float:"right", color:"#fff"}}>Green</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</Dialog>
|
||||
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
139
app/components/Notify.jsx
Normal file
139
app/components/Notify.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover'
|
||||
|
||||
// - Import app components
|
||||
import NotifyItem from 'NotifyItem'
|
||||
// - Import API
|
||||
|
||||
// - Import actions
|
||||
import * as userActions from 'userActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class Notify extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* It will be true if the notification is open
|
||||
*/
|
||||
open: PropTypes.bool,
|
||||
/**
|
||||
* Pass anchor element
|
||||
*/
|
||||
anchorEl: PropTypes.any,
|
||||
/**
|
||||
* Fire to close notificaion
|
||||
*/
|
||||
onRequestClose: PropTypes.func,
|
||||
/**
|
||||
* If user's seen notification box or not (true/false)
|
||||
*/
|
||||
isSeen: PropTypes.bool
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
|
||||
}
|
||||
|
||||
notifyItemList = () => {
|
||||
let { notifications, info, onRequestClose } = this.props
|
||||
let parsedDOM = []
|
||||
if (notifications) {
|
||||
Object.keys(notifications).forEach((key) => {
|
||||
const {notifierUserId} = notifications[key]
|
||||
parsedDOM.push(
|
||||
<NotifyItem
|
||||
key={key}
|
||||
description={(notifications[key] ? notifications[key].description || '' : '')}
|
||||
fullName={(info[notifierUserId] ? info[notifierUserId].fullName || '' : '')}
|
||||
avatar={(info[notifierUserId] ? info[notifierUserId].avatar || '' : '')}
|
||||
id={key}
|
||||
isSeen={(notifications[key] ? notifications[key].isSeen || false : false )}
|
||||
url={(notifications[key] ? notifications[key].url || '' : '')}
|
||||
notifierUserId={notifierUserId}
|
||||
closeNotify={onRequestClose}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
return parsedDOM
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
let { open, anchorEl, onRequestClose } = this.props
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className='homeHeader__notify-menu'
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: 'left', vertical: 'top' }}
|
||||
onRequestClose={onRequestClose}
|
||||
>
|
||||
<div className='container'>
|
||||
<div className='title'>Green </div>
|
||||
<div className='content'>
|
||||
{this.notifyItemList()}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
notifications: state.notify.userNotifies,
|
||||
info: state.user.info
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Notify)
|
||||
155
app/components/NotifyItem.jsx
Normal file
155
app/components/NotifyItem.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { push } from 'react-router-redux'
|
||||
import SvgClose from 'material-ui/svg-icons/navigation/close'
|
||||
import { grey400 } from 'material-ui/styles/colors'
|
||||
|
||||
// - Import app components
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
// - Import API
|
||||
|
||||
// - Import actions
|
||||
import * as notifyActions from 'notifyActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class NotifyItem extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* Notification description
|
||||
*/
|
||||
description: PropTypes.string,
|
||||
/**
|
||||
* Which user relates to the notification item
|
||||
*/
|
||||
fullName: PropTypes.string,
|
||||
/**
|
||||
* Avatar of the user who relate to the notification item
|
||||
*/
|
||||
avatar: PropTypes.string,
|
||||
/**
|
||||
* Notification identifier
|
||||
*/
|
||||
id: PropTypes.string,
|
||||
/**
|
||||
* If user's seen the notification or not (true/false)
|
||||
*/
|
||||
isSeen: PropTypes.bool,
|
||||
/**
|
||||
* Which address notification refers
|
||||
*/
|
||||
url: PropTypes.string,
|
||||
/**
|
||||
* The notifier user identifier
|
||||
*/
|
||||
notifierUserId: PropTypes.string,
|
||||
/**
|
||||
* Close notification popover
|
||||
*/
|
||||
closeNotify: PropTypes.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleSeenNotify = this.handleSeenNotify.bind(this)
|
||||
}
|
||||
|
||||
handleSeenNotify = (evt) => {
|
||||
evt.preventDefault()
|
||||
const { seenNotify, id, url, goTo, isSeen, closeNotify } = this.props
|
||||
if (id) {
|
||||
if (!isSeen)
|
||||
seenNotify(id)
|
||||
|
||||
closeNotify()
|
||||
goTo(url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
let { description, fullName, avatar, isSeen, id, goTo,closeNotify, notifierUserId, url, deleteNotiy } = this.props
|
||||
|
||||
return (
|
||||
|
||||
<div className='item' style={isSeen ? { opacity: 0.6 } : {}} key={id}>
|
||||
<div className='avatar'>
|
||||
<NavLink
|
||||
to={`/${notifierUserId}`}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault()
|
||||
closeNotify()
|
||||
goTo(`/${notifierUserId}`)
|
||||
}}
|
||||
>
|
||||
<UserAvatar fileName={avatar} />
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className='info'>
|
||||
<NavLink to={url} onClick={this.handleSeenNotify}>
|
||||
<div className='user-name'>
|
||||
{fullName}
|
||||
</div>
|
||||
<div className='description'>
|
||||
{description}
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className='close' onClick={() => deleteNotiy(id)}>
|
||||
<SvgClose hoverColor={grey400} style={{ cursor: 'pointer' }} />
|
||||
</div>
|
||||
</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 {
|
||||
goTo: (url) => dispatch(push(url)),
|
||||
seenNotify: (id) => dispatch(notifyActions.dbSeenNotify(id)),
|
||||
deleteNotiy: (id) => dispatch(notifyActions.dbDeleteNotify(id))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NotifyItem)
|
||||
174
app/components/People.jsx
Normal file
174
app/components/People.jsx
Normal file
@@ -0,0 +1,174 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import {withRouter} from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Tabs, Tab } from 'material-ui/Tabs'
|
||||
import { grey50, grey200, grey400, grey600, cyan500 } from 'material-ui/styles/colors'
|
||||
import {push} from 'react-router-redux'
|
||||
|
||||
|
||||
// - Import app components
|
||||
import FindPeople from 'FindPeople'
|
||||
import Following from 'Following'
|
||||
import Followers from 'Followers'
|
||||
import YourCircles from 'YourCircles'
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as circleActions from 'circleActions'
|
||||
import * as globalActions from 'globalActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class People extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {tab} = this.props.match.params
|
||||
switch (tab) {
|
||||
case undefined:
|
||||
case '':
|
||||
this.props.setHeaderTitle('People')
|
||||
break;
|
||||
case 'circles':
|
||||
this.props.setHeaderTitle('Circles')
|
||||
break;
|
||||
case 'followers':
|
||||
this.props.setHeaderTitle('Followers')
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
/**
|
||||
* Component styles
|
||||
*/
|
||||
const styles = {
|
||||
people: {
|
||||
margin: '0 auto',
|
||||
width: '90%'
|
||||
},
|
||||
headline: {
|
||||
fontSize: 24,
|
||||
paddingTop: 16,
|
||||
marginBottom: 12,
|
||||
fontWeight: 400,
|
||||
},
|
||||
slide: {
|
||||
padding: 10,
|
||||
}
|
||||
}
|
||||
|
||||
const {circlesLoaded} = this.props
|
||||
const {tab} = this.props.match.params
|
||||
let tabIndex = 0
|
||||
switch (tab) {
|
||||
case undefined:
|
||||
case '':
|
||||
tabIndex = 0
|
||||
break;
|
||||
case 'circles':
|
||||
tabIndex = 1
|
||||
break;
|
||||
case 'followers':
|
||||
tabIndex = 2
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div style={styles.people}>
|
||||
<Tabs inkBarStyle={{backgroundColor: grey50}} initialSelectedIndex={tabIndex} >
|
||||
<Tab label="Find People" onActive={() => {
|
||||
this.props.goTo('/people')
|
||||
this.props.setHeaderTitle('People')
|
||||
}} >
|
||||
{circlesLoaded ? <FindPeople /> : ''}
|
||||
</Tab>
|
||||
<Tab label="Following" onActive={() => {
|
||||
this.props.goTo('/people/circles')
|
||||
this.props.setHeaderTitle('Circles')
|
||||
}} >
|
||||
{circlesLoaded ? <Following/> : ''}
|
||||
{circlesLoaded ? <YourCircles /> : ''}
|
||||
</Tab>
|
||||
<Tab label="Followers" onActive={() => {
|
||||
this.props.goTo('/people/followers')
|
||||
this.props.setHeaderTitle('Followers')
|
||||
}}>
|
||||
{circlesLoaded ? <Followers /> : ''}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</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 {
|
||||
goTo: (url)=> dispatch(push(url)),
|
||||
setHeaderTitle : (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) => {
|
||||
|
||||
return {
|
||||
uid: state.authorize.uid,
|
||||
circlesLoaded: state.circle.loaded
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(People))
|
||||
495
app/components/Post.jsx
Normal file
495
app/components/Post.jsx
Normal file
@@ -0,0 +1,495 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import {NavLink} from 'react-router-dom'
|
||||
import { push } from 'react-router-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import moment from 'moment'
|
||||
import { createAction as action } from 'redux-actions'
|
||||
import { Card, CardActions, CardHeader, CardMedia, CardTitle, CardText } from 'material-ui/Card'
|
||||
import FloatingActionButton from 'material-ui/FloatingActionButton'
|
||||
import SvgShare from 'material-ui/svg-icons/social/share'
|
||||
import SvgLink from 'material-ui/svg-icons/content/link'
|
||||
import SvgComment from 'material-ui/svg-icons/Communication/comment'
|
||||
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 FlatButton from 'material-ui/FlatButton'
|
||||
import Divider from 'material-ui/Divider'
|
||||
import { grey200, grey400, grey600 } from 'material-ui/styles/colors'
|
||||
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 Dialog from 'material-ui/Dialog'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
|
||||
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'
|
||||
|
||||
// - Create component class
|
||||
export class Post extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* The context of a post
|
||||
*/
|
||||
body: PropTypes.string,
|
||||
/**
|
||||
* The number of comment on a post
|
||||
*/
|
||||
commentCounter:PropTypes.number,
|
||||
/**
|
||||
* Creation post date
|
||||
*/
|
||||
creationDate:PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
]),
|
||||
/**
|
||||
* Post identifier
|
||||
*/
|
||||
id:PropTypes.string,
|
||||
/**
|
||||
* Post image address
|
||||
*/
|
||||
image:PropTypes.string,
|
||||
/**
|
||||
* The last time date when post has was edited
|
||||
*/
|
||||
lastEditDate:PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
]),
|
||||
/**
|
||||
* The name of the user who created the post
|
||||
*/
|
||||
ownerDisplayName:PropTypes.string,
|
||||
/**
|
||||
* The identifier of the user who created the post
|
||||
*/
|
||||
ownerUserId:PropTypes.string,
|
||||
/**
|
||||
* The avatar address of the user who created the post
|
||||
*/
|
||||
ownerAvatar:PropTypes.string,
|
||||
/**
|
||||
* If post is only [0]text, [1]whith picture, ...
|
||||
*/
|
||||
postTypeId:PropTypes.number,
|
||||
/**
|
||||
* The number votes on a post
|
||||
*/
|
||||
score:PropTypes.number,
|
||||
/**
|
||||
* Array of tags on a post
|
||||
*/
|
||||
tags:PropTypes.array,
|
||||
/**
|
||||
* The video address of a post
|
||||
*/
|
||||
video:PropTypes.string,
|
||||
/**
|
||||
* If it's true comment will be disabled on a post
|
||||
*/
|
||||
disableComments:PropTypes.bool,
|
||||
/**
|
||||
* If it's true sharing will be disabled on a post
|
||||
*/
|
||||
disableSharing:PropTypes.bool,
|
||||
/**
|
||||
* The number of users who has visited the post
|
||||
*/
|
||||
viewCount:PropTypes.number
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
// Binding functions to this
|
||||
this.handleReadMore = this.handleReadMore.bind(this)
|
||||
this.getOpenCommentGroup = this.getOpenCommentGroup.bind(this)
|
||||
this.handleVote = this.handleVote.bind(this)
|
||||
this.handleOpenShare = this.handleOpenShare.bind(this)
|
||||
this.handleCloseShare = this.handleCloseShare.bind(this)
|
||||
this.handleCopyLink = this.handleCopyLink.bind(this)
|
||||
this.handleDelete = this.handleDelete.bind(this)
|
||||
this.handleOpenPostWrite = this.handleOpenPostWrite.bind(this)
|
||||
this.handleClosePostWrite = this.handleClosePostWrite.bind(this)
|
||||
this.handleOpenComments = this.handleOpenComments.bind(this)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 = () => {
|
||||
this.setState({
|
||||
shareOpen: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
/**
|
||||
* DOM styles
|
||||
*
|
||||
*
|
||||
* @memberof Post
|
||||
*/
|
||||
const styles = {
|
||||
counter: {
|
||||
lineHeight: '36px',
|
||||
color: '#777',
|
||||
fontSize: '12px',
|
||||
marginRight: '6px'
|
||||
},
|
||||
dialog: {
|
||||
width: '',
|
||||
maxWidth: '530px',
|
||||
borderRadius: "4px"
|
||||
},
|
||||
rightIconMenu: {
|
||||
position: 'absolute',
|
||||
right: 18,
|
||||
top: 8,
|
||||
},
|
||||
iconButton: {
|
||||
width: 24,
|
||||
height: 24
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const RightIconMenu = () => (
|
||||
<IconMenu iconButtonElement={IconButtonElement} style={{ display: "block", position: "absolute", top: "0px", right: "4px" }}>
|
||||
<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, isPostOwner,image, body} = this.props
|
||||
// Define variables
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={<NavLink to={`/${ownerUserId}`}>{ownerDisplayName}</NavLink>}
|
||||
subtitle={moment.unix(creationDate).fromNow() + " | public"}
|
||||
avatar={<NavLink to={`/${ownerUserId}`}><UserAvatar fileName={avatar} size={36} /></NavLink>}
|
||||
>
|
||||
{isPostOwner ? ( <div style={styles.rightIconMenu}><RightIconMenu /></div>) : ''}
|
||||
</CardHeader>
|
||||
{image ? (
|
||||
<CardMedia>
|
||||
<Img fileName={image} />
|
||||
</CardMedia>) : ''}
|
||||
|
||||
|
||||
<CardText>
|
||||
{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>
|
||||
|
||||
))}
|
||||
</CardText>
|
||||
<CardActions>
|
||||
<div style={{ margin: "16px 8px", display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{/*<FloatingActionButton style={{ margin: "0 8px" }} zDepth={1} backgroundColor={grey200} iconStyle={{ color: grey600, fill: grey600, height: "36px", width: "36px" }} zDepth={1} secondary={false}>*/}
|
||||
<div className='g__circle' onClick={this.handleVote}>
|
||||
<Checkbox
|
||||
checkedIcon={<SvgFavorite style={{fill:'#4CAF50'}}/>}
|
||||
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: 'inherit'}}><FloatingActionButton onClick={this.handleOpenComments} style={{ margin: "0 8px" }} zDepth={1} backgroundColor={grey200} iconStyle={{ color: grey600, fill: grey600, height: "36px", width: "36px" }} zDepth={1} secondary={false}>
|
||||
<SvgComment viewBox="0 -9 24 34" style={{ height: "30px", width: "30px" }} /> 3
|
||||
</FloatingActionButton>
|
||||
<div style={styles.counter}>{this.props.commentCount > 0 ? this.props.commentCount : ''} </div></div>) : ''}
|
||||
{!this.props.disableSharing ? (<FloatingActionButton onClick={this.handleOpenShare} style={{ margin: "0 8px" }} zDepth={1} backgroundColor={grey200} iconStyle={{ color: grey600, fill: grey600, height: "36px", width: "36px" }} zDepth={1} secondary={false}>
|
||||
<SvgShare viewBox="0 -9 24 34" style={{ height: "30px", width: "30px" }} />
|
||||
</FloatingActionButton>) : ''}
|
||||
</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} />
|
||||
|
||||
{/* Copy link dialog*/}
|
||||
<Dialog
|
||||
title="Share On"
|
||||
modal={false}
|
||||
open={this.state.shareOpen}
|
||||
onRequestClose={this.handleCloseShare}
|
||||
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
|
||||
contentStyle={styles.dialog}
|
||||
autoDetectWindowHeight={false}
|
||||
actionsContainerStyle={{ borderTop: "1px solid rgb(224, 224, 224)" }}
|
||||
>
|
||||
{!this.state.openCopyLink
|
||||
? (<Paper >
|
||||
<Menu>
|
||||
<MenuItem primaryText="Copy Link" leftIcon={<SvgLink />} onClick={this.handleCopyLink} />
|
||||
</Menu>
|
||||
</Paper>)
|
||||
: <TextField fullWidth={true} id="text-field-default" defaultValue={`${location.origin}/${this.props.ownerUserId}/posts/${this.props.id}`} />
|
||||
}
|
||||
</Dialog>
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
||||
</Card>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || '' : '',
|
||||
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)
|
||||
89
app/components/PostPage.jsx
Normal file
89
app/components/PostPage.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
|
||||
// - Import app components
|
||||
import Blog from 'Blog'
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as postActions from 'postActions'
|
||||
import * as userActions from 'userActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class PostPage extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
componentWillMount() {
|
||||
this.props.loadPost()
|
||||
this.props.loadUserInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Blog posts={this.props.posts} displayWriting={false} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const {userId,postId} = ownProps.match.params
|
||||
return{
|
||||
loadPost: () => dispatch(postActions.dbGetPostById(userId,postId)),
|
||||
loadUserInfo: () => dispatch(userActions.dbGetUserInfoByUserId(userId,'header'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {userId,postId} = ownProps.match.params
|
||||
return{
|
||||
avatar: state.user.info && state.user.info[userId] ? state.user.info[userId].avatar : '',
|
||||
name: state.user.info && state.user.info[userId] ? state.user.info[userId].fullName : '',
|
||||
posts: state.post.userPosts && state.post.userPosts[userId] ? {[postId] : { ...state.post.userPosts[userId][postId]}} : {}
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps,mapDispatchToProps)(PostPage)
|
||||
484
app/components/PostWrite.jsx
Normal file
484
app/components/PostWrite.jsx
Normal file
@@ -0,0 +1,484 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { List, ListItem } from 'material-ui/List'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import { grey400, grey800, darkBlack, lightBlack } from 'material-ui/styles/colors'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import TextField from 'material-ui/TextField'
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import SvgRemoveImage from 'material-ui/svg-icons/content/remove-circle'
|
||||
import SvgCamera from 'material-ui/svg-icons/image/photo-camera'
|
||||
import IconMenu from 'material-ui/IconMenu'
|
||||
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
|
||||
|
||||
|
||||
|
||||
// - Import app components
|
||||
import ImageGallery from 'ImageGallery'
|
||||
import Img from 'Img'
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
// - Import API
|
||||
import * as AuthAPI from 'AuthAPI'
|
||||
import * as PostAPI from 'PostAPI'
|
||||
|
||||
// - Import actions
|
||||
import * as imageGalleryActions from 'imageGalleryActions'
|
||||
import * as postActions from 'postActions'
|
||||
|
||||
// - Create PostWrite component class
|
||||
export class PostWrite extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* If it's true post writing page will be open
|
||||
*/
|
||||
open: PropTypes.bool,
|
||||
/**
|
||||
* Recieve request close function
|
||||
*/
|
||||
onRequestClose: PropTypes.func,
|
||||
/**
|
||||
* Post write style
|
||||
*/
|
||||
style: PropTypes.object,
|
||||
/**
|
||||
* If it's true, post will be in edit view
|
||||
*/
|
||||
edit: PropTypes.bool.isRequired,
|
||||
/**
|
||||
* The text of post in editing state
|
||||
*/
|
||||
text: PropTypes.string,
|
||||
/**
|
||||
* The image of post in editing state
|
||||
*/
|
||||
image: PropTypes.string,
|
||||
/**
|
||||
* If post state is editing this id sould be filled with post identifier
|
||||
*/
|
||||
id: PropTypes.string
|
||||
|
||||
}
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
|
||||
|
||||
|
||||
super(props)
|
||||
|
||||
// Default state
|
||||
this.state = {
|
||||
/**
|
||||
* Post text
|
||||
*/
|
||||
postText: this.props.edit ? this.props.text : '',
|
||||
/**
|
||||
* The image of the post
|
||||
*/
|
||||
image: this.props.edit ? this.props.image : '',
|
||||
/**
|
||||
* If it's true gallery will be open
|
||||
*/
|
||||
galleryOpen: false,
|
||||
/**
|
||||
* If it's true post button will be disabled
|
||||
*/
|
||||
disabledPost: true,
|
||||
/**
|
||||
* If it's true comment will be disabled on post
|
||||
*/
|
||||
disableComments: this.props.edit ? this.props.disableComments : false,
|
||||
/**
|
||||
* If it's true share will be disabled on post
|
||||
*/
|
||||
disableSharing: this.props.edit ? this.props.disableSharing : false,
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleOnChange = this.handleOnChange.bind(this)
|
||||
this.handleCloseGallery = this.handleCloseGallery.bind(this)
|
||||
this.handleOpenGallery = this.handleOpenGallery.bind(this)
|
||||
this.onRequestSetImage = this.onRequestSetImage.bind(this)
|
||||
this.handlePost = this.handlePost.bind(this)
|
||||
this.handleRemoveImage = this.handleRemoveImage.bind(this)
|
||||
this.handleToggleComments = this.handleToggleComments.bind(this)
|
||||
this.handleToggleSharing = this.handleToggleSharing.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle comments of the post to disable/enable
|
||||
*
|
||||
*
|
||||
* @memberof PostWrite
|
||||
*/
|
||||
handleToggleComments = () => {
|
||||
this.setState({
|
||||
disableComments: !this.state.disableComments,
|
||||
disabledPost: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle sharing of the post to disable/enable
|
||||
*
|
||||
*
|
||||
* @memberof PostWrite
|
||||
*/
|
||||
handleToggleSharing = () => {
|
||||
this.setState({
|
||||
disableSharing: !this.state.disableSharing,
|
||||
disabledPost: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Romove the image of post
|
||||
*
|
||||
*
|
||||
* @memberof PostWrite
|
||||
*/
|
||||
handleRemoveImage = () => {
|
||||
this.setState({
|
||||
image: '',
|
||||
disabledPost: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle send post to the server
|
||||
* @param {event} evt passed by clicking on the post button
|
||||
*/
|
||||
handlePost = (evt) => {
|
||||
|
||||
var image = this.state.image
|
||||
var tags = PostAPI.getContentTags(this.state.postText)
|
||||
|
||||
// In edit status we should fire update if not we should fire post function
|
||||
if (!this.props.edit) {
|
||||
if (image !== '') {
|
||||
this.props.post({
|
||||
body: this.state.postText,
|
||||
tags: tags,
|
||||
image: image,
|
||||
avatar: this.props.avatar,
|
||||
name: this.props.name,
|
||||
disableComments: this.state.disableComments,
|
||||
disableSharing: this.state.disableSharing
|
||||
}, this.props.onRequestClose)
|
||||
}
|
||||
else {
|
||||
this.props.post({
|
||||
body: this.state.postText,
|
||||
tags: tags,
|
||||
avatar: this.props.avatar,
|
||||
name: this.props.name,
|
||||
disableComments: this.state.disableComments,
|
||||
disableSharing: this.state.disableSharing
|
||||
}, this.props.onRequestClose)
|
||||
}
|
||||
}
|
||||
// In edit status we pass post to update functions
|
||||
else {
|
||||
this.props.update({
|
||||
id: this.props.id,
|
||||
body: this.state.postText,
|
||||
tags: tags,
|
||||
image: image,
|
||||
disableComments: this.state.disableComments,
|
||||
disableSharing: this.state.disableSharing
|
||||
}, this.props.onRequestClose)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set post image url
|
||||
*/
|
||||
onRequestSetImage = (url) => {
|
||||
this.setState({
|
||||
image: url,
|
||||
disabledPost: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* When the post text changed
|
||||
* @param {event} evt is an event passed by change post text callback funciton
|
||||
* @param {string} data is the post content which user writes
|
||||
*/
|
||||
handleOnChange = (evt, data) => {
|
||||
this.setState({ postText: data })
|
||||
if (data.length === 0 || data.trim() === '' || (this.props.edit && data.trim() === this.props.text)) {
|
||||
this.setState({
|
||||
postText: data,
|
||||
disabledPost: true
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
postText: data,
|
||||
disabledPost: false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Close image gallery
|
||||
*/
|
||||
handleCloseGallery = () => {
|
||||
this.setState({
|
||||
galleryOpen: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open image gallery
|
||||
*/
|
||||
handleOpenGallery = () => {
|
||||
this.setState({
|
||||
galleryOpen: true
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if(!nextProps.open){
|
||||
this.setState({
|
||||
/**
|
||||
* Post text
|
||||
*/
|
||||
postText: this.props.edit ? this.props.text : '',
|
||||
/**
|
||||
* The image of the post
|
||||
*/
|
||||
image: this.props.edit ? this.props.image : '',
|
||||
/**
|
||||
* If it's true gallery will be open
|
||||
*/
|
||||
galleryOpen: false,
|
||||
/**
|
||||
* If it's true post button will be disabled
|
||||
*/
|
||||
disabledPost: true,
|
||||
/**
|
||||
* If it's true comment will be disabled on post
|
||||
*/
|
||||
disableComments: this.props.edit ? this.props.disableComments : false,
|
||||
/**
|
||||
* If it's true share will be disabled on post
|
||||
*/
|
||||
disableSharing: this.props.edit ? this.props.disableSharing : false,
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const iconButtonElement = (
|
||||
<IconButton
|
||||
touch={true}
|
||||
tooltip="more"
|
||||
tooltipPosition="bottom-left"
|
||||
>
|
||||
<MoreVertIcon color={grey400} />
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
const rightIconMenu = (
|
||||
<IconMenu iconButtonElement={iconButtonElement}>
|
||||
<MenuItem onClick={this.handleToggleComments} style={{ fontSize: "14px" }}>{!this.state.disableComments ? 'Disable comments' : 'Enable comments'} </MenuItem>
|
||||
<MenuItem onClick={this.handleToggleSharing} style={{ fontSize: "14px" }}>{!this.state.disableSharing ? 'Disable sharing' : 'Enable sharing'}</MenuItem>
|
||||
</IconMenu>
|
||||
)
|
||||
var postAvatar = <UserAvatar fileName={this.props.avatar} style={{ top: "8px" }} size={40} />
|
||||
|
||||
var author = (
|
||||
<div>
|
||||
<span style={{
|
||||
fontSize: "14px",
|
||||
paddingRight: "10px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(0,0,0,0.87)",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
paddingLeft: "50px",
|
||||
lineHeight: "25px"
|
||||
}}>{this.props.name}</span><span style={{
|
||||
fontWeight: 100,
|
||||
fontSize: "10px"
|
||||
}}> | Public</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const writeActions = [
|
||||
<FlatButton
|
||||
label="Cancel"
|
||||
primary={true}
|
||||
keyboardFocused={false}
|
||||
onTouchTap={this.props.onRequestClose}
|
||||
style={{ color: grey800 }}
|
||||
/>,
|
||||
<FlatButton
|
||||
label={this.props.edit ? 'UPDATE' : 'POST'}
|
||||
primary={true}
|
||||
keyboardFocused={false}
|
||||
onTouchTap={this.handlePost}
|
||||
disabled={this.state.disabledPost}
|
||||
/>
|
||||
]
|
||||
|
||||
const galleryActions = [
|
||||
<FlatButton
|
||||
label="Cancel"
|
||||
primary={true}
|
||||
keyboardFocused={false}
|
||||
onTouchTap={this.handleCloseGallery}
|
||||
style={{ color: grey800 }}
|
||||
/>
|
||||
]
|
||||
|
||||
const styles = {
|
||||
dialog: {
|
||||
width: '',
|
||||
maxWidth: '530px',
|
||||
borderRadius: "4px"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
{this.props.children}
|
||||
<Dialog
|
||||
id= {this.props.id || 0}
|
||||
actions={writeActions}
|
||||
modal={false}
|
||||
open={this.props.open}
|
||||
contentStyle={styles.dialog}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
autoDetectWindowHeight={false}
|
||||
actionsContainerStyle={{ borderTop: "1px solid rgb(224, 224, 224)" }}
|
||||
|
||||
>
|
||||
|
||||
<ListItem
|
||||
disabled={true}
|
||||
|
||||
leftAvatar={postAvatar}
|
||||
rightIconButton={rightIconMenu}
|
||||
primaryText={author}
|
||||
style={{ padding: "16px 4px 30px 16px" }}
|
||||
/>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexGrow: 1, overflow: "hidden" }}>
|
||||
<div style={{ position: "relative", flexDirection: "column", display: "flex", flexGrow: 1, overflow: "hidden", overflowY: "auto", maxHeight: "300px" }}>
|
||||
<TextField
|
||||
value={this.state.postText}
|
||||
onChange={this.handleOnChange}
|
||||
hintText="What is new with you?"
|
||||
underlineShow={false}
|
||||
multiLine={true}
|
||||
rows={2}
|
||||
hintStyle={{ fontWeight: 200, fontSize: "14px" }}
|
||||
textareaStyle={{ fontWeight: 200, fontSize: "14px" }}
|
||||
style={{ margin: "0 16px", flexShrink: 0, width: "initial", flexGrow: 1 }}
|
||||
|
||||
/>
|
||||
|
||||
{(this.state.image && this.state.image !== '')
|
||||
? (<div>
|
||||
<div style={{ position: "relative", overflowY: "hidden", overflowX: "auto" }}>
|
||||
<ul style={{ position: "relative", whiteSpace: "nowrap", padding: "0 0 0 16px", margin: "8px 0 0 0", paddingRight: "16px", verticalAlign: "bottom", flexShrink: 0, listStyleType: "none" }}>
|
||||
<div style={{ display: "flex", position: "relative" }}>
|
||||
<span onClick={this.handleRemoveImage} style={{
|
||||
position: "absolute", width: "28px", backgroundColor: "rgba(255, 255, 255, 0.22)",
|
||||
height: "28px", right: 12, top: 4, cursor: "pointer", borderRadius: "50%",
|
||||
display: "flex", alignItems: "center", justifyContent: "center"
|
||||
}}>
|
||||
<SvgRemoveImage hoverColor="rgba(0, 0, 0, 0.65)" style={{ color: "rgba(0, 0, 0, 0.53)" }} />
|
||||
</span>
|
||||
|
||||
<div style={{ display: "inline-block", width: "100%", marginRight: "8px", transition: "transform .25s" }}>
|
||||
<li style={{ width: "100%", margin: 0, verticalAlign: "bottom", position: "static" }}>
|
||||
<Img fileName={this.state.image} style={{ width: "100%", height: "auto" }} />
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>) : ''}
|
||||
</div>
|
||||
<div style={{ flexShrink: 0, boxFlex: 0, flexGrow: 0, maxHeight: "48px", width: "100%" }}>
|
||||
<div style={{ flexDirection: "row", display: "flex" }}>
|
||||
<div onClick={this.handleOpenGallery} style={{ outline: "none", width: "48px", zIndex: 0, overflow: "hidden", position: "relative", textAlign: "center", transition: "background .3s", border: 0, borderRadius: "50%", display: "inlineBlock", height: "48px" }}>
|
||||
<span style={{ top: "15px", display: "block", position: "relative", cursor: "pointer" }}>
|
||||
<SvgCamera color="grey" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
actions={galleryActions}
|
||||
modal={false}
|
||||
open={this.state.galleryOpen}
|
||||
contentStyle={styles.dialog}
|
||||
onRequestClose={this.handleCloseGallery}
|
||||
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
|
||||
autoDetectWindowHeight={false}
|
||||
|
||||
>
|
||||
<ImageGallery set={this.onRequestSetImage} close={this.handleCloseGallery} />
|
||||
</Dialog>
|
||||
|
||||
</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 {
|
||||
post: (post, callBack) => dispatch(postActions.dbAddImagePost(post, callBack)),
|
||||
update: (post,callBack) => dispatch(postActions.dbUpdatePost(post, callBack))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
postImageState: state.imageGallery.status,
|
||||
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : '',
|
||||
name: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].fullName : ''
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostWrite)
|
||||
165
app/components/Profile.jsx
Normal file
165
app/components/Profile.jsx
Normal file
@@ -0,0 +1,165 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import Dialog from 'material-ui/Dialog'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
|
||||
// - Import app components
|
||||
import ProfileHead from 'ProfileHead'
|
||||
import Blog from 'Blog'
|
||||
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as postActions from 'postActions'
|
||||
import * as userActions from 'userActions'
|
||||
import * as globalActions from 'globalActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class Profile extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.loadPosts()
|
||||
this.props.loadUserInfo()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
/**
|
||||
* Component styles
|
||||
*/
|
||||
const styles = {
|
||||
profile: {
|
||||
margin: '0 auto',
|
||||
width: '90%'
|
||||
},
|
||||
header: {
|
||||
|
||||
|
||||
},
|
||||
content: {
|
||||
|
||||
},
|
||||
showcover: {
|
||||
height: '450px'
|
||||
},
|
||||
avatar: {
|
||||
border: '2px solid rgb(255, 255, 255)'
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Actions for material ui dialog
|
||||
*/
|
||||
const actions = [
|
||||
<FlatButton
|
||||
label="Cancel"
|
||||
primary={true}
|
||||
onTouchTap={this.handleClose}
|
||||
/>,
|
||||
<FlatButton
|
||||
label="Submit"
|
||||
primary={true}
|
||||
keyboardFocused={true}
|
||||
onTouchTap={this.handleClose}
|
||||
/>
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={styles.profile}>
|
||||
<div style={styles.header}>
|
||||
|
||||
<ProfileHead avatar={this.props.avatar} isAuthedUser={this.props.isAuthedUser} banner={this.props.banner} fullName={this.props.name} followerCount={0} userId={this.props.userId}/>
|
||||
</div>
|
||||
{this.props.posts && Object.keys(this.props.posts).length !==0
|
||||
? (<div style={styles.content}>
|
||||
<div className='profile__title'>
|
||||
{this.props.name}'s posts
|
||||
</div>
|
||||
<div style={{ height: '24px' }}></div>
|
||||
|
||||
<Blog posts={this.props.posts} displayWriting={false} />
|
||||
</div>)
|
||||
: (<div className='profile__title'>
|
||||
Nothing shared
|
||||
</div>)
|
||||
}
|
||||
|
||||
</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) => {
|
||||
const { userId } = ownProps.match.params
|
||||
return {
|
||||
loadPosts: () => dispatch(postActions.dbGetPostsByUserId(userId)),
|
||||
loadUserInfo: () => dispatch(userActions.dbGetUserInfoByUserId(userId, 'header'))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { userId } = ownProps.match.params
|
||||
const {uid} = state.authorize
|
||||
return {
|
||||
avatar: state.user.info && state.user.info[userId] ? state.user.info[userId].avatar || '' : '',
|
||||
name: state.user.info && state.user.info[userId] ? state.user.info[userId].fullName || '' : '',
|
||||
banner: state.user.info && state.user.info[userId] ? state.user.info[userId].banner || '' : '',
|
||||
posts: state.post.userPosts ? state.post.userPosts[userId] : {},
|
||||
isAuthedUser: userId === uid,
|
||||
userId
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Profile)
|
||||
246
app/components/ProfileHead.jsx
Normal file
246
app/components/ProfileHead.jsx
Normal file
@@ -0,0 +1,246 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
|
||||
import IconButton from 'material-ui/IconButton'
|
||||
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
|
||||
import IconMenu from 'material-ui/IconMenu'
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
import EventListener, { withOptions } from 'react-event-listener'
|
||||
import { Parallax, Background } from 'react-parallax'
|
||||
|
||||
|
||||
// - Import app components
|
||||
import ImgCover from 'ImgCover'
|
||||
import EditProfile from 'EditProfile'
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as globalActions from 'globalActions'
|
||||
import * as userActions from 'userActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class ProfileHead extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* User avatar address
|
||||
*/
|
||||
avatar: PropTypes.string,
|
||||
/**
|
||||
* User avatar address
|
||||
*/
|
||||
banner: PropTypes.string,
|
||||
/**
|
||||
* User full name
|
||||
*/
|
||||
fullName: PropTypes.string.isRequired,
|
||||
/**
|
||||
* The number of followers
|
||||
*/
|
||||
followerCount: PropTypes.number,
|
||||
/**
|
||||
* User identifier
|
||||
*/
|
||||
userId: PropTypes.string,
|
||||
/**
|
||||
* If the user profile identifier of param is equal to the user authed identifier
|
||||
*/
|
||||
isAuthedUser: PropTypes.bool
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
/**
|
||||
* Defaul state
|
||||
*/
|
||||
this.state = {
|
||||
/**
|
||||
* If it's true , the window is in small size
|
||||
*/
|
||||
isSmall: false,
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
/**
|
||||
* Handle resize event for window to change sidebar status
|
||||
* @param {event} evt is the event is passed by winodw resize event
|
||||
*/
|
||||
handleResize = (evt) => {
|
||||
|
||||
// Set initial state
|
||||
var width = window.innerWidth
|
||||
|
||||
if (width > 900) {
|
||||
this.setState({
|
||||
isSmall: false
|
||||
})
|
||||
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
isSmall: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
componentDidMount = () => {
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const styles = {
|
||||
avatar: {
|
||||
border: '2px solid rgb(255, 255, 255)'
|
||||
},
|
||||
iconButton: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
height: '24px',
|
||||
width: '24px',
|
||||
|
||||
},
|
||||
iconButtonSmall: {
|
||||
fill: 'rgb(0, 0, 0)',
|
||||
height: '24px',
|
||||
width: '24px',
|
||||
},
|
||||
|
||||
editButton: {
|
||||
|
||||
marginLeft: '20px'
|
||||
|
||||
},
|
||||
editButtonSmall: {
|
||||
|
||||
marginLeft: '20px',
|
||||
color: 'white',
|
||||
fill: 'blue'
|
||||
|
||||
},
|
||||
aboutButton: {
|
||||
color: 'white'
|
||||
},
|
||||
aboutButtonSmall: {
|
||||
color: 'black'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const iconButtonElement = (
|
||||
<IconButton style={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton} iconStyle={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton}
|
||||
touch={true}
|
||||
>
|
||||
<MoreVertIcon color={grey400} viewBox='10 0 24 24' />
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
|
||||
const RightIconMenu = () => (
|
||||
<IconMenu iconButtonElement={iconButtonElement}>
|
||||
<MenuItem style={{ fontSize: "14px" }}>Reply</MenuItem>
|
||||
<MenuItem style={{ fontSize: "14px" }}>Edit</MenuItem>
|
||||
<MenuItem style={{ fontSize: "14px" }}>Delete</MenuItem>
|
||||
</IconMenu>
|
||||
)
|
||||
|
||||
|
||||
const {isAuthedUser} = this.props
|
||||
|
||||
return (
|
||||
|
||||
<div>
|
||||
<Parallax strength={500} className="profile__parallax" bgStyle={{ position: 'relative' }}>
|
||||
<Background>
|
||||
<ImgCover width='100%' height='510px' borderRadius='2px' fileName={this.props.banner || '13e1fc2f-0484-42ef-a61f-75f4b5b20a37.jpeg'} />
|
||||
</Background>
|
||||
|
||||
</Parallax>
|
||||
<div className={this.state.isSmall ? 'profile__head-info-s' : 'profile__head-info'}>
|
||||
<EventListener
|
||||
target="window"
|
||||
onResize={this.handleResize}
|
||||
/>
|
||||
<div className='left'>
|
||||
{/* User avatar*/}
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}><UserAvatar fileName={this.props.avatar} size={60} style={styles.avatar} /></div>
|
||||
<div className='info'>
|
||||
<div className='fullName'>
|
||||
{this.props.fullName}
|
||||
</div>
|
||||
{/*<div className='followers'>
|
||||
{this.props.followerCount} Followers
|
||||
</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
<div className='right'>
|
||||
{isAuthedUser ? (<div style={this.state.isSmall ? styles.editButtonSmall : styles.editButton}><RaisedButton label="EDIT PROFILE" onClick={this.props.openEditor} /></div>) : ''}
|
||||
</div>
|
||||
</div>
|
||||
{isAuthedUser ? (<EditProfile
|
||||
onRequestClose={this.handleCloseEditor}
|
||||
avatar={this.props.avatar}
|
||||
banner={this.props.banner}
|
||||
fullName={this.props.fullName}
|
||||
/>): ''}
|
||||
</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 {
|
||||
openEditor: () => dispatch(userActions.openEditProfile())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
|
||||
return {
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProfileHead)
|
||||
223
app/components/Settings.jsx
Normal file
223
app/components/Settings.jsx
Normal file
@@ -0,0 +1,223 @@
|
||||
// - Import external components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { NavLink, withRouter } from 'react-router-dom'
|
||||
import { push } from 'react-router-redux'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import TextField from 'material-ui/TextField'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
|
||||
// - Import actions
|
||||
import * as authorizeActions from 'authorizeActions'
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*
|
||||
* @export
|
||||
* @class Settings
|
||||
* @extends {Component}
|
||||
*/
|
||||
export class Settings extends Component {
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
passwordInput: '',
|
||||
passwordInputError: '',
|
||||
confirmInput: '',
|
||||
confirmInputError: '',
|
||||
|
||||
|
||||
}
|
||||
// Binding function to `this`
|
||||
this.handleForm = this.handleForm.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data on input change
|
||||
* @param {event} evt is an event of inputs of element on change
|
||||
*/
|
||||
handleInputChange = (evt) => {
|
||||
const target = evt.target;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
this.setState({
|
||||
[name]: value
|
||||
});
|
||||
|
||||
|
||||
switch (name) {
|
||||
case 'passwordInput':
|
||||
this.setState({
|
||||
passwordInputError: ''
|
||||
})
|
||||
break
|
||||
case 'confirmInput':
|
||||
this.setState({
|
||||
confirmInputError: '',
|
||||
passwordInputError: ''
|
||||
})
|
||||
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle register form
|
||||
*/
|
||||
handleForm = () => {
|
||||
|
||||
var error = false
|
||||
if (this.state.passwordInput === '') {
|
||||
this.setState({
|
||||
passwordInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
else if (this.state.confirmInput === '') {
|
||||
this.setState({
|
||||
confirmInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
else if(this.state.confirmInput !== this.state.passwordInput) {
|
||||
this.setState({
|
||||
confirmInputError: 'Password and confirm password should be equal!'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
this.props.login(
|
||||
this.state.passwordInput,
|
||||
this.state.confirmInput
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const paperStyle = {
|
||||
minHeight: 370,
|
||||
width: 450,
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
margin: "auto"
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
|
||||
<h1 style={{
|
||||
textAlign: "center",
|
||||
padding: "20px",
|
||||
fontSize: "30px",
|
||||
fontWeight: 500,
|
||||
lineHeight: "32px",
|
||||
margin: "auto",
|
||||
color: "rgba(138, 148, 138, 0.2)"
|
||||
}}>Green</h1>
|
||||
|
||||
<div className="animate-bottom">
|
||||
<Paper style={paperStyle} zDepth={1} rounded={false} >
|
||||
<div style={{ padding: "48px 40px 36px" }}>
|
||||
<div style={{
|
||||
paddingLeft: "40px",
|
||||
paddingRight: "40px"
|
||||
}}>
|
||||
|
||||
<h2 style={{
|
||||
textAlign: "left",
|
||||
paddingTop: "16px",
|
||||
fontSize: "24px",
|
||||
fontWeight: 400,
|
||||
lineHeight: "32px",
|
||||
margin: 0
|
||||
}}>Change Password</h2>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.passwordInputError}
|
||||
name="passwordInput"
|
||||
floatingLabelStyle={{ fontSize: "15px" }}
|
||||
floatingLabelText="New password"
|
||||
type="password"
|
||||
/><br />
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.confirmInputError}
|
||||
name="confirmInput"
|
||||
floatingLabelStyle={{ fontSize: "15px" }}
|
||||
floatingLabelText="Confirm password"
|
||||
type="password"
|
||||
/><br />
|
||||
<br />
|
||||
<br />
|
||||
<div className="settings__button-box">
|
||||
<div>
|
||||
<FlatButton label="Home" onClick={this.props.homePage} />
|
||||
</div>
|
||||
<div>
|
||||
<RaisedButton label="Change password" primary={true} onClick={this.handleForm} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</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 {
|
||||
login: (password) => {
|
||||
dispatch(authorizeActions.dbUpdatePassword(password))
|
||||
},
|
||||
homePage: () => {
|
||||
dispatch(push("/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Settings))
|
||||
296
app/components/Sidebar.jsx
Normal file
296
app/components/Sidebar.jsx
Normal file
@@ -0,0 +1,296 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import EventListener, { withOptions } from 'react-event-listener'
|
||||
import keycode from 'keycode'
|
||||
|
||||
// - Import app components
|
||||
|
||||
|
||||
// - Import API
|
||||
import * as AuthAPI from 'AuthAPI'
|
||||
|
||||
// - Import actions
|
||||
import * as authorizeActions from 'authorizeActions'
|
||||
import * as globalActions from 'globalActions'
|
||||
|
||||
// - Feilds
|
||||
const color = 'teal';
|
||||
const colorKey = 'blue';
|
||||
const sizeCondition = (width) => (width >= 750)
|
||||
|
||||
|
||||
|
||||
// - Create Sidebar component class
|
||||
export class Sidebar extends Component {
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
// Default state
|
||||
this.state = {
|
||||
sidebarClass: "",
|
||||
overlay: false,
|
||||
mainStyle: { marginLeft: "210px" },
|
||||
// Is sidebar open or not
|
||||
open: true,
|
||||
// If sidebar is closed by resizing or not
|
||||
auto: false,
|
||||
// If overlay should be open or not
|
||||
overlayOpen: false,
|
||||
// If side bar should be closed
|
||||
shouldBeClosed: false,
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleLogout = this.handleLogout.bind(this)
|
||||
this.open = this.open.bind(this)
|
||||
this.getChildren = this.getChildren.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle open sidebar
|
||||
* @param {boolean} status if is true, sidebar will be open
|
||||
* @param {string} source is the element that fired the function
|
||||
*/
|
||||
open = (status, source) => {
|
||||
|
||||
const width = window.innerWidth
|
||||
|
||||
if (status) {
|
||||
// Sidebar style when it's open
|
||||
const openStyle = {
|
||||
width: "210px",
|
||||
transform: "translate(0px, 0px)",
|
||||
transition: "transform 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms"
|
||||
}
|
||||
this.setState({
|
||||
open: true,
|
||||
mainStyle: { marginLeft: "210px" },
|
||||
sidebarStyle: openStyle,
|
||||
sidebarClass: (sizeCondition(width)) ? "sidebar sidebar__large" : "sidebar sidebar__over",
|
||||
overlay: (sizeCondition(width)) ? false : true
|
||||
|
||||
|
||||
})
|
||||
|
||||
if (sizeCondition(width)) {
|
||||
this.setState({
|
||||
auto: false,
|
||||
shouldBeClosed: false
|
||||
})
|
||||
|
||||
} else {
|
||||
this.setState({
|
||||
overlayOpen: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function fired to determine sidebar and overlay sidebar status
|
||||
* @param {boolean} if true, the sidebar is open
|
||||
*/
|
||||
this.props.status(true)
|
||||
|
||||
|
||||
}
|
||||
// If it's false sidebar should be closed
|
||||
else {
|
||||
// Sidebar style when it's closed
|
||||
const closeStyle = {
|
||||
transform: "translate(-100%, 0px)",
|
||||
transition: "transform 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms"
|
||||
}
|
||||
this.setState({
|
||||
open: false,
|
||||
mainStyle: { marginLeft: "0px" },
|
||||
sidebarStyle: closeStyle,
|
||||
sidebarClass: (sizeCondition(width)) ? "sidebar sidebar__large"
|
||||
: ((source === 'auto') ? "sidebar " : "sidebar sidebar__over"),
|
||||
overlay: false
|
||||
|
||||
})
|
||||
|
||||
switch (source) {
|
||||
case 'auto':
|
||||
this.setState({
|
||||
auto: true
|
||||
})
|
||||
break;
|
||||
case 'overlay':
|
||||
this.setState({
|
||||
shouldBeClosed: true
|
||||
})
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
if (sizeCondition(width)) {
|
||||
// TODO: Get ride of this
|
||||
} else {
|
||||
this.setState({
|
||||
overlayOpen: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function fired to determine sidebar and overlay sidebar status
|
||||
* @param {boolean} if true, the sidebar is open
|
||||
*/
|
||||
this.props.status(false)
|
||||
|
||||
}
|
||||
this.props.overlay((sizeCondition(width)) ? false : true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resize event for window to change sidebar status
|
||||
* @param {event} evt is the event is passed by winodw resize event
|
||||
*/
|
||||
handleResize = (evt) => {
|
||||
|
||||
// Set initial state
|
||||
var width = window.innerWidth
|
||||
|
||||
if (sizeCondition(width)) {
|
||||
|
||||
this.setState({
|
||||
sidebarClass: "sidebar sidebar__large",
|
||||
overlay: false,
|
||||
overlayOpen: false
|
||||
})
|
||||
|
||||
this.props.overlay(false)
|
||||
if (this.state.auto && !this.state.shouldBeClosed) {
|
||||
this.open(true)
|
||||
this.setState({ auto: false })
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!this.state.overlayOpen) {
|
||||
if (!this.state.auto && this.state.open) {
|
||||
this.open(false, 'auto')
|
||||
|
||||
}else{
|
||||
this.setState({
|
||||
overlayOpen:true,
|
||||
overlay:true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.setState({ sidebarClass: "sidebar sidebar__over", overlay: true })
|
||||
this.props.overlay(true)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle logout user
|
||||
*/
|
||||
handleLogout = () => {
|
||||
var { dispatch } = this.props
|
||||
dispatch(authorizeActions.dbLogout())
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle keyup event for window to close sidebar
|
||||
* @param {event} evt is the event is passed by winodw key event
|
||||
*/
|
||||
handleKeyUp = (evt) => {
|
||||
if (this.state.overlayOpen) {
|
||||
if (this.state.open && keycode(event) === 'esc') {
|
||||
this.open(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
this.props.open(this.open)
|
||||
}
|
||||
|
||||
getChildren = () => {
|
||||
return React.Children.map(this.props.children, (childe) => {
|
||||
if (childe.type.qcName === 'SidebarContent') {
|
||||
const sideBarContent = React.cloneElement(childe, {
|
||||
className: this.state.sidebarClass,
|
||||
cstyle: this.state.sidebarStyle,
|
||||
sidebar: this.open,
|
||||
overlay: this.state.overlay
|
||||
})
|
||||
return sideBarContent
|
||||
}
|
||||
else if (childe.type.qcName === 'SidebarMain') {
|
||||
return React.cloneElement(childe, { cstyle: this.state.mainStyle })
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div id='sidebar'>
|
||||
<EventListener
|
||||
target="window"
|
||||
onResize={this.handleResize}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
/>
|
||||
{this.getChildren()}
|
||||
|
||||
</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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar)
|
||||
54
app/components/SidebarContent.jsx
Normal file
54
app/components/SidebarContent.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
|
||||
// - Import components
|
||||
|
||||
// - Import actions
|
||||
|
||||
// - Create component class
|
||||
export default class SidebarContent extends Component {
|
||||
|
||||
static qcName= 'SidebarContent'
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
// Default state
|
||||
this.state = {}
|
||||
|
||||
// Binding function to `this`
|
||||
this.handleClickCover = this.handleClickCover.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click on cover of sidebar
|
||||
* @param {event} evt is a click event passed to funciton
|
||||
*/
|
||||
handleClickCover = (evt) => {
|
||||
this.props.sidebar(false,'overlay')
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
let showCoverStyle = {position: "fixed", height: "100%", width: "100%", top: "0px", left: "0px", opacity: "1", backgroundColor: "rgba(255, 255, 255, 0.54)", WebkitTapHighlightColor: "rgba(0, 0, 0, 0)", willChange: "opacity", transform: "translateZ(0px)", transition: "left 0ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, opacity 400ms cubic-bezier(0.23, 1, 0.32, 1) 0ms", zIndex: "1111", pointerEvents: "auto"}
|
||||
let hideCoverStyle = {position: "fixed", height: "100%", width: "100%", top: "0px", left: "-100%", opacity: "0", backgroundColor: "rgba(255, 255, 255, 0.54)", WebkitTapHighlightColor: "rgba(0, 0, 0, 0)", willChange: "opacity", transform: "translateZ(0px)", transition: "left 0ms cubic-bezier(0.23, 1, 0.32, 1) 400ms, opacity 400ms cubic-bezier(0.23, 1, 0.32, 1) 0ms", zIndex: "1111", pointerEvents: "none"}
|
||||
return (
|
||||
<div id='sidebar-content'>
|
||||
<div style={this.props.overlay ? showCoverStyle : hideCoverStyle} style={{overflow:'hidden'}} onClick={this.handleClickCover}></div>
|
||||
<div className={this.props.className} style={this.props.cstyle}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
36
app/components/SidebarMain.jsx
Normal file
36
app/components/SidebarMain.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
|
||||
// - Import components
|
||||
|
||||
// - Import actions
|
||||
|
||||
// - Create component class
|
||||
export default class SidebarMain extends Component {
|
||||
|
||||
static qcName = 'SidebarMain'
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
// Default state
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div className="home__main" style={this.props.cstyle} >
|
||||
<div style={{height:"64px", width:"100%"}}></div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
271
app/components/Signup.jsx
Normal file
271
app/components/Signup.jsx
Normal file
@@ -0,0 +1,271 @@
|
||||
// - Import react components
|
||||
import React,{Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {push} from 'react-router-redux'
|
||||
import {NavLink, withRouter} from 'react-router-dom'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import TextField from 'material-ui/TextField'
|
||||
import RaisedButton from 'material-ui/RaisedButton'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
|
||||
// - Import actions
|
||||
import * as authorizeActions from 'authorizeActions'
|
||||
import * as globalActions from 'globalActions'
|
||||
|
||||
|
||||
|
||||
|
||||
// - Create Signup componet class
|
||||
export class Signup extends Component {
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fullNameInput: '',
|
||||
fullNameInputError: '',
|
||||
emailInput: '',
|
||||
emailInputError: '',
|
||||
passwordInput: '',
|
||||
passwordInputError: '',
|
||||
confirmInput: '',
|
||||
confirmInputError: ''
|
||||
|
||||
}
|
||||
// Binding function to `this`
|
||||
this.handleForm = this.handleForm.bind(this)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data on input change
|
||||
* @param {event} evt is an event of inputs of element on change
|
||||
*/
|
||||
handleInputChange = (evt) => {
|
||||
const target = evt.target
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value
|
||||
const name = target.name
|
||||
this.setState({
|
||||
[name]: value
|
||||
})
|
||||
|
||||
|
||||
switch (name) {
|
||||
case 'fullNameInput':
|
||||
this.setState({
|
||||
fullNameInputError: '',
|
||||
})
|
||||
break
|
||||
case 'emailInput':
|
||||
this.setState({
|
||||
emailInputError: ''
|
||||
})
|
||||
break
|
||||
case 'passwordInput':
|
||||
this.setState({
|
||||
confirmInputError: '',
|
||||
passwordInputError: ''
|
||||
})
|
||||
break
|
||||
case 'confirmInput':
|
||||
this.setState({
|
||||
confirmInputError: '',
|
||||
passwordInputError: ''
|
||||
})
|
||||
break
|
||||
case 'checkInput':
|
||||
this.setState({
|
||||
checkInputError: ''
|
||||
})
|
||||
break;
|
||||
default:
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle register form
|
||||
*/
|
||||
handleForm = () => {
|
||||
|
||||
var error = false
|
||||
if (this.state.fullNameInput === '') {
|
||||
this.setState({
|
||||
fullNameInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
}
|
||||
if (this.state.emailInput === '') {
|
||||
this.setState({
|
||||
emailInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
if (this.state.passwordInput === '') {
|
||||
this.setState({
|
||||
passwordInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
if (this.state.confirmInput === '') {
|
||||
this.setState({
|
||||
confirmInputError: 'This field is required'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
else if(this.state.confirmInput !== this.state.passwordInput){
|
||||
this.setState({
|
||||
passwordInputError: 'This field sould be equal to confirm password',
|
||||
confirmInputError: 'This field sould be equal to password'
|
||||
})
|
||||
error = true
|
||||
|
||||
}
|
||||
if (!error) {
|
||||
this.props.register({
|
||||
email: this.state.emailInput,
|
||||
password: this.state.passwordInput,
|
||||
fullName: this.state.fullNameInput
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
const paperStyle = {
|
||||
minHeight: 500,
|
||||
width: 450,
|
||||
margin: 20,
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
margin: "auto"
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div>
|
||||
|
||||
<h1 style={{
|
||||
textAlign: "center",
|
||||
padding:"20px",
|
||||
fontSize: "30px",
|
||||
fontWeight: 500,
|
||||
lineHeight: "32px",
|
||||
margin: "auto",
|
||||
color: "rgba(138, 148, 138, 0.2)"
|
||||
}}>Green</h1>
|
||||
|
||||
<div className="animate-bottom">
|
||||
<Paper style={paperStyle} zDepth={1} rounded={false} >
|
||||
<div style={{padding: "48px 40px 36px"}}>
|
||||
<div style={{paddingLeft: "40px",
|
||||
paddingRight: "40px"}}>
|
||||
|
||||
<h2 style={{
|
||||
textAlign: "left",
|
||||
paddingTop: "16px",
|
||||
fontSize: "24px",
|
||||
fontWeight: 400,
|
||||
lineHeight: "32px",
|
||||
margin: 0}}>Sign up</h2>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.fullNameInputError}
|
||||
name="fullNameInput"
|
||||
floatingLabelStyle={{fontSize:"15px"}}
|
||||
floatingLabelText="Full Name"
|
||||
type="text"
|
||||
/><br />
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.emailInputError}
|
||||
name="emailInput"
|
||||
floatingLabelStyle={{fontSize:"15px"}}
|
||||
floatingLabelText="Email"
|
||||
type="email"
|
||||
/><br />
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.passwordInputError }
|
||||
name="passwordInput"
|
||||
floatingLabelStyle={{fontSize:"15px"}}
|
||||
floatingLabelText="Password"
|
||||
type="password"
|
||||
/><br />
|
||||
<TextField
|
||||
onChange={this.handleInputChange}
|
||||
errorText={this.state.confirmInputError }
|
||||
name="confirmInput"
|
||||
floatingLabelStyle={{fontSize:"15px"}}
|
||||
floatingLabelText="Confirm Password"
|
||||
type="password"
|
||||
/><br />
|
||||
<br />
|
||||
<div className="signup__button-box">
|
||||
<div>
|
||||
<FlatButton label="Login" onClick={this.props.loginPage} />
|
||||
</div>
|
||||
<div>
|
||||
<RaisedButton label="Create" primary={true} onClick={this.handleForm}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</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 {
|
||||
showError: (message) => {
|
||||
dispatch(action(types.SHOW_ERROR_MESSAGE_GLOBAL)(error.message))
|
||||
},
|
||||
register: (data) => {
|
||||
dispatch(authorizeActions.dbSignup(data))
|
||||
},
|
||||
loginPage: () => {
|
||||
dispatch(push("/login"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default withRouter(connect(mapStateToProps,mapDispatchToProps)(Signup))
|
||||
129
app/components/UserAvatar.jsx
Normal file
129
app/components/UserAvatar.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import Avatar from 'material-ui/Avatar'
|
||||
|
||||
// - Import app components
|
||||
|
||||
|
||||
// - Import API
|
||||
|
||||
// - Import actions
|
||||
import * as imageGalleryActions from 'imageGalleryActions'
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class ImgCover extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
/**
|
||||
* Use for getting url address from server
|
||||
*/
|
||||
fileName: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Avatar style
|
||||
*/
|
||||
style: PropTypes.object,
|
||||
/**
|
||||
* Avatar size
|
||||
*/
|
||||
size: PropTypes.number,
|
||||
/**
|
||||
* Trigger on touch tap
|
||||
*/
|
||||
onTouchTap: PropTypes.func
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.getImageURL = this.getImageURL.bind(this)
|
||||
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { fileName, avatarURL } = this.props
|
||||
if (fileName && !avatarURL[fileName]) {
|
||||
if (this.props.imageRequests.indexOf(fileName) > -1)
|
||||
return
|
||||
this.getImageURL()
|
||||
}
|
||||
}
|
||||
|
||||
getImageURL = () => {
|
||||
let { fileName } = this.props
|
||||
if (fileName && fileName !== '') {
|
||||
if (this.props.imageRequests.indexOf(fileName) > -1)
|
||||
return
|
||||
this.props.getImage(fileName)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
let { fileName } = this.props
|
||||
|
||||
if (this.props.imageRequests.indexOf(fileName) > -1)
|
||||
return
|
||||
this.getImageURL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
let { fileName, style, size, onTouchTap, avatarURL } = this.props
|
||||
|
||||
return (
|
||||
<Avatar src={avatarURL[fileName] || ''} size={size || 36} style={style} onTouchTap={onTouchTap} />
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return {
|
||||
avatarURL: state.imageGallery.imageURLList,
|
||||
imageRequests: state.imageGallery.imageRequests
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ImgCover)
|
||||
293
app/components/UserBox.jsx
Normal file
293
app/components/UserBox.jsx
Normal file
@@ -0,0 +1,293 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {push} from 'react-router-redux'
|
||||
import Paper from 'material-ui/Paper'
|
||||
import FlatButton from 'material-ui/FlatButton'
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem'
|
||||
import Checkbox from 'material-ui/Checkbox'
|
||||
import TextField from 'material-ui/TextField'
|
||||
|
||||
// - Import app components
|
||||
import UserAvatar from 'UserAvatar'
|
||||
|
||||
|
||||
// - Import API
|
||||
import CircleAPI from 'CircleAPI'
|
||||
|
||||
|
||||
// - Import actions
|
||||
import * as circleActions from 'circleActions'
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class UserBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* User identifier
|
||||
*/
|
||||
userId: PropTypes.string,
|
||||
/**
|
||||
* User information
|
||||
*/
|
||||
user:PropTypes.object
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
/**
|
||||
* It will be true if user follow popover is open
|
||||
*/
|
||||
open: false,
|
||||
/**
|
||||
* The value of circle input
|
||||
*/
|
||||
circleName: '',
|
||||
/**
|
||||
* It will be true if the text field for adding group is empty
|
||||
*/
|
||||
disabledAddCircle: true
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
this.handleChangeName = this.handleChangeName.bind(this)
|
||||
this.handleCreateCricle = this.handleCreateCricle.bind(this)
|
||||
this.handleFollowUser = this.handleFollowUser.bind(this)
|
||||
|
||||
}
|
||||
|
||||
handleFollowUser = (checked,cid) => {
|
||||
const {userId,user} = this.props
|
||||
const {avatar,fullName} = user
|
||||
if (checked) {
|
||||
this.props.addFollowingUser(cid,{avatar,userId,fullName})
|
||||
} else {
|
||||
this.props.deleteFollowingUser(cid,userId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle create circle
|
||||
*
|
||||
*
|
||||
* @memberof UserBox
|
||||
*/
|
||||
handleCreateCricle = () => {
|
||||
const {circleName} = this.state
|
||||
if(circleName && circleName.trim() !== '' ){
|
||||
this.props.createCircle(this.state.circleName)
|
||||
this.setState({
|
||||
circleName: '',
|
||||
disabledAddCircle: true
|
||||
})}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle change group name input to the state
|
||||
*
|
||||
*
|
||||
* @memberof UserBox
|
||||
*/
|
||||
handleChangeName = (evt) => {
|
||||
this.setState({
|
||||
circleName: evt.target.value,
|
||||
disabledAddCircle: (evt.target.value === undefined || evt.target.value.trim() === '')
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch tab on follow popover
|
||||
*
|
||||
*
|
||||
* @memberof UserBox
|
||||
*/
|
||||
handleTouchTap = (evt) => {
|
||||
// This prevents ghost click.
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
open: true,
|
||||
anchorEl: evt.currentTarget,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle close follow popover
|
||||
*
|
||||
*
|
||||
* @memberof UserBox
|
||||
*/
|
||||
handleRequestClose = () => {
|
||||
this.setState({
|
||||
open: false,
|
||||
})
|
||||
}
|
||||
|
||||
circleList = () => {
|
||||
let { circles, userId, userBelongCircles } = this.props
|
||||
|
||||
if (circles) {
|
||||
|
||||
return Object.keys(circles).map((key, index) => {
|
||||
if(key.trim() !== '-Followers'){
|
||||
let isBelong = userBelongCircles.indexOf(key) > -1
|
||||
|
||||
return <Checkbox
|
||||
key={key}
|
||||
style={{ padding: '10px' }}
|
||||
label={circles[key].name}
|
||||
labelStyle={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%'
|
||||
}}
|
||||
onCheck={(evt,checked) => this.handleFollowUser(checked,key)}
|
||||
checked={isBelong}
|
||||
/>}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const styles = {
|
||||
paper: {
|
||||
height: 254,
|
||||
width: 243,
|
||||
margin: 10,
|
||||
textAlign: 'center',
|
||||
maxWidth: '257px'
|
||||
},
|
||||
followButton: {
|
||||
position: 'absolute',
|
||||
bottom: '8px',
|
||||
left: 0,
|
||||
right: 0
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper style={styles.paper} zDepth={1} className='grid-cell'>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
padding: '30px'
|
||||
|
||||
}}>
|
||||
<div onClick={() => this.props.goTo(`/${this.props.userId}`)} style={{cursor:'pointer'}}>
|
||||
<UserAvatar
|
||||
fileName={this.props.avatar}
|
||||
size={90}
|
||||
/>
|
||||
</div>
|
||||
<div onClick={() => this.props.goTo(`/${this.props.userId}`)} className='people__name' style={{cursor:'pointer'}}>
|
||||
<div>
|
||||
{this.props.user.fullName}
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.followButton}>
|
||||
<FlatButton
|
||||
label={(this.props.belongCirclesCount && this.props.belongCirclesCount < 1) ? 'Follow'
|
||||
: (this.props.belongCirclesCount > 1 ? `${this.props.belongCirclesCount} Circles` : ((this.props.firstBelongCircle) ? this.props.firstBelongCircle.name : 'Follow'))}
|
||||
primary={true}
|
||||
onTouchTap={this.handleTouchTap}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
open={this.state.open}
|
||||
anchorEl={this.state.anchorEl}
|
||||
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: 'left', vertical: 'top' }}
|
||||
onRequestClose={this.handleRequestClose}
|
||||
animation={PopoverAnimationVertical}
|
||||
>
|
||||
<Menu >
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
maxHeight: '220px'
|
||||
}}>
|
||||
<div style={{ overflowY: 'auto', height: '100%' }}>
|
||||
{this.circleList()}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '10px' }}>
|
||||
<TextField
|
||||
hintText="Group name"
|
||||
onChange={this.handleChangeName}
|
||||
value={this.state.circleName}
|
||||
/><br />
|
||||
<FlatButton label="ADD" primary={true} disabled={this.state.disabledAddCircle} onClick={this.handleCreateCricle} />
|
||||
</div>
|
||||
</Menu>
|
||||
</Popover>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
createCircle: (name) => dispatch(circleActions.dbAddCircle(name)),
|
||||
addFollowingUser: (cid,user) => dispatch(circleActions.dbAddFollowingUser(cid,user)),
|
||||
deleteFollowingUser: (cid,followingId) => dispatch(circleActions.dbDeleteFollowingUser(cid,followingId)),
|
||||
goTo: (url)=> dispatch(push(url))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const circles = state.circle ? (state.circle.userCircles[uid]|| {}) : {}
|
||||
const userBelongCircles = CircleAPI.getUserBelongCircles(circles,ownProps.userId)
|
||||
|
||||
return {
|
||||
circles: circles,
|
||||
userBelongCircles: userBelongCircles || [],
|
||||
belongCirclesCount: userBelongCircles.length || 0,
|
||||
firstBelongCircle: userBelongCircles ? (circles ? circles[userBelongCircles[0]] : {}) : {},
|
||||
avatar: state.user.info && state.user.info[ownProps.userId] ? state.user.info[ownProps.userId].avatar || '' : ''
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UserBox)
|
||||
102
app/components/UserBoxList.jsx
Normal file
102
app/components/UserBoxList.jsx
Normal file
@@ -0,0 +1,102 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
|
||||
// - Import app components
|
||||
import UserBox from 'UserBox'
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class UserBoxList extends Component {
|
||||
|
||||
static propTypes = {
|
||||
/**
|
||||
* List of users
|
||||
*/
|
||||
users: PropTypes.object
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
userList = () => {
|
||||
let { users, uid } = this.props
|
||||
|
||||
if (users) {
|
||||
return Object.keys(users).map((key, index) => {
|
||||
if(uid !== key)
|
||||
return <UserBox key={key} userId={key} user={users[key]}/>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
|
||||
const styles = {
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<div className='grid grid__1of4 grid__space-around'>
|
||||
{this.userList()}
|
||||
</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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
return {
|
||||
uid
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UserBoxList)
|
||||
113
app/components/YourCircles.jsx
Normal file
113
app/components/YourCircles.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// - Import react components
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { List } from 'material-ui/List'
|
||||
|
||||
|
||||
// - Import app components
|
||||
import Circle from 'Circle'
|
||||
|
||||
// - Import API
|
||||
|
||||
|
||||
// - Import actions
|
||||
|
||||
|
||||
/**
|
||||
* Create component class
|
||||
*/
|
||||
export class YourCircles extends Component {
|
||||
|
||||
static propTypes = {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//Defaul state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
|
||||
// Binding functions to `this`
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
circleList = () => {
|
||||
let { circles,uid } = this.props
|
||||
let parsedCircles = []
|
||||
|
||||
if (circles) {
|
||||
Object.keys(circles).map((key, index) => {
|
||||
if(key.trim() !== '-Followers')
|
||||
parsedCircles.push(<Circle key={key} circle={circles[key]} id={key} uid={uid} />)
|
||||
|
||||
})
|
||||
}
|
||||
return parsedCircles
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render() {
|
||||
let circleItems = this.circleList()
|
||||
return (
|
||||
|
||||
<div style={{
|
||||
maxWidth: '800px',
|
||||
margin: '40px auto'
|
||||
}}>
|
||||
{(circleItems && circleItems.length !== 0 ) ? (<div>
|
||||
<div className='profile__title'>
|
||||
Your circles
|
||||
</div>
|
||||
<List>
|
||||
{circleItems}
|
||||
</List>
|
||||
<div style={{ height: '24px' }}></div>
|
||||
</div>) : ''}
|
||||
</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 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
return {
|
||||
uid,
|
||||
circles: state.circle ? state.circle.userCircles[uid] : {},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(YourCircles)
|
||||
63
app/components/pattern
Normal file
63
app/components/pattern
Normal file
@@ -0,0 +1,63 @@
|
||||
// - Import react components
|
||||
import React, {Component} from 'react'
|
||||
|
||||
|
||||
// - Import components
|
||||
|
||||
// - Import actions
|
||||
|
||||
// - Create component class
|
||||
export default class {component name} extends Component {
|
||||
|
||||
/**
|
||||
* Component constructor
|
||||
* @param {object} props is an object properties of component
|
||||
*/
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
// Default state
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reneder component DOM
|
||||
* @return {react element} return the DOM which rendered by component
|
||||
*/
|
||||
render(){
|
||||
return(
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
return{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// - Connect component to redux store
|
||||
export default connect(mapStateToProps,mapDispatchToProps)(Sidebar)
|
||||
Reference in New Issue
Block a user