Migrate components to typescript

This commit is contained in:
Qolzam
2017-10-30 20:48:18 +07:00
parent 97c2e0f157
commit 7bbb1679ad
346 changed files with 6045 additions and 3946 deletions

View File

@@ -0,0 +1,270 @@
// - 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 'layouts/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 'components/userAvatar'
// - Import API
// - Import actions
import * as circleActions from 'actions/circleActions'
import { ICircleComponentProps } from './ICircleComponentProps'
import { ICircleComponentState } from './ICircleComponentState'
import { Circle } from 'core/domain/circles'
import { Profile } from 'core/domain/users/profile'
/**
* Create component class
*/
export class CircleComponent extends Component<ICircleComponentProps, ICircleComponentState> {
styles = {
userListItem: {
backgroundColor: '#e2e2e2'
},
rightIconMenu: {
display: 'block',
position: 'absolute',
top: '0px',
right: '12px'
},
settingOverlay: {
background: 'rgba(0,0,0,0.12)'
},
settingContent: {
maxWidth: '400px'
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: ICircleComponentProps) {
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 CircleComponent
*/
handleChangeCircleName = (evt: any) => {
const { value } = evt.target
this.setState({
circleName: value,
disabledSave: (!value || value.trim() === '')
})
}
/**
* Update user's circle
*
*
* @memberof CircleComponent
*/
handleUpdateCircle = () => {
const { circleName } = this.state
if (circleName && circleName.trim() !== '') {
this.props.updateCircle!({ name: circleName, id: this.props.id })
}
}
/**
* Handle delete circle
*
*
* @memberof CircleComponent
*/
handleDeleteCircle = () => {
this.props.deleteCircle!(this.props.id)
}
/**
* Toggle circle to close/open
*
*
* @memberof CircleComponent
*/
handleToggleCircle = () => {
this.setState({
open: !this.state.open
})
}
userList = () => {
const { users } = this.props.circle
const { userInfo } = this.props
let usersParsed: any = []
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={this.styles.userListItem as any}
value={2}
primaryText={fullName}
leftAvatar={<UserAvatar fullName={fullName} fileName={avatar as any} />}
onClick={() => this.props.goTo!(`/${key}`)}
/>)
})
return usersParsed
}
}
/**
* Right icon menue of circle
*
*
* @memberof CircleComponent
*/
// tslint:disable-next-line:member-ordering
rightIconMenu: any = (
<IconMenu iconButtonElement={IconButtonElement} style={this.styles.rightIconMenu as any}>
<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
key={this.props.id}
title={circleTitle}
modal={false}
open={this.props.openSetting!}
onRequestClose={this.props.closeCircleSettings}
overlayStyle={this.styles.settingOverlay as any}
contentStyle={this.styles.settingContent as any}
>
<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: any, ownProps: ICircleComponentProps) => {
let { uid } = ownProps
return {
deleteCircle: (id: string) => dispatch(circleActions.dbDeleteCircle(id)),
updateCircle: (circle: Circle) => dispatch(circleActions.dbUpdateCircle(circle)),
closeCircleSettings: () => dispatch(circleActions.closeCircleSettings(uid, ownProps.id)),
openCircleSettings: () => dispatch(circleActions.openCircleSettings(uid, ownProps.id)),
goTo: (url: string) => 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: any, ownProps: ICircleComponentProps) => {
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)(CircleComponent as any)

View File

@@ -0,0 +1,84 @@
import { Comment } from 'core/domain/comments'
import { Profile } from 'core/domain/users'
import { Circle } from 'core/domain/circles'
export interface ICircleComponentProps {
/**
*
*
* @type {Circle}
* @memberof ICircleComponentProps
*/
circle: Circle
/**
* Circle identifier
*
* @type {string}
* @memberof ICircleComponentProps
*/
id: string
/**
* User identifier
*
* @type {string}
* @memberof ICircleComponentProps
*/
uid: string
/**
*
*
* @type {Function}
* @memberof ICircleComponentProps
*/
updateCircle?: Function
/**
*
*
* @type {Function}
* @memberof ICircleComponentProps
*/
deleteCircle?: Function
/**
* Users profile
*
* @type {{[userId: string]: Profile}}
* @memberof ICircleComponentProps
*/
userInfo?: {[userId: string]: Profile}
/**
* Close setting box of circle
*
* @type {Function}
* @memberof ICircleComponentProps
*/
closeCircleSettings?: any
/**
* Circle setting dialog is open {true} or not {false}
*
* @type {Function}
* @memberof ICircleComponentProps
*/
openSetting?: boolean
/**
* Change route location
*
* @memberof ICircleComponentProps
*/
goTo?: (url: string) => void
/**
* Open setting box for a circle
*
* @memberof ICircleComponentProps
*/
openCircleSettings?: () => any
}

View File

@@ -0,0 +1,27 @@
export interface ICircleComponentState {
/**
* Circle name
*
* @type {string}
* @memberof ICircleComponentState
*/
circleName: string
/**
* If circle user list is open {true} or not {false}
*
* @type {boolean}
* @memberof ICircleComponentState
*/
open: boolean
/**
* Save button is disabled {true} or not false
*
* @type {boolean}
* @memberof ICircleComponentState
*/
disabledSave: boolean
}

View File

@@ -0,0 +1,2 @@
import CircleComponent from './CircleComponent'
export default CircleComponent

View File

@@ -0,0 +1,335 @@
// - 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 moment from 'moment'
import Linkify from 'react-linkify'
// - Import material UI libraries
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 UserAvatarComponent from 'components/userAvatar'
// - Import API
// - Import action types
import * as types from 'constants/actionTypes'
// - Import actions
import * as commentActions from 'actions/commentActions'
import * as userActions from 'actions/userActions'
import { ICommentComponentProps } from './ICommentComponentProps'
import { ICommentComponentState } from './ICommentComponentState'
/**
* Create component class
*/
export class CommentComponent extends Component<ICommentComponentProps,ICommentComponentState> {
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
}
/**
* DOM styles
*
*
* @memberof CommentComponent
*/
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'
},
rightIconMenuItem: {
fontSize: '14px'
},
textarea: {
fontWeight: 100,
fontSize: '14px',
border: 'none',
width: '100%',
outline: 'none',
resize: 'none'
},
cancel: {
float: 'right',
clear: 'both',
zIndex: 5,
margin: '0px 5px 5px 0px',
fontWeight: 400
}
}
/**
* Fields
*
* @type {*}
* @memberof CommentComponent
*/
textareaRef: any
divCommentRef: any
inputText: any
divComment: any
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: ICommentComponentProps) {
super(props)
this.textareaRef = (i: any) => { this.inputText = i }
this.divCommentRef = (i: any) => { 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: false
}
// 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: any) => {
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: any) => {
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: any) => {
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: any) => {
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: any, id?: string| null, postId?: string) => {
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 () {
/**
* Comment object from props
*/
const {comment} = this.props
const iconButtonElement = (
<IconButton style={this.styles.iconButton} iconStyle={this.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={this.styles.rightIconMenuItem} 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={this.styles.author as any}>{comment.userDisplayName}</span><span style={{
fontWeight: 100,
fontSize: '10px'
}}>{moment.unix(comment.creationDate!).fromNow()}</span>
</div>
)
const commentBody = (
<p style={this.styles.commentBody as any}>{comment.text}</p>
)
const {userId} = comment
return (
<div className='animate-top' style={this.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}`}><UserAvatarComponent fullName={this.props.fullName} 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={ Object.assign({}, this.styles.textarea, { display: (this.props.comment.editorStatus ? 'block' : 'none') })} onChange={this.handleOnChange} value={this.state.text!}></textarea>
<Linkify properties={{target: '_blank', style: {color: 'blue'}}}>
<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>
</Linkify>
</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={this.styles.cancel as any} 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: any, ownProps: any) => {
return {
delete: (id: string| null, postId: string) => dispatch(commentActions.dbDeleteComment(id, postId)),
update: (id: string, postId: string, comment: string) => 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: any, ownProps: any) => {
const {uid} = state.authorize
const avatar = state.user.info && state.user.info[ownProps.comment.userId] ? state.user.info[ownProps.comment.userId].avatar || '' : ''
const fullName = state.user.info && state.user.info[ownProps.comment.userId] ? state.user.info[ownProps.comment.userId].fullName || '' : ''
return {
uid: uid,
isCommentOwner: (uid === ownProps.comment.userId),
info: state.user.info,
avatar,
fullName
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(CommentComponent as any)

View File

@@ -0,0 +1,98 @@
import { Comment } from 'core/domain/comments'
import { Profile } from 'core/domain/users'
export interface ICommentComponentProps {
/**
* Comment
*
* @type {Comment}
* @memberof ICommentComponentProps
*/
comment: Comment
/**
* Open profile editor
*
* @type {Function}
* @memberof ICommentComponentProps
*/
openEditor: Function
/**
* Close comment editor
*
* @type {Function}
* @memberof ICommentComponentProps
*/
closeEditor: () => any
/**
* Current user is comment owner {true} or not {false}
*
* @type {boolean}
* @memberof ICommentComponentProps
*/
isCommentOwner: boolean
/**
* Current user is post owner {true} or not {false}
*
* @type {boolean}
* @memberof ICommentComponentProps
*/
isPostOwner: boolean
/**
* Update comment
*
* @memberof ICommentComponentProps
*/
update: (id?: string | null, postId?: string, comment?: string | null) => any
/**
* Delete comment
*
* @memberof ICommentComponentProps
*/
delete: (id?: string| null, postId?: string) => any
/**
* Get user profile
*
* @memberof ICommentComponentProps
*/
getUserInfo: () => void
/**
* User profile
*
* @type {{[userId: string]: Profile}}
* @memberof ICommentComponentProps
*/
info: {[userId: string]: Profile}
/**
* User full name
*
* @type {string}
* @memberof ICommentComponentProps
*/
fullName: string
/**
* User avatar address
*
* @type {string}
* @memberof Comment
*/
avatar: string
/**
* Writing comment on the post is disabled {true} or not false
*
* @type {boolean}
* @memberof ICommentComponentProps
*/
disableComments: boolean
}

View File

@@ -0,0 +1,43 @@
export interface ICommentComponentState {
/**
* Initialt text comment
*
* @type {string}
* @memberof ICommentComponentProps
*/
initialText?: string | null
/**
* Initialt text comment
*
* @type {string}
* @memberof ICommentComponentProps
*/
text?: string | null
/**
* Comment is in edit state {true} or not {false}
*
* @type {boolean}
* @memberof ICommentComponentState
*/
editDisabled: boolean
/**
* Current user is the post owner {true} or not falses
*
* @type {boolean}
* @memberof ICommentComponentState
*/
isPostOwner: boolean
/**
* Display comment {true} or not {false}
*
* @type {boolean}
* @memberof ICommentComponentState
*/
display?: boolean
}

View File

@@ -0,0 +1,2 @@
import CommentComponent from './CommentComponent'
export default CommentComponent

View File

@@ -0,0 +1,279 @@
// - 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 'actions/commentActions'
// - Import app components
import CommentListComponent from 'components/CommentList'
import UserAvatarComponent from 'components/userAvatar'
import { ICommentGroupComponentProps } from './ICommentGroupComponentProps'
import { ICommentGroupComponentState } from './ICommentGroupComponentState'
import { Comment } from 'core/domain/comments/comment'
/**
* Create component class
*/
export class CommentGroupComponent extends Component<ICommentGroupComponentProps, ICommentGroupComponentState> {
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
}
styles = {
commentItem: {
height: '60px',
position: '',
zIndex: ''
},
toggleShowList: {
height: '60px',
zIndex: 5
},
writeCommentTextField: {
width: '100%'
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: ICommentGroupComponentProps) {
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: any, data: any) => {
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 = () => {
let comments = this.props.comments
if (comments) {
let parsedComments: Comment[] = []
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 || '' : ''
const commentFullName = userInfo && userInfo[comment.userId!] ? userInfo[comment.userId!].fullName || '' : ''
return (<ListItem key={index} style={this.styles.commentItem as any} innerDivStyle={{ padding: '6px 16px 16px 72px' }}
leftAvatar={<UserAvatarComponent fullName={commentFullName} 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={this.styles.toggleShowList} 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' }}>
<CommentListComponent 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' }}>
<UserAvatarComponent fullName={this.props.fullName!} 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={this.styles.writeCommentTextField}
/>
</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: any, ownProps: ICommentGroupComponentProps) => {
return {
send: (text: string, postId: string, callBack: Function) => {
dispatch(commentActions.dbAddComment(ownProps.ownerPostUserId,{
postId: postId,
text: text
}, 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: any, ownProps: ICommentGroupComponentProps) => {
return {
comments: state.comment.postComments[ownProps.postId],
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar || '' : '',
fullName: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].fullName || '' : '',
userInfo: state.user.info
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(CommentGroupComponent as any)

View File

@@ -0,0 +1,93 @@
import { Profile } from 'core/domain/users'
import { Comment } from 'core/domain/comments'
export interface ICommentGroupComponentProps {
/**
* Commnets
*
* @type {{[commentId: string]: Comment}}
* @memberof ICommentGroupComponentProps
*/
comments?: {[commentId: string]: Comment}
/**
* The post identifier which comment belong to
*
* @type {string}
* @memberof ICommentGroupComponentProps
*/
postId: string
/**
* Users` profile
*
* @type {{[userId: string]: Profile}}
* @memberof ICommentGroupComponentProps
*/
userInfo?: {[userId: string]: Profile}
/**
* Comment group is open {true} or not {false}
*
* @type {boolean}
* @memberof ICommentGroupComponentProps
*/
open: boolean
/**
* Comment is disabled {true} or not {false}
*
* @type {boolean}
* @memberof ICommentGroupComponentProps
*/
disableComments: boolean
/**
* Current user is the post owner {true} or not {false}
*
* @type {boolean}
* @memberof ICommentGroupComponentProps
*/
isPostOwner: boolean
/**
* User full name
*
* @type {string}
* @memberof ICommentGroupComponentProps
*/
fullName?: string
/**
* Avatar URL address
*
* @type {string}
* @memberof ICommentGroupComponentProps
*/
avatar?: string
/**
* Toggle comment list open/close
*
* @type {Function}
* @memberof ICommentGroupComponentProps
*/
onToggleRequest: () => void
/**
* The identifier of post owner
*
* @type {string}
* @memberof ICommentGroupComponentProps
*/
ownerPostUserId: string
/**
* Send comment
*
* @type {(commentText: string, postId: string, clearCommentWrite: Function)}
* @memberof ICommentGroupComponentProps
*/
send?: (commentText: string, postId: string, clearCommentWrite: () => void) => any
}

View File

@@ -0,0 +1,19 @@
export interface ICommentGroupComponentState {
/**
* Comment text
*
* @type {string}
* @memberof ICommentGroupComponentState
*/
commentText: string
/**
* Disable post comment
*
* @type {boolean}
* @memberof ICommentGroupComponentState
*/
postDisable: boolean
}

View File

@@ -0,0 +1,2 @@
import CommentGroupComponent from './CommentGroupComponent'
export default CommentGroupComponent

View File

@@ -0,0 +1,127 @@
// - 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 CommentComponent from 'components/Comment'
import * as PostAPI from 'api/PostAPI'
import { ICommentListComponentProps } from './ICommentListComponentProps'
import { ICommentListComponentState } from './ICommentListComponentState'
import { Comment } from 'core/domain/comments'
// - Import actions
/**
* Create component class
*/
export class CommentListComponent extends Component<ICommentListComponentProps, ICommentListComponentState> {
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: ICommentListComponentProps) {
super(props)
/**
* Default state
*/
this.state = {
}
// Binding functions to `this`
}
/**
* Get comments' DOM
* @return {DOM} list of comments' DOM
*/
commentList = () => {
let comments = this.props.comments
if (comments) {
let parsedComments: Comment[] = []
Object.keys(comments).forEach((commentId) => {
parsedComments.push({
id: commentId,
...comments[commentId]
})
})
let sortedComments = PostAPI.sortObjectsDate(parsedComments)
return sortedComments.map((comment: Comment, index: number, array: Comment) => {
return <CommentComponent 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: any = {
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: any, ownProps: ICommentListComponentProps) => {
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: any) => {
return {
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(CommentListComponent as any)

View File

@@ -0,0 +1,28 @@
import { Comment } from 'core/domain/comments'
export interface ICommentListComponentProps {
/**
* Ad dictionary of comment
*
* @type {{[commentId: string]: Comment}}
* @memberof ICommentListComponentProps
*/
comments: {[commentId: string]: Comment}
/**
* Current user is post the post owner {true} or not false
*
* @type {boolean}
* @memberof ICommentListComponentProps
*/
isPostOwner: boolean
/**
* Comment on the post is disabled {false} or not {true}
*
* @type {boolean}
* @memberof ICommentListComponentProps
*/
disableComments: boolean
}

View File

@@ -0,0 +1,4 @@
export interface ICommentListComponentState {
}

View File

@@ -0,0 +1,2 @@
import CommentListComponent from './CommentListComponent'
export default CommentListComponent

View File

@@ -0,0 +1,435 @@
// - 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 'components/imgCover'
import UserAvatarComponent from 'components/userAvatar'
import ImageGallery from 'components/imageGallery'
import DialogTitle from 'layouts/DialogTitle'
// - Import API
import FileAPI from 'api/FileAPI'
// - Import actions
import * as userActions from 'actions/userActions'
import * as globalActions from 'actions/globalActions'
import * as imageGalleryActions from 'actions/imageGalleryActions'
import { IEditProfileComponentProps } from './IEditProfileComponentProps'
import { IEditProfileComponentState } from './IEditProfileComponentState'
import { Profile } from 'core/domain/users'
/**
* Create component class
*/
export class EditProfileComponent extends Component<IEditProfileComponentProps,IEditProfileComponentState> {
static propTypes = {
/**
* User avatar address
*/
avatar: PropTypes.string,
/**
* User avatar address
*/
banner: PropTypes.string,
/**
* User full name
*/
fullName: PropTypes.string.isRequired
}
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'
},
iconButtonSmall: {
},
iconButton: {
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IEditProfileComponentProps) {
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: props.banner || 'https://firebasestorage.googleapis.com/v0/b/open-social-33d92.appspot.com/o/images%2F751145a1-9488-46fd-a97e-04018665a6d3.JPG?alt=media&token=1a1d5e21-5101-450e-9054-ea4a20e06c57',
/**
* User avatar address
*/
avatar: 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.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: string) => {
this.setState({
banner: url
})
}
/**
* Set avatar image url
*/
handleRequestSetAvatar = (fileName: string) => {
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 = (event: any) => {
const target = event.target
const value = target.type === 'checkbox' ? target.checked : target.value
const name = target.name
this.setState({
[name]: value
})
}
/**
* Handle resize event for window to change sidebar status
* @param {any} event is the event is passed by winodw resize event
*/
handleResize = (event: any) => {
// Set initial state
let width = window.innerWidth
if (width > 900) {
this.setState({
isSmall: false
})
} else {
this.setState({
isSmall: true
})
}
}
componentDidMount () {
this.handleResize(null)
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render () {
const iconButtonElement = (
<IconButton style={this.state.isSmall ? this.styles.iconButtonSmall : this.styles.iconButton} iconStyle={this.state.isSmall ? this.styles.iconButtonSmall : this.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
key='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>
<UserAvatarComponent fullName={(this.props.info ? this.props.info.fullName : '')} fileName={this.state.avatar} size={90} style={this.styles.avatar} />
</div>
<div className='info'>
<div className='fullName'>
{this.props.fullName}
</div>
</div>
</div>
</div>
{/* Edit user information box*/}
<Paper style={this.styles.paper} zDepth={1}>
<div style={this.styles.title as any}>Personal Information</div>
<div style={this.styles.box}>
<TextField
floatingLabelText='Full name'
onChange={this.handleInputChange}
name='fullNameInput'
errorText={this.state.fullNameInputError}
value={this.state.fullNameInput}
/>
</div>
<br />
<div style={this.styles.box}>
<TextField
floatingLabelText='Tag Line'
onChange={this.handleInputChange}
name='tagLineInput'
value={this.state.tagLineInput}
/>
</div>
<br />
<div style={this.styles.actions as any}>
<FlatButton label='CANCEL' onClick={this.props.onRequestClose} />
<RaisedButton label='UPDATE' primary={true} onClick={this.handleUpdate} style={this.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={this.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={this.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: any, ownProps: IEditProfileComponentProps) => {
return {
update: (info: Profile) => dispatch(userActions.dbUpdateUserInfo(info)),
onRequestClose: () => dispatch(userActions.closeEditProfile())
}
}
/**
* 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: any, ownProps: IEditProfileComponentProps) => {
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)(EditProfileComponent as any)

View File

@@ -0,0 +1,58 @@
import { Profile } from 'core/domain/users'
export interface IEditProfileComponentProps {
/**
* User profile
*
* @type {Profile}
* @memberof IEditProfileComponentProps
*/
info?: Profile
/**
* User profile banner addresss
*
* @type {string}
* @memberof IEditProfileComponentProps
*/
banner: string
/**
* User avatar address
*
* @type {string}
* @memberof IEditProfileComponentProps
*/
avatar: string
/**
* User full name
*
* @type {string}
* @memberof IEditProfileComponentProps
*/
fullName: string
/**
* Edit profile dialog is open {true} or not {false}
*
* @type {boolean}
* @memberof IEditProfileComponentProps
*/
open?: boolean
/**
* Update user profile
*
* @memberof IEditProfileComponentProps
*/
update?: (profile: Profile) => void
/**
* On edit profile dialog close event
*
* @memberof IEditProfileComponentProps
*/
onRequestClose?: () => void
}

View File

@@ -0,0 +1,68 @@
export interface IEditProfileComponentState {
/**
* Full name input value
*
* @type {string}
* @memberof IEditProfileComponentState
*/
fullNameInput: string
/**
* Full name input error message
*
* @type {string}
* @memberof IEditProfileComponentState
*/
fullNameInputError: string
/**
* Tag line input value
*
* @type {string}
* @memberof IEditProfileComponentState
*/
tagLineInput: string
/**
* Edit profile page is small size {true} or big {false}
*
* @type {boolean}
* @memberof IEditProfileComponentState
*/
isSmall: boolean
/**
* User's banner URL
*
* @type {string}
* @memberof IEditProfileComponentState
*/
banner: string
/**
* User's avatar URL address
*
* @type {string}
* @memberof IEditProfileComponentState
*/
avatar: string
/**
* Image gallery dialog is open for choosing banner image {true} or not {false}
*
* @type {boolean}
* @memberof IEditProfileComponentState
*/
openBanner: boolean
/**
* Image gallery dialog is open for choosing avatar image {true} or not {false}
*
* @type {boolean}
* @memberof IEditProfileComponentState
*/
openAvatar: boolean
}

View File

@@ -0,0 +1,2 @@
import EditProfileComponent from './EditProfileComponent'
export default EditProfileComponent

View File

@@ -0,0 +1,108 @@
// - 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 'components/userBoxList'
// - Import API
// - Import actions
import * as userActions from 'actions/userActions'
import { IFindPeopleComponentProps } from './IFindPeopleComponentProps'
import { IFindPeopleComponentState } from './IFindPeopleComponentState'
/**
* Create component class
*/
export class FindPeopleComponent extends Component<IFindPeopleComponentProps, IFindPeopleComponentState> {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IFindPeopleComponentProps) {
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: any, ownProps: IFindPeopleComponentProps) => {
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: any, ownProps: IFindPeopleComponentProps) => {
return {
peopleInfo: state.user.info
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(FindPeopleComponent as any)

View File

@@ -0,0 +1,20 @@
import { Profile } from 'core/domain/users/profile'
export interface IFindPeopleComponentProps {
/**
* Load users' profile
*
* @memberof IFindPeopleComponentProps
*/
loadPeople?: () => any
/**
* Users' profile
*
* @type {{[userId: string]: Profile}}
* @memberof IFindPeopleComponentProps
*/
peopleInfo?: {[userId: string]: Profile}
}

View File

@@ -0,0 +1,4 @@
export interface IFindPeopleComponentState {
}

View File

@@ -0,0 +1,2 @@
import FindPeopleComponent from './FindPeopleComponent'
export default FindPeopleComponent

View File

@@ -0,0 +1,90 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
// - Import app components
import UserBoxList from 'components/userBoxList'
import { IFollowersComponentProps } from './IFollowersComponentProps'
import { IFollowersComponentState } from './IFollowersComponentState'
// - Import API
// - Import actions
/**
* Create component class
*/
export class FollowersComponent extends Component<IFollowersComponentProps,IFollowersComponentState> {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IFollowersComponentProps) {
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: any,ownProps: IFollowersComponentProps) => {
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: any,ownProps: IFollowersComponentProps) => {
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)(FollowersComponent as any)

View File

@@ -0,0 +1,12 @@
import { UserFollower } from 'core/domain/circles'
export interface IFollowersComponentProps {
/**
* User followers info
*
* @type {{[userId: string]: UserFollower}}
* @memberof IFindPeopleComponentProps
*/
followers?: {[userId: string]: UserFollower}
}

View File

@@ -0,0 +1,4 @@
export interface IFollowersComponentState {
}

View File

@@ -0,0 +1,2 @@
import FollowersComponent from './FollowersComponent'
export default FollowersComponent

View 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 'components/userBoxList'
// - Import API
import CircleAPI from 'api/CircleAPI'
import { IFollowingComponentProps } from './IFollowingComponentProps'
import { IFollowingComponentState } from './IFollowingComponentState'
// - Import actions
/**
* Create component class
*/
export class FollowingComponent extends Component<IFollowingComponentProps,IFollowingComponentState> {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IFollowingComponentProps) {
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: any,ownProp: IFollowingComponentProps) => {
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: any,ownProps: IFollowingComponentProps) => {
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)(FollowingComponent as any)

View File

@@ -0,0 +1,6 @@
import { UserFollower } from 'core/domain/circles'
export interface IFollowingComponentProps {
followingUsers?: {[userId: string]: UserFollower}
}

View File

@@ -0,0 +1,4 @@
export interface IFollowingComponentState {
}

View File

@@ -0,0 +1,2 @@
import FollowingComponent from './FollowingComponent'
export default FollowingComponent

View File

@@ -0,0 +1,200 @@
// - 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 'components/sidebar'
import StreamComponent from 'components/stream'
import HomeHeader from 'components/homeHeader'
import SidebarContent from 'components/sidebarContent'
import SidebarMain from 'components/sidebarMain'
import Profile from 'components/profile'
import PostPage from 'components/postPage'
import People from 'components/people'
// - Import API
import CircleAPI from 'api/CircleAPI'
// - Import actions
import * as globalActions from 'actions/globalActions'
import { IHomeComponentProps } from './IHomeComponentProps'
import { IHomeComponentState } from './IHomeComponentState'
// - Create Home component class
export class HomeComponent extends Component<IHomeComponentProps, IHomeComponentState> {
// Constructor
constructor (props: IHomeComponentProps) {
super(props)
// Default state
this.state = {
sidebarOpen: () => _,
sidebarStatus: true,
sidebarOverlay: 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: boolean) => {
this.setState({
sidebarOverlay: status
})
}
/**
* Pass the function to change sidebar status
* @param {boolean} open is a function callback to change sidebar status out of sidebar component
*/
sidebar = (open: (status: boolean, source: string) => void) => {
this.setState({
sidebarOpen: open
})
}
/**
* Change sidebar status if is open or not
* @param {boolean} status is true, if the sidebar is open
*/
sidebarStatus = (status: boolean) => {
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'><StreamComponent 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'><StreamComponent 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: any, ownProps: IHomeComponentProps) => {
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: any, ownProps: IHomeComponentProps) => {
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)(HomeComponent as any))

View File

@@ -0,0 +1,28 @@
import { Post } from 'core/domain/posts'
export interface IHomeComponentProps {
/**
* Current user is authenticated {true} or not {false}
*
* @type {boolean}
* @memberof IHomeComponentProps
*/
authed?: boolean
/**
* User identifier
*
* @type {string}
* @memberof IHomeComponentProps
*/
uid: string
/**
* Merged all users posts to show in stream
*
* @type {{[postId: string]: Post}}
* @memberof IHomeComponentProps
*/
mergedPosts?: {[postId: string]: Post}
}

View File

@@ -0,0 +1,27 @@
export interface IHomeComponentState {
/**
* Change sidebar status to {open(status:true)/close(status:false)}
*
* @type {(status: boolean, state: string)}
* @memberof IHomeComponentState
*/
sidebarOpen: (status: boolean, source: string) => void
/**
* Sidebar status
*
* @type {boolean}
* @memberof IHomeComponentState
*/
sidebarStatus: boolean
/**
* Sidebar overlay status
*
* @type {boolean}
* @memberof IHomeComponentState
*/
sidebarOverlay: boolean
}

View File

@@ -0,0 +1,2 @@
import HomeComponent from './HomeComponent'
export default HomeComponent

View File

@@ -0,0 +1,278 @@
// - 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 UserAvatarComponent from 'components/userAvatar'
import Notify from 'components/notify'
// - Import actions
import * as globalActions from 'actions/globalActions'
import { authorizeActions } from 'actions'
import { IHomeHeaderComponentProps } from './IHomeHeaderComponentProps'
import { IHomeHeaderComponentState } from './IHomeHeaderComponentState'
// - Create HomeHeader component class
export class HomeHeaderComponent extends Component<IHomeHeaderComponentProps,IHomeHeaderComponentState> {
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'
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IHomeHeaderComponentProps) {
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,'onToggle')
} else {
this.props.sidebar!(true,'onToggle')
}
}
/**
* Handle notification touch
*
*
* @memberof HomeHeader
*/
handleNotifyTouchTap = (event: any) => {
// This prevents ghost click.
event.preventDefault()
this.setState({
openNotifyMenu: true,
anchorEl: event.currentTarget
})
}
/**
* Handle touch on user avatar for popover
*
*
* @memberof HomeHeader
*/
handleAvatarTouchTap = (event: any) => {
// 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
})
}
handleKeyUp = () => {
// TODO: Handle key up on press ESC to close menu
}
/**
* Handle resize event for window to manipulate home header status
* @param {event} evt is the event is passed by winodw resize event
*/
handleResize = (event: any) => {
// Set initial state
let 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(null)
}
// Render app DOM component
render () {
return (
<Toolbar style={this.styles.toolbarStyle as any} 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*/}
<UserAvatarComponent
onTouchTap={this.handleAvatarTouchTap}
fullName={this.props.fullName!}
fileName={this.props.avatar!}
size={32}
style={this.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: Function, ownProps: IHomeHeaderComponentProps) => {
return {
logout: () => dispatch(authorizeActions.dbLogout())
}
}
// - Map state to props
const mapStateToProps = (state: any, ownProps: IHomeHeaderComponentProps) => {
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 : '',
fullName: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].fullName : '',
title: state.global.headerTitle,
notifyCount
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(HomeHeaderComponent as any)

View File

@@ -0,0 +1,63 @@
import StringAPI from 'api/StringAPI'
export interface IHomeHeaderComponentProps {
/**
* Sidebar is open {true} or not {false}
*
* @type {boolean}
* @memberof IHomeHeaderComponentProps
*/
sidebarStatus?: boolean
/**
* Logout user
*
* @memberof IHomeHeaderComponentProps
*/
logout?: () => void
/**
* Handle on resize window event
*
* @memberof IHomeHeaderComponentProps
*/
handleResize?: (event: any) => void
/**
* Number of notifications
*
* @memberof IHomeHeaderComponentProps
*/
notifyCount?: number
/**
* User full name
*
* @type {string}
* @memberof IHomeHeaderComponentProps
*/
fullName?: string
/**
* User's avatar URL address
*
* @type {string}
* @memberof IHomeHeaderComponentProps
*/
avatar?: string
/**
* Top bar title
*
* @type {string}
* @memberof IHomeHeaderComponentProps
*/
title?: string
/**
* Toggle sidebar
*
* @memberof IHomeHeaderComponentProps
*/
sidebar?: (status: boolean, source: string) => void
}

View File

@@ -0,0 +1,35 @@
export interface IHomeHeaderComponentState {
/**
* Popover menu on avatar is open {true} or not {false}
*
* @type {boolean}
* @memberof IHomeHeaderComponentState
*/
openAvatarMenu: boolean
/**
* Show top bar title {true} or not {false}
*
* @type {boolean}
* @memberof IHomeHeaderComponentState
*/
showTitle: boolean
/**
* Notification menu is open {true} or not {false}
*
* @type {boolean}
* @memberof IHomeHeaderComponentState
*/
openNotifyMenu: boolean
/**
* This is the DOM element that will be used to set the position of the popover.
*
* @type {*}
* @memberof IHomeHeaderComponentState
*/
anchorEl?: HTMLElement
}

View File

@@ -0,0 +1,2 @@
import HomeHeaderComponent from './HomeHeaderComponent'
export default HomeHeaderComponent

View File

@@ -0,0 +1,48 @@
import { Image } from 'core/domain/imageGallery'
export interface IImageGalleryComponentProps {
/**
* Select image from image gallery
*
* @type {(URL: string,fullPath: string)}
* @memberof IImageGalleryComponentProps
*/
set?: (URL: string,fullPath: string) => void
/**
* Delete an image
*
* @memberof IImageGalleryComponentProps
*/
deleteImage?: (imageId: string) => void
/**
* Save image in image gallery
*
* @memberof IImageGalleryComponentProps
*/
saveImageGallery?: (URL: string,fullPath: string) => void
/**
* Change progress state
*
* @memberof IImageGalleryComponentProps
*/
progressChange?: (percentage: number, status: boolean) => void
/**
* Close image gallery
*
* @memberof IImageGalleryComponentProps
*/
close?: () => void
/**
* List of image in image gallery
*
* @type {Image[]}
* @memberof IImageGalleryComponentProps
*/
images?: Image[]
}

View File

@@ -0,0 +1,4 @@
export interface IImageGalleryComponentState {
}

View File

@@ -0,0 +1,256 @@
// - 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 'actions/imageGalleryActions'
import * as globalActions from 'actions/globalActions'
// - Import app components
import Img from 'components/img'
// - Import API
import FileAPI from 'api/FileAPI'
import { IImageGalleryComponentProps } from './IImageGalleryComponentProps'
import { IImageGalleryComponentState } from './IImageGalleryComponentState'
import { Image } from 'core/domain/imageGallery'
/**
* Create ImageGallery component class
*/
export class ImageGalleryComponent extends Component<IImageGalleryComponentProps, IImageGalleryComponentState> {
static propTypes = {
/**
* Callback function to ser image url on parent component
*/
open: PropTypes.func
}
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
},
deleteImage: {
marginLeft: '5px',
cursor: 'pointer'
},
addImage: {
marginRight: '5px',
cursor: 'pointer'
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IImageGalleryComponentProps) {
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 = (event: any, URL: string,fullPath: string) => {
this.props.set!(URL,fullPath)
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 = (event: any, id: string) => {
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: any) => {
const { resizedImage, fileName } = event.detail
const {saveImageGallery, progressChange} = this.props
FileAPI.uploadImage(resizedImage, fileName, (percent: number, status: boolean) => {
progressChange!(percent,status)
}).then((result) => {
/* Add image to image gallery */
saveImageGallery!(result.downloadURL,result.metadata.fullPath)
})
}
/**
* Handle on change file upload
*/
onFileChange = (event: any) => {
const extension = FileAPI.getExtension(event.target.files[0].name)
let fileName = (`${uuid()}.${extension}`)
let image = FileAPI.constraintImage(event.target.files[0], fileName)
}
/**
* Hide image gallery
*/
close = () => {
this.props.close!()
}
imageList = () => {
return this.props.images!.map((image: Image, index) => {
return (<GridTile
key={image.id!}
title={<SvgDelete hoverColor={grey200} color='white' style={this.styles.deleteImage as any} onClick={evt => this.handleDeleteImage(evt, image.id!)} />}
subtitle={<span></span>}
actionIcon={<SvgAddImage hoverColor={grey200} color='white' style={this.styles.addImage as any} onClick={evt => this.handleSetImage(evt, image.URL,image.fullPath)} />}
>
<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.URL} 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}
*/
return (
<div style={this.styles.root as any}>
<GridList
cellHeight={180}
style={this.styles.gridList as any}
>
<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={this.styles.uploadButton}
containerElement='label'
>
<input type='file' onChange={this.onFileChange} accept='image/*' style={this.styles.uploadInput as any} />
</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: any, ownProps: IImageGalleryComponentProps) => {
return {
saveImageGallery: (imageURL: string,imageFullPath: string) => dispatch(imageGalleryActions.dbSaveImage(imageURL,imageFullPath)),
deleteImage: (id: string) => dispatch(imageGalleryActions.dbDeleteImage(id)),
progressChange : (percent: number,status: boolean) => dispatch(globalActions.progressChange(percent, status))
}
}
/**
* 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: any) => {
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)(ImageGalleryComponent as any)

View File

@@ -0,0 +1,2 @@
import ImageGalleryComponent from './ImageGalleryComponent'
export default ImageGalleryComponent

View File

@@ -0,0 +1,19 @@
export interface IImgComponentProps {
/**
* Image file name
*
* @type {string}
* @memberof IImgComponentProps
*/
fileName: string
/**
* Image style sheet
*
* @type {{}}
* @memberof IImgComponentProps
*/
style?: {}
}

View File

@@ -0,0 +1,11 @@
export interface IImgComponentState {
/**
* Image is loaded {true} or not {false}
*
* @type {boolean}
* @memberof IImgComponentProps
*/
isImageLoaded?: boolean
}

View File

@@ -0,0 +1,120 @@
// - 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 'actions/imageGalleryActions'
import { IImgComponentProps } from './IImgComponentProps'
import { IImgComponentState } from './IImgComponentState'
/**
* Create component class
*/
export class ImgComponent extends Component<IImgComponentProps,IImgComponentState> {
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'
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IImgComponentProps) {
super(props)
// Defaul state
this.state = {
isImageLoaded: false
}
// Binding functions to `this`
this.handleLoadImage = this.handleLoadImage.bind(this)
}
/**
* Will be called on loading image
*
* @memberof Img
*/
handleLoadImage = () => {
this.setState({
isImageLoaded: true
})
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render () {
let { fileName, style } = this.props
let { isImageLoaded } = this.state
return (
<div>
<img onLoad={this.handleLoadImage} src={fileName || ''} style={isImageLoaded ? style : { display: 'none' }} />
<div style={Object.assign({},{ backgroundColor: 'white' }, isImageLoaded ? { display: 'none' } : this.styles.loding) }>
<div style={this.styles.loadingContent as any}>
<SvgImage style={this.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: any, ownProps: IImgComponentProps) => {
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: any, ownProps: IImgComponentProps) => {
return {
avatarURL: state.imageGallery.imageURLList,
imageRequests: state.imageGallery.imageRequests
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ImgComponent as any)

View File

@@ -0,0 +1,2 @@
import ImgComponent from './ImgComponent'
export default ImgComponent

View File

@@ -0,0 +1,42 @@
export interface IImgCoverComponentProps {
/**
* Image file name
*
* @type {string}
* @memberof IImgCoverComponentProps
*/
fileName: string
/**
* Image style sheet
*
* @type {{}}
* @memberof IImgCoverComponentProps
*/
style?: {}
/**
* Image with
*
* @type {string}
* @memberof IImgCoverComponentProps
*/
width?: string
/**
* Image height
*
* @type {string}
* @memberof IImgCoverComponentProps
*/
height?: string
/**
* Image border radius
*
* @type {string}
* @memberof IImgCoverComponentProps
*/
borderRadius?: string
}

View File

@@ -0,0 +1,12 @@
export interface IImgCoverComponentState {
/**
* Image is loaded {true} or not {false}
*
* @type {boolean}
* @memberof IImgCoverComponentProps
*/
isImageLoaded: boolean
}

View File

@@ -0,0 +1,162 @@
// - 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 'actions/imageGalleryActions'
import { IImgCoverComponentProps } from './IImgCoverComponentProps'
import { IImgCoverComponentState } from './IImgCoverComponentState'
/**
* Create component class
*/
export class ImgCoverComponent extends Component<IImgCoverComponentProps,IImgCoverComponentState> {
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
])
}
styles = {
cover: {
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center'
},
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'
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IImgCoverComponentProps) {
super(props)
// Defaul state
this.state = {
isImageLoaded: false
}
// Binding functions to `this`
this.handleLoadImage = this.handleLoadImage.bind(this)
}
/**
* Will be called on loading image
*
* @memberof ImgCoverComponent
*/
handleLoadImage = () => {
this.setState({
isImageLoaded: true
})
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render () {
let { fileName, style } = this.props
let { isImageLoaded } = this.state
return (
<div>
<div style={Object.assign({},this.styles.cover,{
backgroundImage: 'url(' + (fileName || '') + ')',
width: this.props.width,
height: this.props.height,
borderRadius: this.props.borderRadius
},style)}>
{this.props.children}
</div>
<div style={Object.assign({},{ backgroundColor: 'blue' },isImageLoaded ? { display: 'none' } : this.styles.loding)}>
<div style={this.styles.loadingContent as any}>
<SvgImage style={this.styles.loadingImage} />
<div>Image has not loaded</div>
</div>
</div>
<img onLoad={this.handleLoadImage} src={fileName || ''} style={{ display: 'none'}} />
</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: any, ownProps: IImgCoverComponentProps) => {
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: any, ownProps: IImgCoverComponentProps) => {
return {
avatarURL: state.imageGallery.imageURLList,
imageRequests: state.imageGallery.imageRequests
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ImgCoverComponent as any)

View File

@@ -0,0 +1,2 @@
import ImgCoverComponent from './ImgCoverComponent'
export default ImgCoverComponent

View File

@@ -0,0 +1,16 @@
export interface ILoginComponentProps {
/**
* Login a user
*
* @memberof ILoginComponentProps
*/
login?: (email: string , password: string) => any
/**
* Redirect to signup page
*
* @memberof ILoginComponentProps
*/
signupPage?: () => any
}

View File

@@ -0,0 +1,43 @@
export interface ILoginComponentState {
/**
* Email input value
*
* @type {string}
* @memberof ILoginComponentState
*/
emailInput: string
/**
* Email input error text
*
* @type {string}
* @memberof ILoginComponentState
*/
emailInputError: string
/**
* Password input value
*
* @type {string}
* @memberof ILoginComponentState
*/
passwordInput: string
/**
* Password input error text
*
* @type {string}
* @memberof ILoginComponentState
*/
passwordInputError: string
/**
* Confirm input error text
*
* @type {string}
* @memberof ILoginComponentState
*/
confirmInputError: string
}

View File

@@ -0,0 +1,209 @@
// - 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 'actions/authorizeActions'
import { ILoginComponentProps } from './ILoginComponentProps'
import { ILoginComponentState } from './ILoginComponentState'
// - Create Login component class
export class LoginComponent extends Component<ILoginComponentProps,ILoginComponentState> {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: ILoginComponentProps) {
super(props)
this.state = {
emailInput: '',
emailInputError: '',
passwordInput: '',
passwordInputError: '',
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 = (event: any) => {
const target = event.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 = () => {
let 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,
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: any, ownProps: ILoginComponentProps) => {
return {
login: (email: string, password: string) => {
dispatch(authorizeActions.dbLogin(email, 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: any, ownProps: ILoginComponentProps) => {
return {
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginComponent as any))

View File

@@ -0,0 +1,2 @@
import LoginComponent from './LoginComponent'
export default LoginComponent

View File

@@ -0,0 +1,100 @@
export interface IMasterComponentProps {
/**
* Close gloal message
*
* @type {Function}
* @memberof IMasterProps
*/
closeMessage: Function,
/**
* Show progress bar information
*
* @type {*}
* @memberof IMasterProps
*/
progress: any,
/**
* Login a user
*
* @type {Function}
* @memberof IMasterProps
*/
login: Function,
/**
* Global state
*
* @type {*}
* @memberof IMasterProps
*/
global: any,
/**
* Set flag {false} which user data has not loaded
*
* @type {Function}
* @memberof IMasterProps
*/
defaultDataDisable: Function,
/**
* Logout current user
*
* @type {Function}
* @memberof IMasterProps
*/
logout: Function,
/**
* Clear user date from store
*
* @type {Function}
* @memberof IMasterProps
*/
clearData: Function,
/**
* Prepare default data for a guest user
*
* @type {Function}
* @memberof IMasterProps
*/
loadDataGuest: Function,
/**
* Set flag {true} which all user data has loaded
*
* @type {Function}
* @memberof IMasterProps
*/
defaultDataEnable: Function,
/**
* Load user data into store
*
* @type {Function}
* @memberof IMasterProps
*/
loadData: Function,
/**
* If all data from all entities are loaded {true} if not {false}
*
* @type {Boolean}
* @memberof IMasterProps
*/
loaded: Boolean,
/**
* If current user is guest {true} if no
*
* @type {Boolean}
* @memberof IMasterProps
*/
guest: Boolean,
/**
* If current user is authed {true} if not {false}
*
* @type {Boolean}
* @memberof IMasterProps
*/
authed: Boolean,
/**
* Authed user identifier
*
* @type {string}
* @memberof IMasterProps
*/
uid: string
}

View File

@@ -0,0 +1,24 @@
export interface IMasterComponentState {
/**
* Loding will be appeared if it's true
*
* @type {boolean}
* @memberof IMasterState
*/
loading: boolean,
/**
* It's true if user is authorized
*
* @type {boolean}
* @memberof IMasterState
*/
authed: boolean
/**
* It's true if all default data loaded from database
*
* @type {boolean}
* @memberof IMasterState
*/
dataLoaded: boolean
}

View File

@@ -0,0 +1,218 @@
/// <reference types="@types/material-ui" />
// - 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 'data/firebaseClient'
import { push } from 'react-router-redux'
import Snackbar from 'material-ui/Snackbar'
import LinearProgress from 'material-ui/LinearProgress'
// - Import components
import Home from 'components/home'
import Signup from 'components/signup'
import Login from 'components/login'
import Setting from 'components/setting'
import MasterLoading from 'components/masterLoading'
import { IMasterComponentProps } from './IMasterComponentProps'
import { IMasterComponentState } from './IMasterComponentState'
// - Import actions
import {
authorizeActions,
imageGalleryActions,
postActions,
commentActions,
voteActions,
userActions,
globalActions,
circleActions,
notifyActions
} from 'actions'
/* ------------------------------------ */
// - Create Master component class
export class MasterComponent extends Component<IMasterComponentProps, IMasterComponentState> {
static isPrivate = true
// Constructor
constructor (props: IMasterComponentProps) {
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: any) => {
this.props.closeMessage()
}
// Handle loading
handleLoading = (status: boolean) => {
this.setState({
loading: status,
authed: false
})
}
componentDidCatch (error: any, info: any) {
console.log('===========Catched by React componentDidCatch==============')
console.log(error, info)
alert({error, info})
console.log('====================================')
}
componentWillMount () {
firebaseAuth().onAuthStateChanged((user: any) => {
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
*/
public render (): React.ReactElement<{}> {
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={Setting} />
<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: any, ownProps: IMasterComponentProps) => {
return {
loadData: () => {
dispatch(commentActions.dbGetComments())
dispatch(imageGalleryActions.dbGetImageGallery())
dispatch(postActions.dbGetPosts())
dispatch(userActions.dbGetUserInfo())
dispatch(voteActions.dbGetVotes())
dispatch(notifyActions.dbGetNotifications())
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())
dispatch(globalActions.clearTemp())
},
login: (user: any) => {
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
* @param {object} state
*/
const mapStateToProps = (state: any) => {
const { authorize, global, user, post, comment, imageGallery, vote, notify, circle } = state
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)(MasterComponent as any))

View File

@@ -0,0 +1,2 @@
import MasterComponent from './MasterComponent'
export default MasterComponent

View File

@@ -0,0 +1,12 @@
export interface IMasterLoadingComponentProps {
/**
* Loading is active {true} or not {false}
*
* @type {boolean}
* @memberof IMasterLoadingComponentProps
*/
activeLoading: boolean
handleLoading: (status: boolean) => void
}

View File

@@ -0,0 +1,4 @@
export interface IMasterLoadingComponentState {
}

View File

@@ -0,0 +1,47 @@
// - Import react components
import React, { Component } from 'react'
import CircularProgress from 'material-ui/CircularProgress'
import Dialog from 'material-ui/Dialog'
import { IMasterLoadingComponentProps } from './IMasterLoadingComponentProps'
import { IMasterLoadingComponentState } from './IMasterLoadingComponentState'
// - Import app components
// - Create MasterLoading component class
export default class MasterLoadingComponent extends Component<IMasterLoadingComponentProps,IMasterLoadingComponentState> {
// Constructor
constructor (props: IMasterLoadingComponentProps) {
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>
)
}
}

View File

@@ -0,0 +1,2 @@
import MasterLoadingComponent from './MasterLoadingComponent'
export default MasterLoadingComponent

View File

@@ -0,0 +1,45 @@
import { Profile } from 'core/domain/users'
import { Notification } from 'core/domain/notifications'
export interface INotifyComponentProps {
/**
* Notifications
*
* @type {{[notificationId: string]: Notification}}
* @memberof INotifyComponentProps
*/
notifications?: {[notificationId: string]: Notification}
/**
* Users' profile
*
* @type {{[userId: string]: Profile}}
* @memberof INotifyComponentProps
*/
info?: {[userId: string]: Profile}
/**
* Close notification
*
* @memberof INotifyComponentProps
*/
onRequestClose: () => void
/**
* User notifications popover is opem {true} or not {false}
*
* @type {boolean}
* @memberof INotifyComponentProps
*/
open: boolean
/**
* Keep element
*
* @type {*}
* @memberof INotifyComponentProps
*/
anchorEl: any
}

View File

@@ -0,0 +1,4 @@
export interface INotifyComponentState {
}

View File

@@ -0,0 +1,136 @@
// - 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 'components/notifyItem'
// - Import API
// - Import actions
import * as userActions from 'actions/userActions'
import { INotifyComponentProps } from './INotifyComponentProps'
import { INotifyComponentState } from './INotifyComponentState'
import { Notification } from 'core/domain/notifications'
/**
* Create component class
*/
export class NotifyComponent extends Component<INotifyComponentProps,INotifyComponentState> {
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: INotifyComponentProps) {
super(props)
// Defaul state
this.state = {
}
// Binding functions to `this`
}
notifyItemList = () => {
let { notifications, info, onRequestClose } = this.props
let parsedDOM: any[] = []
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: any, ownProps: INotifyComponentProps) => {
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: any, ownProps: INotifyComponentProps) => {
return {
notifications: state.notify.userNotifies,
info: state.user.info
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(NotifyComponent as any)

View File

@@ -0,0 +1,2 @@
import NotifyComponent from './NotifyComponent'
export default NotifyComponent

View File

@@ -0,0 +1,86 @@
export interface INotifyItemComponentProps {
/**
* Notification description
*
* @type {string}
* @memberof INotifyItemComponentProps
*/
description: string
/**
* User full name
*
* @type {string}
* @memberof INotifyItemComponentProps
*/
fullName: string
/**
* User avatar
*
* @type {string}
* @memberof INotifyItemComponentProps
*/
avatar: string
/**
* Notification has seen {true} or not {false}
*
* @type {boolean}
* @memberof INotifyItemComponentProps
*/
isSeen: boolean
/**
* Notification identifier
*
* @type {string}
* @memberof INotifyItemComponentProps
*/
id: string
/**
* Rediret to {url} route
*
* @memberof INotifyItemComponentProps
*/
goTo?: (url: string) => any
/**
* Close a notification
*
* @memberof INotifyItemComponentProps
*/
closeNotify?: () => void
/**
* Notifier identifier
*
* @type {string}
* @memberof INotifyItemComponentProps
*/
notifierUserId: string
/**
* The URL which notification mention
*
* @type {string}
* @memberof INotifyItemComponentProps
*/
url: string
/**
* Delete a notification
*
* @memberof INotifyItemComponentProps
*/
deleteNotiy?: (notificationId: string) => any
/**
* Change notification status to has seen
*
* @memberof INotifyItemComponentProps
*/
seenNotify?: (notificationId: string) => any
}

View File

@@ -0,0 +1,4 @@
export interface INotifyItemComponentState {
}

View File

@@ -0,0 +1,156 @@
// - 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 'components/userAvatar'
// - Import API
// - Import actions
import * as notifyActions from 'actions/notifyActions'
import { INotifyItemComponentProps } from './INotifyItemComponentProps'
import { INotifyItemComponentState } from './INotifyItemComponentState'
/**
* Create component class
*/
export class NotifyItemComponent extends Component<INotifyItemComponentProps,INotifyItemComponentState> {
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: INotifyItemComponentProps) {
super(props)
// Defaul state
this.state = {
}
// Binding functions to `this`
this.handleSeenNotify = this.handleSeenNotify.bind(this)
}
handleSeenNotify = (event: any) => {
event.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 fullName={fullName} 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: any, ownProps: INotifyItemComponentProps) => {
return {
goTo: (url: string) => dispatch(push(url)),
seenNotify: (id: string) => dispatch(notifyActions.dbSeenNotification(id)),
deleteNotiy: (id: string) => dispatch(notifyActions.dbDeleteNotification(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: any, ownProps: INotifyItemComponentProps) => {
return {
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(NotifyItemComponent as any)

View File

@@ -0,0 +1,2 @@
import NotifyItemComponent from './NotifyItemComponent'
export default NotifyItemComponent

View File

@@ -0,0 +1,32 @@
export interface IPeopleComponentProps {
/**
* Router match
*
* @type {*}
* @memberof IPeopleComponentProps
*/
match?: any
/**
* Circles loaded {true} or not {false}
*
* @type {boolean}
* @memberof IPeopleComponentProps
*/
circlesLoaded?: boolean
/**
* Rediret to another route
*
* @memberof IPeopleComponentProps
*/
goTo?: (url: string) => any
/**
* Set title of top bar
*
* @memberof IPeopleComponentProps
*/
setHeaderTitle?: (title: string) => any
}

View File

@@ -0,0 +1,4 @@
export interface IPeopleComponentState {
}

View File

@@ -0,0 +1,169 @@
// - 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 'components/findPeople'
import Following from 'components/following'
import Followers from 'components/followers'
import YourCircles from 'components/yourCircles'
// - Import API
// - Import actions
import * as circleActions from 'actions/circleActions'
import * as globalActions from 'actions/globalActions'
import { IPeopleComponentProps } from './IPeopleComponentProps'
import { IPeopleComponentState } from './IPeopleComponentState'
/**
* Create component class
*/
export class PeopleComponent extends Component<IPeopleComponentProps,IPeopleComponentState> {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IPeopleComponentProps) {
super(props)
// Defaul state
this.state = {
}
// Binding functions to `this`
}
componentWillMount () {
const { setHeaderTitle} = this.props
const {tab} = this.props.match.params
switch (tab) {
case undefined:
case '':
setHeaderTitle!('People')
break
case 'circles':
setHeaderTitle!('Circles')
break
case 'followers':
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, goTo, setHeaderTitle} = 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={() => {
goTo!('/people')
setHeaderTitle!('People')
}} >
{circlesLoaded ? <FindPeople /> : ''}
</Tab>
<Tab label='Following' onActive={() => {
goTo!('/people/circles')
setHeaderTitle!('Circles')
}} >
{circlesLoaded ? <Following/> : ''}
{circlesLoaded ? <YourCircles/> : ''}
</Tab>
<Tab label='Followers' onActive={() => {
goTo!('/people/followers')
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: any, ownProps: IPeopleComponentProps) => {
return {
goTo: (url: string) => dispatch(push(url)),
setHeaderTitle : (title: string) => 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: any, ownProps: IPeopleComponentProps) => {
return {
uid: state.authorize.uid,
circlesLoaded: state.circle.loaded
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(PeopleComponent as any))

View File

@@ -0,0 +1,2 @@
import PeopleComponent from './PeopleComponent'
export default PeopleComponent

View File

@@ -0,0 +1,161 @@
export interface IPostComponentProps {
/**
* The context of a post
*/
body: string
/**
* The number of comment on a post
*/
commentCounter: number
/**
* Creation post date
*/
creationDate: number
/**
* Post identifier
*/
id: string
/**
* Post image address
*/
image: string
/**
* The last time date when post has was edited
*/
lastEditDate: number
/**
* The name of the user who created the post
*/
ownerDisplayName: string
/**
* The identifier of the user who created the post
*/
ownerUserId: string
/**
* The avatar address of the user who created the post
* //TODO: User avatar should be as an attribute and [avatar] should be deleted
*/
ownerAvatar: string
/**
* The avatar address of the user who created the post
*/
avatar?: string
/**
* If post is only [0]text, [1]whith picture, ...
*/
postTypeId: string
/**
* The number votes on a post
*/
score: number
/**
* Array of tags on a post
*/
tags: string[]
/**
* The video address of a post
*/
video: string
/**
* If it's true comment will be disabled on a post
*/
disableComments: boolean
/**
* If it's true sharing will be disabled on a post
*/
disableSharing: boolean
/**
* The number of users who has visited the post
*/
viewCount: boolean
/**
* User full name
*
* @type {string}
* @memberof IPostComponentProps
*/
fullName?: string
/**
* Number of comments on the post
*
* @type {number}
* @memberof IPostComponentProps
*/
commentCount?: number
/**
* Number of vote on a post
*
* @type {number}
* @memberof IPostComponentProps
*/
voteCount?: number
/**
* Current user vote the post {true} or not {false}
*
* @type {boolean}
* @memberof IPostComponentProps
*/
userVoteStatus?: boolean
/**
* Current user is the owner of the post {true} or not {false}
*
* @type {boolean}
* @memberof IPostComponentProps
*/
isPostOwner?: boolean
/**
* Vote a post
*
* @memberof IPostComponentProps
*/
vote?: () => any
/**
* Delete a vote on the post
*
* @memberof IPostComponentProps
*/
unvote?: () => any
/**
* Delte a post
*
* @memberof IPostComponentProps
*/
delete?: (id: string) => any
/**
* Toggle comment disable/enable
*
* @memberof IPostComponentProps
*/
toggleDisableComments?: (status: boolean) => any
/**
* Toggle sharing disable/enable
*
* @memberof IPostComponentProps
*/
toggleSharingComments?: (status: boolean) => any
/**
* Redirect to {url} route
*
* @memberof IPostComponentProps
*/
goTo?: (url: string) => any
/**
* Set tile of top bar
*
* @memberof IPostComponentProps
*/
setHomeTitle?: (title: string) => any
}

View File

@@ -0,0 +1,42 @@
export interface IPostComponentState {
/**
* Post text
*/
text: string
/**
* It's true if whole the text post is visible
*/
readMoreState: boolean
/**
* Handle open comment from parent component
*/
openComments: boolean
/**
* If it's true, share dialog will be open
*/
shareOpen: boolean
/**
* If it's true comment will be disabled on post
*/
disableComments: boolean
/**
* If it's true share will be disabled on post
*/
disableSharing: boolean
/**
* Title of share post
*/
shareTitle: string
/**
* If it's true, post link will be visible in share post dialog
*/
openCopyLink: boolean
/**
* If it's true, post write will be open
*/
openPostWrite: boolean
openCommentGroup?: () => void
}

View File

@@ -0,0 +1,485 @@
// - 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 Linkify from 'react-linkify'
// - Material UI
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 'components/commentGroup'
import PostWrite from 'components/postWrite'
import Img from 'components/img'
import IconButtonElement from 'layouts/IconButtonElement'
import UserAvatar from 'components/userAvatar'
// - Import actions
import * as voteActions from 'actions/voteActions'
import * as postActions from 'actions/postActions'
import * as globalActions from 'actions/globalActions'
import { IPostComponentProps } from './IPostComponentProps'
import { IPostComponentState } from './IPostComponentState'
// - Create component class
export class PostComponent extends Component<IPostComponentProps,IPostComponentState> {
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
}
styles = {
counter: {
lineHeight: '36px',
color: '#777',
fontSize: '12px',
marginRight: '6px'
},
postBody: {
wordWrap: 'break-word'
},
dialog: {
width: '',
maxWidth: '530px',
borderRadius: '4px'
},
rightIconMenu: {
position: 'absolute',
right: 18,
top: 8
},
iconButton: {
width: 24,
height: 24
}
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IPostComponentProps) {
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 = () => {
this.setState({
openComments: !this.state.openComments
})
}
/**
* Open post write
*
*
* @memberof StreamComponent
*/
handleOpenPostWrite = () => {
this.setState({
openPostWrite: true
})
}
/**
* Close post write
*
*
* @memberof StreamComponent
*/
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: () => void) => {
this.setState({
openCommentGroup: open
})
}
/**
* Handle read more event
* @param {event} evt is the event passed by click on read more
*/
handleReadMore (event: any) {
this.setState({
readMoreState: !this.state.readMoreState
})
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render () {
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, fullName, 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 fullName={fullName!} fileName={avatar!} size={36} /></NavLink>}
>
{isPostOwner ? ( <div style={this.styles.rightIconMenu as any}><RightIconMenu /></div>) : ''}
</CardHeader>
{image ? (
<CardMedia>
<Img fileName={image} />
</CardMedia>) : ''}
<CardText style={this.styles.postBody}>
<Linkify properties={{target: '_blank', style: {color: 'blue'}}}>
{reactStringReplace(body,/#(\w+)/g, (match: string, i: string) => (
<NavLink
style={{color: 'green'}}
key={match + i}
to={`/tag/${match}`}
onClick ={evt => {
evt.preventDefault()
goTo!(`/tag/${match}`)
setHomeTitle!(`#${match}`)
}}
>
#{match}
</NavLink>
))}
</Linkify>
</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={this.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' }} 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={this.styles.counter}>{this.props.commentCount! > 0 ? this.props.commentCount : ''} </div></div>) : ''}
{!this.props.disableSharing ? (<FloatingActionButton onClick={this.handleOpenShare} style={{ margin: '0 8px' }} 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={this.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: any, ownProps: IPostComponentProps) => {
return {
vote: () => dispatch(voteActions.dbAddVote(ownProps.id,ownProps.ownerUserId)),
unvote: () => dispatch(voteActions.dbDeleteVote(ownProps.id)) ,
delete: (id: string) => dispatch(postActions.dbDeletePost(id)),
toggleDisableComments: (status: boolean) => dispatch(postActions.dbUpdatePost({id: ownProps.id, disableComments: status}, (x: any) => x)),
toggleSharingComments: (status: boolean) => dispatch(postActions.dbUpdatePost({id: ownProps.id, disableSharing: status},(x: any) => x)),
goTo: (url: string) => dispatch(push(url)),
setHomeTitle: (title: string) => 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: any, ownProps: IPostComponentProps) => {
const {uid} = state.authorize
let votes = state.vote.postVotes[ownProps.id]
const post = (state.post.userPosts[uid] ? Object.keys(state.post.userPosts[uid]).filter((key) => { return ownProps.id === key }).length : 0)
return {
avatar: state.user.info && state.user.info[ownProps.ownerUserId] ? state.user.info[ownProps.ownerUserId].avatar || '' : '',
fullName: state.user.info && state.user.info[ownProps.ownerUserId] ? state.user.info[ownProps.ownerUserId].fullName || '' : '',
commentCount: state.comment.postComments[ownProps.id] ? Object.keys(state.comment.postComments[ownProps.id]).length : 0,
voteCount: state.vote.postVotes[ownProps.id] ? Object.keys(state.vote.postVotes[ownProps.id]).length : 0,
userVoteStatus: votes && Object.keys(votes).filter((key) => votes[key].userId === state.authorize.uid)[0] ? true : false,
isPostOwner: post > 0
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(PostComponent as any)

View File

@@ -0,0 +1,2 @@
import PostComponent from './PostComponent'
export default PostComponent

View File

@@ -0,0 +1,28 @@
import { Post } from 'core/domain/posts'
export interface IPostPageComponentProps {
/**
* Load the post
*
* @memberof IPostPageComponentProps
*/
loadPost?: () => any
/**
* Load user profile
*
* @memberof IPostPageComponentProps
*/
loadUserInfo?: () => any
/**
* Route match
*
* @type {*}
* @memberof IPostPageComponentProps
*/
match?: any
posts: {[postId: string]: Post}
}

View File

@@ -0,0 +1,4 @@
export interface IPostPageComponentState {
}

View File

@@ -0,0 +1,88 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
// - Import app components
import Stream from 'components/stream'
// - Import API
// - Import actions
import * as postActions from 'actions/postActions'
import * as userActions from 'actions/userActions'
import { IPostPageComponentProps } from './IPostPageComponentProps'
import { IPostPageComponentState } from './IPostPageComponentState'
/**
* Create component class
*/
export class PostPageComponent extends Component<IPostPageComponentProps,IPostPageComponentState> {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IPostPageComponentProps) {
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 (
<Stream 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: any,ownProps: IPostPageComponentProps) => {
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: any,ownProps: IPostPageComponentProps) => {
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)(PostPageComponent as any)

View File

@@ -0,0 +1,2 @@
import PostPageComponent from './PostPageComponent'
export default PostPageComponent

View File

@@ -0,0 +1,95 @@
import { Post } from 'core/domain/posts'
export interface IPostWriteComponentProps {
/**
* If it's true post writing page will be open
*/
open: boolean
/**
* Recieve request close function
*/
onRequestClose: () => void
/**
* Post write style
*/
style?: {}
/**
* If it's true, post will be in edit view
*/
edit?: boolean
/**
* The text of post in editing state
*/
text?: string
/**
* The image of post in editing state
*/
image?: string
/**
* If post state is editing this id sould be filled with post identifier
*/
id?: string
/**
* The post has image {true} or not {false}
*
* @type {boolean}
* @memberof IPostWriteComponentProps
*/
postImageState?: boolean
/**
* User avatar address
*
* @type {string}
* @memberof IPostWriteComponentProps
*/
avatar?: string
/**
* User name
*
* @type {string}
* @memberof IPostWriteComponentProps
*/
name?: string
/**
* Post image full path
*
* @type {string}
* @memberof IPostWriteComponentProps
*/
imageFullPath?: string
/**
* Comment on the post is disabled {true} or not {false}
*
* @type {boolean}
* @memberof IPostWriteComponentProps
*/
disableComments?: boolean
/**
* Sharing on a post is disabled {true} or not {false}
*
* @type {boolean}
* @memberof IPostWriteComponentProps
*/
disableSharing?: boolean
/**
* Save a post
*
* @memberof IPostWriteComponentProps
*/
post?: (post: Post, callback: Function) => any
/**
* Update a post
*
* @memberof IPostWriteComponentProps
*/
update?: (post: Post, callback: Function) => any
}

View File

@@ -0,0 +1,33 @@
export interface IPostWriteComponentState {
/**
* Post text
*/
postText: string
/**
* The URL image of the post
*/
image?: string
/**
* The path identifier of image on the server
*/
imageFullPath: string
/**
* If it's true gallery will be open
*/
galleryOpen: boolean
/**
* If it's true post button will be disabled
*/
disabledPost: boolean
/**
* If it's true comment will be disabled on post
*/
disableComments: boolean
/**
* If it's true share will be disabled on post
*/
disableSharing: boolean
}

View File

@@ -0,0 +1,507 @@
// - 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 'components/imageGallery'
import Img from 'components/img'
import UserAvatarComponent from 'components/userAvatar'
// - Import API
import * as PostAPI from 'api/PostAPI'
// - Import actions
import * as imageGalleryActions from 'actions/imageGalleryActions'
import * as postActions from 'actions/postActions'
import { IPostWriteComponentProps } from './IPostWriteComponentProps'
import { IPostWriteComponentState } from './IPostWriteComponentState'
import { Post } from 'core/domain/posts'
// - Create PostWrite component class
export class PostWriteComponent extends Component<IPostWriteComponentProps,IPostWriteComponentState> {
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: IPostWriteComponentProps) {
super(props)
// Default state
this.state = {
/**
* Post text
*/
postText: this.props.edit ? this.props.text! : '',
/**
* The URL image of the post
*/
image: this.props.edit ? this.props.image : '',
/**
* The path identifier of image on the server
*/
imageFullPath: this.props.edit ? this.props.imageFullPath! : '',
/**
* 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: '',
imageFullPath: '',
disabledPost: false
})
}
/**
* Handle send post to the server
* @param {event} evt passed by clicking on the post button
*/
handlePost = () => {
const {
image,
imageFullPath,
disableComments,
disableSharing,
postText } = this.state
const {
id,
avatar,
name,
edit,
onRequestClose,
post,
update } = this.props
let tags = PostAPI.getContentTags(postText!)
// In edit status we should fire update if not we should fire post function
if (!edit) {
if (image !== '') {
post!({
body: postText,
tags: tags,
image: image,
imageFullPath: imageFullPath,
ownerAvatar: avatar,
ownerDisplayName: name,
disableComments: disableComments,
disableSharing: disableSharing,
postTypeId: 1,
score: 0,
viewCount: 0
}, onRequestClose)
} else {
post!({
body: postText,
tags: tags,
ownerAvatar: avatar,
ownerDisplayName: name,
disableComments: disableComments,
disableSharing: disableSharing,
postTypeId: 0,
score: 0,
viewCount: 0
}, onRequestClose)
}
} else { // In edit status we pass post to update functions
update!({
id: id,
body: postText,
tags: tags,
image: image,
imageFullPath: imageFullPath,
disableComments: disableComments,
disableSharing: disableSharing
}, onRequestClose)
}
}
/**
* Set post image url
*/
onRequestSetImage = (url: string, fullPath: string) => {
this.setState({
image: url,
imageFullPath: fullPath,
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 = (event: any, data: any) => {
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: IPostWriteComponentProps) {
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>
)
let postAvatar = <UserAvatarComponent fullName={this.props.name!} fileName={this.props.avatar!} style={{ top: '8px' }} size={40} />
let 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
key={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: any, ownProps: IPostWriteComponentProps) => {
return {
post: (post: Post, callBack: Function) => dispatch(postActions.dbAddImagePost(post, callBack)),
update: (post: Post, callBack: Function) => 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: any, ownProps: IPostWriteComponentProps) => {
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)(PostWriteComponent as any)

View File

@@ -0,0 +1,2 @@
import PostWriteComponent from './PostWriteComponent'
export default PostWriteComponent

View File

@@ -0,0 +1,82 @@
import { Post } from 'core/domain/posts'
export interface IProfileComponentProps {
/**
* Router match
*
* @type {*}
* @memberof IProfileComponentProps
*/
match: any
/**
* User's post
*
* @type {{[postId: string]: Post}}
* @memberof IProfileComponentProps
*/
posts: {[postId: string]: Post}
/**
* String user full name
*
* @type {string}
* @memberof IProfileComponentProps
*/
name: string
/**
* User tag line
*
* @type {string}
* @memberof IProfileComponentProps
*/
tagLine: string
/**
* User's avatar address
*
* @type {string}
* @memberof IProfileComponentProps
*/
avatar: string
/**
* It's current user profile {true} or not {false}
*
* @type {boolean}
* @memberof IProfileComponentProps
*/
isAuthedUser: boolean
/**
* User's banner
*
* @type {string}
* @memberof IProfileComponentProps
*/
banner: string
/**
* User identifier
*
* @type {string}
* @memberof IProfileComponentProps
*/
userId: string
/**
* Load user's post
*
* @memberof IProfileComponentProps
*/
loadPosts: () => any
/**
* Load user's profile
*
* @memberof IProfileComponentProps
*/
loadUserInfo: () => any
}

View File

@@ -0,0 +1,4 @@
export interface IProfileComponentState {
}

View File

@@ -0,0 +1,143 @@
// - 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 ProfileHeader from 'components/profileHeader'
import StreamComponent from 'components/stream'
// - Import API
// - Import actions
import * as postActions from 'actions/postActions'
import * as userActions from 'actions/userActions'
import * as globalActions from 'actions/globalActions'
import { IProfileComponentProps } from './IProfileComponentProps'
import { IProfileComponentState } from './IProfileComponentState'
/**
* Create component class
*/
export class ProfileComponent extends Component<IProfileComponentProps,IProfileComponentState> {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: IProfileComponentProps) {
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)'
}
}
return (
<div style={styles.profile}>
<div style={styles.header}>
<ProfileHeader tagLine={this.props.tagLine} 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>
<StreamComponent 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: any, ownProps: IProfileComponentProps) => {
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: any, ownProps: IProfileComponentProps) => {
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 || '' : '',
tagLine: state.user.info && state.user.info[userId] ? state.user.info[userId].tagLine || '' : '',
posts: state.post.userPosts ? state.post.userPosts[userId] : {},
isAuthedUser: userId === uid,
userId
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ProfileComponent as any)

View File

@@ -0,0 +1,2 @@
import ProfileComponent from './ProfileComponent'
export default ProfileComponent

View File

@@ -0,0 +1,65 @@
export interface IProfileHeaderComponentProps {
/**
* Profile for current user {true} or not {false}
*
* @type {boolean}
* @memberof IProfileHeaderComponentProps
*/
isAuthedUser: boolean
/**
* Image cover address
*
* @type {string}
* @memberof IProfileHeaderComponentProps
*/
banner: string
/**
* User full name
*
* @type {string}
* @memberof IProfileHeaderComponentProps
*/
fullName: string
/**
* User tag line
*
* @type {string}
* @memberof IProfileHeaderComponentProps
*/
tagLine: string
/**
* User's avatar address
*
* @type {string}
* @memberof IProfileHeaderComponentProps
*/
avatar: string
/**
* Open edit profile dialog
*
* @memberof IProfileHeaderComponentProps
*/
openEditor?: () => void
/**
* Number of user followers
*
* @type {number}
* @memberof IProfileHeaderComponentProps
*/
followerCount?: number
/**
* User identifier
*
* @type {string}
* @memberof IProfileHeaderComponentProps
*/
userId: string
}

View File

@@ -0,0 +1,11 @@
export interface IProfileHeaderComponentState {
/**
* Window size is small {true} or not {false}
*
* @type {boolean}
* @memberof IProfileHeaderComponentState
*/
isSmall: boolean
}

View File

@@ -0,0 +1,241 @@
// - 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 'components/imgCover'
import EditProfile from 'components/editProfile'
import UserAvatar from 'components/userAvatar'
// - Import API
// - Import actions
import * as globalActions from 'actions/globalActions'
import * as userActions from 'actions/userActions'
import { IProfileHeaderComponentProps } from './IProfileHeaderComponentProps'
import { IProfileHeaderComponentState } from './IProfileHeaderComponentState'
/**
* Create component class
*/
export class ProfileHeaderComponent extends Component<IProfileHeaderComponentProps, IProfileHeaderComponentState> {
static propTypes = {
/**
* User avatar address
*/
avatar: PropTypes.string,
/**
* User banner address
*/
banner: PropTypes.string,
/**
* User tagline
*/
tagLine: 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: IProfileHeaderComponentProps) {
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 = () => {
// Set initial state
let 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 || 'https://firebasestorage.googleapis.com/v0/b/open-social-33d92.appspot.com/o/images%2F751145a1-9488-46fd-a97e-04018665a6d3.JPG?alt=media&token=1a1d5e21-5101-450e-9054-ea4a20e06c57'} />
</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 fullName={this.props.fullName} fileName={this.props.avatar} size={60} style={styles.avatar} /></div>
<div className='info'>
<div className='fullName'>
{this.props.fullName}
</div>
<div className='tagLine'>
{this.props.tagLine}
</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
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: any, ownProps: IProfileHeaderComponentProps) => {
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: any, ownProps: IProfileHeaderComponentProps) => {
return {
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ProfileHeaderComponent as any)

View File

@@ -0,0 +1,2 @@
import ProfileHeaderComponent from './ProfileHeaderComponent'
export default ProfileHeaderComponent

View File

@@ -0,0 +1,16 @@
export interface ISettingComponentProps {
/**
* Login user
*
* @memberof ISettingComponentProps
*/
login: (email: string, password: string) => any
/**
* Redirect to home page
*
* @memberof ISettingComponentProps
*/
homePage: () => void
}

View File

@@ -0,0 +1,35 @@
export interface ISettingComponentState {
/**
* Password input value
*
* @type {string}
* @memberof ISettingComponentState
*/
passwordInput: string
/**
* Password input error text
*
* @type {string}
* @memberof ISettingComponentState
*/
passwordInputError: string
/**
* Confirm input value
*
* @type {string}
* @memberof ISettingComponentState
*/
confirmInput: string
/**
* Confirm input error
*
* @type {string}
* @memberof ISettingComponentState
*/
confirmInputError: string
}

View File

@@ -0,0 +1,219 @@
// - 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 'actions/authorizeActions'
import { ISettingComponentProps } from './ISettingComponentProps'
import { ISettingComponentState } from './ISettingComponentState'
/**
* Create component class
*
* @export
* @class SettingComponent
* @extends {Component}
*/
export class SettingComponent extends Component<ISettingComponentProps,ISettingComponentState> {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor (props: ISettingComponentProps) {
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 = (event: any) => {
const target = event.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 = () => {
let 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: any, ownProps: ISettingComponentProps) => {
return {
login: (password: string) => {
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: any, ownProps: ISettingComponentProps) => {
return {
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SettingComponent as any))

View File

@@ -0,0 +1,2 @@
import SettingComponent from './SettingComponent'
export default SettingComponent

Some files were not shown because too many files have changed in this diff Show More