diff options
Diffstat (limited to 'src/containers/ProfilePage')
-rw-r--r-- | src/containers/ProfilePage/Highlight.tsx | 39 | ||||
-rw-r--r-- | src/containers/ProfilePage/MoreMenu.tsx | 72 | ||||
-rw-r--r-- | src/containers/ProfilePage/ProfileInfo.tsx | 166 | ||||
-rw-r--r-- | src/containers/ProfilePage/ProfilePage.tsx | 55 |
4 files changed, 332 insertions, 0 deletions
diff --git a/src/containers/ProfilePage/Highlight.tsx b/src/containers/ProfilePage/Highlight.tsx new file mode 100644 index 0000000..ebc3f56 --- /dev/null +++ b/src/containers/ProfilePage/Highlight.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +interface PropTypes { + text: string; + value: string | number; +} + +const useStyles = makeStyles({ + root: { + position: 'relative' + }, + menuButton: { + width: 200, + height: 50, + textAlign: 'center' + }, + menuNumber: { + fontWeight: 800, + color: 'black' + }, + menuText: { + color: 'darkgray' + } +}); + + +const Highlight: React.FC<PropTypes> = ({ text, value }) => { + const classes = useStyles(); + + return ( + <div className={classes.menuButton}> + <div className={classes.menuNumber}>{value}</div> + <div className={classes.menuText}>{text}</div> + </div> + ); +}; + +export default Highlight; diff --git a/src/containers/ProfilePage/MoreMenu.tsx b/src/containers/ProfilePage/MoreMenu.tsx new file mode 100644 index 0000000..1f41879 --- /dev/null +++ b/src/containers/ProfilePage/MoreMenu.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import IconButton from '@material-ui/core/IconButton'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; +import { makeStyles } from '@material-ui/core'; +import { useAuth } from '../../hooks/useAuth'; + +const ITEM_HEIGHT = 48; + +const useStyles = makeStyles({ + moreMenu: { + position: 'absolute', + right: 10, + zIndex: 100 + } +}); + +const MoreMenu: React.FC = () => { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null); + const { logout } = useAuth(); + const history = useHistory(); + + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget); + }; + + const handleLogout = () => { + logout(); + history.push('/login'); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <div className={classes.moreMenu}> + <div> + <IconButton + aria-label="more" + aria-controls="long-menu" + aria-haspopup="true" + onClick={handleClick} + > + <MoreHorizIcon /> + </IconButton> + <Menu + id="long-menu" + anchorEl={anchorEl} + keepMounted + open={open} + onClose={handleClose} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: '20ch' + } + }} + > + <MenuItem onClick={handleLogout}>Log out</MenuItem> + </Menu> + </div> + </div> + ); +}; + +export default MoreMenu; diff --git a/src/containers/ProfilePage/ProfileInfo.tsx b/src/containers/ProfilePage/ProfileInfo.tsx new file mode 100644 index 0000000..9eee4c1 --- /dev/null +++ b/src/containers/ProfilePage/ProfileInfo.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { Avatar, Badge, Typography } from '@material-ui/core/'; +import { makeStyles } from '@material-ui/core/styles'; +import { User } from 'which-types'; +import CameraAltIcon from '@material-ui/icons/CameraAlt'; +import VerifiedIcon from '@material-ui/icons/CheckCircleOutline'; +import Skeleton from '@material-ui/lab/Skeleton'; +import MoreMenu from './MoreMenu'; +import Highlight from './Highlight'; +import UploadImage from '../../components/UploadImage/UploadImage'; +import { patch } from '../../requests'; +import { useAuth } from '../../hooks/useAuth'; + +interface PropTypes { + savedPolls: number; + totalVotes: number; + userInfo: User | undefined; + setUserInfo: (userInfo: User) => void; +} + +const useStyles = makeStyles(theme => ({ + root: { + position: 'relative' + }, + avatar: { + width: 150, + height: 150, + margin: '0 auto' + }, + name: { + margin: theme.spacing(1, 0), + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + verified: { + marginLeft: theme.spacing(0.5), + width: theme.spacing(3), + height: theme.spacing(3) + }, + profileMenu: { + display: 'flex', + width: '100%', + height: 50, + margin: '50px 0', + borderBottom: '1px solid lightgray' + }, + menuButton: { + width: 200, + height: 50, + textAlign: 'center' + }, + badge: { + width: theme.spacing(5), + height: theme.spacing(5), + borderRadius: '50%', + cursor: 'pointer', + background: '#d3d3d3', + display: 'flex', + alignItems: 'center', + '& svg': { + margin: '0 auto' + } + }, + avatarContainer: { + position: 'relative', + textAlign: 'center' + }, + menuNumber: { + fontWeight: 800, + color: 'black' + }, + menuText: { + color: 'darkgray' + }, + skeleton: { + margin: '10px auto', + borderRadius: 2 + } + +})); + + +const ProfileInfo: React.FC<PropTypes> = ({ + savedPolls, totalVotes, setUserInfo, userInfo +}) => { + const classes = useStyles(); + const [input, setInput] = useState(false); + const { user } = useAuth(); + const dateSince = new Date(userInfo?.createdAt || '').toLocaleDateString(); + + const handleClick = () => { + setInput(!input); + }; + + const patchAvatar = (url: string) => { + const id = user?._id; + patch(`/users/${id}`, { avatarUrl: url }).then(res => { + setUserInfo(res.data); + }); + }; + + return ( + <div className={classes.root}> + { + !userInfo + ? <Skeleton animation="wave" variant="circle" width={150} height={150} className={classes.avatar} /> + : userInfo?._id === user?._id + ? ( + <div> + <MoreMenu /> + <div className={classes.avatarContainer}> + <Badge + overlap="circle" + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right' + }} + badgeContent={( + <div className={classes.badge}> + <CameraAltIcon onClick={handleClick} /> + </div> + )} + > + <Avatar className={classes.avatar} src={userInfo?.avatarUrl} /> + </Badge> + </div> + <UploadImage isOpen={input} setIsOpen={setInput} callback={patchAvatar} /> + </div> + ) + : <Avatar className={classes.avatar} src={userInfo?.avatarUrl} /> + } + { + !userInfo + ? <Skeleton animation="wave" variant="rect" width={100} height={20} className={classes.skeleton} /> + : ( + <Typography variant="h5" className={classes.name}> + {userInfo?.username} + {userInfo?.verified && <VerifiedIcon className={classes.verified} color="primary" />} + </Typography> + ) + } + <div className={classes.profileMenu}> + { + !userInfo + ? ( + <> + <Skeleton animation="wave" variant="rect" width={170} height={20} className={classes.skeleton} /> + <Skeleton animation="wave" variant="rect" width={170} height={20} className={classes.skeleton} /> + <Skeleton animation="wave" variant="rect" width={170} height={20} className={classes.skeleton} /> + </> + ) + : ( + <> + <Highlight text="Polls" value={savedPolls} /> + <Highlight text="Since" value={dateSince} /> + <Highlight text="Total votes" value={totalVotes} /> + </> + ) + } + </div> + </div> + ); +}; + +export default ProfileInfo; diff --git a/src/containers/ProfilePage/ProfilePage.tsx b/src/containers/ProfilePage/ProfilePage.tsx new file mode 100644 index 0000000..db27d25 --- /dev/null +++ b/src/containers/ProfilePage/ProfilePage.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useCallback } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { Poll } from 'which-types'; +import { Container } from '@material-ui/core'; + +import ProfileInfo from './ProfileInfo'; +import Feed from '../../components/Feed/Feed'; +import Loading from '../../components/Loading/Loading'; +import { useAuth } from '../../hooks/useAuth'; +import { useUser, useProfile } from '../../hooks/APIClient'; + + +const ProfilePage: React.FC = () => { + const history = useHistory(); + const { username } = useParams(); + const { user } = useAuth(); + + const { data: userInfo, mutate: setUserInfo } = useUser(username); + const { data: polls, isValidating } = useProfile(userInfo?._id); + + useEffect(() => { + if (!username) { + if (user) history.push(`/profile/${user.username}`); + else history.push('/login'); + } + }, [username, history, user]); + + + const totalVotes = useCallback( + polls.reduce( + (total: number, current: Poll) => { + const { left, right } = current.contents; + return total + left.votes + right.votes; + }, 0 + ), + [polls] + ); + + return ( + <Container maxWidth="sm" disableGutters> + <ProfileInfo + userInfo={userInfo} + setUserInfo={setUserInfo} + savedPolls={polls.length} + totalVotes={totalVotes} + /> + {!polls.length && isValidating + ? <Loading /> + : <Feed polls={polls} /> + } + </Container> + ); +}; + +export default ProfilePage; |