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