Initial git

This commit is contained in:
Qolzam
2017-07-06 11:20:18 +04:30
commit 7c691d8e4d
142 changed files with 13046 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
public/bundle.js
config/
.vscode/

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2017 Amir Movahedi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-cayman

View File

@@ -0,0 +1,138 @@
// - Import react components
import {firebaseRef, firebaseAuth} from 'app/firebase/'
import moment from 'moment'
import {push} from 'react-router-redux'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
/* _____________ CRUD DB _____________ */
/**
* Log in user in server
* @param {string} email
* @param {string} password
*/
export var dbLogin = (email, password) => {
return (dispatch, getState) => {
dispatch(globalActions.showNotificationRequest())
return firebaseAuth().signInWithEmailAndPassword(email, password).then((result) => {
dispatch(globalActions.showNotificationSuccess())
dispatch(login(result.uid))
dispatch(push('/'))
}, (error) => dispatch(globalActions.showErrorMessage(error.code)))
}
}
/**
* Log out user in server
*/
export var dbLogout = () => {
return (dispatch, getState) => {
return firebaseAuth().signOut().then((result) => {
dispatch(logout())
dispatch(push('/login'))
}, (error) => dispatch(globalActions.showErrorMessage(error.code)))
}
}
/**
* Register user in database
* @param {object} user
*/
export var dbSignup = (user) => {
return (dispatch, getState) => {
dispatch(globalActions.showNotificationRequest())
return firebaseAuth().createUserWithEmailAndPassword(user.email, user.password).then((signupResult) => {
firebaseRef.child(`users/${signupResult.uid}/info`).set({
...user
}).then((result) => {
dispatch(globalActions.showNotificationSuccess())
}, (error) => dispatch(globalActions.showErrorMessage(error.code)))
dispatch(signup({
uid: signupResult.uid,
...user
}))
dispatch(push('/'))
}, (error) => dispatch(globalActions.showErrorMessage(error.code)))
}
}
/**
* Change user's password
* @param {string} newPassword
*/
export const dbUpdatePassword = (newPassword) => {
return (dispatch, getState) => {
dispatch(globalActions.showNotificationRequest())
firebaseAuth().onAuthStateChanged((user) => {
if (user) {
user.updatePassword(newPassword).then(() => {
// Update successful.
dispatch(globalActions.showNotificationSuccess())
dispatch(updatePassword())
dispatch(push('/'))
}, (error) => {
// An error happened.
switch (error.code) {
case 'auth/requires-recent-login':
dispatch(globalActions.showErrorMessage(error.code))
dispatch(dbLogout())
break;
default:
}
})
}
})
}
}
/* _____________ CRUD State _____________ */
/**
* Loing user
* @param {string} uid
*/
export var login = (uid) => {
return {type: types.LOGIN, authed: true, uid}
}
/**
* Logout user
*/
export var logout = () => {
return {type: types.LOGOUT}
}
/**
* Register user
* @param {object} user
*/
export var signup = (user) => {
return {
type: types.SIGNUP,
...user
}
}
/**
* Update user's password
*/
export const updatePassword = () => {
return {type: types.UPDATE_PASSWORD}
}

View File

@@ -0,0 +1,337 @@
// - Import firebase component
import firebase, { firebaseRef } from 'app/firebase/'
// - Import utility components
import moment from 'moment'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
import * as postActions from 'postActions'
import * as userActions from 'userActions'
import * as notifyActions from 'notifyActions'
/* _____________ CRUD DB _____________ */
/**
* Add a circle
* @param {string} circleName
*/
export var dbAddCircle = (circleName) => {
return (dispatch, getState) => {
let uid = getState().authorize.uid
let circle = {
creationDate: moment().unix(),
name: circleName,
users: {}
}
let circleRef = firebaseRef.child(`userCircles/${uid}/circles`).push(circle)
return circleRef.then(() => {
dispatch(addCircle(uid, {
...circle,
id: circleRef.key
}))
}, (error) => dispatch(globalActions.showErrorMessage(error.message)))
}
}
/**
* Add a user in a circle
* @param {string} cid is circle identifier
* @param {object} userFollowing is the user for following
*/
export var dbAddFollowingUser = (cid, userFollowing) => {
return (dispatch, getState) => {
let uid = getState().authorize.uid
let user = getState().user.info[uid]
let userCircle = {
creationDate: moment().unix(),
fullName: userFollowing.fullName,
avatar: userFollowing.avatar || ''
}
let userFollower = {
creationDate: moment().unix(),
fullName: user.fullName,
avatar: user.avatar || '',
approved: false
}
let updates = {}
updates[`userCircles/${uid}/circles/${cid}/users/${userFollowing.userId}`] = userCircle
updates[`userCircles/${userFollowing.userId}/circles/-Followers/users/${uid}`] = userFollower
return firebaseRef.update(updates).then((result) => {
dispatch(addFollowingUser(uid, cid, userFollowing.userId, { ...userCircle }))
dispatch(notifyActions.dbAddNotify(
{
description: `${user.fullName} follow you.`,
url: `/${uid}`,
notifyRecieverUserId: userFollowing.userId, notifierUserId: uid
}))
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
})
}
}
/**
* Add a user in a circle
* @param {string} cid is circle identifier
* @param {string} followingId following user identifier
*/
export var dbDeleteFollowingUser = (cid, followingId) => {
return (dispatch, getState) => {
let uid = getState().authorize.uid
let updates = {}
updates[`userCircles/${uid}/circles/${cid}/users/${followingId}`] = null
updates[`userCircles/${followingId}/circles/-Followers/users/${uid}`] = null
return firebaseRef.update(updates).then((result) => {
dispatch(deleteFollowingUser(uid, cid, followingId))
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
})
}
}
/**
* Update a circle from database
* @param {object} newCircle
*/
export const dbUpdateCircle = (newCircle) => {
return (dispatch, getState) => {
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
let updates = {}
let circle = getState().circle.userCircles[uid][newCircle.id]
let updatedCircle = {
name: newCircle.name || circle.name,
users: newCircle.users ? newCircle.users : (circle.users || [])
}
updates[`userCircles/${uid}/circles/${newCircle.id}`] = updatedCircle
return firebaseRef.update(updates).then((result) => {
dispatch(updateCircle(uid, { id: newCircle.id, ...updatedCircle }))
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
})
}
}
/**
* Delete a circle from database
* @param {string} id is circle identifier
*/
export const dbDeleteCircle = (id) => {
return (dispatch, getState) => {
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
var updates = {};
updates[`userCircles/${uid}/circles/${id}`] = null;
return firebaseRef.update(updates).then((result) => {
dispatch(deleteCircle(uid, id))
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
});
}
}
/**
* Get all user circles from data base
*/
export const dbGetCircles = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var circlesRef = firebaseRef.child(`userCircles/${uid}/circles`);
return circlesRef.once('value').then((snapshot) => {
var circles = snapshot.val() || {};
var parsedCircles = {};
Object.keys(circles).forEach((circleId) => {
if (circleId !== '-Followers' && circles[circleId].users) {
Object.keys(circles[circleId].users).filter((v, i, a) => a.indexOf(v) === i).forEach((userId) => {
dispatch(postActions.dbGetPostsByUserId(userId))
dispatch(userActions.dbGetUserInfoByUserId(userId))
})
}
parsedCircles[circleId] = {
id: circleId,
...circles[circleId]
}
})
dispatch(addCircles(uid, parsedCircles));
});
}
}
}
/**
* Get all user circles from data base by user id
*/
export const dbGetCirclesByUserId = (uid) => {
return (dispatch, getState) => {
if (uid) {
var circlesRef = firebaseRef.child(`userCircles/${uid}/circles`);
return circlesRef.once('value').then((snapshot) => {
var circles = snapshot.val() || {};
var parsedCircles = {};
Object.keys(circles).forEach((circleId) => {
parsedCircles[circleId] = {
id: circleId,
...circles[circleId]
}
})
dispatch(addCircles(uid, parsedCircles));
})
}
}
}
/* _____________ CRUD State _____________ */
/**
* Add a normal circle
* @param {string} uid is user identifier
* @param {object} circle
*/
export const addCircle = (uid, circle) => {
return {
type: types.ADD_CIRCLE,
payload: { uid, circle }
}
}
/**
* Update a circle
* @param {string} uid is user identifier
* @param {object} circle
*/
export const updateCircle = (uid, circle) => {
return {
type: types.UPDATE_CIRCLE,
payload: { uid, circle }
}
}
/**
* Delete a circle
* @param {string} uid is user identifier
* @param {string} id is circle identifier
*/
export const deleteCircle = (uid, id) => {
return {
type: types.DELETE_CIRCLE,
payload: { uid, id }
}
}
/**
* Add a list of circle
* @param {string} uid
* @param {[object]} circles
*/
export const addCircles = (uid, circles) => {
return {
type: types.ADD_LIST_CIRCLE,
payload: { uid, circles }
}
}
/**
* Clea all data in circle store
*/
export const clearAllCircles = () => {
return {
type: types.CLEAR_ALL_CIRCLES
}
}
/**
* Open circle settings
*/
export const openCircleSettings = (uid, id) => {
return {
type: types.OPEN_CIRCLE_SETTINGS,
payload: { uid, id }
}
}
/**
* Close open circle settings
*/
export const closeCircleSettings = (uid, id) => {
return {
type: types.CLOSE_CIRCLE_SETTINGS,
payload: { uid, id }
}
}
/**
* Add following user in a circle
* @param {string} uid user identifire who want to follow the following user
* @param {string} cid circle identifier that following user should be added in
* @param {string} followingId following user identifier
* @param {object} userCircle information about following user
*/
export const addFollowingUser = (uid, cid, followingId, userCircle) => {
return {
type: types.ADD_FOLLOWING_USER,
payload: { uid, cid, followingId, userCircle }
}
}
/**
* Delete following user from a circle
* @param {string} uid user identifire who want to follow the following user
* @param {string} cid circle identifier that following user should be added in
* @param {string} followingId following user identifier
*/
export const deleteFollowingUser = (uid, cid, followingId) => {
return {
type: types.DELETE_FOLLOWING_USER,
payload: { uid, cid, followingId }
}
}

View File

@@ -0,0 +1,231 @@
// - Import react components
import moment from 'moment'
import { firebaseRef } from 'app/firebase/'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
import * as notifyActions from 'notifyActions'
/* _____________ CRUD DB _____________ */
/**
* Add comment to database
* @param {object} newComment user comment
* @param {function} callBack will be fired when server responsed
*/
export const dbAddComment = (newComment, callBack) => {
return (dispatch, getState) => {
dispatch(globalActions.showTopLoading())
var uid = getState().authorize.uid
var comment = {
postId: newComment.postId,
score: 0,
text: newComment.text,
creationDate: moment().unix(),
userDisplayName: getState().user.info[uid].fullName,
userAvatar: getState().user.info[uid].avatar,
userId: uid
}
var commentRef = firebaseRef.child(`postComments/${newComment.postId}`).push(comment)
return commentRef.then(() => {
dispatch(addComment(
{
comment,
postId: newComment.postId,
id: commentRef.key,
editorStatus: false
}))
callBack()
dispatch(globalActions.hideTopLoading())
if (newComment.ownerPostUserId !== uid)
dispatch(notifyActions.dbAddNotify(
{
description: 'Add comment on your post.',
url: `/${newComment.ownerPostUserId}/posts/${newComment.postId}`,
notifyRecieverUserId: newComment.ownerPostUserId, notifierUserId: uid
}))
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
dispatch(globalActions.hideTopLoading())
})
}
}
/**
* Get all comments from database
*/
export const dbGetComments = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var commentsRef = firebaseRef.child(`postComments`);
return commentsRef.on('value', (snapshot) => {
var comments = snapshot.val() || {};
dispatch(addCommentList(comments))
})
}
}
}
/**
* Update a comment from database
* @param {string} id of comment
* @param {string} postId is the identifier of the post which comment belong to
*/
export const dbUpdateComment = (id, postId, text) => {
return (dispatch, getState) => {
dispatch(globalActions.showTopLoading())
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
var updates = {};
let comment = getState().comment.postComments[postId][id]
updates[`postComments/${postId}/${id}`] = {
postId: postId,
score: comment.score,
text: text,
creationDate: comment.creationDate,
userDisplayName: comment.userDisplayName,
userAvatar: comment.userAvatar,
userId: uid
}
return firebaseRef.update(updates).then((result) => {
dispatch(updateComment({ id, postId, text, editorStatus: false }))
dispatch(globalActions.hideTopLoading())
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
dispatch(globalActions.hideTopLoading())
})
}
}
/**
* Delete a comment from database
* @param {string} id of comment
* @param {string} postId is the identifier of the post which comment belong to
*/
export const dbDeleteComment = (id, postId) => {
return (dispatch, getState) => {
dispatch(globalActions.showTopLoading())
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
var updates = {};
updates[`postComments/${postId}/${id}`] = null;
return firebaseRef.update(updates).then((result) => {
dispatch(deleteComment(id, postId))
dispatch(globalActions.hideTopLoading())
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
dispatch(globalActions.hideTopLoading())
})
}
}
/* _____________ CRUD State _____________ */
/**
* Add comment
* @param {object} data
*/
export const addComment = (data) => {
return {
type: types.ADD_COMMENT,
payload: data
}
}
/**
* Update comment
* @param {object} data
*/
export const updateComment = (data) => {
return {
type: types.UPDATE_COMMENT,
payload: data
}
}
/**
* Add comment list
* @param {[object]} postComments an array of comments
*/
export const addCommentList = (postComments) => {
return {
type: types.ADD_COMMENT_LIST,
payload: postComments
}
}
/**
* Delete a comment
* @param {string} id of comment
* @param {string} postId is the identifier of the post which comment belong to
*/
export const deleteComment = (id, postId) => {
return { type: types.DELETE_COMMENT, payload: { id, postId } }
}
/**
* Clear all data
*/
export const clearAllData = () => {
return {
type: types.CLEAR_ALL_DATA_COMMENT
}
}
export const openCommentEditor = (comment) => {
return {
type: types.OPEN_COMMENT_EDITOR,
payload: comment
}
}
export const closeCommentEditor = (comment) => {
return {
type: types.CLOSE_COMMENT_EDITOR,
payload: comment
}
}

View File

@@ -0,0 +1,52 @@
// - Import react components
import moment from 'moment'
// - Import image gallery action types
import * as types from 'actionTypes'
// - Import actions
import * as imageGalleryActions from 'imageGalleryActions'
// - Import firebase
import {storageRef,firebaseRef} from 'app/firebase/'
// - Upload file start
export const uploadFile = (file) => {
return (dispatch,getState) => {
}
}
// - Upload file error
export const uploadError = (error) => {
return{
type: types.UPLOAD_FILE_ERROR,
error
}
}
// - Uplaod file complete
export const uploadComplete = (result) => {
return{
type: types.UPLOAD_FILE_COMPLETE,
result
}
}
// - Download file
export const downloadFile = (fileName) => {
return {
type: types.DOWNLOAD_FILE,
fileName
}
}

View File

@@ -0,0 +1,218 @@
// - Import image gallery action types
import * as types from 'actionTypes'
// - Import actions
import * as postActions from 'postActions'
import * as commentActions from 'commentActions'
import * as userActions from 'userActions'
/**
* Progress change
* @param {string} percent
* @param {boolean} visible
*/
export const progressChange = (percent, visible) => {
return {
type: types.PROGRESS_CHANGE,
payload: {percent, visible}
}
}
/**
* Default data loaded status will be true
* @param {boolean} status
*/
export const defaultDataEnable = (status) => {
return{
type: types.DEFAULT_DATA_ENABLE
}
}
/**
* Default data loaded status will be false
* @param {boolean} status
*/
export const defaultDataDisable = (status) => {
return{
type: types.DEFAULT_DATA_DISABLE
}
}
/**
* Show notification normally
* @param {string} message
*/
export const showNotificationNormal = (message) => {
return (dispatch,getState) => {
dispatch(showNormalMessage(message))
}
}
// - Show global normal message
export const showNormalMessage = (message) => {
return{
type: types.SHOW_NORMAL_MESSAGE_GLOBAL,
message
}
}
// - Show notification of request
export const showNotificationRequest = () => {
return (dispatch,getState) => {
dispatch(showSendRequestMessage())
}
}
// - Show global request sent message
export const showSendRequestMessage = () => {
return{
type: types.SHOW_SEND_REQUEST_MESSAGE_GLOBAL
}
}
// - Show notification of success
export const showNotificationSuccess = () => {
return (dispatch,getState) => {
dispatch(showRequestSuccessMessage())
}
}
// - Show global request successful message
export const showRequestSuccessMessage = () => {
return{
type: types.SHOW_REQUEST_SUCCESS_MESSAGE_GLOBAL
}
}
/**
* Hide global message
*/
export const hideMessage = () => {
return{
type: types.HIDE_MESSAGE_GLOBAL
}
}
/**
* Show error message
* @param {string} message
*/
export const showErrorMessage = (message) => {
return {
type: types.SHOW_ERROR_MESSAGE_GLOBAL,
payload: message
}
}
/**
* Set header title
*/
export const setHeaderTitleOpt = (opt,payload) => {
return (dispatch,getState) => {
switch (opt) {
case 'profile':
const userName = getState().user.info && getState().user.info[payload] ? getState().user.info[payload].fullName : ''
dispatch(setHeaderTitle(userName))
break;
default:
break;
}
}
}
/**
* Set header title
*/
export const setHeaderTitle = (text) => {
return{
type: types.SET_HEADER_TITLE,
payload: text
}
}
/**
* Open post write
*/
export const openPostWrite = () => {
return{
type: types.OPEN_POST_WRITE
}
}
/**
* Close post write
*/
export const closePostWrite = () => {
return{
type: types.CLOSE_POST_WRITE
}
}
/**
* Show top loading
*/
export const showTopLoading = () => {
return{
type: types.SHOW_TOP_LOADING
}
}
/**
* Hide top loading
*/
export const hideTopLoading = () => {
return{
type: types.HIDE_TOP_LOADING
}
}
/**
* Store temp data
*/
export const temp = (data) => {
return{
type: types.TEMP,
payload: data
}
}
// - Load data for guest
export const loadDataGuest = () => {
return (dispatch,getState) => {
var userString = "{\"avatar\":\"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg\",\"contact\":\"amir.gholzam@live.com\",\"email\":\"amir.gholzam@live.com\",\"fullName\":\"React Social Blog\",\"password\":\"123qwe\",\"summary\":\" The React Social Blog (RSB) Application is a diary app blog\"}"
var postString = '[{"id":"-KkauHBOZXlALsHIrNsvsq","body":"The React Social Blog (RSB) Application is a diary app blog based on Semantic ui React for UI, Redux with react-redux for managing states and React for managing DOM .It is an open source project as a portfolio.\\n\\nI will be really grateful to receive any issue: \\nhttps://github.com/Qolzam/react-blog/issues\\n\\n \\n","commentCounter":0,"creationDate":1495301432,"deletationDate":"","deleted":false,"image":"http://www.freeimageslive.com/galleries/nature/abstract/preview/frosty_grass.jpg","lastEditDate":"","ownerAvatar":"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg","ownerDisplayName":"React Social Blog","ownerUserId":"5flWuB1RieZR7GIAwHYMPYaI5o33","postTypeId":1,"score":0,"video":"","viewCount":0},{"id":"-KkauHBOZXlALsHIrNIq","body":"It is a demo website","commentCounter":0,"creationDate":1495301432,"deletationDate":"","deleted":false,"image":"http://www.freeimageslive.com/galleries/nature/environment/pics/eaten%20_flower0905.jpg","lastEditDate":"","ownerAvatar":"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg","ownerDisplayName":"React Social Blog","ownerUserId":"5flWuB1RieZR7GIAwHYMPYaI5o33","postTypeId":1,"score":0,"video":"","viewCount":0},{"id":"-KkauHBOZXlsLsHIrNIq","body":"This is an open source project","commentCounter":0,"creationDate":1495301432,"deletationDate":"","deleted":false,"image":"http://www.freeimageslive.com/galleries/nature/environment/pics/eaten%20_flower0905.jpg","lastEditDate":"","ownerAvatar":"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg","ownerDisplayName":"React Social Blog","ownerUserId":"5flWuB1RieZR7GIAwHYMPYaI5o33","postTypeId":1,"score":0,"video":"","viewCount":0},{"id":"-KkaurBOZXlALsHIrNIq","body":"I have documentaion, testing, add some features DEBUG in my todo list","commentCounter":0,"creationDate":1495301432,"deletationDate":"","deleted":false,"image":"http://www.freeimageslive.com/galleries/nature/environment/pics/eaten%20_flower0905.jpg","lastEditDate":"","ownerAvatar":"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg","ownerDisplayName":"React Social Blog","ownerUserId":"5flWuB1RieZR7GIAwHYMPYaI5o33","postTypeId":1,"score":0,"video":"","viewCount":0}]'
var postCommentString = "{\"postComments\":{\"-KkauHBOZXlALsHIrNIq\":{\"-KkaxkH1WmfcQaidsNHK3R\":{\"creationDate\":1495302341,\"postId\":\"-KkauHBOZXlALsHIrNIq\",\"score\":0,\"text\":\"On developing :)\",\"userAvatar\":\"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg\",\"userDisplayName\":\"React Social Blog\",\"userId\":\"5flWuB1RieZR7GIAwHYMPYaI5o33\"},\"-KkafsdfxkH1WmfcQaiNHK3R\":{\"creationDate\":1495302341,\"postId\":\"-KkauHBOZXlALsHIrNIq\",\"score\":0,\"text\":\"This is a good project for lorem if you want to learn more and deeply about react and ui frameworkes. I'm preparing a good document for lorem :)\",\"userAvatar\":\"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg\",\"userDisplayName\":\"React Social Blog\",\"userId\":\"5flWuB1RieZR7GIAwHYMPYaI5o33\"},\"-KkaxkH1WfrmfcQaiNHK3R\":{\"creationDate\":1495302341,\"postId\":\"-KkauHBOZXlALsHIrNIq\",\"score\":0,\"text\":\"On I'm so happy now that I have you react-blog so far I was looking for you oh it's just lorem so don't make it serious :)\",\"userAvatar\":\"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg\",\"userDisplayName\":\"React Social Blog\",\"userId\":\"5flWuB1RieZR7GIAwHYMPYaI5o33\"},\"-KkaxkH1WmfcQakeiNHK3R\":{\"creationDate\":1495302341,\"postId\":\"-KkauHBOZXlALsHIrNIq\",\"score\":0,\"text\":\"On developing :)\",\"userAvatar\":\"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg\",\"userDisplayName\":\"React Social Blog\",\"userId\":\"5flWuB1RieZR7GIAwHYMPYaI5o33\"},\"-KkaxkH1WmfcQpraiNHK3R\":{\"creationDate\":1495302341,\"postId\":\"-KkauHBOZXlALsHIrNIq\",\"score\":0,\"text\":\"On developing :)\",\"userAvatar\":\"http://www.freeimageslive.com/galleries/nature/abstract/preview/frostyleaves00406.jpg\",\"userDisplayName\":\"React Social Blog\",\"userId\":\"5flWuB1RieZR7GIAwHYMPYaI5o33\"}}}}"
var user = JSON.parse(userString)
dispatch(userActions.addUserInfo(user))
var post = JSON.parse(postString)
dispatch(postActions.addPosts(post))
var postComment = JSON.parse(postCommentString)
dispatch(commentActions.addCommentList(postComment.postComments))
}
}

View File

@@ -0,0 +1,248 @@
// - Import react componetns
import moment from 'moment'
import { firebaseRef, firebaseAuth, storageRef } from 'app/firebase/'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
// - Import app API
import FileAPI from 'FileAPI'
/* _____________ UI Actions _____________ */
/**
* Clear selected data
*/
export const clearSelectData = () => {
return { type: types.CLEARS_SELECT_IMAGE_GALLERY }
}
/**
* Clear all data in image gallery store
*/
export const clearAllData = () => {
return {
type: types.CLEAT_ALL_DATA_IMAGE_GALLERY
}
}
/**
* Download images for image gallery
*/
export const downloadForImageGallery = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var imagesRef = firebaseRef.child(`userFiles/${uid}/files/images`);
return imagesRef.once('value').then((snapshot) => {
var images = snapshot.val() || {};
var parsedImages = []
Object.keys(images).forEach((imageId) => {
parsedImages.push({
id: imageId,
...images[imageId]
})
})
dispatch(addImageList(parsedImages));
})
}
}
}
/* _____________ CRUD Database_____________ */
/**
* Save image in the server
* @param {string} imageName is the name of image
*/
export const dbSaveImage = (imageName) => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
var image = {
creationDate: moment().unix(),
deletationDate: '',
name: imageName,
ownerUserId: uid,
lastEditDate: '',
deleted: false
}
var imageRef = firebaseRef.child(`userFiles/${uid}/files/images`).push(image)
return imageRef.then(() => {
dispatch(addImage({
...image,
id: imageRef.key
}))
})
}
}
/**
* Delete an image from database
* @param {string} id of image
*/
export const dbDeleteImage = (id) => {
return (dispatch, getState) => {
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
var updates = {};
updates[`userFiles/${uid}/files/images/${id}`] = null;
return firebaseRef.update(updates).then((result) => {
dispatch(deleteImage(id))
console.log('image removed: ', id);
}, (error) => {
console.log(error);
});
}
}
/**
* Upload image on the server
* @param {file} file
* @param {string} fileName
*/
export const dbUploadImage = (file, fileName) => {
return (dispatch, getState) => {
// Create a storage refrence
var storegeFile = storageRef.child(`images/${fileName}`)
// Upload file
var task = storegeFile.put(file)
dispatch(globalActions.showTopLoading())
// Upload storage bar
task.on('state_changed', (snapshot) => {
var percentage = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
dispatch(globalActions.progressChange(percentage, true))
}, (error) => {
dispatch(globalActions.showErrorMessage(error.code))
dispatch(globalActions.hideTopLoading())
}, (complete) => {
dispatch(globalActions.progressChange(100, false))
dispatch(dbSaveImage(fileName))
dispatch(globalActions.hideTopLoading())
})
}
}
/**
* Download image from server
* @param {string} fileName
*/
export const dbDownloadImage = (fileName) => {
return (dispatch, getState) => {
if (getState().imageGallery.imageURLList[fileName] && fileName !== '')
return
if(getState().imageGallery.imageRequests.indexOf(fileName) > -1)
return
dispatch(sendImageRequest(fileName))
// Create a reference to the file we want to download
var starsRef = storageRef.child(`images/${fileName}`);
// Get the download URL
starsRef.getDownloadURL().then((url) => {
// Insert url into an <img> tag to "download"
if (!getState().imageGallery.imageURLList[fileName] || fileName === '')
dispatch(setImageURL(fileName, url))
}).catch((error) => {
// A full list of error codes is available at
// https://firebase.google.com/docs/storage/web/handle-errors
switch (error.code) {
case 'storage/object_not_found':
// File doesn't exist
dispatch(globalActions.showErrorMessage('storage/object_not_found'))
break;
case 'storage/unauthorized':
// User doesn't have permission to access the object
dispatch(globalActions.showErrorMessage('storage/unauthorized'))
break;
case 'storage/canceled':
// User canceled the upload
dispatch(globalActions.showErrorMessage('storage/canceled'))
break;
case 'storage/unknown':
// Unknown error occurred, inspect the server response
dispatch(globalActions.showErrorMessage('storage/unknown'))
break;
}
});
}
}
/* _____________ CRUD State _____________ */
/**
* Add image list to image gallery
* @param {[object]} images is an array of images
*/
export const addImageList = (images) => {
return { type: types.ADD_IMAGE_LIST_GALLERY, images }
}
/**
* Add image to image gallery
* @param {object} image
*/
export const addImage = (image) => {
return { type: types.ADD_IMAGE_GALLERY, image }
}
/**
* Delete an image
* @param {string} id is an image identifier
*/
export const deleteImage = (id) => {
return { type: types.DELETE_IMAGE, id }
}
/**
* Delete an image
*/
export const setImageURL = (name, url) => {
return {
type: types.SET_IMAGE_URL,
payload: { name, url }
}
}
/**
* Send image request
*/
export const sendImageRequest = (name) => {
return {
type: types.SEND_IMAGE_REQUEST,
payload: name
}
}

View File

@@ -0,0 +1,20 @@
// - Import image uploader action types
import * as types from 'actionTypes'
// - Image uploader actions
export const openImageUploader = (status)=> {
return {
type: types.OPEN_IMAGE_UPLOADER,
status
}
}
export const openImageEditor = (editStatus) =>{
return{
type: types.OPEN_IMAGE_EDITOR,
editStatus
}
}

View File

@@ -0,0 +1,168 @@
// - Import react components
import moment from 'moment'
import { firebaseRef } from 'app/firebase/'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
import * as userActions from 'userActions'
/* _____________ CRUD DB _____________ */
/**
* Add notificaition to database
* @param {object} newNotify user notificaition
*/
export const dbAddNotify = (newNotify) => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
var notify = {
description: newNotify.description,
url: newNotify.url,
notifierUserId: newNotify.notifierUserId,
isSeen: false
}
var notifyRef = firebaseRef.child(`userNotifies/${newNotify.notifyRecieverUserId}`).push(notify)
return notifyRef.then(() => {
dispatch(addNotify())
}, (error) => dispatch(globalActions.showErrorMessage(error.message)))
}
}
/**
* Get all notificaitions from database
*/
export const dbGetNotifies = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var notifiesRef = firebaseRef.child(`userNotifies/${uid}`);
return notifiesRef.on('value', (snapshot) => {
var notifies = snapshot.val() || {};
Object.keys(notifies).forEach((key => {
if (!getState().user.info[notifies[key].notifierUserId]) {
dispatch(userActions.dbGetUserInfoByUserId(notifies[key].notifierUserId))
}
}))
dispatch(addNotifyList(notifies))
})
}
}
}
/**
* Delete a notificaition from database
* @param {string} id of notificaition
*/
export const dbDeleteNotify = (id) => {
return (dispatch, getState) => {
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
var updates = {};
updates[`userNotifies/${uid}/${id}`] = null;
return firebaseRef.update(updates).then((result) => {
dispatch(deleteNotify(id))
}, (error) => dispatch(globalActions.showErrorMessage(error.message)))
}
}
/**
* Make seen a notificaition from database
* @param {string} id of notificaition
*/
export const dbSeenNotify = (id) => {
return (dispatch, getState) => {
// Get current user id
var uid = getState().authorize.uid
let notify = getState().notify.userNotifies[id]
let updatedNotify = {
description: notify.description,
url: notify.url,
notifierUserId: notify.notifierUserId,
isSeen: true
}
// Write the new data simultaneously in the list
var updates = {};
updates[`userNotifies/${uid}/${id}`] = updatedNotify;
return firebaseRef.update(updates).then((result) => {
dispatch(seenNotify(id))
}, (error) => dispatch(globalActions.showErrorMessage(error.message)))
}
}
/* _____________ CRUD State _____________ */
/**
* Add notificaition
* @param {object} data
*/
export const addNotify = () => {
return {
type: types.ADD_NOTIFY
}
}
/**
* Add notificaition list
* @param {[object]} userNotifies an array of notificaitions
*/
export const addNotifyList = (userNotifies) => {
return {
type: types.ADD_NOTIFY_LIST,
payload: userNotifies
}
}
/**
* Delete a notificaition
* @param {string} id of notificaition
*/
export const deleteNotify = (id) => {
return { type: types.DELETE_NOTIFY, payload: id }
}
/**
* Change notificaition to has seen status
* @param {string} id of notificaition
*/
export const seenNotify = (id) => {
return { type: types.SEEN_NOTIFY, payload: id }
}
/**
* Clear all data
*/
export const clearAllNotifications = () => {
return {
type: types.CLEAR_ALL_DATA_NOTIFY
}
}

339
app/actions/postActions.jsx Normal file
View File

@@ -0,0 +1,339 @@
// - Import firebase component
import firebase, {firebaseRef} from 'app/firebase/'
// - Import utility components
import moment from 'moment'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
/* _____________ CRUD DB _____________ */
/**
* Add a normal post
* @param {object} newPost
* @param {function} callBack
*/
export var dbAddPost = (newPost,callBack) => {
return(dispatch,getState) => {
var uid = getState().authorize.uid
var post = {
postTypeId: 0,
creationDate: moment().unix(),
deletationDate: '',
score: 0,
viewCount: 0,
body: newPost.body,
ownerUserId: uid,
ownerDisplayName: newPost.name,
ownerAvatar: newPost.avatar,
lastEditDate: '',
tags: newPost.tags || [],
commentCounter: 0,
image:'',
video:'',
disableComments: newPost.disableComments,
disableSharing: newPost.disableSharing,
deleted:false
}
var postRef = firebaseRef.child(`userPosts/${uid}/posts`).push(post)
return postRef.then(()=>{
dispatch(addPost(uid,{
...post,
id: postRef.key
}))
callBack()
},(error) => dispatch(globalActions.showErrorMessage(error.message)))
}
}
/**
* Add a post with image
* @param {object} newPost
* @param {function} callBack
*/
export const dbAddImagePost = (newPost,callBack) => {
return(dispatch,getState) => {
dispatch(globalActions.showTopLoading())
var uid = getState().authorize.uid
var post = {
postTypeId: 1,
creationDate: moment().unix(),
deletationDate: '',
score: 0,
viewCount: 0,
body: newPost.body,
ownerUserId: uid,
ownerDisplayName: newPost.name,
ownerAvatar: newPost.avatar,
lastEditDate: '',
tags: newPost.tags || [],
commentCounter: 0,
image: newPost.image || '',
video:'',
disableComments: newPost.disableComments ? newPost.disableComments : false,
disableSharing: newPost.disableSharing ? newPost.disableSharing : false,
deleted:false
}
var postRef = firebaseRef.child(`userPosts/${uid}/posts`).push(post)
return postRef.then(()=>{
dispatch(addPost(uid,{
...post,
id: postRef.key
}))
callBack()
dispatch(globalActions.hideTopLoading())
})
}
}
/**
* Update a post from database
* @param {object} newPost
* @param {func} callBack //TODO: anti pattern should change to parent state or move state to redux
*/
export const dbUpdatePost = (newPost,callBack) => {
console.log(newPost)
return (dispatch, getState) => {
dispatch(globalActions.showTopLoading())
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
let updates = {};
let post = getState().post.userPosts[uid][newPost.id]
let updatedPost = {
postTypeId: post.postTypeId,
creationDate: post.creationDate,
deletationDate: '',
score: post.score,
viewCount: post.viewCount,
body: newPost.body ? newPost.body : post.body || '',
ownerUserId: uid,
ownerDisplayName: post.ownerDisplayName,
ownerAvatar: post.ownerAvatar,
lastEditDate: moment().unix(),
tags: newPost.tags ? newPost.tags : (post.tags || []),
commentCounter: post.commentCounter,
image: newPost.image ? newPost.image : post.image,
video:'',
disableComments: newPost.disableComments !== undefined ? newPost.disableComments : (post.disableComments ? post.disableComments : false),
disableSharing: newPost.disableSharing !== undefined ? newPost.disableSharing : (post.disableSharing ? post.disableSharing : false),
deleted:false
}
updates[`userPosts/${uid}/posts/${newPost.id}`] = updatedPost
return firebaseRef.update(updates).then((result) => {
dispatch(updatePost(uid,{id:newPost.id, ...updatedPost}))
callBack()
dispatch(globalActions.hideTopLoading())
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
dispatch(globalActions.hideTopLoading())
})
}
}
/**
* Delete a post from database
* @param {string} id is post identifier
*/
export const dbDeletePost = (id) => {
return (dispatch, getState) => {
dispatch(globalActions.showTopLoading())
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
var updates = {};
updates[`userPosts/${uid}/posts/${id}`] = null;
return firebaseRef.update(updates).then((result) => {
dispatch(deletePost(uid,id))
dispatch(globalActions.hideTopLoading())
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
dispatch(globalActions.hideTopLoading())
});
}
}
/**
* Get all user posts from data base
*/
export const dbGetPosts = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var postsRef = firebaseRef.child(`userPosts/${uid}/posts`);
return postsRef.once('value').then((snapshot) => {
var posts = snapshot.val() || {};
var parsedPosts = {};
Object.keys(posts).forEach((postId) => {
parsedPosts[postId]={
id: postId,
...posts[postId]
};
});
dispatch(addPosts(uid,parsedPosts));
});
}
}
}
/**
* Get all user posts from data base
*/
export const dbGetPostById = (uid,postId) => {
return (dispatch, getState) => {
if (uid) {
var postsRef = firebaseRef.child(`userPosts/${uid}/posts/${postId}`);
return postsRef.once('value').then((snapshot) => {
const newPost = snapshot.val() || {};
const post = {
id : postId,
...newPost
}
dispatch(addPost(uid,post));
});
}
}
}
/**
* Get all user posts from data base by user id
*/
export const dbGetPostsByUserId = (uid) => {
return (dispatch, getState) => {
if (uid) {
var postsRef = firebaseRef.child(`userPosts/${uid}/posts`);
return postsRef.once('value').then((snapshot) => {
var posts = snapshot.val() || {};
var parsedPosts = {};
Object.keys(posts).forEach((postId) => {
parsedPosts[postId]={
id: postId,
...posts[postId]
};
});
dispatch(addPosts(uid,parsedPosts));
});
}
}
}
/* _____________ CRUD State _____________ */
/**
* Add a normal post
* @param {string} uid is user identifier
* @param {object} post
*/
export const addPost = (uid,post) => {
return{
type: types.ADD_POST,
payload: {uid,post}
}
}
/**
* Update a post
* @param {string} uid is user identifier
* @param {object} post
*/
export const updatePost = (uid,post) => {
return{
type: types.UPDATE_POST,
payload: {uid,post}
}
}
/**
* Delete a post
* @param {string} uid is user identifier
* @param {string} id is post identifier
*/
export const deletePost = (uid,id) => {
return{
type: types.DELETE_POST,
payload: {uid,id}
}
}
/**
* Add a list of post
* @param {string} uid
* @param {[object]} posts
*/
export const addPosts = (uid,posts) => {
return {
type: types.ADD_LIST_POST,
payload: {uid,posts}
}
}
/**
* Clea all data in post store
*/
export const clearAllData = () => {
return{
type: types.CLEAR_ALL_DATA_POST
}
}
/**
* Add a post with image
* @param {object} post
*/
export const addImagePost = (uid,post) => {
return{
type: types.ADD_IMAGE_POST,
payload: {uid,post}
}
}

192
app/actions/userActions.jsx Normal file
View File

@@ -0,0 +1,192 @@
// - Import react components
import {firebaseRef} from 'app/firebase/'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
import * as userActions from 'userActions'
/* _____________ CRUD DB _____________ */
/**
* Get user info from database
*/
export const dbGetUserInfo = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var userInfoRef = firebaseRef.child(`users/${uid}/info`);
return userInfoRef.once('value').then((snapshot) => {
var userInfo = snapshot.val() || {};
dispatch(addUserInfo(uid,userInfo))
},error => console.log(error));
}
}
}
/**
* Get user info from database
* @param {string} uid
*/
export const dbGetUserInfoByUserId = (uid,sw) => {
return (dispatch, getState) => {
if (uid) {
var userInfoRef = firebaseRef.child(`users/${uid}/info`);
return userInfoRef.once('value').then((snapshot) => {
var userInfo = snapshot.val() || {};
dispatch(addUserInfo(uid,userInfo))
switch (sw) {
case 'header':
dispatch(globalActions.setHeaderTitle(userInfo.fullName))
break;
default:
break;
}
},error => console.log(error));
}
}
}
/**
* Updata user information
* @param {object} newInfo
*/
export const dbUpdateUserInfo = (newInfo) => {
return (dispatch,getState) => {
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
let updates = {};
let info = getState().user.info[uid]
let updatedInfo = {
avatar: newInfo.avatar || info.avatar || '',
banner:newInfo.banner || info.banner || '',
email: newInfo.email || info.email || '',
fullName: newInfo.fullName || info.fullName || '',
tagLine: newInfo.tagLine || info.tagLine || ''
}
updates[`users/${uid}/info`] = updatedInfo
return firebaseRef.update(updates).then((result) => {
dispatch(updateUserInfo(uid,updatedInfo))
dispatch(closeEditProfile())
}, (error) => {
dispatch(globalActions.showErrorMessage(error.message))
})
}
}
// - Get people info from database
export const dbGetPeopleInfo = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var peopleRef = firebaseRef.child(`users`);
return peopleRef.once('value').then((snapshot) => {
let people = snapshot.val() || {};
let parsedPeople = {};
Object.keys(people).forEach((userId) => {
if(userId !== uid){
parsedPeople[userId]={
...people[userId].info
}}
})
dispatch(addPeopleInfo(parsedPeople))
},error => console.log(error));
}
}
}
/* _____________ CRUD State _____________ */
/**
* Add user information
* @param {string} uid is the user identifier
* @param {object} info is the information about user
*/
export const addUserInfo = (uid,info) => {
return{
type: types.ADD_USER_INFO,
payload: {uid,info}
}
}
/**
* Add people information
* @param {[object]} infoList is the lst of information about users
*/
export const addPeopleInfo = (infoList) => {
return{
type: types.ADD_PEOPLE_INFO,
payload: infoList
}
}
/**
* Update user information
* @param {string} uid is the user identifier
* @param {object} info is the information about user
*/
export const updateUserInfo = (uid,info) => {
return{
type: types.UPDATE_USER_INFO,
payload: {uid,info}
}
}
/**
* User info
* @param {object} info
*/
export const userInfo = (info) => {
return{
type: types.USER_INFO,
info
}
}
export const clearAllData = () => {
return {
type: types.CLEAR_ALL_DATA_USER
}
}
/**
* Open edit profile
*/
export const openEditProfile = () => {
return{
type: types.OPEN_EDIT_PROFILE
}
}
/**
* Close edit profile
*/
export const closeEditProfile = () => {
return{
type: types.CLOSE_EDIT_PROFILE
}
}

130
app/actions/voteActions.jsx Normal file
View File

@@ -0,0 +1,130 @@
// - Import react components
import {createAction as action} from 'redux-actions'
import moment from 'moment'
import { firebaseRef } from 'app/firebase/'
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as globalActions from 'globalActions'
import * as notifyActions from 'notifyActions'
/* _____________ CRUD DB _____________ */
/**
* Add vote to database
* @param {string} postId is the identifier of the post which user vote
*/
export const dbAddVote = (postId,ownerPostUserId) => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
var vote = {
postId: postId,
creationDate: moment().unix(),
userDisplayName: getState().user.info[uid].fullName,
userAvatar: getState().user.info[uid].avatar,
userId: uid
}
var voteRef = firebaseRef.child(`postVotes/${postId}`).push(vote)
return voteRef.then(() => {
dispatch(addVote(
{
vote,
postId: postId,
id: voteRef.key
}))
if(uid !== ownerPostUserId)
dispatch(notifyActions.dbAddNotify(
{
description:'Vote on your post.',
url:`/${ownerPostUserId}/posts/${postId}`,
notifyRecieverUserId:ownerPostUserId,notifierUserId:uid
}))
}, (error) => dispatch(globalActions.showErrorMessage(error.message)))
}
}
/**
* Get all votes from database
*/
export const dbGetVotes = () => {
return (dispatch, getState) => {
var uid = getState().authorize.uid
if (uid) {
var votesRef = firebaseRef.child(`postVotes`);
return votesRef.on('value',(snapshot) => {
var votes = snapshot.val() || {};
dispatch(addVoteList(votes))
})
}
}
}
/**
* Delete a vote from database
* @param {string} id of vote
* @param {string} postId is the identifier of the post which vote belong to
*/
export const dbDeleteVote = (postId) => {
return (dispatch, getState) => {
// Get current user id
var uid = getState().authorize.uid
// Write the new data simultaneously in the list
var updates = {};
let votes = getState().vote.postVotes[postId]
let id = Object.keys(votes).filter((key)=> votes[key].userId === uid)[0]
console.log(' Id : ',id)
updates[`postVotes/${postId}/${id}`] = null;
return firebaseRef.update(updates).then((result) => {
dispatch(deleteVote({id, postId}))
}, (error) => dispatch(globalActions.showErrorMessage(error.message)))
}
}
/**
* Add a vote
* @param {object} data
*/
export const addVote = (data) => {
return { type: types.ADD_VOTE, payload: data }
}
/**
* delete a vote
* @param {object} data
*/
export const deleteVote = (data) => {
return { type: types.DELETE_VOTE, payload: data }
}
/**
* Ad a list of vote
* @param {object} data
*/
export const addVoteList = (data) => {
return { type: types.ADD_VOTE_LIST, payload: data }
}
/**
* Clear all data
*/
export const clearAllvotes = () => {
return { type: types.CLEAR_ALL_DATA_VOTE }
}

16
app/api/AuthAPI.jsx Normal file
View File

@@ -0,0 +1,16 @@
// - Import firebase components
import {firebaseAuth, firebaseRef} from 'app/firebase/'
import store from 'configureStore'
// - Check user if is authorized
export var isAuthorized = () => {
var state = store.getState()
return state.authorize.authed
}
export var isAdmin = () =>{
return true;
}

30
app/api/AuthRouterAPI.jsx Normal file
View File

@@ -0,0 +1,30 @@
// - Import react components
import React, {Component} from 'react'
import {Route, Redirect} from 'react-router-dom'
// - Import API
import * as AuthAPI from 'AuthAPI'
export var PrivateRoute = ({component: Component, ...rest}) => {
console.log('is auth ; ', AuthAPI.isAuthorized());
return (
<Route
{...rest}
render={(props) => AuthAPI.isAuthorized()
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {from: props.location}}} />}
/>
)
}
export var PublicRoute = ({component: Component,...rest}) => {
return (
<Route
{...rest}
render={(props) => !(AuthRouterAPI.isAuthorized())
? <Component {...props} />
: <Redirect to='/' />}
/>
)
}

51
app/api/CircleAPI.jsx Normal file
View File

@@ -0,0 +1,51 @@
/**
* Get the circles which the specify users is in that circle
* @param {object} circles
* @param {string} followingId
*/
export const getUserBelongCircles = (circles,followingId) => {
let userBelongCircles = []
Object.keys(circles).forEach((cid) => {
if(cid.trim() !== '-Followers' && circles[cid].users){
let isExist = Object.keys(circles[cid].users).indexOf(followingId) > -1
if(isExist){
userBelongCircles.push(cid)
}
}
})
return userBelongCircles
}
/**
* Get the following users
* @param {object} circles
*/
export const getFollowingUsers = (circles) => {
let followingUsers = {}
Object.keys(circles).forEach((cid) => {
if(cid.trim() !== '-Followers' && circles[cid].users){
Object.keys(circles[cid].users).forEach((userId)=>{
let isExist = Object.keys(followingUsers).indexOf(userId) > -1
if(!isExist){
followingUsers[userId] = {
...circles[cid].users[userId]
}
}
})
}
})
return followingUsers
}
export default {
getUserBelongCircles,
getFollowingUsers
}

189
app/api/FileAPI.jsx Normal file
View File

@@ -0,0 +1,189 @@
// - Import react component
import { storageRef } from 'app/firebase/'
//- Import actions
// - Get file Extension
const getExtension = (fileName) => {
var re = /(?:\.([^.]+))?$/;
return re.exec(fileName)[1];
}
// Converts image to canvas; returns new canvas element
const convertImageToCanvas = (image) => {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
}
/**
* Constraint image size
* @param {file} file
* @param {number} maxWidth
* @param {number} maxHeight
*/
const constraintImage = (file,fileName, maxWidth, maxHeight) => {
// Ensure it's an image
if(file.type.match(/image.*/)) {
// Load the image
var reader = new FileReader();
reader.onload = function (readerEvent) {
var image = new Image();
image.onload = function (imageEvent) {
// Resize the image
var canvas = document.createElement('canvas'),
max_size = 986,// TODO : pull max size from a site config
width = image.width,
height = image.height;
if (width > height) {
if (width > max_size) {
height *= max_size / width;
width = max_size;
}
} else {
if (height > max_size) {
width *= max_size / height;
height = max_size;
}
}
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
var dataUrl = canvas.toDataURL();
var resizedImage = dataURLToBlob(dataUrl);
let evt = new CustomEvent('onSendResizedImage', { detail: {resizedImage,fileName} });
window.dispatchEvent(evt);
}
image.src = readerEvent.target.result;
}
reader.readAsDataURL(file);
}
}
// - Delete file from storage
const deleteFile = (folderName, fileName, callBackSuccess, callBackError) => {
// Create a reference to the file to delete
var desertRef = storageRef.child(`${folderName}/${filename}`);
// Delete the file
desertRef.delete().then(function () {
// File deleted successfully
callBackSuccess()
console.log('File has been deleted successfully');
}).catch(function (error) {
// Uh-oh, an error occurred!
callBackError(error)
console.log(error);
});
}
/**
* Convert data URL to blob
* @param {object} dataURL
*/
const dataURLToBlob = (dataURL) => {
var BASE64_MARKER = ';base64,'
if (dataURL.indexOf(BASE64_MARKER) == -1) {
var parts = dataURL.split(',')
var contentType = parts[0].split(':')[1]
var raw = parts[1]
return new Blob([raw], {type: contentType})
}
var parts = dataURL.split(BASE64_MARKER)
var contentType = parts[0].split(':')[1]
var raw = window.atob(parts[1])
var rawLength = raw.length
var uInt8Array = new Uint8Array(rawLength)
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i)
}
return new Blob([uInt8Array], {type: contentType})
}
// - Upload file
const uploadFile = (file, folderName, fileName, progressCallBack, errorCallBack, completeCallBack) => {
// Create a storage refrence
var storegeFile = storageRef.child(folderName + '/' + fileName)
// Upload file
var task = storegeFile.put(file)
// Upload storage bar
task.on('state_changed', (snapshot) => {
var percentage = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
progressCallBack(percentage)
// Set uploader progress value
}, (error) => {
errorCallBack(error)
}, (complete) => {
completeCallBack(complete)
})
}
const downloadFile = (folderName, fileName, callBack) => {
// Create a reference to the file we want to download
var starsRef = storageRef.child(`${folderName}/${fileName}`);
// Get the download URL
starsRef.getDownloadURL().then((url) => {
// Insert url into an <img> tag to "download"
callBack(url)
}).catch((error) => {
// A full list of error codes is available at
// https://firebase.google.com/docs/storage/web/handle-errors
switch (error.code) {
case 'storage/object_not_found':
// File doesn't exist
console.log('storage/object_not_found');
break;
case 'storage/unauthorized':
// User doesn't have permission to access the object
console.log('storage/unauthorized');
break;
case 'storage/canceled':
// User canceled the upload
console.log('storage/canceled');
break;
case 'storage/unknown':
// Unknown error occurred, inspect the server response
console.log('storage/unknown');
break;
}
});
}
export default {
downloadFile,
uploadFile,
dataURLToBlob,
deleteFile,
convertImageToCanvas,
getExtension,
constraintImage
}

29
app/api/PostAPI.jsx Normal file
View File

@@ -0,0 +1,29 @@
// Get tags from post content
export const detectTags = (content,character) => {
return content.split(" ").filter((word) => {
return (word.slice(0,1) === character)
})
}
export const getContentTags = (content) => {
var newTags = []
var tags = detectTags(content,'#')
tags.forEach((tag)=>{
newTags.push(tag.slice(1))
})
return newTags
}
export const sortObjectsDate = function (objects) {
var sortedObjects = objects;
// Sort posts with creation date
sortedObjects.sort((a, b) => {
return parseInt(b.creationDate) - parseInt(a.creationDate)
});
return sortedObjects;
}

56
app/app.jsx Normal file
View File

@@ -0,0 +1,56 @@
// Import external components refrence
import React from 'react'
import ReactDOM from 'react-dom'
import injectTapEventPlugin from 'react-tap-event-plugin'
import { cyan500 } from 'material-ui/styles/colors'
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import getMuiTheme from 'material-ui/styles/getMuiTheme'
import { Provider } from 'react-redux'
import store, { history } from 'configureStore'
import { ConnectedRouter } from 'react-router-redux'
// - Import app components
import Master from 'Master'
// - Impport actions
import * as authorizeActions from 'authorizeActions'
import * as globalActions from 'globalActions'
import * as userActions from 'userActions'
// Set default data
store.subscribe(() => {
var state = store.getState();
});
// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
injectTapEventPlugin();
// This replaces the textColor value on the palette
// and then update the keys for each component that depends on it.
// More on Colors: http://www.material-ui.com/#/customization/colors
const muiTheme = getMuiTheme({
});
// App css
require('applicationStyles');
const supportsHistory = 'pushState' in window.history
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}>
<Master />
</MuiThemeProvider>
</ConnectedRouter>
</Provider>,
document.getElementById('app')
);

271
app/components/Blog.jsx Normal file
View File

@@ -0,0 +1,271 @@
// - Import react components
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { Card, CardActions, CardHeader, CardMedia, CardTitle, CardText } from 'material-ui/Card'
import FlatButton from 'material-ui/FlatButton'
import { grey400, grey800, darkBlack, lightBlack } from 'material-ui/styles/colors'
import SvgCamera from 'material-ui/svg-icons/image/photo-camera'
import Paper from 'material-ui/Paper'
import { List, ListItem } from 'material-ui/List'
// - Import app components
import Post from 'Post'
import PostWrite from 'PostWrite'
import UserAvatar from 'UserAvatar'
// - Import API
import * as AuthAPI from 'AuthAPI'
import * as PostAPI from 'PostAPI'
// - Import actions
import * as globalActions from 'globalActions'
// - Create Blog component class
export class Blog extends Component {
static propTypes = {
/**
* If it's true , writing post block will be visible
*/
displayWriting: PropTypes.bool.isRequired,
/**
* A list of post
*/
posts: PropTypes.object,
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
this.state = {
/**
* It's true if we want to have two column of posts
*/
divided: false,
/**
* If it's true comment will be disabled on post
*/
disableComments: this.props.disableComments,
/**
* If it's true share will be disabled on post
*/
disableSharing: this.props.disableSharing,
/**
* If it's true, post write will be open
*/
openPostWrite: false,
/**
* The title of home header
*/
homeTitle: PropTypes.string
}
// Binding functions to `this`
this.postLoad = this.postLoad.bind(this)
this.handleOpenPostWrite = this.handleOpenPostWrite.bind(this)
this.handleClosePostWrite = this.handleClosePostWrite.bind(this)
}
/**
* Open post write
*
*
* @memberof Blog
*/
handleOpenPostWrite = () => {
this.setState({
openPostWrite: true
})
}
/**
* Close post write
*
*
* @memberof Blog
*/
handleClosePostWrite = () => {
this.setState({
openPostWrite: false
})
}
/**
* Create a list of posts
* @return {DOM} posts
*/
postLoad = () => {
let { posts,match } = this.props
let {tag} = match.params
if (posts === undefined || !Object.keys(posts).length > 0) {
return (
<h1>
'Nothing has shared.'
</h1>
)
}
else {
var postBack = { oddPostList: [], evenPostList: [] }
var parsedPosts = []
Object.keys(posts).forEach((postId) => {
if(tag){
let regex = new RegExp("#" + tag,'g')
let postMatch = posts[postId].body.match(regex)
if(postMatch !==null)
parsedPosts.push({ ...posts[postId]})
}else{
parsedPosts.push({ ...posts[postId]})
}
})
const sortedPosts = PostAPI.sortObjectsDate(parsedPosts)
if (sortedPosts.length > 6) {
postBack.divided = true
} else {
postBack.divided = false
}
sortedPosts.forEach((post, index) => {
var newPost = (
<div key={post.id}>
{index > 1 || (!postBack.divided && index > 0) ? <div style={{ height: "16px" }}></div> : ''}
<Post
body={post.body}
commentCounter={post.commentCounter}
creationDate={post.creationDate}
id={post.id}
image={post.image}
lastEditDate={post.lastEditDate}
ownerDisplayName={post.ownerDisplayName}
ownerUserId={post.ownerUserId}
ownerAvatar={post.ownerAvatar}
postTypeId={post.postTypeId}
score={post.score}
tags={post.tags}
video={post.video}
disableComments={post.disableComments}
disableSharing={post.disableSharing}
viewCount={posts.viewCount}
pictureState={true} />
</div>
)
if ((index % 2) === 1 && postBack.divided) {
postBack.oddPostList.push(newPost)
}
else {
postBack.evenPostList.push(newPost)
}
})
return postBack
}
}
componentWillMount() {
const {setHomeTitle} = this.props
setHomeTitle()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
let postList = this.postLoad()
const {tag, displayWriting, } = this.props
return (
<div >
<div className='grid grid__gutters grid__1of2 grid__space-around animate-top'>
<div className='grid-cell animate-top' style={{maxWidth: '530px', minWidth: '280px'}}>
{displayWriting && !tag
? (<PostWrite open={this.state.openPostWrite} onRequestClose={this.handleClosePostWrite} edit={false} >
<Paper zDepth={2} style={{ height: "68px", width: "100%" }}>
<ListItem
primaryText={<span style={{ color: grey400, cursor: "text" }}> What's new with you? </span>}
leftAvatar={<UserAvatar fileName={this.props.avatar} size={36} />}
rightIcon={<SvgCamera />}
style={{ padding: "7px 0px", fontWeight: "200" }}
onTouchTap={this.handleOpenPostWrite}
/>
</Paper>
<div style={{ height: "16px" }}></div>
</PostWrite>)
: ''}
{postList.evenPostList}
<div style={{ height: "16px" }}></div>
</div>
{postList.divided
? (<div className='grid-cell animate-top' style={{maxWidth: '530px', minWidth: '280px'}}>
<div className="blog__right-list">
{postList.oddPostList}
<div style={{ height: "16px" }}></div>
</div>
</div>)
: ''}
</div>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
setHomeTitle: () => dispatch(globalActions.setHeaderTitle(ownProps.homeTitle || '')),
showTopLoading: () => dispatch(globalActions.showTopLoading()),
hideTopLoading: () => dispatch(globalActions.hideTopLoading())
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : '',
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Blog))

266
app/components/Circle.jsx Normal file
View File

@@ -0,0 +1,266 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import {push} from 'react-router-redux'
import {grey400, darkBlack, lightBlack} from 'material-ui/styles/colors'
import { List, ListItem } from 'material-ui/List'
import SvgGroup from 'material-ui/svg-icons/action/group-work'
import IconButton from 'material-ui/IconButton'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
import IconMenu from 'material-ui/IconMenu'
import TextField from 'material-ui/TextField'
import MenuItem from 'material-ui/MenuItem'
import IconButtonElement from 'IconButtonElement'
import Dialog from 'material-ui/Dialog'
import Divider from 'material-ui/Divider'
import FlatButton from 'material-ui/FlatButton'
import RaisedButton from 'material-ui/RaisedButton'
import SvgClose from 'material-ui/svg-icons/navigation/close'
import AppBar from 'material-ui/AppBar'
// - Import app components
import UserAvatar from 'UserAvatar'
// - Import API
// - Import actions
import * as circleActions from 'circleActions'
/**
* Create component class
*/
export class Circle extends Component {
static propTypes = {
/**
* Circle object
*/
circle: PropTypes.object.isRequired,
/**
* Circle identifier
*/
id: PropTypes.string.isRequired
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
/**
* If is true circle is open to show users in circle list
*/
open:false,
/**
* Circle name on change
*/
circleName: this.props.circle.name,
/**
* Save operation will be disable if user doesn't meet requirement
*/
disabledSave: false
}
// Binding functions to `this`
this.handleToggleCircle = this.handleToggleCircle.bind(this)
this.handleDeleteCircle = this.handleDeleteCircle.bind(this)
this.handleUpdateCircle = this.handleUpdateCircle.bind(this)
this.handleChangeCircleName = this.handleChangeCircleName.bind(this)
}
/**
* Handle chage circle name
*
*
* @memberof Circle
*/
handleChangeCircleName = (evt) => {
const {value} = evt.target
this.setState({
circleName: value,
disabledSave: (!value || value.trim() === '')
})
}
/**
* Update user's circle
*
*
* @memberof Circle
*/
handleUpdateCircle = () => {
const {circleName} = this.state
if(circleName && circleName.trim() !== ''){
this.props.updateCircle({name:circleName,id: this.props.id})
}
}
/**
* Handle delete circle
*
*
* @memberof Circle
*/
handleDeleteCircle = () => {
this.props.deleteCircle(this.props.id)
}
/**
* Toggle circle to close/open
*
*
* @memberof Circle
*/
handleToggleCircle = () => {
this.setState({
open: !this.state.open
})
}
userList = () => {
const {users} = this.props.circle
const {userInfo} = this.props
let usersParsed =[]
if(users){
Object.keys(users).forEach((key, index) => {
const { fullName} = users[key]
let avatar = userInfo && userInfo[key] ? userInfo[key].avatar || '' : ''
usersParsed.push(<ListItem
key={`${this.props.id}.${key}`}
style={{backgroundColor: '#e2e2e2'}}
value={2}
primaryText={fullName}
leftAvatar={<UserAvatar fileName={avatar}/>}
onClick={()=> this.props.goTo(`/${key}`)}
/>)
})
return usersParsed
}
}
/**
* Right icon menue of circle
*
*
* @memberof Circle
*/
rightIconMenu =(
<IconMenu iconButtonElement={IconButtonElement} style={{ display: "block", position: "absolute", top: "0px", right: "12px" }}>
<MenuItem primaryText="Delete circle" onClick={this.handleDeleteCircle} />
<MenuItem primaryText="Circle settings" onClick={this.props.openCircleSettings} />
</IconMenu>
)
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const circleTitle =(
<div>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: "space-between" }}>
<div style={{paddingRight: '10px'}}>
<SvgClose onClick={this.props.closeCircleSettings} hoverColor={grey400} style={{cursor: 'pointer'}} />
</div>
<div style={{
color: 'rgba(0,0,0,0.87)',
flex: '1 1',
font: '500 20px Roboto,RobotoDraft,Helvetica,Arial,sans-serif'
}}>
Circle settings
</div>
<div style={{marginTop: '-9px'}}>
<FlatButton label="SAVE" primary={true} disabled={this.state.disabledSave} onClick={this.handleUpdateCircle} />
</div>
</div>
<Divider />
</div>
)
return (
<div>
<ListItem
key={this.props.id}
style={{backgroundColor: '#fff', borderBottom: '1px solid rgba(0,0,0,0.12)',height: '72px',padding: '12px 0' }}
primaryText={<span style={{color:'rgba(0,0,0,0.87)',fontSize:'16px',marginRight:'8px',whiteSpace:'nowrap',textOverflow:'ellipsis',overflow:'hidden'}}>{this.props.circle.name}</span>}
leftIcon={<SvgGroup style={{width:'40px',height:'40px',transform: 'translate(0px, -9px)',fill:'#bdbdbd'}} />}
rightIconButton={this.rightIconMenu}
initiallyOpen={false}
onClick={this.handleToggleCircle}
open={this.state.open}
nestedItems={this.userList()}
>
<Dialog
id={this.props.id}
title={circleTitle}
modal={false}
open={this.props.openSetting}
onRequestClose={this.props.closeCircleSettings}
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
contentStyle={{maxWidth: '400px'}}
>
<div>
<TextField
hintText="Circle name"
floatingLabelText="Circle name"
onChange={this.handleChangeCircleName}
value={this.state.circleName}
/>
</div>
</Dialog>
</ListItem>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
let {uid} = ownProps
return {
deleteCircle: (id) => dispatch(circleActions.dbDeleteCircle(id)),
updateCircle: (circle) => dispatch(circleActions.dbUpdateCircle(circle)),
closeCircleSettings: () => dispatch(circleActions.closeCircleSettings(uid,ownProps.id)),
openCircleSettings: () => dispatch(circleActions.openCircleSettings(uid,ownProps.id)),
goTo: (url)=> dispatch(push(url))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
let {uid} = state.authorize
return {
openSetting: state.circle ? (state.circle.userCircles[uid] ? (state.circle.userCircles[uid][ownProps.id].openCircleSettings || false) : false ) : false,
userInfo: state.user.info
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Circle)

314
app/components/Comment.jsx Normal file
View File

@@ -0,0 +1,314 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { NavLink} from 'react-router-dom'
import PropTypes from 'prop-types'
import { createAction as action } from 'redux-actions'
import moment from 'moment'
import { List, ListItem } from 'material-ui/List'
import Divider from 'material-ui/Divider'
import Paper from 'material-ui/Paper'
import FlatButton from 'material-ui/FlatButton'
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
import IconButton from 'material-ui/IconButton'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
import IconMenu from 'material-ui/IconMenu'
import MenuItem from 'material-ui/MenuItem'
import TextField from 'material-ui/TextField'
// - Import app components
import UserAvatar from 'UserAvatar'
// - Import API
// - Import action types
import * as types from 'actionTypes'
// - Import actions
import * as commentActions from 'commentActions'
import * as userActions from 'userActions'
/**
* Create component class
*/
export class Comment extends Component {
static propTypes = {
/**
* Comment object
*/
comment: PropTypes.object,
/**
* If it's true the post owner is the logged in user which this post be long to the comment
*/
isPostOwner: PropTypes.bool,
/**
* If it's true the comment is disable to write
*/
disableComments: PropTypes.bool,
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
this.textareaRef = i => { this.inputText = i }
this.divCommentRef = i => { this.divComment = i }
//Defaul state
this.state = {
/**
* Comment text
*/
text: this.props.comment.text,
/**
* Comment text to match edit with new comment that is edited
*/
initialText: this.props.comment.text,
/**
* If comment text dosn't take any change it will be true
*/
editDisabled: true,
/**
* If it's true the post owner is the logged in user which this post be long to the comment
*/
isPostOwner: PropTypes.bool
}
// Binding functions to `this`
this.handleDelete = this.handleDelete.bind(this)
this.handleUpdateComment = this.handleUpdateComment.bind(this)
this.handleOnChange = this.handleOnChange.bind(this)
this.handleCancelEdit = this.handleCancelEdit.bind(this)
this.handleEditComment = this.handleEditComment.bind(this)
}
/**
* Handle show edit comment
* @param {event} evt is an event passed by clicking on edit button
*/
handleEditComment = (evt) => {
this.inputText.style.height = this.divComment.clientHeight + 'px'
this.props.openEditor()
}
/**
* Handle cancel edit
* @param {event} evt is an event passed by clicking on cancel button
*/
handleCancelEdit = (evt) => {
this.setState({
text: this.state.initialText
})
this.props.closeEditor()
}
/**
* Handle edit comment
* @param {event} evt is an event passed by clicking on post button
*/
handleUpdateComment = (evt) => {
this.props.update(this.props.comment.id, this.props.comment.postId, this.state.text)
this.setState({
initialText: this.state.text
})
}
/**
* When comment text changed
* @param {event} evt is an event passed by change comment text callback funciton
* @param {string} data is the comment text which user writes
*/
handleOnChange = (evt) => {
const data = evt.target.value
this.inputText.style.height = evt.target.scrollHeight + 'px'
if (data.length === 0 || data.trim() === '' || data.trim() === this.state.initialText) {
this.setState({
text: data,
editDisabled: true
})
}
else {
this.setState({
text: data,
editDisabled: false
})
}
}
/**
* Delete a comment
* @param {event} evt an event passed by click on delete comment
* @param {string} id comment identifire
* @param {string} postId post identifier which comment belong to
*/
handleDelete = (evt, id, postId) => {
this.props.delete(id, postId)
}
componentWillMount() {
const {userId} = this.props.comment
if (!this.props.isCommentOwner && !this.props.info[userId]) {
this.props.getUserInfo()
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
/**
* DOM styles
*
*
* @memberof Comment
*/
const styles = {
comment: {
marginBottom: '12px'
},
iconButton: {
width: 16,
height: 16
},
author:{
fontSize: "13px",
paddingRight: "10px",
fontWeight: 400,
color: "rgba(0,0,0,0.87)",
textOverflow: "ellipsis",
overflow: "hidden"
},
commentBody:{
fontSize: "13px",
lineHeight: "20px",
color: "rgba(0,0,0,0.87)",
fontWeight: 300,
height: "",
display: "block"
}
}
/**
* Comment object from props
*/
const {comment} = this.props
const iconButtonElement = (
<IconButton style={styles.iconButton} iconStyle={styles.iconButton}
touch={true}
>
<MoreVertIcon color={grey400} viewBox='9 0 24 24' />
</IconButton>
)
const RightIconMenu = () => (
<IconMenu iconButtonElement={iconButtonElement} style={{ display: "block", position: "absolute", top: "0px", right: "4px" }}>
<MenuItem style={{ fontSize: "14px" }}>Reply</MenuItem>
{this.props.isCommentOwner ? (<MenuItem style={{ fontSize: "14px" }} onClick={this.handleEditComment}>Edit</MenuItem>): ''}
{(this.props.isCommentOwner || this.props.isPostOwner) ? ( <MenuItem style={{ fontSize: "14px" }} onClick={(evt) => this.handleDelete(evt, comment.id, comment.postId)}>Delete</MenuItem>): ''}
</IconMenu>
)
const Author = () => (
<div style={{ marginTop: "-11px" }}>
<span style={styles.author}>{comment.userDisplayName}</span><span style={{
fontWeight: 100,
fontSize: "10px"
}}>{moment.unix(comment.creationDate).fromNow()}</span>
</div>
)
const commentBody = (
<p style={styles.commentBody}>{comment.text}</p>
)
const {userId} = comment
return (
<div className="animate-top" style={styles.comment} key={comment.id}>
<Paper zDepth={0} className="animate2-top10" style={{ position: "relative", padding: "", display: (!this.state.display ? "block" : "none") }}>
<div style={{ marginLeft: "0px", padding: "16px 56px 0px 72px", position: "relative" }}>
<NavLink to={`/${userId}`}><UserAvatar fileName={this.props.avatar} style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", position: "absolute", top: "8px", left: "16px" }} size={36} /></NavLink>
<NavLink to={`/${userId}`}> <Author /></NavLink>
{(!this.props.isCommentOwner && !this.props.isPostOwner && this.props.disableComments )? '' : (<RightIconMenu />)}
<div style={{ outline: "none", marginLeft: "16px", flex: "auto", flexGrow: 1 }}>
<textarea ref={this.textareaRef} className="animate2-top10" style={{ fontWeight: 100, fontSize: "14px", border: "none", width: "100%", outline: "none", resize: "none", display: (this.props.comment.editorStatus ? 'block' : 'none') }} onChange={this.handleOnChange} value={this.state.text}></textarea>
<div ref={this.divCommentRef} className="animate2-top10" style={{ fontWeight: 100, fontSize: "14px", height: "100%", border: "none", width: "100%", outline: "none", resize: "none", display: (!this.props.comment.editorStatus ? 'block' : 'none') }}>{this.state.text}</div>
</div>
</div>
<div style={{ display: (this.props.comment.editorStatus ? "flex" : "none"), flexDirection: "row-reverse" }}>
<FlatButton primary={true} disabled={this.state.editDisabled} label="Update" style={{ float: "right", clear: "both", zIndex: 5, margin: "0px 5px 5px 0px", fontWeight: 400 }} onClick={this.handleUpdateComment} />
<FlatButton primary={true} label="Cancel" style={{ float: "right", clear: "both", zIndex: 5, margin: "0px 5px 5px 0px", fontWeight: 400 }} onClick={this.handleCancelEdit} />
</div>
</Paper>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
delete: (id, postId) => dispatch(commentActions.dbDeleteComment(id, postId)),
update: (id, postId, comment) => dispatch(commentActions.dbUpdateComment(id, postId, comment)),
openEditor: () => dispatch(commentActions.openCommentEditor({ id: ownProps.comment.id, postId: ownProps.comment.postId })),
closeEditor: () => dispatch(commentActions.closeCommentEditor({ id: ownProps.comment.id, postId: ownProps.comment.postId })),
getUserInfo: () => dispatch(userActions.dbGetUserInfoByUserId(ownProps.comment.userId))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const {uid} = state.authorize
const avatar = state.user.info && state.user.info[ownProps.comment.userId] ? state.user.info[ownProps.comment.userId].avatar || '' : ''
return {
uid: uid,
isCommentOwner: (uid === ownProps.comment.userId),
info: state.user.info,
avatar
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Comment)

View File

@@ -0,0 +1,270 @@
// - Import react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import Paper from 'material-ui/Paper'
import FlatButton from 'material-ui/FlatButton'
import TextField from 'material-ui/TextField'
import Divider from 'material-ui/Divider'
import { ListItem } from 'material-ui/List'
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
// - Import actions
import * as commentActions from 'commentActions'
// - Import app components
import CommentList from 'CommentList'
import CommentWrite from 'CommentWrite'
import UserAvatar from 'UserAvatar'
/**
* Create component class
*/
export class CommentGroup extends Component {
static propTypes = {
/**
* If it's true comment box will be open
*/
open: PropTypes.bool,
/**
* If it's true the comment is disable to write
*/
disableComments: PropTypes.bool,
/**
* The post identifier which comment belong to
*/
postId: PropTypes.string,
/**
* If it's true the post owner is the logged in user which this post be long to the comment
*/
isPostOwner: PropTypes.bool,
/**
* Toggle on show/hide comment by passing callback from parent component
*/
onToggleRequest: PropTypes.func,
/**
* The user identifier of the post owner which comment belong to
*/
ownerPostUserId:PropTypes.string
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
/**
* Defaul state
*/
this.state = {
commentText: "",
postDisable: true
}
// Binding functions to `this`
this.commentList = this.commentList.bind(this)
this.handlePostComment = this.handlePostComment.bind(this)
this.clearCommentWrite = this.clearCommentWrite.bind(this)
}
/**
* Clear comment text field
*/
clearCommentWrite = () => {
this.setState({
commentText: '',
postDisable: false
})
}
/**
* Post comment
*/
handlePostComment = () => {
this.props.send(this.state.commentText, this.props.postId, this.clearCommentWrite)
}
/**
* When comment text changed
* @param {event} evt is an event passed by change comment text callback funciton
* @param {string} data is the comment text which user writes
*/
handleOnChange = (evt, data) => {
this.setState({ commentText: data })
if (data.length === 0 || data.trim() === '') {
this.setState({
commentText: '',
postDisable: true
})
}
else {
this.setState({
commentText: data,
postDisable: false
})
}
}
/**
* Get comments' DOM
* @return {DOM} list of comments' DOM
*/
commentList = () => {
var comments = this.props.comments
if (comments) {
var parsedComments = [];
Object.keys(comments).slice(0, 3).forEach((commentId) => {
parsedComments.push({
id: commentId,
...comments[commentId]
});
});
if (parsedComments.length === 2) {
parsedComments.push(parsedComments[0])
}
else if (parsedComments.length === 1) {
parsedComments.push(parsedComments[0])
parsedComments.push(parsedComments[0])
}
return parsedComments.map((comment, index) => {
const {userInfo} = this.props
const commentAvatar = userInfo && userInfo[comment.userId] ? userInfo[comment.userId].avatar || '' : ''
return (<ListItem key={index} style={{ height: "60px", position: "", zIndex: "" }} innerDivStyle={{ padding: "6px 16px 16px 72px" }}
leftAvatar={<UserAvatar fileName={commentAvatar} style={{ top: "8px" }} size={36} />}
secondaryText={<div style={{ height: "" }}>
<span style={{
fontSize: "13px",
paddingRight: "10px",
fontWeight: 400,
color: "rgba(0,0,0,0.87)",
textOverflow: "ellipsis",
overflow: "hidden"
}}>
{comment.userDisplayName}:
</span>
<span style={{
fontSize: "13px",
lineHeight: "20px",
color: "rgba(0,0,0,0.87)",
fontWeight: 300,
whiteSpace: "pre-wrap"
}}>{comment.text}</span>
</div>}
secondaryTextLines={2}
/>
)
})
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
return (
<div>
<div style={this.props.comments && Object.keys(this.props.comments).length > 0 ? { display: "block" } : { display: "none" }}>
<Divider />
<Paper zDepth={0} className="animate-top" style={!this.props.open ? { display: "block" } : { display: "none" }}>
<div style={{ position: "relative", height: "60px" }} >
<FlatButton label=" " style={{ height: "60px", zIndex: 5 }} fullWidth={true} onClick={this.props.onToggleRequest} />
<div className="comment__list-show">
{this.commentList()}
</div>
</div>
</Paper>
{(this.props.comments && Object.keys(this.props.comments).length > 0)
? (<Paper zDepth={0} style={this.props.open ? { display: "block", padding: "0px 0px" } : { display: "none", padding: "12px 16px" }}>
<CommentList comments={this.props.comments} isPostOwner={this.props.isPostOwner} disableComments={this.props.disableComments}/>
</Paper>) : ''}
</div>
{!this.props.disableComments ? (<div>
<Divider />
<Paper zDepth={0} className="animate2-top10" style={{ position: "relative", overflowY: "auto", padding: "12px 16px", display: (this.props.open ? "block" : "none") }}>
<div style={{ display: "flex" }}>
<UserAvatar fileName={this.props.avatar} style={{ flex: "none", margin: "4px 0px" }} size={36} />
<div style={{ outline: "none", marginLeft: "16px", flex: "auto", flexGrow: 1 }}>
<TextField
value={this.state.commentText}
onChange={this.handleOnChange}
hintText="Add a comment..."
underlineShow={false}
multiLine={true}
rows={1}
hintStyle={{ fontWeight: 100, fontSize: "14px" }}
rowsMax={4}
textareaStyle={{ fontWeight: 100, fontSize: "14px" }}
style={{ width: '100%' }}
/>
</div>
</div>
<FlatButton primary={true} disabled={this.state.postDisable} label="Post" style={{ float: "right", clear: "both", zIndex: 5, margin: "0px 5px 5px 0px", fontWeight: 400 }} onClick={this.handlePostComment} />
</Paper>
</div>): ''}
</div>
);
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
send: (text, postId, callBack) => {
dispatch(commentActions.dbAddComment({
postId: postId,
text: text,
ownerPostUserId:ownProps.ownerPostUserId
}, callBack))
}
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
comments: state.comment.postComments[ownProps.postId],
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar || '' : '',
userInfo: state.user.info
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(CommentGroup)

View File

@@ -0,0 +1,133 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux';
import PropTypes from 'prop-types'
import { List, ListItem } from 'material-ui/List'
// - Import app components
import Comment from 'Comment'
import * as PostAPI from 'PostAPI'
// - Import actions
/**
* Create component class
*/
export class CommentList extends Component {
static propTypes = {
/**
* If it's true the post owner is the logged in user which this post be long to the comment
*/
isPostOwner: PropTypes.bool,
/**
* If it's true the comment is disable to write
*/
disableComments: PropTypes.bool,
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
/**
* Default state
*/
this.state = {
}
// Binding functions to `this`
}
/**
* Get comments' DOM
* @return {DOM} list of comments' DOM
*/
commentList = () => {
var comments = this.props.comments
if (comments) {
var parsedComments = [];
Object.keys(comments).forEach((commentId) => {
parsedComments.push({
id: commentId,
...comments[commentId]
});
});
let sortedComments = PostAPI.sortObjectsDate(parsedComments)
return sortedComments.map((comment, index, array) => {
return <Comment key={comment.id} comment={comment} isPostOwner={this.props.isPostOwner} disableComments={this.props.disableComments}/>
})
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
list: {
width: "100%",
maxHeight: 450,
overflowY: 'auto'
}
}
return (
<List style={styles.list}>
{this.commentList()}
</List>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state) => {
return {
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(CommentList)

View File

@@ -0,0 +1,84 @@
// - Import react components
import React, {Component} from 'react'
import {connect} from 'react-redux'
import Faker from 'faker'
// - Import app components
// - Import actions
import * as commentActions from 'commentActions'
// - Define variable
const buttonStyle = {
marginTop: '5px'
};
// - Create CommentWrite component class
export class CommentWrite extends Component {
constructor(props) {
super(props);
this.state = {
inputValue:''
}
// Binding functions to `this`
this.handleRef = this.handleRef.bind(this);
this.focus = this.focus.bind(this);
this.handleAddComment = this.handleAddComment.bind(this);
this.handleOnChange = this.handleOnChange.bind(this)
}
handleOnChange = (evt) =>{
this.setState({inputValue:evt.target.value})
}
handleRef = c => {
this.inputRef = c
}
focus = () => {
this.inputRef.focus()
}
handleAddComment = (evt) => {
this.props.send(this.state.inputValue,this.props.postId,this.props.close)
}
// Render DOM
render() {
return (
<div>
<textarea autoFocus defaultValue={this.props.commentText} onChange={this.handleOnChange}/>
<Button basic style={buttonStyle} onClick={this.handleAddComment} color='teal'>Add Comment</Button>
</div>
);
}
}
// - Map dispatch to props
const mapDispatchToProps = (dispatch,ownProps) => {
return{
send: (text,postId,callBack) => {
dispatch(commentActions.dbAddComment({
postId: postId,
text: text
},callBack))
}
}
}
// - Map state to props
const mapStateToProps = (state) => {
return{
}
}
// - Connect component to store
export default connect(mapStateToProps,mapDispatchToProps)(CommentWrite)

View File

@@ -0,0 +1,458 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
import IconButton from 'material-ui/IconButton'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
import SvgCamera from 'material-ui/svg-icons/image/photo-camera'
import IconMenu from 'material-ui/IconMenu'
import MenuItem from 'material-ui/MenuItem'
import FlatButton from 'material-ui/FlatButton'
import RaisedButton from 'material-ui/RaisedButton'
import EventListener, { withOptions } from 'react-event-listener'
import Dialog from 'material-ui/Dialog'
import Divider from 'material-ui/Divider'
import Paper from 'material-ui/Paper'
import TextField from 'material-ui/TextField'
// - Import app components
import ImgCover from 'ImgCover'
import DialogTitle from 'DialogTitle'
import ImageGallery from 'ImageGallery'
import FileAPI from 'FileAPI'
import UserAvatar from 'UserAvatar'
// - Import API
// - Import actions
import * as userActions from 'userActions'
import * as globalActions from 'globalActions'
import * as imageGalleryActions from 'imageGalleryActions'
/**
* Create component class
*/
export class EditProfile extends Component {
static propTypes = {
/**
* User avatar address
*/
avatar: PropTypes.string,
/**
* User avatar address
*/
banner: PropTypes.string,
/**
* User full name
*/
fullName: PropTypes.string.isRequired
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
/**
* If it's true the winow is in small size
*/
isSmall: false,
/**
* User tag line input value
*/
tagLineInput: props.info.tagLine || '',
/**
* User full name input value
*/
fullNameInput: props.info.fullName || '',
/**
* Error message of full name input
*/
fullNameInputError: '',
/**
* User banner address
*/
banner: this.props.banner || '',
/**
* User avatar address
*/
avatar: this.props.avatar || '',
/**
* It's true if the image galley for banner is open
*/
openBanner: false,
/**
* It's true if the image gallery for avatar is open
*/
openAvatar: false
}
// Binding functions to `this`
this.handleChangeDate = this.handleChangeDate.bind(this)
this.handleUpdate = this.handleUpdate.bind(this)
this.handleRequestSetAvatar = this.handleRequestSetAvatar.bind(this)
this.handleRequestSetBanner = this.handleRequestSetBanner.bind(this)
}
/**
* Close image gallery of banner
*/
handleCloseBannerGallery = () => {
this.setState({
openBanner: false
})
}
/**
* Open image gallery of banner
*/
handleOpenBannerGallery = () => {
this.setState({
openBanner: true
})
}
/**
* Close image gallery of avatar
*/
handleCloseAvatarGallery = () => {
this.setState({
openAvatar: false
})
}
/**
* Open image gallery of avatar
*/
handleOpenAvatarGallery = () => {
this.setState({
openAvatar: true
})
}
/**
* Set banner image url
*/
handleRequestSetBanner = (url) => {
this.setState({
banner: url
})
}
/**
* Set avatar image url
*/
handleRequestSetAvatar = (fileName) => {
this.setState({
avatar: fileName
})
}
/**
* Update profile on the server
*
*
* @memberof EditProfile
*/
handleUpdate = () => {
const {fullNameInput, tagLineInput, avatar, banner} = this.state
if (this.state.fullNameInput.trim() === '') {
this.setState({
fullNameInputError: 'This field is required'
})
}
else {
this.setState({
fullNameInputError: ''
})
this.props.update({
fullName: fullNameInput,
tagLine: tagLineInput,
avatar: avatar,
banner: banner
})
}
}
/**
* Handle data on input change
* @param {event} evt is an event of inputs of element on change
*/
handleInputChange = (evt) => {
const target = evt.target
const value = target.type === 'checkbox' ? target.checked : target.value
const name = target.name
this.setState({
[name]: value
})
}
/**
* Handle change date
*/
handleChangeDate = (evt, date) => {
this.setState({
birthdayInput: date,
})
}
/**
* Handle resize event for window to change sidebar status
* @param {event} evt is the event is passed by winodw resize event
*/
handleResize = (evt) => {
// Set initial state
var width = window.innerWidth
if (width > 900) {
this.setState({
isSmall: false
})
}
else {
this.setState({
isSmall: true
})
}
}
componentDidMount = () => {
this.handleResize()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
avatar: {
border: '2px solid rgb(255, 255, 255)'
},
paper: {
width: '90%',
height: '100%',
margin: '0 auto',
display: 'block',
},
title: {
padding: '24px 24px 20px 24px',
font: '500 20px Roboto,RobotoDraft,Helvetica,Arial,sans-serif',
display: 'flex',
wordWrap: 'break-word',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
flexGrow: 1
},
actions: {
display: 'flex',
justifyContent: "flex-end",
padding: '24px 24px 20px'
},
updateButton: {
marginLeft: '10px'
},
box: {
padding: '0px 24px 20px 24px',
display: 'flex'
},
dialogGallery: {
width: '',
maxWidth: '530px',
borderRadius: "4px"
}
}
const iconButtonElement = (
<IconButton style={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton} iconStyle={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton}
touch={true}
>
<MoreVertIcon color={grey400} viewBox='10 0 24 24' />
</IconButton>
)
const RightIconMenu = () => (
<IconMenu iconButtonElement={iconButtonElement}>
<MenuItem style={{ fontSize: "14px" }}>Reply</MenuItem>
<MenuItem style={{ fontSize: "14px" }}>Edit</MenuItem>
<MenuItem style={{ fontSize: "14px" }}>Delete</MenuItem>
</IconMenu>
)
return (
<div>
{/* Edit profile dialog */}
<Dialog
id='Edit-Profile'
modal={false}
open={this.props.open}
onRequestClose={this.props.onRequestClose}
autoScrollBodyContent={true}
bodyStyle={{ backgroundColor: "none", padding: 'none', borderTop: 'none', borderBottom: 'none' }}
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
contentStyle={{ backgroundColor: "none", maxWidth: '450px', maxHeight: 'none', height: '100%' }}
style={{ backgroundColor: "none", maxHeight: 'none', height: '100%' }}
>
{/* Banner */}
<div style={{ position: 'relative' }}>
<ImgCover width='100%' height='250px' borderRadius='2px' fileName={this.state.banner} />
<div className='g__circle-black' onClick={this.handleOpenBannerGallery} style={{ position: 'absolute', right: '10px', top: '10px' }}>
<SvgCamera style={{ fill: 'rgba(255, 255, 255, 0.88)', transform: 'translate(6px, 6px)' }} />
</div>
</div>
<div className='profile__edit'>
<EventListener
target="window"
onResize={this.handleResize}
/>
<div className='left'>
<div style={{ display: 'flex', justifyContent: 'center' }}>
{/* Avatar */}
<div className='g__circle-black' onClick={this.handleOpenAvatarGallery} style={{ position: 'absolute', left: '50%', display: 'inline-block', top: '52px', margin: '-18px' }}>
<SvgCamera style={{ fill: 'rgba(255, 255, 255, 0.88)', transform: 'translate(6px, 6px)' }} />
</div>
<UserAvatar fileName={this.state.avatar} size={90} style={styles.avatar} />
</div>
<div className='info'>
<div className='fullName'>
{this.props.fullName}
</div>
</div>
</div>
</div>
{/* Edit user information box*/}
<Paper style={styles.paper} zDepth={1}>
<div style={styles.title}>Personal Information</div>
<div style={styles.box}>
<TextField
floatingLabelText="Full name"
onChange={this.handleInputChange}
name='fullNameInput'
errorText={this.state.fullNameInputError}
value={this.state.fullNameInput}
/>
</div>
<br />
<div style={styles.box}>
<TextField
floatingLabelText="Tag Line"
onChange={this.handleInputChange}
name='tagLineInput'
value={this.state.tagLineInput}
/>
</div>
<br />
<div style={styles.actions}>
<FlatButton label="CANCEL" onClick={this.props.onRequestClose} />
<RaisedButton label="UPDATE" primary={true} onClick={this.handleUpdate} style={styles.updateButton} />
</div>
</Paper>
<div style={{ height: '16px' }}></div>
</Dialog>
{/* Image gallery for banner*/}
<Dialog
title={<DialogTitle title='Choose an banner image' onRequestClose={this.handleCloseBannerGallery} />}
modal={false}
open={this.state.openBanner}
contentStyle={styles.dialogGallery}
onRequestClose={this.handleCloseBannerGallery}
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
autoDetectWindowHeight={false}
>
<ImageGallery set={this.handleRequestSetBanner} close={this.handleCloseBannerGallery} />
</Dialog>
{/* Image gallery for avatar */}
<Dialog
title={<DialogTitle title='Choose an avatar image' onRequestClose={this.handleCloseAvatarGallery} />}
modal={false}
open={this.state.openAvatar}
contentStyle={styles.dialogGallery}
onRequestClose={this.handleCloseAvatarGallery}
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
autoDetectWindowHeight={false}
>
<ImageGallery set={this.handleRequestSetAvatar} close={this.handleCloseAvatarGallery} />
</Dialog>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
update: (info) => dispatch(userActions.dbUpdateUserInfo(info)),
onRequestClose: () => dispatch(userActions.closeEditProfile()),
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
open: state.user.openEditProfile,
info: state.user.info[state.authorize.uid],
avatarURL: state.imageGallery.imageURLList
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(EditProfile)

View File

@@ -0,0 +1,110 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import Paper from 'material-ui/Paper'
// - Import app components
import UserBoxList from 'UserBoxList'
// - Import API
// - Import actions
import * as userActions from 'userActions'
/**
* Create component class
*/
export class FindPeople extends Component {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
componentWillMount() {
this.props.loadPeople()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
paper: {
height: 254,
width: 243,
margin: 10,
textAlign: 'center',
maxWidth: '257px'
},
followButton:{
position: 'absolute',
bottom: '8px',
left: 0,
right: 0
}
}
return (
<div>
{this.props.peopleInfo && Object.keys(this.props.peopleInfo).length !== 0 ? (<div>
<div className='profile__title'>
Suggestions for you
</div>
<UserBoxList users={this.props.peopleInfo}/>
<div style={{ height: '24px' }}></div>
</div>) : (<div className='g__title-center'>
Nothing to show! :(
</div>)}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
loadPeople: () => dispatch(userActions.dbGetPeopleInfo())
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
peopleInfo: state.user.info
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(FindPeople)

View File

@@ -0,0 +1,89 @@
// - Import react components
import React, {Component} from 'react'
import {connect} from 'react-redux'
import PropTypes from 'prop-types'
// - Import app components
import UserBoxList from 'UserBoxList'
// - Import API
// - Import actions
/**
* Create component class
*/
export class Followers extends Component {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props){
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
return (
<div>
{(this.props.followers && Object.keys(this.props.followers).length !==0) ? (<div>
<div className='profile__title'>
Followers
</div>
<UserBoxList users={this.props.followers} />
<div style={{ height: '24px' }}></div>
</div>)
: (<div className='g__title-center'>
No followers!
</div>)}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch,ownProps) => {
return{
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state,ownProps) => {
const { uid } = state.authorize
const circles = state.circle ? state.circle.userCircles[uid] : {}
return{
followers: circles ? (circles['-Followers'] ? circles['-Followers'].users || {} : {}) : {}
}
}
// - Connect component to redux store
export default connect(mapStateToProps,mapDispatchToProps)(Followers)

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 'UserBoxList'
// - Import API
import CircleAPI from 'CircleAPI'
// - Import actions
/**
* Create component class
*/
export class Following extends Component {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props){
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
return (
<div>
{(this.props.followingUsers && Object.keys(this.props.followingUsers).length !== 0 )? (<div>
<div className='profile__title'>
Following
</div>
<UserBoxList users={this.props.followingUsers} />
<div style={{ height: '24px' }}></div>
</div>) : (<div className='g__title-center'>
No following user!
</div>)}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch,ownProps) => {
return{
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state,ownProps) => {
const { uid } = state.authorize
const circles = state.circle ? state.circle.userCircles[uid] : {}
const followingUsers = CircleAPI.getFollowingUsers(circles)
return {
uid,
circles,
followingUsers,
}
}
// - Connect component to redux store
export default connect(mapStateToProps,mapDispatchToProps)(Following)

202
app/components/Home.jsx Normal file
View File

@@ -0,0 +1,202 @@
// - Import react components
import React, { Component } from 'react'
import _ from 'lodash'
import { Route, Switch, withRouter, Redirect, NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import { push } from 'react-router-redux'
import Menu from 'material-ui/Menu'
import MenuItem from 'material-ui/MenuItem'
import Divider from 'material-ui/Divider'
import SvgArrowLeft from 'material-ui/svg-icons/hardware/keyboard-arrow-left'
import SvgHome from 'material-ui/svg-icons/action/home'
import SvgFeedback from 'material-ui/svg-icons/action/feedback'
import SvgSettings from 'material-ui/svg-icons/action/settings'
import SvgAccountCircle from 'material-ui/svg-icons/action/account-circle'
import SvgPeople from 'material-ui/svg-icons/social/people'
// - Import app components
import Sidebar from 'Sidebar'
import Blog from 'Blog'
import HomeHeader from 'HomeHeader'
import SidebarContent from 'SidebarContent'
import SidebarMain from 'SidebarMain'
import Profile from 'Profile'
import PostPage from 'PostPage'
import People from 'People'
// - Import API
import CircleAPI from 'CircleAPI'
// - Import actions
import * as globalActions from 'globalActions'
// - Create Home component class
export class Home extends Component {
// Constructor
constructor(props) {
super(props)
// Default state
this.state = {
sidebarOpen: () => _,
sidebarStatus: true,
sidebaOverlay: false
}
// Binding function to `this`
this.sidebar = this.sidebar.bind(this)
this.sidebarStatus = this.sidebarStatus.bind(this)
this.sidebarOverlay = this.sidebarOverlay.bind(this)
this.handleCloseSidebar = this.handleCloseSidebar.bind(this)
}
/**
* handle close sidebar
*/
handleCloseSidebar = () => {
this.state.sidebarOpen(false, 'overlay')
}
/**
* Change sidebar overlay status
* @param {boolean} status if is true, the sidebar is on overlay status
*/
sidebarOverlay = (status) => {
this.setState({
sidebarOverlay: status
})
}
/**
* Pass function to change sidebar status
* @param {boolean} open is a function callback to change sidebar status out of sidebar component
*/
sidebar = (open) => {
this.setState({
sidebarOpen: open
})
}
/**
* Change sidebar status if is open or not
* @param {boolean} status is true, if the sidebar is open
*/
sidebarStatus = (status) => {
this.setState({
sidebarStatus: status
})
}
/**
* Render DOM component
*
* @returns DOM
*
* @memberof Home
*/
render() {
return (
<div id="home">
<HomeHeader sidebar={this.state.sidebarOpen} sidebarStatus={this.state.sidebarStatus} />
<Sidebar overlay={this.sidebarOverlay} open={this.sidebar} status={this.sidebarStatus}>
<SidebarContent>
<Menu style={{ color: "rgb(117, 117, 117)", width: '210px' }}>
{this.state.sidebarOverlay
? <div><MenuItem onClick={this.handleCloseSidebar} primaryText={<span style={{ color: "rgb(117, 117, 117)" }} className="sidebar__title">Green</span>} rightIcon={<SvgArrowLeft viewBox="0 3 24 24" style={{ color: "#fff", marginLeft: "15px", width: "32px", height: "32px", cursor: "pointer" }} />} /><Divider /></div>
: ""
}
<NavLink to='/'><MenuItem primaryText="Home" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgHome />} /></NavLink>
<NavLink to={`/${this.props.uid}`}><MenuItem primaryText="Profile" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgAccountCircle />} /></NavLink>
<NavLink to='/people'><MenuItem primaryText="People" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgPeople />} /></NavLink>
<Divider />
<NavLink to='/settings'><MenuItem primaryText="Settings" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgSettings />} /></NavLink>
<NavLink to='#'><MenuItem primaryText="Send feedback" style={{ color: "rgb(117, 117, 117)" }} leftIcon={<SvgFeedback />} /></NavLink>
</Menu>
</SidebarContent>
<SidebarMain>
<Switch>
<Route path="/people/:tab?" render={() => {
return (
this.props.authed
? <People />
: <Redirect to="/login" />
)
}} />
<Route path="/tag/:tag" render={({match}) => {
return (
this.props.authed
? <div className="blog"><Blog displayWriting={false} homeTitle={`#${match.params.tag}`} posts={this.props.mergedPosts} /></div>
: <Redirect to="/login" />
)
}} />
<Route path="/:userId/posts/:postId/:tag?" component={PostPage} />
<Route path="/:userId" component={Profile} />
<Route path="/" render={() => {
return (
this.props.authed
? <div className="blog"><Blog homeTitle='Home' posts={this.props.mergedPosts} displayWriting={true} /></div>
: <Redirect to="/login" />
)
}} />
</Switch>
</SidebarMain>
</Sidebar>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const { uid } = state.authorize
let mergedPosts = {}
const circles = state.circle ? (state.circle.userCircles[uid] || {}) : {}
const followingUsers = CircleAPI.getFollowingUsers(circles)
const posts = state.post.userPosts ? state.post.userPosts[state.authorize.uid] : {}
Object.keys(followingUsers).forEach((userId)=>{
let newPosts = state.post.userPosts ? state.post.userPosts[userId] : {}
_.merge(mergedPosts,newPosts)
})
_.merge(mergedPosts,posts)
return {
authed: state.authorize.authed,
mainStyle: state.global.sidebarMainStyle,
mergedPosts
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Home))

View File

@@ -0,0 +1,285 @@
// - Import react components
import React, { Component } from 'react'
import { NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import SvgDehaze from 'material-ui/svg-icons/image/dehaze'
import { green700, grey400, blue500 } from 'material-ui/styles/colors'
import { Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle } from 'material-ui/Toolbar'
import IconButton from 'material-ui/IconButton'
import RaisedButton from 'material-ui/RaisedButton'
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover'
import Menu from 'material-ui/Menu'
import MenuItem from 'material-ui/MenuItem'
import Paper from 'material-ui/Paper'
import NotificationsIcon from 'material-ui/svg-icons/social/notifications'
import EventListener, { withOptions } from 'react-event-listener'
// - Import components
import UserAvatar from 'UserAvatar'
import Notify from 'Notify'
// - Import actions
import * as globalActions from 'globalActions'
import * as authorizeActions from 'authorizeActions'
// - Create HomeHeader component class
export class HomeHeader extends Component {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
// Default state
this.state = {
/**
* User avatar popover is open if true
*/
openAvatarMenu: false,
/**
* Show header title or not (true/false)
*/
showTitle: true,
/**
* If true notification menu will be open
*/
openNotifyMenu: false
}
// Binding functions to `this`
this.onToggleSidebar = this.onToggleSidebar.bind(this)
this.handleCloseNotify = this.handleCloseNotify.bind(this)
}
/**
* Handle close notification menu
*
*
* @memberof HomeHeader
*/
handleCloseNotify = () => {
this.setState({
openNotifyMenu: false
})
}
// On click toggle sidebar
onToggleSidebar = () => {
if (this.props.sidebarStatus) {
this.props.sidebar(false)
} else {
this.props.sidebar(true)
}
}
/**
* Handle notification touch
*
*
* @memberof HomeHeader
*/
handleNotifyTouchTap = (event) => {
// This prevents ghost click.
event.preventDefault();
this.setState({
openNotifyMenu: true,
anchorEl: event.currentTarget,
});
}
/**
* Handle touch on user avatar for popover
*
*
* @memberof HomeHeader
*/
handleAvatarTouchTap = (event) => {
// This prevents ghost click.
event.preventDefault();
this.setState({
openAvatarMenu: true,
anchorEl: event.currentTarget,
});
}
/**
* Handle logout user
*
*
* @memberof HomeHeader
*/
handleLogout = () => {
this.props.logout()
}
/**
* Handle close popover
*
*
* @memberof HomeHeader
*/
handleRequestClose = () => {
this.setState({
openAvatarMenu: false,
});
};
/**
* Handle resize event for window to manipulate home header status
* @param {event} evt is the event is passed by winodw resize event
*/
handleResize = (evt) => {
// Set initial state
var width = window.innerWidth
if (width >= 600 && !this.state.showTitle) {
this.setState({
showTitle: true
})
}
else if (width < 600 && this.state.showTitle) {
this.setState({
showTitle: false
})
}
}
componentDidMount = () => {
this.handleResize()
}
// Render app DOM component
render() {
/**
* Styles
*/
var styles = {
toolbarStyle: {
backgroundColor: "",
transition: "all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms",
boxSizing: "border-box",
fontFamily: "Roboto, sans-serif",
position: "fixed",
zIndex: "1101",
width: "100%",
top: "0px",
boxShadow: '0 1px 8px rgba(0,0,0,.3)'
},
avatarStyle: {
margin: 5,
cursor: 'pointer'
}
}
return (
<Toolbar style={styles.toolbarStyle} className="g__greenBox">
<EventListener
target="window"
onResize={this.handleResize}
onKeyUp={this.handleKeyUp}
/>
{/* Left side */}
<ToolbarGroup firstChild={true}>
<IconButton iconStyle={{ color: "#fff" }} onClick={this.onToggleSidebar} >
<SvgDehaze style={{ color: "#fff", marginLeft: "15px", cursor: "pointer" }} />
</IconButton>
{/* Header title */}
<ToolbarTitle style={{ color: "#fff", marginLeft: "15px" }} text="Green" />
{this.state.showTitle ? <div className="homeHeader__page">{this.props.title}</div> : ''}
</ToolbarGroup>
<ToolbarGroup>
</ToolbarGroup>
{/* Notification */}
<ToolbarGroup lastChild={true}>
<div className="homeHeader__right">
{this.props.notifyCount > 0 ? (<IconButton tooltip="Notifications" onTouchTap={this.handleNotifyTouchTap}>
<div className="homeHeader__notify">
<div className='title'>{this.props.notifyCount}</div>
</div>
</IconButton>)
: (<IconButton tooltip="Notifications" onTouchTap={this.handleNotifyTouchTap}>
<NotificationsIcon color='rgba(255, 255, 255, 0.87)' />
</IconButton>)}
<Notify open={this.state.openNotifyMenu} anchorEl={this.state.anchorEl} onRequestClose={this.handleCloseNotify}/>
{/* User avatar*/}
<UserAvatar
onTouchTap={this.handleAvatarTouchTap}
fileName={this.props.avatar}
size={32}
style={styles.avatarStyle}
/>
<Popover
open={this.state.openAvatarMenu}
anchorEl={this.state.anchorEl}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'left', vertical: 'top' }}
onRequestClose={this.handleRequestClose}
>
<Menu>
<MenuItem style={{ backgroundColor: 'white', color: blue500, fontSize: '14px' }} primaryText="MY ACCOUNT" />
<MenuItem primaryText="LOGOUT" style={{ fontSize: '14px' }} onClick={this.handleLogout.bind(this)} />
</Menu>
</Popover>
</div>
</ToolbarGroup>
</Toolbar>
)
}
}
// - Map dispatch to props
const mapDispatchToProps = (dispatch, ownProps) => {
return {
logout: () => dispatch(authorizeActions.dbLogout())
}
}
// - Map state to props
const mapStateToProps = (state, ownProps) => {
let notifyCount = state.notify.userNotifies
? Object
.keys(state.notify.userNotifies)
.filter((key)=> !state.notify.userNotifies[key].isSeen).length
: 0
return {
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : '',
title: state.global.headerTitle,
notifyCount
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(HomeHeader)

View File

@@ -0,0 +1,246 @@
// - Impoer react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { GridList, GridTile } from 'material-ui/GridList'
import IconButton from 'material-ui/IconButton'
import Subheader from 'material-ui/Subheader'
import StarBorder from 'material-ui/svg-icons/toggle/star-border'
import FloatingActionButton from 'material-ui/FloatingActionButton';
import SvgUpload from 'material-ui/svg-icons/file/cloud-upload'
import SvgAddImage from 'material-ui/svg-icons/image/add-a-photo'
import SvgDelete from 'material-ui/svg-icons/action/delete'
import { grey200, grey600 } from 'material-ui/styles/colors'
import FlatButton from 'material-ui/FlatButton'
import uuid from 'uuid'
// - Import actions
import * as imageGalleryActions from 'imageGalleryActions'
import * as fileActions from 'fileActions'
import * as globalActions from 'globalActions'
// - Import app components
import Img from 'Img'
// - Import API
import FileAPI from 'FileAPI'
/**
* Create ImageGallery component class
*/
export class ImageGallery extends Component {
static propTypes = {
/**
* Callback function to ser image url on parent component
*/
open: PropTypes.func
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props);
// Binding function to `this`
this.close = this.close.bind(this)
this.onFileChange = this.onFileChange.bind(this)
this.handleSetImage = this.handleSetImage.bind(this)
this.handleDeleteImage = this.handleDeleteImage.bind(this)
this.imageList = this.imageList.bind(this)
}
/**
* Handle set image
* @param {event} evt passed by on click event on add image
* @param {string} name is the name of the image
*/
handleSetImage = (evt, name) => {
this.props.set(name)
this.close()
}
/**
* Handle delete image
* @param {event} evt passed by on click event on delete image
* @param {integer} id is the image identifier which selected to delete
*/
handleDeleteImage = (evt, id) => {
this.props.deleteImage(id)
}
componentDidMount() {
window.addEventListener("onSendResizedImage", this.handleSendResizedImage)
}
componentWillUnmount() {
window.removeEventListener("onSendResizedImage", this.handleSendResizedImage)
}
/**
* Handle send image resize event that pass the resized image
*
*
* @memberof ImageGallery
*/
handleSendResizedImage = (event) => {
const { resizedImage, fileName } = event.detail
this.props.saveImage(resizedImage, fileName)
}
/**
* Handle on change file upload
*/
onFileChange = (evt) => {
const extension = FileAPI.getExtension(evt.target.files[0].name)
var fileName = (`${uuid()}.${extension}`)
let image = FileAPI.constraintImage(evt.target.files[0], fileName)
}
/**
* Hide image gallery
*/
close = () => {
this.props.close()
}
imageList = () => {
console.log('====================================');
console.log(this.props.images);
console.log('====================================');
return this.props.images.map((image, index) => {
return (<GridTile
key={image.id}
title={<SvgDelete hoverColor={grey200} color="white" color="white" style={{ marginLeft: "5px", cursor: "pointer" }} onClick={evt => this.handleDeleteImage(evt, image.id)} />}
subtitle={<span></span>}
actionIcon={<SvgAddImage hoverColor={grey200} color="white" style={{ marginRight: "5px", cursor: "pointer" }} onClick={evt => this.handleSetImage(evt, image.name)} />}
>
<div>
<div style={{ overflowY: "hidden", overflowX: "auto" }}>
<ul style={{ whiteSpace: "nowrap", padding: "0 6px", margin: "8px 0 0 0", verticalAlign: "bottom", flexShrink: 0, listStyleType: "none" }}>
<div style={{ display: "block" }}>
<div style={{ display: "block", marginRight: "8px", transition: "transform .25s" }}>
<li style={{ width: "100%", margin: 0, verticalAlign: "bottom", position: "static", display: "inline-block" }}>
<Img fileName={image.name} style={{ width: "100%", height: "auto" }} />
</li>
</div>
</div>
</ul>
</div>
</div>
</GridTile>)
})
}
/**
* When the post text changed
* @param {event} evt is an event passed by change post text callback funciton
* @param {string} data is the post content which user writes
*/
render() {
/**
* Component styles
* @type {Object}
*/
const styles = {
root: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-around',
},
gridList: {
width: 500,
height: 450,
overflowY: 'auto',
},
uploadButton: {
verticalAlign: 'middle',
},
uploadInput: {
cursor: 'pointer',
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
width: '100%',
opacity: 0,
}
}
return (
<div style={styles.root}>
<GridList
cellHeight={180}
style={styles.gridList}
>
<GridTile >
<div style={{ display: "flex", backgroundColor: "rgba(222, 222, 222, 0.52)", flexDirection: "column", justifyContent: "center", alignItems: "center", height: "100%" }}>
<FlatButton
label="Upload Photo"
labelStyle={{ fontWeight: 100 }}
labelPosition="before"
style={styles.uploadButton}
containerElement="label"
>
<input type="file" onChange={this.onFileChange} accept="image/*" style={styles.uploadInput} />
</FlatButton>
</div>
</GridTile>
{this.imageList()}
</GridList>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
saveImage: (file, fileName) => {
dispatch(imageGalleryActions.dbUploadImage(file, fileName))
},
deleteImage: (id) => {
dispatch(imageGalleryActions.dbDeleteImage(id))
}
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state) => {
return {
images: state.imageGallery.images,
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : ''
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ImageGallery)

158
app/components/Img.jsx Normal file
View File

@@ -0,0 +1,158 @@
// - Import react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import SvgImage from 'material-ui/svg-icons/image/image'
// - Import app components
// - Import API
// - Import actions
import * as imageGalleryActions from 'imageGalleryActions'
/**
* Create component class
*/
export class Img extends Component {
static propTypes = {
/**
* Use for getting url address from server
*/
fileName: PropTypes.string,
/**
* Avatar style
*/
style: PropTypes.object
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
isImageLoaded: false
}
// Binding functions to `this`
this.getImageURL = this.getImageURL.bind(this)
this.handleLoadImage = this.handleLoadImage.bind(this)
}
/**
* Will be called on loading image
*
* @memberof Img
*/
handleLoadImage = () => {
this.setState({
isImageLoaded: true
})
}
/**
* Get image url if it is not exist on redux store
*
* @memberof Img
*/
getImageURL = () => {
let { fileName } = this.props
if (fileName && fileName !== '') {
if (this.props.imageRequests.indexOf(fileName) > -1)
return
this.props.getImage(fileName)
}
}
componentWillMount() {
let { fileName } = this.props
if (this.props.imageRequests.indexOf(fileName) > -1)
return
this.getImageURL()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
loding: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100px',
position: 'relative',
color: '#cacecd',
fontWeight: 100
},
loadingContent: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
},
loadingImage: {
fill: 'aliceblue',
width: '50px',
height: '50px'
}
}
let { fileName, style } = this.props
let { isImageLoaded } = this.state
return (
<div>
<img onLoad={this.handleLoadImage} src={this.props.avatarURL[fileName] || ''} style={isImageLoaded ? style : { display: 'none' }} />
<div style={{ backgroundColor: 'blue' }} style={isImageLoaded ? { display: 'none' } : styles.loding}>
<div style={styles.loadingContent}>
<SvgImage style={styles.loadingImage} />
<div>Image has not loaded</div>
</div>
</div>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
avatarURL: state.imageGallery.imageURLList,
imageRequests: state.imageGallery.imageRequests
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Img)

191
app/components/ImgCover.jsx Normal file
View File

@@ -0,0 +1,191 @@
// - Import react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import SvgImage from 'material-ui/svg-icons/image/image'
// - Import app components
// - Import API
// - Import actions
import * as imageGalleryActions from 'imageGalleryActions'
/**
* Create component class
*/
export class ImgCover extends Component {
static propTypes = {
/**
* Use for getting url address from server
*/
fileName: PropTypes.string,
/**
* Image width
*/
width: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
/**
* Image height
*/
height: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
/**
* Image border radius
*/
borderRadius: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
isImageLoaded: false
}
// Binding functions to `this`
this.getImageURL = this.getImageURL.bind(this)
this.handleLoadImage = this.handleLoadImage.bind(this)
}
/**
* Will be called on loading image
*
* @memberof Img
*/
handleLoadImage = () => {
this.setState({
isImageLoaded: true
})
}
/**
* Get image url if it is not exist on redux store
*
* @memberof Img
*/
getImageURL = () => {
let { fileName } = this.props
if (fileName && fileName !== '') {
if (this.props.imageRequests.indexOf(fileName) > -1)
return
this.props.getImage(fileName)
}
}
componentWillMount() {
let { fileName } = this.props
if (this.props.imageRequests.indexOf(fileName) > -1)
return
this.getImageURL()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
let { fileName, style } = this.props
let { isImageLoaded } = this.state
/**
* Styles
*/
const styles = {
cover: {
backgroundImage: 'url(' + (this.props.avatarURL[fileName] || '#') + ')',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
width: this.props.width,
height: this.props.height,
borderRadius: this.props.borderRadius
},
loding: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100px',
position: 'relative',
color: '#cacecd',
fontWeight: 100
},
loadingContent: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
},
loadingImage: {
fill: 'aliceblue',
width: '50px',
height: '50px'
}
}
return (
<div>
<div style={Object.assign({},styles.cover,style)}>
{this.props.children}
</div>
<div style={{ backgroundColor: 'blue' }} style={isImageLoaded ? { display: 'none' } : styles.loding}>
<div style={styles.loadingContent}>
<SvgImage style={styles.loadingImage} />
<div>Image has not loaded</div>
</div>
</div>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
avatarURL: state.imageGallery.imageURLList,
imageRequests: state.imageGallery.imageRequests
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ImgCover)

212
app/components/Login.jsx Normal file
View File

@@ -0,0 +1,212 @@
// - Import external components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { NavLink, withRouter } from 'react-router-dom'
import { push } from 'react-router-redux'
import Paper from 'material-ui/Paper'
import TextField from 'material-ui/TextField'
import RaisedButton from 'material-ui/RaisedButton'
import FlatButton from 'material-ui/FlatButton'
// - Import actions
import * as authorizeActions from 'authorizeActions'
// - Create Login component class
export class Login extends Component {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props);
this.state = {
emailInput: '',
emailInputError: '',
passwordInput: '',
passwordInputError: '',
}
// Binding function to `this`
this.handleForm = this.handleForm.bind(this)
}
/**
* Handle data on input change
* @param {event} evt is an event of inputs of element on change
*/
handleInputChange = (evt) => {
const target = evt.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
switch (name) {
case 'emailInput':
this.setState({
emailInputError: ''
})
break
case 'passwordInput':
this.setState({
confirmInputError: '',
passwordInputError: ''
})
break;
default:
}
}
/**
* Handle register form
*/
handleForm = () => {
var error = false
if (this.state.emailInput === '') {
this.setState({
emailInputError: 'This field is required'
})
error = true
}
if (this.state.passwordInput === '') {
this.setState({
passwordInputError: 'This field is required'
})
error = true
}
if (!error) {
this.props.login(
this.state.emailInput,
this.state.passwordInput
)
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const paperStyle = {
minHeight: 370,
width: 450,
margin: 20,
textAlign: 'center',
display: 'block',
margin: "auto"
};
return (
<form>
<h1 style={{
textAlign: "center",
padding: "20px",
fontSize: "30px",
fontWeight: 500,
lineHeight: "32px",
margin: "auto",
color: "rgba(138, 148, 138, 0.2)"
}}>Green</h1>
<div className="animate-bottom">
<Paper style={paperStyle} zDepth={1} rounded={false} >
<div style={{ padding: "48px 40px 36px" }}>
<div style={{
paddingLeft: "40px",
paddingRight: "40px"
}}>
<h2 style={{
textAlign: "left",
paddingTop: "16px",
fontSize: "24px",
fontWeight: 400,
lineHeight: "32px",
margin: 0
}}>Sign in</h2>
</div>
<TextField
onChange={this.handleInputChange}
errorText={this.state.emailInputError}
name="emailInput"
floatingLabelStyle={{ fontSize: "15px" }}
floatingLabelText="Email"
type="email"
tabIndex={1}
/><br />
<TextField
onChange={this.handleInputChange}
errorText={this.state.passwordInputError}
name="passwordInput"
floatingLabelStyle={{ fontSize: "15px" }}
floatingLabelText="Password"
type="password"
tabIndex={2}
/><br />
<br />
<br />
<div className="login__button-box">
<div>
<FlatButton label="Create an account" onClick={this.props.signupPage} tabIndex={4} />
</div>
<div >
<RaisedButton label="Login" primary={true} onClick={this.handleForm} tabIndex={3} />
</div>
</div>
</div>
</Paper>
</div>
</form>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
login: (username, password) => {
dispatch(authorizeActions.dbLogin(username, password))
},
signupPage: () => {
dispatch(push("/signup"))
}
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Login))

216
app/components/Master.jsx Normal file
View File

@@ -0,0 +1,216 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Route, Switch, NavLink, withRouter, Redirect } from 'react-router-dom'
import { firebaseAuth, firebaseRef } from 'app/firebase/'
import { push } from 'react-router-redux'
import Snackbar from 'material-ui/Snackbar';
import LinearProgress from 'material-ui/LinearProgress'
// - Import components
import Home from 'Home'
import Signup from 'Signup'
import Login from 'Login'
import Settings from 'Settings'
import MasterLoading from 'MasterLoading'
// - Import API
import { PrivateRoute, PublicRoute } from 'AuthRouterAPI'
// - Import actions
import * as authorizeActions from 'authorizeActions'
import * as imageGalleryActions from 'imageGalleryActions'
import * as postActions from 'postActions'
import * as commentActions from 'commentActions'
import * as voteActions from 'voteActions'
import * as userActions from 'userActions'
import * as globalActions from 'globalActions'
import * as circleActions from 'circleActions'
import * as notifyActions from 'notifyActions'
/* ------------------------------------ */
// - Create Master component class
export class Master extends Component {
static isPrivate = true
// Constructor
constructor(props) {
super(props);
this.state = {
loading: true,
authed: false,
dataLoaded: false
};
// Binding functions to `this`
this.handleLoading = this.handleLoading.bind(this)
this.handleMessage = this.handleMessage.bind(this)
}
// Handle click on message
handleMessage = (evt) => {
this.props.closeMessage()
}
// Handle loading
handleLoading = (status) => {
this.setState({
loading: status,
authed: false
})
}
componentWillMount = () => {
firebaseAuth().onAuthStateChanged((user) => {
if (user) {
this.props.login(user)
this.setState({
loading: false
})
if (!this.props.global.defaultLoadDataStatus) {
this.props.clearData()
this.props.loadData()
this.props.defaultDataEnable()
}
} else {
this.props.logout()
this.setState({
loading: false
})
if (this.props.global.defaultLoadDataStatus) {
this.props.defaultDataDisable()
this.props.clearData()
}
this.props.loadDataGuest()
}
})
}
/**
* Render app DOM component
*
* @returns
*
* @memberof Master
*/
render() {
const {progress, global} = this.props
return (
<div id="master">
<div className='master__progress' style={{display: (progress.visible ? 'block' : 'none' )}}>
<LinearProgress mode="determinate" value={progress.percent} />
</div>
<div className='master__loading animate-fading2' style={{display: ( global.showTopLoading ? 'flex' : 'none' )}}>
<div className='title'> Loading ... </div>
</div>
<MasterLoading activeLoading={this.state.loading || !(this.props.loaded || this.props.guest)} handleLoading={this.handleLoading} />
{(!this.state.loading && (this.props.loaded || this.props.guest))
?(<Switch>
<Route path="/signup" component={Signup} />
<Route path="/settings" component={Settings} />
<Route path="/login" render={() => {
console.log('this.props.authed: ', this.props.authed, "this.props: ", this.props)
return (
this.props.authed
? <Redirect to="/" />
: <Login />
)
}
} />
<Route render={() =><Home uid={this.props.uid}/>} />
</Switch>) : ''
}
<Snackbar
open={this.props.global.messageOpen}
message={this.props.global.message}
autoHideDuration={4000}
style={{left: '1%', transform: 'none'}}
/>
</div>
)
}
}
// - Map dispatch to props
const mapDispatchToProps = (dispatch, ownProps) => {
return {
loadData: () => {
dispatch(commentActions.dbGetComments())
dispatch(imageGalleryActions.downloadForImageGallery())
dispatch(postActions.dbGetPosts())
dispatch(userActions.dbGetUserInfo())
dispatch(voteActions.dbGetVotes())
dispatch(notifyActions.dbGetNotifies())
dispatch(circleActions.dbGetCircles())
},
clearData: () => {
dispatch(imageGalleryActions.clearAllData())
dispatch(postActions.clearAllData())
dispatch(userActions.clearAllData())
dispatch(commentActions.clearAllData())
dispatch(voteActions.clearAllvotes())
dispatch(notifyActions.clearAllNotifications())
dispatch(circleActions.clearAllCircles())
},
login: (user) => {
dispatch(authorizeActions.login(user.uid))
},
logout: () => {
dispatch(authorizeActions.logout())
},
defaultDataDisable: () => {
dispatch(globalActions.defaultDataDisable())
},
defaultDataEnable: () => {
dispatch(globalActions.defaultDataEnable())
},
closeMessage: () => {
dispatch(globalActions.hideMessage())
},
loadDataGuest: () => {
dispatch(globalActions.loadDataGuest())
}
}
}
// - Map state to props
const mapStateToProps = ({authorize, global, user, post, comment, imageGallery , vote, notify,circle }) => {
return {
guest: authorize.guest,
uid: authorize.uid,
authed: authorize.authed,
progress: global.progress,
global: global,
loaded: user.loaded && post.loaded && comment.loaded && imageGallery.loaded && vote.loaded && notify.loaded && circle.loaded
}
}
// - Connect commponent to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Master))

View File

@@ -0,0 +1,53 @@
// - Import react components
import React, {Component} from 'react'
import CircularProgress from 'material-ui/CircularProgress'
import Dialog from 'material-ui/Dialog'
// - Import app components
// - Create MasterLoading component class
export default class MasterLoading extends Component {
// Constructor
constructor(props) {
super(props);
// Binding functions to `this`
}
// Render app DOM component
render() {
return (
<Dialog
modal={true}
open={this.props.activeLoading}
autoDetectWindowHeight={false}
overlayStyle={{backgroundColor: "white"}}
contentClassName="mLoading__content"
bodyStyle={{backgroundColor: ""}}
bodyClassName="mLoading__body"
>
<div>
<div className="mLoading__context">
<CircularProgress color="white" size={80} thickness={7} />
<h1 style={{float:"right", color:"#fff"}}>Green</h1>
</div>
</div>
</Dialog>
);
}
}

139
app/components/Notify.jsx Normal file
View File

@@ -0,0 +1,139 @@
// - Import react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover'
// - Import app components
import NotifyItem from 'NotifyItem'
// - Import API
// - Import actions
import * as userActions from 'userActions'
/**
* Create component class
*/
export class Notify extends Component {
static propTypes = {
/**
* It will be true if the notification is open
*/
open: PropTypes.bool,
/**
* Pass anchor element
*/
anchorEl: PropTypes.any,
/**
* Fire to close notificaion
*/
onRequestClose: PropTypes.func,
/**
* If user's seen notification box or not (true/false)
*/
isSeen: PropTypes.bool
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
notifyItemList = () => {
let { notifications, info, onRequestClose } = this.props
let parsedDOM = []
if (notifications) {
Object.keys(notifications).forEach((key) => {
const {notifierUserId} = notifications[key]
parsedDOM.push(
<NotifyItem
key={key}
description={(notifications[key] ? notifications[key].description || '' : '')}
fullName={(info[notifierUserId] ? info[notifierUserId].fullName || '' : '')}
avatar={(info[notifierUserId] ? info[notifierUserId].avatar || '' : '')}
id={key}
isSeen={(notifications[key] ? notifications[key].isSeen || false : false )}
url={(notifications[key] ? notifications[key].url || '' : '')}
notifierUserId={notifierUserId}
closeNotify={onRequestClose}
/>
)
})
}
return parsedDOM
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
let { open, anchorEl, onRequestClose } = this.props
return (
<Popover
className='homeHeader__notify-menu'
open={open}
anchorEl={anchorEl}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'left', vertical: 'top' }}
onRequestClose={onRequestClose}
>
<div className='container'>
<div className='title'>Green </div>
<div className='content'>
{this.notifyItemList()}
</div>
</div>
</Popover>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
notifications: state.notify.userNotifies,
info: state.user.info
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Notify)

View File

@@ -0,0 +1,155 @@
// - Import react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { push } from 'react-router-redux'
import SvgClose from 'material-ui/svg-icons/navigation/close'
import { grey400 } from 'material-ui/styles/colors'
// - Import app components
import UserAvatar from 'UserAvatar'
// - Import API
// - Import actions
import * as notifyActions from 'notifyActions'
/**
* Create component class
*/
export class NotifyItem extends Component {
static propTypes = {
/**
* Notification description
*/
description: PropTypes.string,
/**
* Which user relates to the notification item
*/
fullName: PropTypes.string,
/**
* Avatar of the user who relate to the notification item
*/
avatar: PropTypes.string,
/**
* Notification identifier
*/
id: PropTypes.string,
/**
* If user's seen the notification or not (true/false)
*/
isSeen: PropTypes.bool,
/**
* Which address notification refers
*/
url: PropTypes.string,
/**
* The notifier user identifier
*/
notifierUserId: PropTypes.string,
/**
* Close notification popover
*/
closeNotify: PropTypes.func
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
this.handleSeenNotify = this.handleSeenNotify.bind(this)
}
handleSeenNotify = (evt) => {
evt.preventDefault()
const { seenNotify, id, url, goTo, isSeen, closeNotify } = this.props
if (id) {
if (!isSeen)
seenNotify(id)
closeNotify()
goTo(url)
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
let { description, fullName, avatar, isSeen, id, goTo,closeNotify, notifierUserId, url, deleteNotiy } = this.props
return (
<div className='item' style={isSeen ? { opacity: 0.6 } : {}} key={id}>
<div className='avatar'>
<NavLink
to={`/${notifierUserId}`}
onClick={(evt) => {
evt.preventDefault()
closeNotify()
goTo(`/${notifierUserId}`)
}}
>
<UserAvatar fileName={avatar} />
</NavLink>
</div>
<div className='info'>
<NavLink to={url} onClick={this.handleSeenNotify}>
<div className='user-name'>
{fullName}
</div>
<div className='description'>
{description}
</div>
</NavLink>
</div>
<div className='close' onClick={() => deleteNotiy(id)}>
<SvgClose hoverColor={grey400} style={{ cursor: 'pointer' }} />
</div>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
goTo: (url) => dispatch(push(url)),
seenNotify: (id) => dispatch(notifyActions.dbSeenNotify(id)),
deleteNotiy: (id) => dispatch(notifyActions.dbDeleteNotify(id))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(NotifyItem)

174
app/components/People.jsx Normal file
View File

@@ -0,0 +1,174 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import {withRouter} from 'react-router-dom'
import PropTypes from 'prop-types'
import { Tabs, Tab } from 'material-ui/Tabs'
import { grey50, grey200, grey400, grey600, cyan500 } from 'material-ui/styles/colors'
import {push} from 'react-router-redux'
// - Import app components
import FindPeople from 'FindPeople'
import Following from 'Following'
import Followers from 'Followers'
import YourCircles from 'YourCircles'
// - Import API
// - Import actions
import * as circleActions from 'circleActions'
import * as globalActions from 'globalActions'
/**
* Create component class
*/
export class People extends Component {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
componentWillMount() {
const {tab} = this.props.match.params
switch (tab) {
case undefined:
case '':
this.props.setHeaderTitle('People')
break;
case 'circles':
this.props.setHeaderTitle('Circles')
break;
case 'followers':
this.props.setHeaderTitle('Followers')
break;
default:
break;
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
/**
* Component styles
*/
const styles = {
people: {
margin: '0 auto',
width: '90%'
},
headline: {
fontSize: 24,
paddingTop: 16,
marginBottom: 12,
fontWeight: 400,
},
slide: {
padding: 10,
}
}
const {circlesLoaded} = this.props
const {tab} = this.props.match.params
let tabIndex = 0
switch (tab) {
case undefined:
case '':
tabIndex = 0
break;
case 'circles':
tabIndex = 1
break;
case 'followers':
tabIndex = 2
break;
default:
break;
}
return (
<div style={styles.people}>
<Tabs inkBarStyle={{backgroundColor: grey50}} initialSelectedIndex={tabIndex} >
<Tab label="Find People" onActive={() => {
this.props.goTo('/people')
this.props.setHeaderTitle('People')
}} >
{circlesLoaded ? <FindPeople /> : ''}
</Tab>
<Tab label="Following" onActive={() => {
this.props.goTo('/people/circles')
this.props.setHeaderTitle('Circles')
}} >
{circlesLoaded ? <Following/> : ''}
{circlesLoaded ? <YourCircles /> : ''}
</Tab>
<Tab label="Followers" onActive={() => {
this.props.goTo('/people/followers')
this.props.setHeaderTitle('Followers')
}}>
{circlesLoaded ? <Followers /> : ''}
</Tab>
</Tabs>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
goTo: (url)=> dispatch(push(url)),
setHeaderTitle : (title) => dispatch(globalActions.setHeaderTitle(title))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
uid: state.authorize.uid,
circlesLoaded: state.circle.loaded
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(People))

495
app/components/Post.jsx Normal file
View File

@@ -0,0 +1,495 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import {NavLink} from 'react-router-dom'
import { push } from 'react-router-redux'
import PropTypes from 'prop-types'
import moment from 'moment'
import { createAction as action } from 'redux-actions'
import { Card, CardActions, CardHeader, CardMedia, CardTitle, CardText } from 'material-ui/Card'
import FloatingActionButton from 'material-ui/FloatingActionButton'
import SvgShare from 'material-ui/svg-icons/social/share'
import SvgLink from 'material-ui/svg-icons/content/link'
import SvgComment from 'material-ui/svg-icons/Communication/comment'
import SvgFavorite from 'material-ui/svg-icons/action/favorite'
import SvgFavoriteBorder from 'material-ui/svg-icons/action/favorite-border'
import Checkbox from 'material-ui/Checkbox'
import FlatButton from 'material-ui/FlatButton'
import Divider from 'material-ui/Divider'
import { grey200, grey400, grey600 } from 'material-ui/styles/colors'
import Paper from 'material-ui/Paper'
import Menu from 'material-ui/Menu'
import MenuItem from 'material-ui/MenuItem'
import TextField from 'material-ui/TextField'
import Dialog from 'material-ui/Dialog'
import IconButton from 'material-ui/IconButton'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
import IconMenu from 'material-ui/IconMenu'
import reactStringReplace from 'react-string-replace'
// - Import app components
import CommentGroup from 'CommentGroup'
import PostWrite from 'PostWrite'
import Img from 'Img'
import IconButtonElement from 'IconButtonElement'
import UserAvatar from 'UserAvatar'
// - Import actions
import * as voteActions from 'voteActions'
import * as postActions from 'postActions'
import * as globalActions from 'globalActions'
// - Create component class
export class Post extends Component {
static propTypes = {
/**
* The context of a post
*/
body: PropTypes.string,
/**
* The number of comment on a post
*/
commentCounter:PropTypes.number,
/**
* Creation post date
*/
creationDate:PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
/**
* Post identifier
*/
id:PropTypes.string,
/**
* Post image address
*/
image:PropTypes.string,
/**
* The last time date when post has was edited
*/
lastEditDate:PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
/**
* The name of the user who created the post
*/
ownerDisplayName:PropTypes.string,
/**
* The identifier of the user who created the post
*/
ownerUserId:PropTypes.string,
/**
* The avatar address of the user who created the post
*/
ownerAvatar:PropTypes.string,
/**
* If post is only [0]text, [1]whith picture, ...
*/
postTypeId:PropTypes.number,
/**
* The number votes on a post
*/
score:PropTypes.number,
/**
* Array of tags on a post
*/
tags:PropTypes.array,
/**
* The video address of a post
*/
video:PropTypes.string,
/**
* If it's true comment will be disabled on a post
*/
disableComments:PropTypes.bool,
/**
* If it's true sharing will be disabled on a post
*/
disableSharing:PropTypes.bool,
/**
* The number of users who has visited the post
*/
viewCount:PropTypes.number
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
this.state = {
/**
* Post text
*/
text: this.props.body,
/**
* It's true if whole the text post is visible
*/
readMoreState: false,
/**
* Handle open comment from parent component
*/
openComments: false,
/**
* If it's true, share dialog will be open
*/
shareOpen: false,
/**
* If it's true comment will be disabled on post
*/
disableComments: this.props.disableComments,
/**
* If it's true share will be disabled on post
*/
disableSharing: this.props.disableSharing,
/**
* Title of share post
*/
shareTitle: 'Share On',
/**
* If it's true, post link will be visible in share post dialog
*/
openCopyLink: false,
/**
* If it's true, post write will be open
*/
openPostWrite: false
}
// Binding functions to this
this.handleReadMore = this.handleReadMore.bind(this)
this.getOpenCommentGroup = this.getOpenCommentGroup.bind(this)
this.handleVote = this.handleVote.bind(this)
this.handleOpenShare = this.handleOpenShare.bind(this)
this.handleCloseShare = this.handleCloseShare.bind(this)
this.handleCopyLink = this.handleCopyLink.bind(this)
this.handleDelete = this.handleDelete.bind(this)
this.handleOpenPostWrite = this.handleOpenPostWrite.bind(this)
this.handleClosePostWrite = this.handleClosePostWrite.bind(this)
this.handleOpenComments = this.handleOpenComments.bind(this)
}
/**
* Toggle on show/hide comment
* @param {event} evt passed by clicking on comment slide show
*/
handleOpenComments = (evt) => {
this.setState({
openComments: !this.state.openComments
})
}
/**
* Open post write
*
*
* @memberof Blog
*/
handleOpenPostWrite = () => {
this.setState({
openPostWrite: true
})
}
/**
* Close post write
*
*
* @memberof Blog
*/
handleClosePostWrite = () => {
this.setState({
openPostWrite: false
})
}
/**
* Delete a post
*
*
* @memberof Post
*/
handleDelete = () => {
this.props.delete(this.props.id)
}
/**
* Show copy link
*
*
* @memberof Post
*/
handleCopyLink = () => {
this.setState({
openCopyLink: true,
shareTitle: 'Copy Link'
})
}
/**
* Open share post
*
*
* @memberof Post
*/
handleOpenShare = () => {
this.setState({
shareOpen: true
})
}
/**
* Close share post
*
*
* @memberof Post
*/
handleCloseShare = () => {
this.setState({
shareOpen: false,
shareTitle: 'Share On',
openCopyLink: false
})
}
/**
* Handle vote on a post
*
*
* @memberof Post
*/
handleVote = () => {
if (this.props.userVoteStatus) {
this.props.unvote()
}
else {
this.props.vote()
}
}
/**
* Set open comment group function on state which passed by CommentGroup component
* @param {function} open the function to open comment list
*/
getOpenCommentGroup = (open) => {
this.setState({
openCommentGroup: open
})
}
/**
* Handle read more event
* @param {event} evt is the event passed by click on read more
*/
handleReadMore(evt) {
this.setState({
readMoreState: !this.state.readMoreState
});
}
componentDidMount() {
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
/**
* DOM styles
*
*
* @memberof Post
*/
const styles = {
counter: {
lineHeight: '36px',
color: '#777',
fontSize: '12px',
marginRight: '6px'
},
dialog: {
width: '',
maxWidth: '530px',
borderRadius: "4px"
},
rightIconMenu: {
position: 'absolute',
right: 18,
top: 8,
},
iconButton: {
width: 24,
height: 24
}
}
const RightIconMenu = () => (
<IconMenu iconButtonElement={IconButtonElement} style={{ display: "block", position: "absolute", top: "0px", right: "4px" }}>
<MenuItem primaryText="Edit" onClick={this.handleOpenPostWrite} />
<MenuItem primaryText="Delete" onClick={this.handleDelete} />
<MenuItem primaryText={this.props.disableComments ? "Enable comments" : "Disable comments"} onClick={() => this.props.toggleDisableComments(!this.props.disableComments)} />
<MenuItem primaryText={this.props.disableSharing ? "Enable sharing" : "Disable sharing"} onClick={() => this.props.toggleSharingComments(!this.props.disableSharing)} />
</IconMenu>
)
const {ownerUserId,setHomeTitle, goTo, ownerDisplayName,creationDate, avatar, isPostOwner,image, body} = this.props
// Define variables
return (
<Card>
<CardHeader
title={<NavLink to={`/${ownerUserId}`}>{ownerDisplayName}</NavLink>}
subtitle={moment.unix(creationDate).fromNow() + " | public"}
avatar={<NavLink to={`/${ownerUserId}`}><UserAvatar fileName={avatar} size={36} /></NavLink>}
>
{isPostOwner ? ( <div style={styles.rightIconMenu}><RightIconMenu /></div>) : ''}
</CardHeader>
{image ? (
<CardMedia>
<Img fileName={image} />
</CardMedia>) : ''}
<CardText>
{reactStringReplace(body,/#(\w+)/g, (match, i) => (
<NavLink
style={{color:'green'}}
key={match + i}
to={`/tag/${match}`}
onClick ={evt => {
evt.preventDefault()
goTo(`/tag/${match}`)
setHomeTitle(`#${match}`)
}}
>
#{match}
</NavLink>
))}
</CardText>
<CardActions>
<div style={{ margin: "16px 8px", display: 'flex', justifyContent: 'space-between' }}>
<div style={{ display: 'flex' }}>
{/*<FloatingActionButton style={{ margin: "0 8px" }} zDepth={1} backgroundColor={grey200} iconStyle={{ color: grey600, fill: grey600, height: "36px", width: "36px" }} zDepth={1} secondary={false}>*/}
<div className='g__circle' onClick={this.handleVote}>
<Checkbox
checkedIcon={<SvgFavorite style={{fill:'#4CAF50'}}/>}
uncheckedIcon={<SvgFavoriteBorder style={{fill:'#757575'}} />}
defaultChecked={this.props.userVoteStatus}
style={{transform: 'translate(6px, 6px)'}}
/>
</div>
<div style={styles.counter}> {this.props.voteCount > 0 ? this.props.voteCount : ''} </div>
</div>
<div style={{ display: 'flex' }}>
{!this.props.disableComments ? (<div style={{display: 'inherit'}}><FloatingActionButton onClick={this.handleOpenComments} style={{ margin: "0 8px" }} zDepth={1} backgroundColor={grey200} iconStyle={{ color: grey600, fill: grey600, height: "36px", width: "36px" }} zDepth={1} secondary={false}>
<SvgComment viewBox="0 -9 24 34" style={{ height: "30px", width: "30px" }} /> 3
</FloatingActionButton>
<div style={styles.counter}>{this.props.commentCount > 0 ? this.props.commentCount : ''} </div></div>) : ''}
{!this.props.disableSharing ? (<FloatingActionButton onClick={this.handleOpenShare} style={{ margin: "0 8px" }} zDepth={1} backgroundColor={grey200} iconStyle={{ color: grey600, fill: grey600, height: "36px", width: "36px" }} zDepth={1} secondary={false}>
<SvgShare viewBox="0 -9 24 34" style={{ height: "30px", width: "30px" }} />
</FloatingActionButton>) : ''}
</div>
</div>
</CardActions>
<CommentGroup open={this.state.openComments} ownerPostUserId={this.props.ownerUserId} onToggleRequest={this.handleOpenComments} isPostOwner={this.props.isPostOwner} disableComments={this.props.disableComments} postId={this.props.id} />
{/* Copy link dialog*/}
<Dialog
title="Share On"
modal={false}
open={this.state.shareOpen}
onRequestClose={this.handleCloseShare}
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
contentStyle={styles.dialog}
autoDetectWindowHeight={false}
actionsContainerStyle={{ borderTop: "1px solid rgb(224, 224, 224)" }}
>
{!this.state.openCopyLink
? (<Paper >
<Menu>
<MenuItem primaryText="Copy Link" leftIcon={<SvgLink />} onClick={this.handleCopyLink} />
</Menu>
</Paper>)
: <TextField fullWidth={true} id="text-field-default" defaultValue={`${location.origin}/${this.props.ownerUserId}/posts/${this.props.id}`} />
}
</Dialog>
<PostWrite
open={this.state.openPostWrite}
onRequestClose={this.handleClosePostWrite}
edit={true}
text= {this.props.body}
image= {this.props.image ? this.props.image : ''}
id= {this.props.id}
disableComments= {this.props.disableComments}
disableSharing= {this.props.disableSharing}
/>
</Card>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
vote: () => dispatch(voteActions.dbAddVote(ownProps.id,ownProps.ownerUserId)),
unvote: () => dispatch(voteActions.dbDeleteVote(ownProps.id)) ,
delete: (id) => dispatch(postActions.dbDeletePost(id)),
toggleDisableComments: (status) => dispatch(postActions.dbUpdatePost({id:ownProps.id, disableComments:status},_ =>_)),
toggleSharingComments: (status) => dispatch(postActions.dbUpdatePost({id:ownProps.id, disableSharing:status},_ => _)),
goTo: (url) => dispatch(push(url)),
setHomeTitle: (title) => dispatch(globalActions.setHeaderTitle(title|| ''))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const {uid} = state.authorize
let votes = state.vote.postVotes[ownProps.id]
const post = (state.post.userPosts[uid] ? Object.keys(state.post.userPosts[uid]).filter((key)=>{ return ownProps.id === key}).length : 0)
return {
avatar: state.user.info && state.user.info[ownProps.ownerUserId] ? state.user.info[ownProps.ownerUserId].avatar || '' : '',
commentCount: state.comment.postComments[ownProps.id] ? Object.keys(state.comment.postComments[ownProps.id]).length : 0,
voteCount: state.vote.postVotes[ownProps.id] ? Object.keys(state.vote.postVotes[ownProps.id]).length : 0,
userVoteStatus: votes && Object.keys(votes).filter((key) => votes[key].userId === state.authorize.uid)[0] ? true : false,
isPostOwner: post > 0
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Post)

View File

@@ -0,0 +1,89 @@
// - Import react components
import React, {Component} from 'react'
import {connect} from 'react-redux'
import PropTypes from 'prop-types'
// - Import app components
import Blog from 'Blog'
// - Import API
// - Import actions
import * as postActions from 'postActions'
import * as userActions from 'userActions'
/**
* Create component class
*/
export class PostPage extends Component {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props){
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
componentWillMount() {
this.props.loadPost()
this.props.loadUserInfo()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
return (
<Blog posts={this.props.posts} displayWriting={false} />
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch,ownProps) => {
const {userId,postId} = ownProps.match.params
return{
loadPost: () => dispatch(postActions.dbGetPostById(userId,postId)),
loadUserInfo: () => dispatch(userActions.dbGetUserInfoByUserId(userId,'header'))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state,ownProps) => {
const {userId,postId} = ownProps.match.params
return{
avatar: state.user.info && state.user.info[userId] ? state.user.info[userId].avatar : '',
name: state.user.info && state.user.info[userId] ? state.user.info[userId].fullName : '',
posts: state.post.userPosts && state.post.userPosts[userId] ? {[postId] : { ...state.post.userPosts[userId][postId]}} : {}
}
}
// - Connect component to redux store
export default connect(mapStateToProps,mapDispatchToProps)(PostPage)

View File

@@ -0,0 +1,484 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { List, ListItem } from 'material-ui/List'
import Paper from 'material-ui/Paper'
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import { grey400, grey800, darkBlack, lightBlack } from 'material-ui/styles/colors'
import IconButton from 'material-ui/IconButton'
import TextField from 'material-ui/TextField'
import MenuItem from 'material-ui/MenuItem'
import SvgRemoveImage from 'material-ui/svg-icons/content/remove-circle'
import SvgCamera from 'material-ui/svg-icons/image/photo-camera'
import IconMenu from 'material-ui/IconMenu'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
// - Import app components
import ImageGallery from 'ImageGallery'
import Img from 'Img'
import UserAvatar from 'UserAvatar'
// - Import API
import * as AuthAPI from 'AuthAPI'
import * as PostAPI from 'PostAPI'
// - Import actions
import * as imageGalleryActions from 'imageGalleryActions'
import * as postActions from 'postActions'
// - Create PostWrite component class
export class PostWrite extends Component {
static propTypes = {
/**
* If it's true post writing page will be open
*/
open: PropTypes.bool,
/**
* Recieve request close function
*/
onRequestClose: PropTypes.func,
/**
* Post write style
*/
style: PropTypes.object,
/**
* If it's true, post will be in edit view
*/
edit: PropTypes.bool.isRequired,
/**
* The text of post in editing state
*/
text: PropTypes.string,
/**
* The image of post in editing state
*/
image: PropTypes.string,
/**
* If post state is editing this id sould be filled with post identifier
*/
id: PropTypes.string
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
// Default state
this.state = {
/**
* Post text
*/
postText: this.props.edit ? this.props.text : '',
/**
* The image of the post
*/
image: this.props.edit ? this.props.image : '',
/**
* If it's true gallery will be open
*/
galleryOpen: false,
/**
* If it's true post button will be disabled
*/
disabledPost: true,
/**
* If it's true comment will be disabled on post
*/
disableComments: this.props.edit ? this.props.disableComments : false,
/**
* If it's true share will be disabled on post
*/
disableSharing: this.props.edit ? this.props.disableSharing : false,
}
// Binding functions to `this`
this.handleOnChange = this.handleOnChange.bind(this)
this.handleCloseGallery = this.handleCloseGallery.bind(this)
this.handleOpenGallery = this.handleOpenGallery.bind(this)
this.onRequestSetImage = this.onRequestSetImage.bind(this)
this.handlePost = this.handlePost.bind(this)
this.handleRemoveImage = this.handleRemoveImage.bind(this)
this.handleToggleComments = this.handleToggleComments.bind(this)
this.handleToggleSharing = this.handleToggleSharing.bind(this)
}
/**
* Toggle comments of the post to disable/enable
*
*
* @memberof PostWrite
*/
handleToggleComments = () => {
this.setState({
disableComments: !this.state.disableComments,
disabledPost: false
})
}
/**
* Toggle sharing of the post to disable/enable
*
*
* @memberof PostWrite
*/
handleToggleSharing = () => {
this.setState({
disableSharing: !this.state.disableSharing,
disabledPost: false
})
}
/**
* Romove the image of post
*
*
* @memberof PostWrite
*/
handleRemoveImage = () => {
this.setState({
image: '',
disabledPost: false
})
}
/**
* Handle send post to the server
* @param {event} evt passed by clicking on the post button
*/
handlePost = (evt) => {
var image = this.state.image
var tags = PostAPI.getContentTags(this.state.postText)
// In edit status we should fire update if not we should fire post function
if (!this.props.edit) {
if (image !== '') {
this.props.post({
body: this.state.postText,
tags: tags,
image: image,
avatar: this.props.avatar,
name: this.props.name,
disableComments: this.state.disableComments,
disableSharing: this.state.disableSharing
}, this.props.onRequestClose)
}
else {
this.props.post({
body: this.state.postText,
tags: tags,
avatar: this.props.avatar,
name: this.props.name,
disableComments: this.state.disableComments,
disableSharing: this.state.disableSharing
}, this.props.onRequestClose)
}
}
// In edit status we pass post to update functions
else {
this.props.update({
id: this.props.id,
body: this.state.postText,
tags: tags,
image: image,
disableComments: this.state.disableComments,
disableSharing: this.state.disableSharing
}, this.props.onRequestClose)
}
}
/**
* Set post image url
*/
onRequestSetImage = (url) => {
this.setState({
image: url,
disabledPost: false
})
}
/**
* When the post text changed
* @param {event} evt is an event passed by change post text callback funciton
* @param {string} data is the post content which user writes
*/
handleOnChange = (evt, data) => {
this.setState({ postText: data })
if (data.length === 0 || data.trim() === '' || (this.props.edit && data.trim() === this.props.text)) {
this.setState({
postText: data,
disabledPost: true
})
}
else {
this.setState({
postText: data,
disabledPost: false
})
}
}
/**
* Close image gallery
*/
handleCloseGallery = () => {
this.setState({
galleryOpen: false
})
}
/**
* Open image gallery
*/
handleOpenGallery = () => {
this.setState({
galleryOpen: true
})
}
componentWillReceiveProps(nextProps) {
if(!nextProps.open){
this.setState({
/**
* Post text
*/
postText: this.props.edit ? this.props.text : '',
/**
* The image of the post
*/
image: this.props.edit ? this.props.image : '',
/**
* If it's true gallery will be open
*/
galleryOpen: false,
/**
* If it's true post button will be disabled
*/
disabledPost: true,
/**
* If it's true comment will be disabled on post
*/
disableComments: this.props.edit ? this.props.disableComments : false,
/**
* If it's true share will be disabled on post
*/
disableSharing: this.props.edit ? this.props.disableSharing : false,
})
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const iconButtonElement = (
<IconButton
touch={true}
tooltip="more"
tooltipPosition="bottom-left"
>
<MoreVertIcon color={grey400} />
</IconButton>
)
const rightIconMenu = (
<IconMenu iconButtonElement={iconButtonElement}>
<MenuItem onClick={this.handleToggleComments} style={{ fontSize: "14px" }}>{!this.state.disableComments ? 'Disable comments' : 'Enable comments'} </MenuItem>
<MenuItem onClick={this.handleToggleSharing} style={{ fontSize: "14px" }}>{!this.state.disableSharing ? 'Disable sharing' : 'Enable sharing'}</MenuItem>
</IconMenu>
)
var postAvatar = <UserAvatar fileName={this.props.avatar} style={{ top: "8px" }} size={40} />
var author = (
<div>
<span style={{
fontSize: "14px",
paddingRight: "10px",
fontWeight: 400,
color: "rgba(0,0,0,0.87)",
textOverflow: "ellipsis",
overflow: "hidden",
paddingLeft: "50px",
lineHeight: "25px"
}}>{this.props.name}</span><span style={{
fontWeight: 100,
fontSize: "10px"
}}> | Public</span>
</div>
)
const writeActions = [
<FlatButton
label="Cancel"
primary={true}
keyboardFocused={false}
onTouchTap={this.props.onRequestClose}
style={{ color: grey800 }}
/>,
<FlatButton
label={this.props.edit ? 'UPDATE' : 'POST'}
primary={true}
keyboardFocused={false}
onTouchTap={this.handlePost}
disabled={this.state.disabledPost}
/>
]
const galleryActions = [
<FlatButton
label="Cancel"
primary={true}
keyboardFocused={false}
onTouchTap={this.handleCloseGallery}
style={{ color: grey800 }}
/>
]
const styles = {
dialog: {
width: '',
maxWidth: '530px',
borderRadius: "4px"
}
}
return (
<div style={this.props.style}>
{this.props.children}
<Dialog
id= {this.props.id || 0}
actions={writeActions}
modal={false}
open={this.props.open}
contentStyle={styles.dialog}
onRequestClose={this.props.onRequestClose}
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
bodyStyle={{ padding: 0 }}
autoDetectWindowHeight={false}
actionsContainerStyle={{ borderTop: "1px solid rgb(224, 224, 224)" }}
>
<ListItem
disabled={true}
leftAvatar={postAvatar}
rightIconButton={rightIconMenu}
primaryText={author}
style={{ padding: "16px 4px 30px 16px" }}
/>
<div style={{ display: "flex", flexDirection: "column", flexGrow: 1, overflow: "hidden" }}>
<div style={{ position: "relative", flexDirection: "column", display: "flex", flexGrow: 1, overflow: "hidden", overflowY: "auto", maxHeight: "300px" }}>
<TextField
value={this.state.postText}
onChange={this.handleOnChange}
hintText="What is new with you?"
underlineShow={false}
multiLine={true}
rows={2}
hintStyle={{ fontWeight: 200, fontSize: "14px" }}
textareaStyle={{ fontWeight: 200, fontSize: "14px" }}
style={{ margin: "0 16px", flexShrink: 0, width: "initial", flexGrow: 1 }}
/>
{(this.state.image && this.state.image !== '')
? (<div>
<div style={{ position: "relative", overflowY: "hidden", overflowX: "auto" }}>
<ul style={{ position: "relative", whiteSpace: "nowrap", padding: "0 0 0 16px", margin: "8px 0 0 0", paddingRight: "16px", verticalAlign: "bottom", flexShrink: 0, listStyleType: "none" }}>
<div style={{ display: "flex", position: "relative" }}>
<span onClick={this.handleRemoveImage} style={{
position: "absolute", width: "28px", backgroundColor: "rgba(255, 255, 255, 0.22)",
height: "28px", right: 12, top: 4, cursor: "pointer", borderRadius: "50%",
display: "flex", alignItems: "center", justifyContent: "center"
}}>
<SvgRemoveImage hoverColor="rgba(0, 0, 0, 0.65)" style={{ color: "rgba(0, 0, 0, 0.53)" }} />
</span>
<div style={{ display: "inline-block", width: "100%", marginRight: "8px", transition: "transform .25s" }}>
<li style={{ width: "100%", margin: 0, verticalAlign: "bottom", position: "static" }}>
<Img fileName={this.state.image} style={{ width: "100%", height: "auto" }} />
</li>
</div>
</div>
</ul>
</div>
</div>) : ''}
</div>
<div style={{ flexShrink: 0, boxFlex: 0, flexGrow: 0, maxHeight: "48px", width: "100%" }}>
<div style={{ flexDirection: "row", display: "flex" }}>
<div onClick={this.handleOpenGallery} style={{ outline: "none", width: "48px", zIndex: 0, overflow: "hidden", position: "relative", textAlign: "center", transition: "background .3s", border: 0, borderRadius: "50%", display: "inlineBlock", height: "48px" }}>
<span style={{ top: "15px", display: "block", position: "relative", cursor: "pointer" }}>
<SvgCamera color="grey" />
</span>
</div>
</div>
</div>
</div>
</Dialog>
<Dialog
actions={galleryActions}
modal={false}
open={this.state.galleryOpen}
contentStyle={styles.dialog}
onRequestClose={this.handleCloseGallery}
overlayStyle={{ background: "rgba(0,0,0,0.12)" }}
autoDetectWindowHeight={false}
>
<ImageGallery set={this.onRequestSetImage} close={this.handleCloseGallery} />
</Dialog>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
post: (post, callBack) => dispatch(postActions.dbAddImagePost(post, callBack)),
update: (post,callBack) => dispatch(postActions.dbUpdatePost(post, callBack))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
postImageState: state.imageGallery.status,
avatar: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].avatar : '',
name: state.user.info && state.user.info[state.authorize.uid] ? state.user.info[state.authorize.uid].fullName : ''
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(PostWrite)

165
app/components/Profile.jsx Normal file
View File

@@ -0,0 +1,165 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import Dialog from 'material-ui/Dialog'
import FlatButton from 'material-ui/FlatButton'
import RaisedButton from 'material-ui/RaisedButton'
// - Import app components
import ProfileHead from 'ProfileHead'
import Blog from 'Blog'
// - Import API
// - Import actions
import * as postActions from 'postActions'
import * as userActions from 'userActions'
import * as globalActions from 'globalActions'
/**
* Create component class
*/
export class Profile extends Component {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
componentWillMount() {
this.props.loadPosts()
this.props.loadUserInfo()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
/**
* Component styles
*/
const styles = {
profile: {
margin: '0 auto',
width: '90%'
},
header: {
},
content: {
},
showcover: {
height: '450px'
},
avatar: {
border: '2px solid rgb(255, 255, 255)'
}
}
/**
* Actions for material ui dialog
*/
const actions = [
<FlatButton
label="Cancel"
primary={true}
onTouchTap={this.handleClose}
/>,
<FlatButton
label="Submit"
primary={true}
keyboardFocused={true}
onTouchTap={this.handleClose}
/>
]
return (
<div style={styles.profile}>
<div style={styles.header}>
<ProfileHead avatar={this.props.avatar} isAuthedUser={this.props.isAuthedUser} banner={this.props.banner} fullName={this.props.name} followerCount={0} userId={this.props.userId}/>
</div>
{this.props.posts && Object.keys(this.props.posts).length !==0
? (<div style={styles.content}>
<div className='profile__title'>
{this.props.name}'s posts
</div>
<div style={{ height: '24px' }}></div>
<Blog posts={this.props.posts} displayWriting={false} />
</div>)
: (<div className='profile__title'>
Nothing shared
</div>)
}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
const { userId } = ownProps.match.params
return {
loadPosts: () => dispatch(postActions.dbGetPostsByUserId(userId)),
loadUserInfo: () => dispatch(userActions.dbGetUserInfoByUserId(userId, 'header'))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const { userId } = ownProps.match.params
const {uid} = state.authorize
return {
avatar: state.user.info && state.user.info[userId] ? state.user.info[userId].avatar || '' : '',
name: state.user.info && state.user.info[userId] ? state.user.info[userId].fullName || '' : '',
banner: state.user.info && state.user.info[userId] ? state.user.info[userId].banner || '' : '',
posts: state.post.userPosts ? state.post.userPosts[userId] : {},
isAuthedUser: userId === uid,
userId
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Profile)

View File

@@ -0,0 +1,246 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { grey400, darkBlack, lightBlack } from 'material-ui/styles/colors'
import IconButton from 'material-ui/IconButton'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
import IconMenu from 'material-ui/IconMenu'
import MenuItem from 'material-ui/MenuItem'
import FlatButton from 'material-ui/FlatButton'
import RaisedButton from 'material-ui/RaisedButton'
import EventListener, { withOptions } from 'react-event-listener'
import { Parallax, Background } from 'react-parallax'
// - Import app components
import ImgCover from 'ImgCover'
import EditProfile from 'EditProfile'
import UserAvatar from 'UserAvatar'
// - Import API
// - Import actions
import * as globalActions from 'globalActions'
import * as userActions from 'userActions'
/**
* Create component class
*/
export class ProfileHead extends Component {
static propTypes = {
/**
* User avatar address
*/
avatar: PropTypes.string,
/**
* User avatar address
*/
banner: PropTypes.string,
/**
* User full name
*/
fullName: PropTypes.string.isRequired,
/**
* The number of followers
*/
followerCount: PropTypes.number,
/**
* User identifier
*/
userId: PropTypes.string,
/**
* If the user profile identifier of param is equal to the user authed identifier
*/
isAuthedUser: PropTypes.bool
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
/**
* Defaul state
*/
this.state = {
/**
* If it's true , the window is in small size
*/
isSmall: false,
}
// Binding functions to `this`
}
/**
* Handle resize event for window to change sidebar status
* @param {event} evt is the event is passed by winodw resize event
*/
handleResize = (evt) => {
// Set initial state
var width = window.innerWidth
if (width > 900) {
this.setState({
isSmall: false
})
}
else {
this.setState({
isSmall: true
})
}
}
componentDidMount = () => {
this.handleResize()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
avatar: {
border: '2px solid rgb(255, 255, 255)'
},
iconButton: {
fill: 'rgb(255, 255, 255)',
height: '24px',
width: '24px',
},
iconButtonSmall: {
fill: 'rgb(0, 0, 0)',
height: '24px',
width: '24px',
},
editButton: {
marginLeft: '20px'
},
editButtonSmall: {
marginLeft: '20px',
color: 'white',
fill: 'blue'
},
aboutButton: {
color: 'white'
},
aboutButtonSmall: {
color: 'black'
}
}
const iconButtonElement = (
<IconButton style={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton} iconStyle={this.state.isSmall ? styles.iconButtonSmall : styles.iconButton}
touch={true}
>
<MoreVertIcon color={grey400} viewBox='10 0 24 24' />
</IconButton>
)
const RightIconMenu = () => (
<IconMenu iconButtonElement={iconButtonElement}>
<MenuItem style={{ fontSize: "14px" }}>Reply</MenuItem>
<MenuItem style={{ fontSize: "14px" }}>Edit</MenuItem>
<MenuItem style={{ fontSize: "14px" }}>Delete</MenuItem>
</IconMenu>
)
const {isAuthedUser} = this.props
return (
<div>
<Parallax strength={500} className="profile__parallax" bgStyle={{ position: 'relative' }}>
<Background>
<ImgCover width='100%' height='510px' borderRadius='2px' fileName={this.props.banner || '13e1fc2f-0484-42ef-a61f-75f4b5b20a37.jpeg'} />
</Background>
</Parallax>
<div className={this.state.isSmall ? 'profile__head-info-s' : 'profile__head-info'}>
<EventListener
target="window"
onResize={this.handleResize}
/>
<div className='left'>
{/* User avatar*/}
<div style={{ display: 'flex', justifyContent: 'center' }}><UserAvatar fileName={this.props.avatar} size={60} style={styles.avatar} /></div>
<div className='info'>
<div className='fullName'>
{this.props.fullName}
</div>
{/*<div className='followers'>
{this.props.followerCount} Followers
</div>*/}
</div>
</div>
<div className='right'>
{isAuthedUser ? (<div style={this.state.isSmall ? styles.editButtonSmall : styles.editButton}><RaisedButton label="EDIT PROFILE" onClick={this.props.openEditor} /></div>) : ''}
</div>
</div>
{isAuthedUser ? (<EditProfile
onRequestClose={this.handleCloseEditor}
avatar={this.props.avatar}
banner={this.props.banner}
fullName={this.props.fullName}
/>): ''}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
openEditor: () => dispatch(userActions.openEditProfile())
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ProfileHead)

223
app/components/Settings.jsx Normal file
View File

@@ -0,0 +1,223 @@
// - Import external components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { NavLink, withRouter } from 'react-router-dom'
import { push } from 'react-router-redux'
import Paper from 'material-ui/Paper'
import TextField from 'material-ui/TextField'
import RaisedButton from 'material-ui/RaisedButton'
import FlatButton from 'material-ui/FlatButton'
// - Import actions
import * as authorizeActions from 'authorizeActions'
/**
* Create component class
*
* @export
* @class Settings
* @extends {Component}
*/
export class Settings extends Component {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props);
this.state = {
passwordInput: '',
passwordInputError: '',
confirmInput: '',
confirmInputError: '',
}
// Binding function to `this`
this.handleForm = this.handleForm.bind(this)
}
/**
* Handle data on input change
* @param {event} evt is an event of inputs of element on change
*/
handleInputChange = (evt) => {
const target = evt.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
switch (name) {
case 'passwordInput':
this.setState({
passwordInputError: ''
})
break
case 'confirmInput':
this.setState({
confirmInputError: '',
passwordInputError: ''
})
break;
default:
}
}
/**
* Handle register form
*/
handleForm = () => {
var error = false
if (this.state.passwordInput === '') {
this.setState({
passwordInputError: 'This field is required'
})
error = true
}
else if (this.state.confirmInput === '') {
this.setState({
confirmInputError: 'This field is required'
})
error = true
}
else if(this.state.confirmInput !== this.state.passwordInput) {
this.setState({
confirmInputError: 'Password and confirm password should be equal!'
})
error = true
}
if (!error) {
this.props.login(
this.state.passwordInput,
this.state.confirmInput
)
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const paperStyle = {
minHeight: 370,
width: 450,
textAlign: 'center',
display: 'block',
margin: "auto"
};
return (
<div>
<h1 style={{
textAlign: "center",
padding: "20px",
fontSize: "30px",
fontWeight: 500,
lineHeight: "32px",
margin: "auto",
color: "rgba(138, 148, 138, 0.2)"
}}>Green</h1>
<div className="animate-bottom">
<Paper style={paperStyle} zDepth={1} rounded={false} >
<div style={{ padding: "48px 40px 36px" }}>
<div style={{
paddingLeft: "40px",
paddingRight: "40px"
}}>
<h2 style={{
textAlign: "left",
paddingTop: "16px",
fontSize: "24px",
fontWeight: 400,
lineHeight: "32px",
margin: 0
}}>Change Password</h2>
</div>
<TextField
onChange={this.handleInputChange}
errorText={this.state.passwordInputError}
name="passwordInput"
floatingLabelStyle={{ fontSize: "15px" }}
floatingLabelText="New password"
type="password"
/><br />
<TextField
onChange={this.handleInputChange}
errorText={this.state.confirmInputError}
name="confirmInput"
floatingLabelStyle={{ fontSize: "15px" }}
floatingLabelText="Confirm password"
type="password"
/><br />
<br />
<br />
<div className="settings__button-box">
<div>
<FlatButton label="Home" onClick={this.props.homePage} />
</div>
<div>
<RaisedButton label="Change password" primary={true} onClick={this.handleForm} />
</div>
</div>
</div>
</Paper>
</div>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
login: (password) => {
dispatch(authorizeActions.dbUpdatePassword(password))
},
homePage: () => {
dispatch(push("/"))
}
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Settings))

296
app/components/Sidebar.jsx Normal file
View File

@@ -0,0 +1,296 @@
// - Import react components
import React, { Component } from 'react'
import { NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import EventListener, { withOptions } from 'react-event-listener'
import keycode from 'keycode'
// - Import app components
// - Import API
import * as AuthAPI from 'AuthAPI'
// - Import actions
import * as authorizeActions from 'authorizeActions'
import * as globalActions from 'globalActions'
// - Feilds
const color = 'teal';
const colorKey = 'blue';
const sizeCondition = (width) => (width >= 750)
// - Create Sidebar component class
export class Sidebar extends Component {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
// Default state
this.state = {
sidebarClass: "",
overlay: false,
mainStyle: { marginLeft: "210px" },
// Is sidebar open or not
open: true,
// If sidebar is closed by resizing or not
auto: false,
// If overlay should be open or not
overlayOpen: false,
// If side bar should be closed
shouldBeClosed: false,
}
// Binding functions to `this`
this.handleLogout = this.handleLogout.bind(this)
this.open = this.open.bind(this)
this.getChildren = this.getChildren.bind(this)
}
/**
* Handle open sidebar
* @param {boolean} status if is true, sidebar will be open
* @param {string} source is the element that fired the function
*/
open = (status, source) => {
const width = window.innerWidth
if (status) {
// Sidebar style when it's open
const openStyle = {
width: "210px",
transform: "translate(0px, 0px)",
transition: "transform 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms"
}
this.setState({
open: true,
mainStyle: { marginLeft: "210px" },
sidebarStyle: openStyle,
sidebarClass: (sizeCondition(width)) ? "sidebar sidebar__large" : "sidebar sidebar__over",
overlay: (sizeCondition(width)) ? false : true
})
if (sizeCondition(width)) {
this.setState({
auto: false,
shouldBeClosed: false
})
} else {
this.setState({
overlayOpen: true
})
}
/**
* Callback function fired to determine sidebar and overlay sidebar status
* @param {boolean} if true, the sidebar is open
*/
this.props.status(true)
}
// If it's false sidebar should be closed
else {
// Sidebar style when it's closed
const closeStyle = {
transform: "translate(-100%, 0px)",
transition: "transform 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms"
}
this.setState({
open: false,
mainStyle: { marginLeft: "0px" },
sidebarStyle: closeStyle,
sidebarClass: (sizeCondition(width)) ? "sidebar sidebar__large"
: ((source === 'auto') ? "sidebar " : "sidebar sidebar__over"),
overlay: false
})
switch (source) {
case 'auto':
this.setState({
auto: true
})
break;
case 'overlay':
this.setState({
shouldBeClosed: true
})
break;
default:
}
if (sizeCondition(width)) {
// TODO: Get ride of this
} else {
this.setState({
overlayOpen: false
})
}
/**
* Callback function fired to determine sidebar and overlay sidebar status
* @param {boolean} if true, the sidebar is open
*/
this.props.status(false)
}
this.props.overlay((sizeCondition(width)) ? false : true)
}
/**
* Handle resize event for window to change sidebar status
* @param {event} evt is the event is passed by winodw resize event
*/
handleResize = (evt) => {
// Set initial state
var width = window.innerWidth
if (sizeCondition(width)) {
this.setState({
sidebarClass: "sidebar sidebar__large",
overlay: false,
overlayOpen: false
})
this.props.overlay(false)
if (this.state.auto && !this.state.shouldBeClosed) {
this.open(true)
this.setState({ auto: false })
}
}
else {
if (!this.state.overlayOpen) {
if (!this.state.auto && this.state.open) {
this.open(false, 'auto')
}else{
this.setState({
overlayOpen:true,
overlay:true
})
}
} else {
this.setState({ sidebarClass: "sidebar sidebar__over", overlay: true })
this.props.overlay(true)
}
}
}
/**
* Handle logout user
*/
handleLogout = () => {
var { dispatch } = this.props
dispatch(authorizeActions.dbLogout())
}
/**
* Handle keyup event for window to close sidebar
* @param {event} evt is the event is passed by winodw key event
*/
handleKeyUp = (evt) => {
if (this.state.overlayOpen) {
if (this.state.open && keycode(event) === 'esc') {
this.open(false);
}
}
}
componentWillMount = () => {
this.props.open(this.open)
}
getChildren = () => {
return React.Children.map(this.props.children, (childe) => {
if (childe.type.qcName === 'SidebarContent') {
const sideBarContent = React.cloneElement(childe, {
className: this.state.sidebarClass,
cstyle: this.state.sidebarStyle,
sidebar: this.open,
overlay: this.state.overlay
})
return sideBarContent
}
else if (childe.type.qcName === 'SidebarMain') {
return React.cloneElement(childe, { cstyle: this.state.mainStyle })
}
})
}
componentDidMount = () => {
this.handleResize()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
return (
<div id='sidebar'>
<EventListener
target="window"
onResize={this.handleResize}
onKeyUp={this.handleKeyUp}
/>
{this.getChildren()}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar)

View File

@@ -0,0 +1,54 @@
// - Import react components
import React, {Component} from 'react'
// - Import components
// - Import actions
// - Create component class
export default class SidebarContent extends Component {
static qcName= 'SidebarContent'
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
// Default state
this.state = {}
// Binding function to `this`
this.handleClickCover = this.handleClickCover.bind(this)
}
/**
* Handle click on cover of sidebar
* @param {event} evt is a click event passed to funciton
*/
handleClickCover = (evt) => {
this.props.sidebar(false,'overlay')
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
let showCoverStyle = {position: "fixed", height: "100%", width: "100%", top: "0px", left: "0px", opacity: "1", backgroundColor: "rgba(255, 255, 255, 0.54)", WebkitTapHighlightColor: "rgba(0, 0, 0, 0)", willChange: "opacity", transform: "translateZ(0px)", transition: "left 0ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, opacity 400ms cubic-bezier(0.23, 1, 0.32, 1) 0ms", zIndex: "1111", pointerEvents: "auto"}
let hideCoverStyle = {position: "fixed", height: "100%", width: "100%", top: "0px", left: "-100%", opacity: "0", backgroundColor: "rgba(255, 255, 255, 0.54)", WebkitTapHighlightColor: "rgba(0, 0, 0, 0)", willChange: "opacity", transform: "translateZ(0px)", transition: "left 0ms cubic-bezier(0.23, 1, 0.32, 1) 400ms, opacity 400ms cubic-bezier(0.23, 1, 0.32, 1) 0ms", zIndex: "1111", pointerEvents: "none"}
return (
<div id='sidebar-content'>
<div style={this.props.overlay ? showCoverStyle : hideCoverStyle} style={{overflow:'hidden'}} onClick={this.handleClickCover}></div>
<div className={this.props.className} style={this.props.cstyle}>
{this.props.children}
</div>
</div>
)
}
}

View File

@@ -0,0 +1,36 @@
// - Import react components
import React, {Component} from 'react'
// - Import components
// - Import actions
// - Create component class
export default class SidebarMain extends Component {
static qcName = 'SidebarMain'
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
// Default state
this.state = {}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
return (
<div className="home__main" style={this.props.cstyle} >
<div style={{height:"64px", width:"100%"}}></div>
{this.props.children}
</div>
)
}
}

271
app/components/Signup.jsx Normal file
View File

@@ -0,0 +1,271 @@
// - Import react components
import React,{Component} from 'react'
import {connect} from 'react-redux'
import {push} from 'react-router-redux'
import {NavLink, withRouter} from 'react-router-dom'
import Paper from 'material-ui/Paper'
import TextField from 'material-ui/TextField'
import RaisedButton from 'material-ui/RaisedButton'
import FlatButton from 'material-ui/FlatButton'
// - Import actions
import * as authorizeActions from 'authorizeActions'
import * as globalActions from 'globalActions'
// - Create Signup componet class
export class Signup extends Component {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props){
super(props);
this.state = {
fullNameInput: '',
fullNameInputError: '',
emailInput: '',
emailInputError: '',
passwordInput: '',
passwordInputError: '',
confirmInput: '',
confirmInputError: ''
}
// Binding function to `this`
this.handleForm = this.handleForm.bind(this)
}
/**
* Handle data on input change
* @param {event} evt is an event of inputs of element on change
*/
handleInputChange = (evt) => {
const target = evt.target
const value = target.type === 'checkbox' ? target.checked : target.value
const name = target.name
this.setState({
[name]: value
})
switch (name) {
case 'fullNameInput':
this.setState({
fullNameInputError: '',
})
break
case 'emailInput':
this.setState({
emailInputError: ''
})
break
case 'passwordInput':
this.setState({
confirmInputError: '',
passwordInputError: ''
})
break
case 'confirmInput':
this.setState({
confirmInputError: '',
passwordInputError: ''
})
break
case 'checkInput':
this.setState({
checkInputError: ''
})
break;
default:
}
}
/**
* Handle register form
*/
handleForm = () => {
var error = false
if (this.state.fullNameInput === '') {
this.setState({
fullNameInputError: 'This field is required'
})
error = true
}
if (this.state.emailInput === '') {
this.setState({
emailInputError: 'This field is required'
})
error = true
}
if (this.state.passwordInput === '') {
this.setState({
passwordInputError: 'This field is required'
})
error = true
}
if (this.state.confirmInput === '') {
this.setState({
confirmInputError: 'This field is required'
})
error = true
}
else if(this.state.confirmInput !== this.state.passwordInput){
this.setState({
passwordInputError: 'This field sould be equal to confirm password',
confirmInputError: 'This field sould be equal to password'
})
error = true
}
if (!error) {
this.props.register({
email: this.state.emailInput,
password: this.state.passwordInput,
fullName: this.state.fullNameInput
})
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const paperStyle = {
minHeight: 500,
width: 450,
margin: 20,
textAlign: 'center',
display: 'block',
margin: "auto"
};
return (
<div>
<h1 style={{
textAlign: "center",
padding:"20px",
fontSize: "30px",
fontWeight: 500,
lineHeight: "32px",
margin: "auto",
color: "rgba(138, 148, 138, 0.2)"
}}>Green</h1>
<div className="animate-bottom">
<Paper style={paperStyle} zDepth={1} rounded={false} >
<div style={{padding: "48px 40px 36px"}}>
<div style={{paddingLeft: "40px",
paddingRight: "40px"}}>
<h2 style={{
textAlign: "left",
paddingTop: "16px",
fontSize: "24px",
fontWeight: 400,
lineHeight: "32px",
margin: 0}}>Sign up</h2>
</div>
<TextField
onChange={this.handleInputChange}
errorText={this.state.fullNameInputError}
name="fullNameInput"
floatingLabelStyle={{fontSize:"15px"}}
floatingLabelText="Full Name"
type="text"
/><br />
<TextField
onChange={this.handleInputChange}
errorText={this.state.emailInputError}
name="emailInput"
floatingLabelStyle={{fontSize:"15px"}}
floatingLabelText="Email"
type="email"
/><br />
<TextField
onChange={this.handleInputChange}
errorText={this.state.passwordInputError }
name="passwordInput"
floatingLabelStyle={{fontSize:"15px"}}
floatingLabelText="Password"
type="password"
/><br />
<TextField
onChange={this.handleInputChange}
errorText={this.state.confirmInputError }
name="confirmInput"
floatingLabelStyle={{fontSize:"15px"}}
floatingLabelText="Confirm Password"
type="password"
/><br />
<br />
<div className="signup__button-box">
<div>
<FlatButton label="Login" onClick={this.props.loginPage} />
</div>
<div>
<RaisedButton label="Create" primary={true} onClick={this.handleForm}/>
</div>
</div>
</div>
</Paper>
</div>
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch,ownProps) => {
return {
showError: (message) => {
dispatch(action(types.SHOW_ERROR_MESSAGE_GLOBAL)(error.message))
},
register: (data) => {
dispatch(authorizeActions.dbSignup(data))
},
loginPage: () => {
dispatch(push("/login"))
}
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state,ownProps) => {
return{
}
}
// - Connect component to redux store
export default withRouter(connect(mapStateToProps,mapDispatchToProps)(Signup))

View File

@@ -0,0 +1,129 @@
// - Import react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import Avatar from 'material-ui/Avatar'
// - Import app components
// - Import API
// - Import actions
import * as imageGalleryActions from 'imageGalleryActions'
/**
* Create component class
*/
export class ImgCover extends Component {
static propTypes = {
/**
* Use for getting url address from server
*/
fileName: PropTypes.string.isRequired,
/**
* Avatar style
*/
style: PropTypes.object,
/**
* Avatar size
*/
size: PropTypes.number,
/**
* Trigger on touch tap
*/
onTouchTap: PropTypes.func
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
this.getImageURL = this.getImageURL.bind(this)
}
componentWillReceiveProps(nextProps) {
const { fileName, avatarURL } = this.props
if (fileName && !avatarURL[fileName]) {
if (this.props.imageRequests.indexOf(fileName) > -1)
return
this.getImageURL()
}
}
getImageURL = () => {
let { fileName } = this.props
if (fileName && fileName !== '') {
if (this.props.imageRequests.indexOf(fileName) > -1)
return
this.props.getImage(fileName)
}
}
componentWillMount() {
let { fileName } = this.props
if (this.props.imageRequests.indexOf(fileName) > -1)
return
this.getImageURL()
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
let { fileName, style, size, onTouchTap, avatarURL } = this.props
return (
<Avatar src={avatarURL[fileName] || ''} size={size || 36} style={style} onTouchTap={onTouchTap} />
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
getImage: (name) => dispatch(imageGalleryActions.dbDownloadImage(name))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
return {
avatarURL: state.imageGallery.imageURLList,
imageRequests: state.imageGallery.imageRequests
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(ImgCover)

293
app/components/UserBox.jsx Normal file
View File

@@ -0,0 +1,293 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import {push} from 'react-router-redux'
import Paper from 'material-ui/Paper'
import FlatButton from 'material-ui/FlatButton'
import RaisedButton from 'material-ui/RaisedButton';
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem'
import Checkbox from 'material-ui/Checkbox'
import TextField from 'material-ui/TextField'
// - Import app components
import UserAvatar from 'UserAvatar'
// - Import API
import CircleAPI from 'CircleAPI'
// - Import actions
import * as circleActions from 'circleActions'
/**
* Create component class
*/
export class UserBox extends Component {
static propTypes = {
/**
* User identifier
*/
userId: PropTypes.string,
/**
* User information
*/
user:PropTypes.object
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
/**
* It will be true if user follow popover is open
*/
open: false,
/**
* The value of circle input
*/
circleName: '',
/**
* It will be true if the text field for adding group is empty
*/
disabledAddCircle: true
}
// Binding functions to `this`
this.handleChangeName = this.handleChangeName.bind(this)
this.handleCreateCricle = this.handleCreateCricle.bind(this)
this.handleFollowUser = this.handleFollowUser.bind(this)
}
handleFollowUser = (checked,cid) => {
const {userId,user} = this.props
const {avatar,fullName} = user
if (checked) {
this.props.addFollowingUser(cid,{avatar,userId,fullName})
} else {
this.props.deleteFollowingUser(cid,userId)
}
}
/**
* Handle create circle
*
*
* @memberof UserBox
*/
handleCreateCricle = () => {
const {circleName} = this.state
if(circleName && circleName.trim() !== '' ){
this.props.createCircle(this.state.circleName)
this.setState({
circleName: '',
disabledAddCircle: true
})}
}
/**
* Handle change group name input to the state
*
*
* @memberof UserBox
*/
handleChangeName = (evt) => {
this.setState({
circleName: evt.target.value,
disabledAddCircle: (evt.target.value === undefined || evt.target.value.trim() === '')
})
}
/**
* Handle touch tab on follow popover
*
*
* @memberof UserBox
*/
handleTouchTap = (evt) => {
// This prevents ghost click.
event.preventDefault();
this.setState({
open: true,
anchorEl: evt.currentTarget,
})
}
/**
* Handle close follow popover
*
*
* @memberof UserBox
*/
handleRequestClose = () => {
this.setState({
open: false,
})
}
circleList = () => {
let { circles, userId, userBelongCircles } = this.props
if (circles) {
return Object.keys(circles).map((key, index) => {
if(key.trim() !== '-Followers'){
let isBelong = userBelongCircles.indexOf(key) > -1
return <Checkbox
key={key}
style={{ padding: '10px' }}
label={circles[key].name}
labelStyle={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: '100%'
}}
onCheck={(evt,checked) => this.handleFollowUser(checked,key)}
checked={isBelong}
/>}
})
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
paper: {
height: 254,
width: 243,
margin: 10,
textAlign: 'center',
maxWidth: '257px'
},
followButton: {
position: 'absolute',
bottom: '8px',
left: 0,
right: 0
}
}
return (
<Paper style={styles.paper} zDepth={1} className='grid-cell'>
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
height: '100%',
position: 'relative',
padding: '30px'
}}>
<div onClick={() => this.props.goTo(`/${this.props.userId}`)} style={{cursor:'pointer'}}>
<UserAvatar
fileName={this.props.avatar}
size={90}
/>
</div>
<div onClick={() => this.props.goTo(`/${this.props.userId}`)} className='people__name' style={{cursor:'pointer'}}>
<div>
{this.props.user.fullName}
</div>
</div>
<div style={styles.followButton}>
<FlatButton
label={(this.props.belongCirclesCount && this.props.belongCirclesCount < 1) ? 'Follow'
: (this.props.belongCirclesCount > 1 ? `${this.props.belongCirclesCount} Circles` : ((this.props.firstBelongCircle) ? this.props.firstBelongCircle.name : 'Follow'))}
primary={true}
onTouchTap={this.handleTouchTap}
/>
</div>
</div>
<Popover
open={this.state.open}
anchorEl={this.state.anchorEl}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'left', vertical: 'top' }}
onRequestClose={this.handleRequestClose}
animation={PopoverAnimationVertical}
>
<Menu >
<div style={{
position: 'relative',
display: 'block',
maxHeight: '220px'
}}>
<div style={{ overflowY: 'auto', height: '100%' }}>
{this.circleList()}
</div>
</div>
<div style={{ padding: '10px' }}>
<TextField
hintText="Group name"
onChange={this.handleChangeName}
value={this.state.circleName}
/><br />
<FlatButton label="ADD" primary={true} disabled={this.state.disabledAddCircle} onClick={this.handleCreateCricle} />
</div>
</Menu>
</Popover>
</Paper>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
createCircle: (name) => dispatch(circleActions.dbAddCircle(name)),
addFollowingUser: (cid,user) => dispatch(circleActions.dbAddFollowingUser(cid,user)),
deleteFollowingUser: (cid,followingId) => dispatch(circleActions.dbDeleteFollowingUser(cid,followingId)),
goTo: (url)=> dispatch(push(url))
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const { uid } = state.authorize
const circles = state.circle ? (state.circle.userCircles[uid]|| {}) : {}
const userBelongCircles = CircleAPI.getUserBelongCircles(circles,ownProps.userId)
return {
circles: circles,
userBelongCircles: userBelongCircles || [],
belongCirclesCount: userBelongCircles.length || 0,
firstBelongCircle: userBelongCircles ? (circles ? circles[userBelongCircles[0]] : {}) : {},
avatar: state.user.info && state.user.info[ownProps.userId] ? state.user.info[ownProps.userId].avatar || '' : ''
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(UserBox)

View File

@@ -0,0 +1,102 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
// - Import app components
import UserBox from 'UserBox'
// - Import API
// - Import actions
/**
* Create component class
*/
export class UserBoxList extends Component {
static propTypes = {
/**
* List of users
*/
users: PropTypes.object
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
userList = () => {
let { users, uid } = this.props
if (users) {
return Object.keys(users).map((key, index) => {
if(uid !== key)
return <UserBox key={key} userId={key} user={users[key]}/>
})
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
}
return (
<div className='grid grid__1of4 grid__space-around'>
{this.userList()}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const {uid} = state.authorize
return {
uid
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(UserBoxList)

View File

@@ -0,0 +1,113 @@
// - Import react components
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { List } from 'material-ui/List'
// - Import app components
import Circle from 'Circle'
// - Import API
// - Import actions
/**
* Create component class
*/
export class YourCircles extends Component {
static propTypes = {
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
circleList = () => {
let { circles,uid } = this.props
let parsedCircles = []
if (circles) {
Object.keys(circles).map((key, index) => {
if(key.trim() !== '-Followers')
parsedCircles.push(<Circle key={key} circle={circles[key]} id={key} uid={uid} />)
})
}
return parsedCircles
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
let circleItems = this.circleList()
return (
<div style={{
maxWidth: '800px',
margin: '40px auto'
}}>
{(circleItems && circleItems.length !== 0 ) ? (<div>
<div className='profile__title'>
Your circles
</div>
<List>
{circleItems}
</List>
<div style={{ height: '24px' }}></div>
</div>) : ''}
</div>
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch, ownProps) => {
return {
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state, ownProps) => {
const { uid } = state.authorize
return {
uid,
circles: state.circle ? state.circle.userCircles[uid] : {},
}
}
// - Connect component to redux store
export default connect(mapStateToProps, mapDispatchToProps)(YourCircles)

63
app/components/pattern Normal file
View File

@@ -0,0 +1,63 @@
// - Import react components
import React, {Component} from 'react'
// - Import components
// - Import actions
// - Create component class
export default class {component name} extends Component {
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props){
super(props)
// Default state
this.state = {
}
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render(){
return(
)
}
}
/**
* Map dispatch to props
* @param {func} dispatch is the function to dispatch action to reducers
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapDispatchToProps = (dispatch,ownProps) => {
return{
}
}
/**
* Map state to props
* @param {object} state is the obeject from redux store
* @param {object} ownProps is the props belong to component
* @return {object} props of component
*/
const mapStateToProps = (state,ownProps) => {
return{
}
}
// - Connect component to redux store
export default connect(mapStateToProps,mapDispatchToProps)(Sidebar)

View File

@@ -0,0 +1,97 @@
/* post actions */
export const ADD_IMAGE_POST = 'ADD_IMAGE_POST'
export const ADD_VIDEO_POST = 'ADD_VIDEO_POST'
export const ADD_POST = 'ADD_POST'
export const UPDATE_POST = 'UPDATE_POST'
export const DELETE_POST = 'DELETE_POST'
export const ADD_LIST_POST = 'ADD_LIST_POST'
export const CLEAR_ALL_DATA_POST = 'CLEAR_ALL_DATA_POST'
/* image gallery actions */
export const OPEN_IMAGE_GALLERY = 'OPEN_IMAGE_GALLERY'
export const ADD_IMAGE_GALLERY = 'ADD_IMAGE_GALLERY'
export const ADD_IMAGE_LIST_GALLERY = 'ADD_IMAGES_LIST_GALLERY'
export const IMAGE_SELECT_GALLERY = 'IMAGE_SELECT_GALLERY'
export const CLEARS_SELECT_IMAGE_GALLERY = 'CLEARS_SELECT_IMAGE_GALLERY'
export const CLEAT_ALL_DATA_IMAGE_GALLERY = 'CLEAT_ALL_DATA_IMAGE_GALLERY'
export const DELETE_IMAGE = 'DELETE_IMAGE'
export const SET_IMAGE_URL = 'SET_IMAGE_URL'
export const SEND_IMAGE_REQUEST = 'SEND_IMAGE_REQUEST'
/* image uploader actions */
export const OPEN_IMAGE_UPLOADER = 'OPEN_IMAGE_UPLOADER'
export const OPEN_IMAGE_EDITOR = 'OPEN_IMAGE_EDITOR'
/* comment actions */
export const ADD_COMMENT = 'ADD_COMMENT'
export const ADD_COMMENT_LIST = 'ADD_COMMENT_LIST'
export const DELETE_COMMENT = 'DELETE_COMMENT'
export const CLEAR_ALL_DATA_COMMENT = 'CLEAR_ALL_DATA_COMMENT'
export const UPDATE_COMMENT = 'UPDATE_COMMENT'
export const CLOSE_COMMENT_EDITOR = 'CLOSE_COMMENT_EDITOR'
export const OPEN_COMMENT_EDITOR = 'OPEN_COMMENT_EDITOR'
/* vote actions */
export const ADD_VOTE = 'ADD_VOTE'
export const DELETE_VOTE = 'DELETE_VOTE'
export const ADD_VOTE_LIST = 'ADD_VOTE_LIST'
export const CLEAR_ALL_DATA_VOTE = 'CLEAR_ALL_DATA_VOTE'
/* users action */
export const ADD_USERS = 'ADD_USERS'
export const ADD_USER_INFO = 'ADD_USER_INFO'
export const UPDATE_USER_INFO = 'UPDATE_USER_INFO'
export const USER_INFO = 'USER_INFO'
export const CLEAR_ALL_DATA_USER = 'CLEAR_ALL_DATA_USER'
export const OPEN_EDIT_PROFILE = 'OPEN_EDIT_PROFILE'
export const CLOSE_EDIT_PROFILE = 'CLOSE_EDIT_PROFILE'
export const ADD_PEOPLE_INFO = 'ADD_PEOPLE_INFO'
/* circle actions */
export const ADD_CIRCLE = 'ADD_CIRCLE'
export const DELETE_CIRCLE = 'DELETE_CIRCLE'
export const UPDATE_CIRCLE = 'UPDATE_CIRCLE'
export const ADD_LIST_CIRCLE = 'ADD_LIST_CIRCLE'
export const CLEAR_ALL_CIRCLES = 'CLEAR_ALL_CIRCLES'
export const OPEN_CIRCLE_SETTINGS = 'OPEN_CIRCLE_SETTINGS'
export const CLOSE_CIRCLE_SETTINGS = 'CLOSE_CIRCLE_SETTINGS'
export const ADD_FOLLOWING_USER = 'ADD_FOLLOWING_USER'
export const DELETE_FOLLOWING_USER = 'DELETE_FOLLOWING_USER'
/* notify actions */
export const ADD_NOTIFY = 'ADD_NOTIFY'
export const ADD_NOTIFY_LIST = 'ADD_NOTIFY_LIST'
export const DELETE_NOTIFY = 'DELETE_NOTIFY'
export const SEEN_NOTIFY = 'SEEN_NOTIFY'
export const CLEAR_ALL_DATA_NOTIFY = 'CLEAR_ALL_DATA_NOTIFY'
/* authorize actions */
export const LOGIN = 'LOGIN'
export const LOGOUT = 'LOGOUT'
export const SIGNUP = 'SIGNUP'
export const UPDATE_PASSWORD = 'UPDATE_PASSWORD'
/* file actions */
export const UPLOAD_FILE = 'UPLOAD_FILE'
export const UPLOAD_FILE_ERROR = 'UPLOAD_FILE_ERROR'
export const UPLOAD_FILE_COMPLETE = 'UPLOAD_FILE_COMPLETE'
export const DOWNLOAD_FILE = 'DOWNLOAD_FILE'
/* global actions */
export const PROGRESS_CHANGE = 'PROGRESS_CHANGE'
export const DEFAULT_DATA_DISABLE = 'DEFAULT_DATA_DISABLE'
export const DEFAULT_DATA_ENABLE = 'DEFAULT_DATA_ENABLE'
export const LOADING = 'LOADING'
export const HIDE_MESSAGE_GLOBAL = 'HIDE_MESSAGE_GLOBAL'
export const SHOW_ERROR_MESSAGE_GLOBAL = 'SHOW_ERROR_MESSAGE_GLOBAL'
export const SHOW_NORMAL_MESSAGE_GLOBAL = 'SHOW_NORMAL_MESSAGE_GLOBAL'
export const SHOW_SEND_REQUEST_MESSAGE_GLOBAL = 'SHOW_SEND_REQUEST_MESSAGE_GLOBAL'
export const SHOW_REQUEST_SUCCESS_MESSAGE_GLOBAL = 'SHOW_REQUEST_SUCCESS_MESSAGE_GLOBAL'
export const SET_HEADER_TITLE = 'SET_HEADER_TITLE'
export const SHOW_TOP_LOADING = 'SHOW_TOP_LOADING'
export const HIDE_TOP_LOADING = 'HIDE_TOP_LOADING'
export const TEMP = 'TEMP'

26
app/firebase/index.js Normal file
View File

@@ -0,0 +1,26 @@
import firebase from 'firebase';
try {
var config = {
apiKey: process.env.API_KEY,
authDomain: process.env.AUTH_DOMAIN,
databaseURL: process.env.DATABASE_URL,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID
}
firebase.initializeApp(config);
} catch (e) {
}
// - Storage reference
export var storageRef = firebase.storage().ref()
// - Database authorize
export var firebaseAuth = firebase.auth
export var firebaseRef = firebase.database().ref()
// - Firebase default
export default firebase;

29
app/helpers/index.jsx Normal file
View File

@@ -0,0 +1,29 @@
import {firebaseAuth, firebaseRef} from 'app/firebase/'
export function auth(email, pw, other) {
return firebaseAuth().createUserWithEmailAndPassword(email, pw)
.then(u => saveUser(u,other))
}
export function logout() {
return firebaseAuth().signOut()
}
export function login(email, pw) {
return firebaseAuth().signInWithEmailAndPassword(email, pw)
}
export function resetPassword(email) {
return firebaseAuth().sendPasswordResetEmail(email)
}
export function saveUser(user,other) {
return firebaseRef.child(`users/${user.uid}/info`)
.set({
...other,
email: user.email,
uid: user.uid
})
.then(() => user)
}

View File

@@ -0,0 +1,95 @@
// - Import react components
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { grey400 } from 'material-ui/styles/colors'
import SvgClose from 'material-ui/svg-icons/navigation/close'
import FlatButton from 'material-ui/FlatButton'
import Divider from 'material-ui/Divider'
/**
* Create component class
*/
export default class DialogTitle extends Component {
static propTypes = {
/**
* The label of right button
*/
buttonLabel: PropTypes.string,
/**
* If it's true button will be disabled
*/
disabledButton: PropTypes.bool,
/**
* Call the funtion the time is clicked on right button
*/
onClickButton: PropTypes.func,
/**
* The function will be called the time is clicked on close
*/
onRequestClose: PropTypes.func.isRequired,
/**
* The title of dialog box
*/
title: PropTypes.string
}
/**
* Component constructor
* @param {object} props is an object properties of component
*/
constructor(props) {
super(props)
//Defaul state
this.state = {
}
// Binding functions to `this`
}
/**
* Reneder component DOM
* @return {react element} return the DOM which rendered by component
*/
render() {
const styles = {
contain: {
display: 'flex',
flexDirection: 'row',
justifyContent: "space-between"
},
title: {
color: 'rgba(0,0,0,0.87)',
flex: '1 1',
font: '500 20px Roboto,RobotoDraft,Helvetica,Arial,sans-serif'
}
}
const { buttonLabel, disabledButton, onClickButton, onRequestClose, title } = this.props
return (
<div className='g__dialog-title'>
<div style={styles.contain}>
<div style={{ paddingRight: '10px' }}>
<SvgClose onClick={onRequestClose} hoverColor={grey400} style={{ cursor: 'pointer' }} />
</div>
<div style={styles.title}>
{title || ''}
</div>
{ buttonLabel ? (<div style={{ marginTop: '-9px' }}>
<FlatButton label={buttonLabel || ''} primary={true} disabled={disabledButton ? disabledButton : false} onClick={onClickButton || (() => _)} />
</div>) : ''}
</div>
<Divider />
</div>
)
}
}

View File

@@ -0,0 +1,32 @@
// - Import react components
import React, { Component } from 'react'
import IconButton from 'material-ui/IconButton'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert'
import { grey200, grey400, grey600 } from 'material-ui/styles/colors'
/**
* DOM styles
*
*
* @memberof Post
*/
const styles = {
iconButton: {
width: 24,
height: 24
}
}
const IconButtonElement = (
<IconButton style={styles.iconButton} iconStyle={styles.iconButton}
touch={true}
>
<MoreVertIcon color={grey400} viewBox='9 0 24 24' />
</IconButton>
)
export default IconButtonElement

View File

View File

@@ -0,0 +1,52 @@
// - Import action types
import * as types from 'actionTypes'
/**
* Default state
*/
var defaultState = {
uid: 0,
authed: false,
updatePassword: false,
guest:false
}
/**
* Authorize reducer
* @param {object} state
* @param {object} action
*/
export var authorizeReducer = (state = defaultState, action) =>{
switch (action.type) {
case types.LOGIN:
return{
uid: action.uid,
authed: true,
guest:false
}
case types.LOGOUT:
return{
...state,
uid: 0,
authed: false,
guest:true
}
case types.SIGNUP:
return{
...state,
uid: action.uid
}
case types.UPDATE_PASSWORD:
return{
...state,
updatePassword: action.updatePassword
}
default:
return state
}
}

View File

@@ -0,0 +1,166 @@
// - Import react components
var uuid = require('uuid');
import moment from 'moment'
import _ from 'lodash'
// - Import action types
import * as types from 'actionTypes'
/* ---------------- */
/**
* Default State
*/
var defaultState = {
userCircles: {},
loaded: false
}
/**
* Circle reducer
* @param {object} state
* @param {object} action
*/
export var circleReducer = (state = defaultState, action) => {
const { payload } = action
switch (action.type) {
case types.CLEAR_ALL_CIRCLES:
return defaultState
case types.ADD_CIRCLE:
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...state.userCircles[payload.uid],
[payload.circle.id]: { ...payload.circle }
}
}
}
case types.UPDATE_CIRCLE:
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...state.userCircles[payload.uid],
[payload.circle.id]: {
...state.userCircles[payload.uid][payload.circle.id],
...payload.circle,
openCircleSettings:false }
}
}
}
case types.DELETE_CIRCLE:
let filteredCircles = {}
Object.keys(state.userCircles[payload.uid]).map((key) => {
if (key !== payload.id) {
return _.merge(filteredCircles, { [key]: { ...state.userCircles[payload.uid][key] } })
}
})
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...filteredCircles
}
}
}
case types.ADD_LIST_CIRCLE:
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...state.userCircles[payload.uid],
...payload.circles
}
},
loaded:true
}
case types.ADD_FOLLOWING_USER:
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...state.userCircles[payload.uid],
[payload.cid]: {
...state.userCircles[payload.uid][payload.cid],
users:{
...state.userCircles[payload.uid][payload.cid].users,
[payload.followingId]: {
...payload.userCircle
}
}
}
}
}
}
case types.DELETE_FOLLOWING_USER:
let filteredUserCircle = {}
Object.keys(state.userCircles[payload.uid][payload.cid].users).map((key) => {
if (key !== payload.followingId) {
return _.merge(filteredUserCircle, { [key]: { ...state.userCircles[payload.uid][payload.cid].users[key] } })
}
})
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...state.userCircles[payload.uid],
[payload.cid]: {
...state.userCircles[payload.uid][payload.cid],
users:{
...filteredUserCircle
}
}
}
}
}
case types.CLOSE_CIRCLE_SETTINGS:
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...state.userCircles[payload.uid],
[payload.id]: {
...state.userCircles[payload.uid][payload.id],
openCircleSettings: false
}
}
}
}
case types.OPEN_CIRCLE_SETTINGS:
return {
...state,
userCircles: {
...state.userCircles,
[payload.uid]: {
...state.userCircles[payload.uid],
[payload.id]: {
...state.userCircles[payload.uid][payload.id],
openCircleSettings: true
}
}
}
}
default:
return state;
}
}

View File

@@ -0,0 +1,127 @@
// - Import react components
import moment from 'moment'
import _ from 'lodash'
// - Import action types
import * as types from 'actionTypes'
/**
* Default state
*/
var defaultState = {
postComments: {},
loaded:false
}
/**
* Comment actions
* @param {object} state
* @param {object} action
*/
export var commentReducer = (state = defaultState, action) => {
var { payload } = action
switch (action.type) {
/* _____________ CRUD _____________ */
case types.ADD_COMMENT:
return {
...state,
postComments: {
...state.postComments,
[payload.postId]: {
...state.postComments[payload.postId],
[payload.id]: {
...payload.comment,
editorStatus: false
}
}
}
}
case types.ADD_COMMENT_LIST:
return {
...state,
postComments: {
...payload
},
loaded:true
}
case types.UPDATE_COMMENT:
return {
...state,
postComments: {
...state.postComments,
[payload.postId]: {
...state.postComments[payload.postId],
[payload.id]: {
...state.postComments[payload.postId][payload.id],
text: payload.text,
editorStatus: payload.editorStatus
}
}
}
}
case types.DELETE_COMMENT:
var parsedComments = {}
if (!state.postComments[payload.postId]) {
return state
}
Object.keys(state.postComments[payload.postId]).map((id) => {
if (id !== payload.id) {
_.merge(parsedComments, { [id]: { ...state.postComments[payload.postId][id] } })
}
})
return {
...state,
postComments: {
...state.postComments,
[payload.postId]: {
...parsedComments
}
}
}
case types.CLOSE_COMMENT_EDITOR:
return {
...state,
postComments: {
...state.postComments,
[payload.postId]: {
...state.postComments[payload.postId],
[payload.id]: {
...state.postComments[payload.postId][payload.id],
editorStatus: false
}
}
}
}
case types.OPEN_COMMENT_EDITOR:
return {
...state,
postComments: {
...state.postComments,
[payload.postId]: {
...state.postComments[payload.postId],
[payload.id]: {
...state.postComments[payload.postId][payload.id],
editorStatus: true
}
}
}
}
case types.CLEAR_ALL_DATA_COMMENT:
return defaultState
default:
return state;
}
}

View File

@@ -0,0 +1,47 @@
// - Import action types
import * as types from 'actionTypes'
/**
* Default state
*/
var defaultState = {
downloadFileName: '',
uploadFileName:'',
error: {},
result: {}
}
/**
* File reducer
* @param {object} state
* @param {object} action
*/
export const fileReducer = (state = defaultState, action) => {
switch (action.type) {
case types.UPLOAD_FILE:
return{
...state,
uploadFileName: action.fileName
}
case types.UPLOAD_FILE_ERROR:
return{
state,
error: action.error
}
case types.UPLOAD_FILE_COMPLETE:
return{
...state,
result: action.result
}
case types.DOWNLOAD_FILE:
return{
...state,
downloadFileName: action.fileName
}
default:
return state
}
}

View File

@@ -0,0 +1,127 @@
// - Import action types
import * as types from 'actionTypes'
/**
* Default state
*/
var defaultState = {
progress: {
percent: 0,
visible: false
},
loadingStatus: true,
defaultLoadDataStatus: false,
messageOpen: false,
message: '',
sidebarMainStyle: {},
sidebarStyle: { width: "210px" },
sidebarClass: "",
sidebarOpen: (window.innerWidth > 750) ? true : false,
windowWidth: 0,
windowHeight: 0,
overSidebarStatus: false,
onResizeOpenSidebar: false,
sidebarAuto: false,
headerTitle: '',
editProfileOpen: false,
showTopLoading: false,
topLoadingQueue: 0,
temp: {}
}
/**
* Global reducer
* @param {object} state
* @param {object} action
*/
export const globalReducer = (state = defaultState, action) => {
const { payload } = action
switch (action.type) {
case types.PROGRESS_CHANGE:
return {
...state,
progress: {
...state.progress,
percent: payload.percent,
visible: payload.visible
}
}
case types.DEFAULT_DATA_DISABLE:
return {
...state,
defaultLoadDataStatus: false
}
case types.DEFAULT_DATA_ENABLE:
return {
...state,
defaultLoadDataStatus: true
}
case types.SHOW_ERROR_MESSAGE_GLOBAL:
return {
...state,
message: action.payload,
messageOpen: true
}
case types.SHOW_NORMAL_MESSAGE_GLOBAL:
return {
...state,
message: action.payload.message,
messageOpen: true
}
case types.SHOW_SEND_REQUEST_MESSAGE_GLOBAL:
return {
...state,
message: "Request has been sent",
messageOpen: true
}
case types.SHOW_REQUEST_SUCCESS_MESSAGE_GLOBAL:
return {
...state,
message: "Your request has processed successfuly",
messageOpen: true
}
case types.HIDE_MESSAGE_GLOBAL:
return {
...state,
message: '',
messageOpen: false,
messageColor: ''
}
case types.SET_HEADER_TITLE:
return {
...state,
headerTitle: action.payload
}
case types.HIDE_TOP_LOADING:
const queue = state.topLoadingQueue > 0 ? (state.topLoadingQueue - 1) : 0
return {
...state,
topLoadingQueue: queue,
showTopLoading: (queue > 0 ? true : false)
}
case types.SHOW_TOP_LOADING:
return {
...state,
topLoadingQueue: (state.topLoadingQueue + 1),
showTopLoading: true
}
case types.TEMP:
return {
...state,
temp: {
...state.temp,
...payload
}
}
default:
return state
}
}

View File

@@ -0,0 +1,95 @@
// - Import react components
import _ from 'lodash'
// - Import image gallery action types
import * as types from 'actionTypes'
/**
* Default state for reducer
*/
var defaultState = {
status: false,
images: [],
selectImage: '',
selectURL: '',
loaded: false,
imageURLList: {},
imageRequests: []
}
/**
* Image gallery reducer
*/
export var imageGalleryReducer = (state = defaultState, action) => {
const { payload } = action
switch (action.type) {
case types.OPEN_IMAGE_GALLERY:
return {
...state,
status: action.status
}
/* ----------------- CRUD ----------------- */
case types.ADD_IMAGE_GALLERY:
return {
...state,
images: [...state.images, action.image]
}
case types.ADD_IMAGE_LIST_GALLERY:
{
return {
...state,
images: [...action.images],
loaded: true
}
}
case types.DELETE_IMAGE:
return {
...state,
images: [
...state.images.filter((item) => {
return item.id !== action.id
})
]
}
case types.IMAGE_SELECT_GALLERY:
return {
...state,
selectImage: action.image,
selectURL: action.URL
}
case types.CLEARS_SELECT_IMAGE_GALLERY:
return {
...state,
selectImage: '',
selectURL: ''
}
case types.SET_IMAGE_URL:
return {
...state,
imageURLList: {
...state.imageURLList,
[payload.name]: payload.url
}
}
case types.SEND_IMAGE_REQUEST:
return {
...state,
imageRequests: [
...state.imageRequests,
payload
]
}
case types.CLEAT_ALL_DATA_IMAGE_GALLERY:
return defaultState
default:
return state;
}
}

View File

@@ -0,0 +1,44 @@
// - Import action types
import * as types from 'actionTypes'
/**
* Default state for reducer
*/
var defaultState = {
status: false,
editStatus:false
}
/**
* Image uploader reducer
* @param {object} state
* @param {object} action
*/
export var imageUploaderReducer = (state = defaultState, action) => {
switch (action.type) {
case types.OPEN_IMAGE_UPLOADER:
if(action.status)
{
return{
...state,
status: true
}
}
else{
return{
...state,
status: false,
editStatus: false
}
}
case types.OPEN_IMAGE_EDITOR:
return{
...state,
editStatus: action.editStatus
}
default:
return state;
}
}

View File

@@ -0,0 +1,81 @@
// - Import react components
import moment from 'moment'
import _ from 'lodash'
// - Import action types
import * as types from 'actionTypes'
/**
* Default state
*/
var defaultState = {
userNotifies: {},
loaded:false
}
/**
* Notify actions
* @param {object} state
* @param {object} action
*/
export var notifyReducer = (state = defaultState, action) => {
var { payload } = action
switch (action.type) {
/* _____________ CRUD _____________ */
case types.ADD_NOTIFY:
return state
case types.ADD_NOTIFY_LIST:
return {
...state,
userNotifies: {
...payload
},
loaded:true
}
case types.SEEN_NOTIFY:
return {
...state,
userNotifies: {
...state.userNotifies,
[payload]:{
...state.userNotifies[payload],
isSeen:true
}
},
loaded:true
}
case types.DELETE_NOTIFY:
var parsedNotifies = {}
Object.keys(state.userNotifies).map((id) => {
if (id !== payload) {
_.merge(parsedNotifies, { [id]: { ...state.userNotifies[id] } })
}
})
return {
...state,
userNotifies: {
...parsedNotifies
}
}
case types.CLEAR_ALL_DATA_NOTIFY:
return defaultState
default:
return state;
}
}

View File

@@ -0,0 +1,105 @@
// - Import react components
var uuid = require('uuid');
import moment from 'moment'
import _ from 'lodash'
// - Import action types
import * as types from 'actionTypes'
/* ---------------- */
/**
* Default State
*/
var defaultState = {
userPosts: {},
loaded: false
}
/**
* Post reducer
* @param {object} state
* @param {object} action
*/
export var postReducer = (state = defaultState, action) => {
const { payload } = action
switch (action.type) {
case types.CLEAR_ALL_DATA_POST:
return defaultState
case types.ADD_IMAGE_POST:
return {
...state,
userPosts: {
...state.userPosts,
[payload.uid]: {
...state.userPosts[payload.uid],
[payload.post.id]: { ...payload.post }
}
}
}
case types.ADD_POST:
return {
...state,
userPosts: {
...state.userPosts,
[payload.uid]: {
...state.userPosts[payload.uid],
[payload.post.id]: { ...payload.post }
}
}
}
case types.UPDATE_POST:
return {
...state,
userPosts: {
...state.userPosts,
[payload.uid]: {
...state.userPosts[payload.uid],
[payload.post.id]: {
...state.userPosts[payload.uid][payload.post.id],
...payload.post
}
}
}
}
case types.DELETE_POST:
let filteredPosts = {}
Object.keys(state.userPosts[payload.uid]).map((key) => {
if (key !== payload.id) {
return _.merge(filteredPosts, { [key]: { ...state.userPosts[payload.uid][key] } })
}
})
return {
...state,
userPosts: {
...state.userPosts,
[payload.uid]: {
...filteredPosts
}
}
}
case types.ADD_LIST_POST:
return {
...state,
userPosts: {
...state.userPosts,
[payload.uid]: {
...state.userPosts[payload.uid],
...payload.posts
}
},
loaded:true
}
default:
return state;
}
}

View File

@@ -0,0 +1,81 @@
// - Import action types
import * as types from 'actionTypes'
/**
* Default state for reducer
*/
var defaultState = {
info: {},
loaded: false,
openEditProfile: false
}
/**
* User reducer
*/
export var userReducer = (state = defaultState, action) => {
const { payload } = action
switch (action.type) {
case types.USER_INFO:
return {
...state,
info: {
...state.info,
[payload.uid]: {
...payload.info
}
}
}
case types.ADD_USER_INFO:
return {
...state,
info: {
...state.info,
[payload.uid]: {
...payload.info
}
},
loaded: true
}
case types.ADD_PEOPLE_INFO:
return {
...state,
info: {
...state.info,
...payload
}
}
case types.UPDATE_USER_INFO:
return {
...state,
info: {
...state.info,
[payload.uid]: {
...state.info[payload.uid],
...payload.info
}
}
}
case types.CLEAR_ALL_DATA_USER:
return defaultState
case types.CLOSE_EDIT_PROFILE:
return {
...state,
openEditProfile: false
}
case types.OPEN_EDIT_PROFILE:
return {
...state,
openEditProfile: true
}
default:
return state;
}
}

View File

@@ -0,0 +1,83 @@
// - Import react components
import moment from 'moment'
import _ from 'lodash'
// - Import action types
import * as types from 'actionTypes'
/**
* Default state
*/
var defaultState = {
postVotes: {},
loaded:false
}
/**
* Vote actions
* @param {object} state
* @param {object} action
*/
export var voteReducer = (state = defaultState, action) => {
var { payload } = action
switch (action.type) {
/* _____________ CRUD _____________ */
case types.ADD_VOTE:
return {
...state,
postVotes: {
...state.postVotes,
[payload.postId]: {
...state.postVotes[payload.postId],
[payload.id]: {
...payload.vote
}
}
}
}
case types.ADD_VOTE_LIST:
return {
...state,
postVotes: {
...payload
},
loaded:true
}
case types.DELETE_VOTE:
var parsedVotes = {}
if (state.postVotes[payload.postId])
Object.keys(state.postVotes[payload.postId]).map((id) => {
if (id !== payload.id) {
_.merge(parsedVotes, { [id]: { ...state.postVotes[payload.postId][id] } })
}
})
return {
...state,
postVotes: {
...state.postVotes,
[payload.postId]: {
...parsedVotes
}
}
}
case types.CLEAR_ALL_DATA_VOTE:
return defaultState
default:
return state;
}
}

View File

@@ -0,0 +1,56 @@
// - Import external components
import * as redux from 'redux'
import thunk from 'redux-thunk'
import {routerReducer, routerMiddleware } from 'react-router-redux'
import createHistory from 'history/createBrowserHistory'
import {createLogger} from 'redux-logger';
// - Import reducers
import {imageGalleryReducer} from 'imageGalleryReducer'
import {imageUploaderReducer} from 'imageUploaderReducer'
import {postReducer} from 'postReducer'
import {commentReducer} from 'commentReducer'
import {voteReducer} from 'voteReducer'
import {authorizeReducer} from 'authorizeReducer'
import {fileReducer} from 'fileReducer'
import {globalReducer} from 'globalReducer'
import {userReducer} from 'userReducer'
import {circleReducer} from 'circleReducer'
import {notifyReducer} from 'notifyReducer'
// Create a history of your choosing (we're using a browser history in this case)
export const history = createHistory()
// - Build the middleware for intercepting and dispatching navigation actions
const middleware = routerMiddleware(history)
const logger = createLogger()
// - Reducers
var reducer = redux.combineReducers({
imageGallery: imageGalleryReducer,
imageUploader: imageUploaderReducer,
post: postReducer,
circle: circleReducer,
comment: commentReducer,
vote: voteReducer,
authorize: authorizeReducer,
router: routerReducer,
file: fileReducer,
user: userReducer,
notify:notifyReducer,
global: globalReducer
})
// - initial state
var initialState = {
}
// - Config and create store of redux
var store = redux.createStore(reducer, initialState, redux.compose(
redux.applyMiddleware(logger,thunk,middleware),
window.devToolsExtension ? window.devToolsExtension() : f => f
))
export default store

23
app/styles/app.scss Normal file
View File

@@ -0,0 +1,23 @@
@import "base/variables";
@import 'base/grid';
@import 'base/animate';
// Component styles
@import 'components/global';
@import 'components/master';
@import 'components/post';
@import 'components/profile';
@import 'components/imageGallery';
@import 'components/postWrite';
@import 'components/homeHeader';
@import 'components/home';
@import 'components/sidebar';
@import 'components/blog';
@import 'components/comment';
@import 'components/people';
@import 'components/login';
@import 'components/signup';
@import 'components/masterLoading';
@import 'components/settings';

View File

@@ -0,0 +1,167 @@
.animate__up {
animation-duration: .3s;
animation-name: staggerItems;
animation-timing-function: ease-out;
}
@keyframes staggerItems {
0% {
-webkit-transform: translateY(30px);
transform: translateY(30px)
}
}
.spin {
animation: spin 2s infinite linear
}
@keyframes spin {
0% {
transform: rotate(0deg)
}
100% {
transform: rotate(359deg)
}
}
.animate-fading {
animation: fading 10s infinite
}
@keyframes fading {
0% {
opacity: 0
}
50% {
opacity: 1
}
100% {
opacity: 0
}
}
.animate-fading2 {
animation: fading 2s infinite
}
@keyframes fading {
0% {
opacity: 0
}
50% {
opacity: 1
}
100% {
opacity: 0
}
}
.animate-opacity {
animation: opac 0.8s
}
@keyframes opac {
from {
opacity: 0
}
to {
opacity: 1
}
}
.animate-top {
position: relative;
animation: animatetop 0.4s
}
@keyframes animatetop {
from {
top: -300px;
opacity: 0
}
to {
top: 0;
opacity: 1
}
}
.animate2-top10 {
position: relative;
animation: animatetop 0.2s
}
@keyframes animatetop {
from {
top: -10px;
opacity: 0
}
to {
top: 0;
opacity: 1
}
}
.animate-left {
position: relative;
animation: animateleft 0.4s
}
@keyframes animateleft {
from {
left: -300px;
opacity: 0
}
to {
left: 0;
opacity: 1
}
}
.animate-right {
position: relative;
animation: animateright 0.4s
}
@keyframes animateright {
from {
right: -300px;
opacity: 0
}
to {
right: 0;
opacity: 1
}
}
.animate-bottom {
position: relative;
animation: animatebottom 0.4s
}
@keyframes animatebottom {
from {
bottom: -300px;
opacity: 0
}
to {
bottom: 0;
opacity: 1
}
}
.animate-bottom50 {
position: relative;
animation: animatebottom 0.4s
}
@keyframes animatebottom {
from {
bottom: -50px;
opacity: 0
}
to {
bottom: 0;
opacity: 1
}
}

143
app/styles/base/_grid.scss Normal file
View File

@@ -0,0 +1,143 @@
.grid {
display: flex;
flex-flow: row;
flex-wrap: wrap;
}
$gutter: 1em;
.grid__gutters {
margin-left: -$gutter;
.grid-cell {
padding-left: $gutter;
}
}
/* Justify per row*/
.grid__right {
justify-content: flex-end;
}
.grid__left{
justify-content: flex-start;
}
.grid__center {
justify-content: center;
}
.grid__space-around{
justify-content: space-around;
}
/* Alignment per row */
.grid__top {
align-items: flex-start;
}
.grid__bottom {
align-items: flex-end;
}
.grid__center {
align-items: center;
}
/* Alignment per cell */
.grid-cell__top {
align-self: flex-start;
}
.grid-cell__bottom {
align-self: flex-end;
}
.grid-cell__center {
align-self: center;
}
.grid__fit > .grid-cell {
flex: 1;
}
.grid__full > .grid-cell {
flex: 0 0 100%;
}
@media (min-width:400px) {
.grid__cols-2 > .grid-cell,
.grid__cols-3 > .grid-cell,
.grid__cols-4 > .grid-cell,
.grid__cols-6 > .grid-cell,
.grid__cols-12 > .grid-cell{
flex: 1;
}
.grid__1of2 > .grid-cell {
flex: 1;
}
.grid__1of6 > .grid-cell {
flex: 0 0 16.6666%;
}
.grid__1of4 > .grid-cell {
flex: 0 0 calc(48.6666% - 1em);
}
.grid__1of3 > .grid-cell {
flex: 0 0 30%;
}
}
@media (min-width:600px) {
.grid__cols-2 > .grid-cell,
.grid__cols-3 > .grid-cell,
.grid__cols-4 > .grid-cell,
.grid__cols-6 > .grid-cell,
.grid__cols-12 > .grid-cell{
flex: 1;
}
.grid__1of2 > .grid-cell {
flex: 1;
}
.grid__1of6 > .grid-cell {
flex: 0 0 16.6666%;
}
.grid__1of4 > .grid-cell {
flex: 0 0 calc(48.6666% - 1em);
}
.grid__1of3 > .grid-cell {
flex: 0 0 30%;
}
}
@media (min-width:1000px) {
.grid__cols-2 > .grid-cell,
.grid__cols-3 > .grid-cell,
.grid__cols-4 > .grid-cell,
.grid__cols-6 > .grid-cell,
.grid__cols-12 > .grid-cell{
flex: 1;
}
.grid__1of2 > .grid-cell {
flex: 0 0 50%;
flex-grow: 1;
}
.grid__1of6 > .grid-cell {
flex: 0 0 16.6666%;
}
.grid__1of4 > .grid-cell {
flex: 0 0 calc(24.33333% - 1em);
flex-grow: 1;
}
.grid__1of3 > .grid-cell {
flex: 0 0 30%;
}
}
@mixin justify-flex($pos){
display: flex;
justify-content: $pos;
}

View File

@@ -0,0 +1,20 @@
// Colors
$grey: #333333;
$light-grey: #fafafa;
$light-grey2:#f7f7f7;
$light-grey3:#ada7a7;
$green-teal:#00ab6b;
$teal:#58d09f;
$gold: #FFD700;
$green: #4CAF50;
$light-grey-border: #eeeeee;
$light-color: #aaa;
// Navigation colors
$nav-background: $grey;
$nav-text-color: white;
// Shadow
$glob-box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), 0 4px 20px 0 rgba(0,0,0,0.19);

View File

@@ -0,0 +1,11 @@
.blog{
margin: 0 auto;
width: 90%;
}
.blog__right-list{
@media (min-width: 993px) {
margin-left: 2%;
}
}

View File

@@ -0,0 +1,39 @@
.comment__list-show{
@mixin com{
position: absolute;
top: 0; left: 0;
background-color: white;
height: 60px;
width: 100%;
}
&>div:nth-of-type(1) {
z-index: 3;
animation: commentSlideShow 12s linear 0s infinite;
@include com;
}
&>div:nth-of-type(2) {
z-index: 2;
animation: commentSlideShow 12s linear 4s infinite;
@include com;
}
&>div:nth-of-type(3) {
z-index: 1;
animation: commentSlideShow 12s linear 8s infinite;
@include com;
}
@keyframes commentSlideShow {
25% { opacity: 1;}
33.33% { opacity: 0;}
91.66% { opacity: 0;}
100% { opacity: 1;}
}
}

View File

@@ -0,0 +1,199 @@
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects;
text-decoration: none;
color: black;
}
a:active,
a:hover {
outline-width: 0
}
img {
border-style: none
}
html,
body {
font-family: Verdana, sans-serif;
font-size: 15px;
line-height: 1.5
}
html {
overflow-x: hidden
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Roboto', sans-serif;
}
/* Change autocomplete styles in WebKit */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus input:-webkit-autofill,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
border: 0px solid white;
-webkit-text-fill-color: black;
-webkit-box-shadow: 0 0 0px 1000px white inset;
transition: background-color 5000s ease-in-out 0s;
}
.g__dialog-title {
margin: 0px;
padding: 24px 24px 20px;
color: rgba(0, 0, 0, 0.87);
font-size: 22px;
line-height: 32px;
font-weight: 400;
}
.g__circle-black {
color: rgba(0, 0, 0, 0.87);
background: rgba(0, 0, 0, 0.26);
transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
box-sizing: border-box;
font-family: Roboto, sans-serif;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px;
border-radius: 50%;
display: inline-block;
margin: 0px 8px;
height: 36px;
cursor: pointer;
width: 36px;
&:hover {
transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
background: rgba(0, 0, 0, 0.42);
top: 0px;
}
}
.g__circle {
color: rgba(0, 0, 0, 0.87);
background-color: rgb(238, 238, 238);
transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
box-sizing: border-box;
font-family: Roboto, sans-serif;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px;
border-radius: 50%;
display: inline-block;
margin: 0px 8px;
height: 36px;
cursor: pointer;
width: 36px;
&:hover {
transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
background-color: rgba(0, 0, 0, 0.04);
top: 0px;
}
}
.g__title {
color: rgba(0, 0, 0, 0.54);
font-size: 16px;
font-weight: 500;
margin: 32px 0 0;
white-space: nowrap;
-webkit-flex-shrink: 1;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.g__title-center {
display: flex;
justify-content: center;
padding: 10px;
margin: 22px auto;
max-width: 300px;
@extend .g__title
}
.g__greenColor {
color: #4CAF50;
}
.g__greenBox {
background-color: #4CAF50!important;
}
.glob__shadow {
box-shadow: $glob-box-shadow;
}
.global__color-teal {
color: $teal;
}
.global__color-gold {
color: $gold;
}
.global__color-lightGrey {
color: $light-grey3;
}
input[type="file"] {
display: none;
}
.global__hidden {
display: none;
}
.global__scroll-CH::-webkit-scrollbar {
width: 5px;
}
.global__scroll-CH::-webkit-scrollbar-track {
background: #ddd;
}
.global__scroll-CH::-webkit-scrollbar-thumb {
background: #666;
}
.global__scroll-IE {
scrollbar-base-color: #C0C0C0;
scrollbar-base-color: #C0C0C0;
scrollbar-dlight-color: #C0C0C0;
scrollbar-highlight-color: #C0C0C0;
scrollbar-track-color: #EBEBEB;
scrollbar-arrow-color: black;
scrollbar-shadow-color: #C0C0C0;
scrollbar-dark-shadow-color: #C0C0C0;
}

View File

@@ -0,0 +1,11 @@
.home__main{
background-color: #eeeeee;
transition: margin-left .4s;
@media screen and (max-width: 750px){
margin-left: 0!important;
margin-right: 0!important;
}
}

View File

@@ -0,0 +1,134 @@
.homeHeader__page {
border-left: 1px solid rgba(255, 255, 255, 0.2);
padding-left: 24px;
margin-left: 24px;
line-height: 32px;
font-weight: 500;
font-family: Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
font-size: 20px;
-webkit-font-smoothing: antialiased;
color: white;
}
.homeHeader__right {
padding-left: 30px;
padding-right: 30px;
position: relative;
margin-left: -24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.homeHeader__left {
padding-left: 30px;
padding-right: 30px;
}
.homeHeader__notify {
color: rgba(0, 0, 0, 0.87);
background-color: #ff1717;
transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
box-sizing: border-box;
font-family: Roboto, sans-serif;
-webkit-tap-highlight-color: transparent;
border-radius: 50%;
display: inline-block;
height: 20px;
cursor: pointer;
width: 21px;
display: flex;
justify-content: center;
align-items: center;
div.title {
color: white;
font-size: 10px;
font-weight: 100;
white-space: nowrap;
-webkit-flex-shrink: 1;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
}
.homeHeader__notify-menu {
background-color: #eee !important;
background: #e5e5e5 !important;
border: 1px solid #ccc !important;
color: #000 !important;
min-width: 376px !important;
width: 376px !important;
height: 380px !important;
display: block !important;
outline: none !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, .2) !important;
div.container {
padding: 10px;
padding: 10px;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
position: relative;
div.title {
display: flex;
justify-content: center;
color: rgba(102, 102, 102, 0.57);
font-size: 26px;
font-weight: 500;
width: 100%;
}
div.content {
width: 100%;
height: 100%;
padding: 10px 0px;
div.item {
width: 100%;
height: 68px;
background-color: white;
margin-bottom: 10px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
display: flex;
padding: 10px;
flex-direction: row;
align-items: center;
position: relative;
div.avatar {}
div.info {
margin-left: 10px;
height: 100%;
width: 100%;
overflow: hidden;
div.user-name {
color: rgba(0, 0, 0, 0.77);
font-size: 15px;
font-weight: 500;
white-space: nowrap;
-webkit-flex-shrink: 1;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
div.description {
color: rgba(0, 0, 0, 0.54);
font-size: 14px;
font-weight: 200;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
div.close {
position: absolute;
right: 17px;
top: 3px;
height: 10px;
width: 10px;
}
}
}
}
}

View File

View File

@@ -0,0 +1,4 @@
.login__button-box {
display: flex;
justify-content: space-around;
}

View File

@@ -0,0 +1,39 @@
.master__progress {
position: fixed;
top: 0;
top: 56px;
z-index: 1501;
width: 100%;
}
.master__loading {
position: fixed;
top: 67px;
z-index: 1501;
width: 100%;
justify-content: center;
div.title {
color: rgb(255, 255, 255);
font-size: 11px;
text-align: center;
font-weight: 500;
white-space: nowrap;
-webkit-flex-shrink: 1;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
background-color: #db4437;
padding: 7px;
border-radius: 40px;
}
}
.master__message {
position: fixed;
top: 5;
left: 0;
text-align: center;
z-index: 1001;
width: 100%;
}

View File

@@ -0,0 +1,12 @@
.mLoading__body {
background-color: rgb(216, 216, 216);
}
.mLoading__content {
max-width: 338px !important;
}
.mLoading__context{
display: flex;
justify-content: space-around;
}

View File

@@ -0,0 +1,24 @@
.people__title{
@extend .profile__title
}
.people__name{
display: flex;
word-break: break-word;
max-width: 100%;
align-items: center;
padding: 10px;
justify-content: center;
div{
color: rgba(0,0,0,0.87);
font-size: 16px;
line-height: 20px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
max-height: 40px;
}
}

View File

View File

View File

@@ -0,0 +1,121 @@
.profile__parallax {
height: 455px;
border-radius: 2px;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14);
}
.profile__title {
color: rgba(0, 0, 0, 0.54);
font-size: 16px;
font-weight: 500;
margin: 32px 0 0;
white-space: nowrap;
-webkit-flex-shrink: 1;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.profile__head-info {
justify-content: space-between;
position: relative;
box-sizing: border-box;
height: 108px;
margin-top: -108px;
padding: 24px;
flex-direction: row;
align-items: center;
background-color: rgba(0, 0, 0, 0);
background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .46));
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .46));
display: flex;
div.left {
display: flex;
flex-direction: row;
justify-content: space-between;
div.info {
display: flex;
text-align: left;
margin-left: 24px;
flex-direction: column;
div.fullName {
font-size: 30px;
font-weight: 500;
color: #fff;
}
div.followers {
color: #fff;
margin-top: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
font-size: 14px;
}
}
}
div.right {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
//------ Profile header in small size -------
.profile__head-info-s {
display: flex;
justify-content: space-between;
position: relative;
box-sizing: border-box;
height: 241px;
margin-top: -45px;
padding: 0 24px;
flex-direction: column;
justify-content: center;
align-items: center;
div.left {
display: flex;
flex-direction: column;
justify-content: space-between;
div.profile__avatar {
@include justify-flex(center)
}
div.info {
display: flex;
text-align: left;
margin-top: 24px;
flex-direction: column;
div.fullName {
font-size: 30px;
font-weight: 500;
color: #191818;
@include justify-flex(center)
}
div.followers {
color: #383838;
margin-top: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
font-size: 14px;
@include justify-flex(center)
}
}
}
div.right {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 24px;
}
}
.profile__edit{
margin-top: -60px;
height: 171px;
@extend .profile__head-info-s
}

View File

@@ -0,0 +1,3 @@
.settings__button-box{
@extend .login__button-box
}

View File

@@ -0,0 +1,35 @@
.sidebar {
padding-top: 64px;
background-color: #fff;
max-width: 210px;
height: 100%;
width: 200px;
position: fixed!important;
z-index: 1;
overflow: auto
}
.sidebar__large {
background-color: #eeeeee !important;
z-index: 1000;
}
.sidebar__over {
z-index: 1102;
padding-top: 0;
background-color: #ffffff;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
z-index: 1111;
}
.sidebar__title {
font-size: 20px;
font-weight: 500;
padding-right: 16px;
font-family: Roboto, sans-serif;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
margin-left: 15px;
}

View File

@@ -0,0 +1,3 @@
.signup__button-box{
@extend .login__button-box
}

View File

@@ -0,0 +1,34 @@
// - Import react components
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
var expect = require('expect')
import firebase, {firebaseRef} from 'app/firebase'
var authorizeActions = require('authorizeActions')
import * as types from 'actionTypes'
var createMockStore = configureMockStore([thunk])
describe('AuthorizeActions', () => {
it('should generate login action', () => {
const uid = 'UID123456'
var action = {
type: types.LOGIN,
authed: true,
uid
}
var res = authorizeActions.login(action.uid)
expect(res).toEqual(action);
})
it('should generate logout action', () => {
var action = {
type: types.LOGOUT
}
var res = authorizeActions.logout()
expect(res).toEqual(action)
})
})

7
app/tests/app.test.jsx Normal file
View File

@@ -0,0 +1,7 @@
var expect = require('expect');
describe('App', () => {
it('should properly run tests', () => {
expect(1).toBe(1);
});
});

View File

@@ -0,0 +1,14 @@
var React = require('react');
var ReactDOM = require('react-dom');
var TestUtils = require('react-addons-test-utils');
var expect = require('expect');
var $ = require('jquery');
var {Blog} = require('Blog');
describe('Blog', () => {
it('should exist', () => {
expect(Blog).toExist();
})
})

View File

@@ -0,0 +1,14 @@
var React = require('react');
var ReactDOM = require('react-dom');
var TestUtils = require('react-addons-test-utils');
var expect = require('expect');
var $ = require('jquery');
var {Circle} = require('Circle');
describe('Circle', () => {
it('should exist', () => {
expect(Circle).toExist();
})
})

View File

@@ -0,0 +1,14 @@
var React = require('react');
var ReactDOM = require('react-dom');
var TestUtils = require('react-addons-test-utils');
var expect = require('expect');
var $ = require('jquery');
var {Comment} = require('Comment');
describe('Comment', () => {
it('should exist', () => {
expect(Comment).toExist();
})
})

View File

@@ -0,0 +1,14 @@
var React = require('react');
var ReactDOM = require('react-dom');
var TestUtils = require('react-addons-test-utils');
var expect = require('expect');
var $ = require('jquery');
var {CommentWrite} = require('CommentWrite');
describe('CommentWrite', () => {
it('should exist', () => {
expect(CommentWrite).toExist();
})
})

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