aboutsummaryrefslogtreecommitdiff
path: root/src/containers
diff options
context:
space:
mode:
Diffstat (limited to 'src/containers')
-rw-r--r--src/containers/FeedPage/FeedPage.tsx27
-rw-r--r--src/containers/FeedPage/PollSubmission.tsx92
-rw-r--r--src/containers/FeedPage/PollSubmissionImage.tsx87
-rw-r--r--src/containers/FeedPage/types.ts7
-rw-r--r--src/containers/HomePage/HomePage.tsx203
-rw-r--r--src/containers/HomePage/ReviewForm.tsx74
-rw-r--r--src/containers/LoginPage/LoginPage.tsx103
-rw-r--r--src/containers/NotificationsPage/NotificationsPage.tsx23
-rw-r--r--src/containers/Page/Page.tsx59
-rw-r--r--src/containers/ProfilePage/Highlight.tsx39
-rw-r--r--src/containers/ProfilePage/MoreMenu.tsx72
-rw-r--r--src/containers/ProfilePage/ProfileInfo.tsx166
-rw-r--r--src/containers/ProfilePage/ProfilePage.tsx55
-rw-r--r--src/containers/RegistrationPage/RegistrationPage.tsx96
14 files changed, 1103 insertions, 0 deletions
diff --git a/src/containers/FeedPage/FeedPage.tsx b/src/containers/FeedPage/FeedPage.tsx
new file mode 100644
index 0000000..da0fb2a
--- /dev/null
+++ b/src/containers/FeedPage/FeedPage.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Poll } from 'which-types';
+import { Container } from '@material-ui/core/';
+
+import Feed from '../../components/Feed/Feed';
+import PollSubmission from './PollSubmission';
+import { useAuth } from '../../hooks/useAuth';
+import { useFeed } from '../../hooks/APIClient';
+
+const FeedPage: React.FC = () => {
+ const { data, mutate } = useFeed();
+ const { isAuthenticated } = useAuth();
+
+ const addPoll = (poll: Poll): void => {
+ mutate([poll, ...data], true);
+ };
+
+ return (
+ <Container maxWidth="sm" disableGutters>
+ {isAuthenticated && <PollSubmission addPoll={addPoll} />}
+ <Feed polls={data} />
+ </Container>
+ );
+};
+
+export default FeedPage;
+
diff --git a/src/containers/FeedPage/PollSubmission.tsx b/src/containers/FeedPage/PollSubmission.tsx
new file mode 100644
index 0000000..347eecc
--- /dev/null
+++ b/src/containers/FeedPage/PollSubmission.tsx
@@ -0,0 +1,92 @@
+import React, { useState } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Collapse from '@material-ui/core/Collapse';
+import {
+ Button,
+ Card,
+ ClickAwayListener,
+ Divider
+} from '@material-ui/core';
+import { Poll, Which } from 'which-types';
+import { useSnackbar } from 'notistack';
+import PollSubmissionImage from './PollSubmissionImage';
+import UserStrip from '../../components/UserStrip/UserStrip';
+import { post } from '../../requests';
+import { Contents } from './types';
+import { useAuth } from '../../hooks/useAuth';
+
+interface PropTypes{
+ addPoll: (poll: Poll) => void;
+}
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ marginBottom: theme.spacing(4)
+ },
+ images: {
+ height: theme.spacing(50),
+ display: 'flex'
+ }
+}));
+
+const emptyContents: Contents = {
+ left: { url: '' },
+ right: { url: '' }
+};
+
+const PollSubmission: React.FC<PropTypes> = ({ addPoll }) => {
+ const classes = useStyles();
+ const [expanded, setExpanded] = useState(false);
+ const [contents, setContents] = useState<Contents>(emptyContents);
+ const { enqueueSnackbar } = useSnackbar();
+ const { user } = useAuth();
+
+ const readyToSubmit = contents.left.url && contents.right.url;
+
+ const setUrl = (which: Which) => (url: string): void => {
+ setContents({ ...contents, [which]: { url } });
+ };
+
+ const handleClickAway = () => {
+ setExpanded(false);
+ };
+
+ const handleClick = () => {
+ if (expanded && readyToSubmit) {
+ post('/polls/', { contents }).then(response => {
+ addPoll(response.data);
+ enqueueSnackbar('Your poll has been successfully created!', {
+ variant: 'success'
+ });
+ });
+ setContents({ ...emptyContents });
+ }
+ setExpanded(!expanded);
+ };
+
+ return (
+ <ClickAwayListener onClickAway={handleClickAway}>
+ <Card className={classes.root}>
+ <Collapse in={expanded} timeout="auto" unmountOnExit>
+ {user && <UserStrip user={user} info="" />}
+ <Divider />
+ <div className={classes.images}>
+ <PollSubmissionImage url={contents.left.url} setUrl={setUrl('left')} />
+ <PollSubmissionImage url={contents.right.url} setUrl={setUrl('right')} />
+ </div>
+ </Collapse>
+ <Button
+ color="primary"
+ disabled={expanded && !readyToSubmit}
+ variant={expanded ? 'contained' : 'outlined'}
+ onClick={handleClick}
+ fullWidth
+ >
+ {expanded ? 'Submit' : 'Create a Poll'}
+ </Button>
+ </Card>
+ </ClickAwayListener>
+ );
+};
+
+export default PollSubmission;
diff --git a/src/containers/FeedPage/PollSubmissionImage.tsx b/src/containers/FeedPage/PollSubmissionImage.tsx
new file mode 100644
index 0000000..8835989
--- /dev/null
+++ b/src/containers/FeedPage/PollSubmissionImage.tsx
@@ -0,0 +1,87 @@
+import React, { useState } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import CloudUploadIcon from '@material-ui/icons/CloudUpload';
+import { CardActionArea, CardMedia, Typography } from '@material-ui/core';
+import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined';
+
+import UploadImage from '../../components/UploadImage/UploadImage';
+
+interface PropTypes {
+ url: string;
+ setUrl: (url: string) => void;
+}
+
+const useStyles = makeStyles({
+ root: {
+ display: 'flex',
+ justifyContent: 'center',
+ flexDirection: 'column',
+ alignItems: 'center'
+ },
+ clearIcon: {
+ opacity: '.5',
+ fontSize: 50
+ },
+ media: {
+ height: '100%',
+ width: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ text: {
+ textAlign: 'center'
+ }
+});
+
+
+const PollSubmissionImage: React.FC<PropTypes> = ({ url, setUrl }) => {
+ const classes = useStyles();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isMediaHover, setIsMediaHover] = useState(false);
+
+ const handleClick = (): void => {
+ if (!isModalOpen) {
+ if (url) setUrl('');
+ else setIsModalOpen(!isModalOpen);
+ }
+ };
+
+ const handleMouseEnter = (): void => {
+ setIsMediaHover(true);
+ };
+
+ const handleMouseLeave = (): void => {
+ setIsMediaHover(false);
+ };
+
+
+ const Upload = (
+ <>
+ <CloudUploadIcon fontSize="large" color="primary" />
+ <Typography variant="h5" className={classes.text}> Upload an image </Typography>
+ </>
+ );
+
+ const Media = (
+ <CardMedia
+ image={url}
+ className={classes.media}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ >
+ {isMediaHover && <CancelOutlinedIcon className={classes.clearIcon} />}
+ </CardMedia>
+ );
+
+ return (
+ <>
+ <CardActionArea onClick={handleClick} className={classes.root}>
+ {url ? Media : Upload}
+ </CardActionArea>
+ <UploadImage isOpen={isModalOpen} setIsOpen={setIsModalOpen} callback={setUrl} />
+ </>
+ );
+};
+
+export default PollSubmissionImage;
diff --git a/src/containers/FeedPage/types.ts b/src/containers/FeedPage/types.ts
new file mode 100644
index 0000000..24ace4e
--- /dev/null
+++ b/src/containers/FeedPage/types.ts
@@ -0,0 +1,7 @@
+export interface ImageData {
+ url: string;
+}
+export interface Contents {
+ left: ImageData;
+ right: ImageData;
+}
diff --git a/src/containers/HomePage/HomePage.tsx b/src/containers/HomePage/HomePage.tsx
new file mode 100644
index 0000000..b1dc506
--- /dev/null
+++ b/src/containers/HomePage/HomePage.tsx
@@ -0,0 +1,203 @@
+import React, { useState, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import {
+ Typography,
+ Divider,
+ Grid,
+ Button,
+ Link,
+ useMediaQuery
+} from '@material-ui/core/';
+import { makeStyles, useTheme } from '@material-ui/core/styles';
+import TrendingUpIcon from '@material-ui/icons/TrendingUp';
+import { Rating } from '@material-ui/lab';
+import { Feedback } from 'which-types';
+
+import { useAuth } from '../../hooks/useAuth';
+import { get } from '../../requests';
+import ReviewCard from '../../components/ReviewCard/ReviewCard';
+import ReviewForm from './ReviewForm';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ overflow: 'hidden',
+ padding: theme.spacing(0, 2)
+ },
+ logo: {
+ width: theme.spacing(20),
+ height: theme.spacing(20)
+ },
+ score: {
+ fontWeight: 'bold'
+ },
+ signup: {
+ marginLeft: theme.spacing(2)
+ },
+ reviews: {
+ [theme.breakpoints.up('md')]: {
+ padding: theme.spacing(0, 10)
+ }
+ }
+}));
+
+const HomePage: React.FC = () => {
+ const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
+ const classes = useStyles();
+ const history = useHistory();
+ const { isAuthenticated, user } = useAuth();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
+
+ const rating = feedbacks.length && feedbacks.reduce(
+ (acc: number, feedback: Feedback) => acc + feedback.score,
+ 0
+ ) / feedbacks.length;
+
+ useEffect(() => {
+ get('/feedback').then(response => {
+ setFeedbacks(response.data);
+ });
+ }, []);
+
+ const handleLetsGo = () => {
+ history.push('/feed');
+ };
+
+ const handleSignUp = () => {
+ history.push('/registration');
+ };
+
+ const GithubLink = <Link href="https://github.com/which-ecosystem">GitHub</Link>;
+ const TypescriptLink = <Link href="https://www.typescriptlang.org/">Typescript</Link>;
+ const ReactLink = <Link href="https://reactjs.org/">React</Link>;
+ const FeathersLink = <Link href="https://feathersjs.com">Feathers</Link>;
+ const MUILink = <Link href="https://material-ui.com">Material-UI</Link>;
+ const EmailLink = <Link href="mailto: eug-vs@keemail.me">eug-vs@keemail.me</Link>;
+
+ const Reviews = (
+ <div className={classes.reviews}>
+ {feedbacks.map(feedback => <ReviewCard feedback={feedback} />)}
+ </div>
+ );
+
+ const FeedbackSection = feedbacks.findIndex((feedback: Feedback) => feedback.author._id === user?._id) >= 0 ? (
+ <p>
+ You have already left feedback for this version.
+ If you have more to say, please open GitHub issue or contact us directly via email: {EmailLink}.
+ Alternatively, you can just wait for another application patch to come out.
+ </p>
+ ) : (
+ <>
+ <p>
+ Here you can share your thougts about Which with us!
+ Note that you can ony leave feedback once per application version (there will be plenty of them later).
+ </p>
+ {isAuthenticated ? <ReviewForm /> : (
+ <>
+ <p> You must be authorized to leave feedback.</p>
+ <Button
+ variant="outlined"
+ color="primary"
+ onClick={handleSignUp}
+ >
+ sign in
+ </Button>
+ </>
+ )}
+ </>
+ );
+
+ return (
+ <div className={classes.root}>
+ <Grid container spacing={4}>
+ <Grid item xs={12} md={4}>
+ <Grid container direction="column" spacing={1} alignItems="center">
+ <Grid item>
+ <img src={`${process.env.PUBLIC_URL}/which-logo-512.png`} alt="logo" className={classes.logo} />
+ </Grid>
+ <Grid item>
+ {rating !== 0 && <Rating value={rating} readOnly size="large" />}
+ </Grid>
+ <Grid item>
+ {rating !== 0 && (
+ <Typography variant="h5" className={classes.score}>
+ User score: {rating.toFixed(1)}
+ </Typography>
+ )}
+ </Grid>
+ </Grid>
+ {isMobile || Reviews}
+ </Grid>
+ <Grid item xs={12} md={5}>
+ <Grid container direction="column" spacing={6}>
+ <Grid item>
+ <Typography variant="h4"> Which one to choose? </Typography>
+ <Divider />
+ <Typography>
+ <p>
+ Have you ever found yourself stuck between two options, not being able to choose any?
+ This is exactly the problem we are going to solve!
+ </p>
+ <p>Share your minor everyday uncertainties with the whole world and see what others think!</p>
+ <Button variant="contained" color="primary" size="large" onClick={handleLetsGo}>
+ {'let\'s go!'}
+ </Button>
+ {!isAuthenticated && (
+ <Button
+ variant="outlined"
+ color="primary"
+ size="large"
+ className={classes.signup}
+ onClick={handleSignUp}
+ >
+ sign up
+ </Button>
+ )}
+ </Typography>
+ </Grid>
+ <Grid item>
+ <Typography variant="h4"> About the project </Typography>
+ <Divider />
+ <Typography>
+ <p>
+ The project is written in {TypescriptLink} and features {ReactLink}, {FeathersLink}, and {MUILink}.
+ It is currently open-source and you can visit our {GithubLink} (make sure to star our repositories)!
+ </p>
+ <p>
+ We encourage any developer to check it out. Feel free to open issues and create Pull Requests!
+ </p>
+ <p>
+ All the development process is being tracked on the KanBan board (thanks GitHub).
+ You can always check it to see what is the current state of the project.
+ </p>
+ <Button
+ variant="outlined"
+ color="primary"
+ startIcon={<TrendingUpIcon />}
+ href="https://github.com/orgs/which-ecosystem/projects/1"
+ >
+ track our progress
+ </Button>
+ </Typography>
+ </Grid>
+ <Grid item>
+ <Typography variant="h4"> Leave feedback </Typography>
+ <Divider />
+ <Typography>
+ {FeedbackSection}
+ </Typography>
+ </Grid>
+ {isMobile && (
+ <Grid item>
+ {Reviews}
+ </Grid>
+ )}
+ </Grid>
+ </Grid>
+ </Grid>
+ </div>
+ );
+};
+
+export default HomePage;
+
diff --git a/src/containers/HomePage/ReviewForm.tsx b/src/containers/HomePage/ReviewForm.tsx
new file mode 100644
index 0000000..b626ce2
--- /dev/null
+++ b/src/containers/HomePage/ReviewForm.tsx
@@ -0,0 +1,74 @@
+import React, { useState } from 'react';
+import { useHistory } from 'react-router-dom';
+import { makeStyles } from '@material-ui/core/styles';
+import { TextField, Button } from '@material-ui/core';
+import { Rating } from '@material-ui/lab';
+import { useSnackbar } from 'notistack';
+
+import { post } from '../../requests';
+
+const version = 'v1.0.0';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ textField: {
+ margin: theme.spacing(2, 0)
+ }
+}));
+
+const ReviewForm: React.FC = () => {
+ const [contents, setContents] = useState<string>('');
+ const [score, setScore] = useState<number>(0);
+ const classes = useStyles();
+ const history = useHistory();
+ const { enqueueSnackbar } = useSnackbar();
+
+ const handleSubmit = (): void => {
+ if (score) {
+ post('/feedback', { contents, score, version }).then(() => {
+ enqueueSnackbar('Your feedback has been submitted!', {
+ variant: 'success'
+ });
+ history.push('/feed');
+ });
+ }
+ };
+
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
+ setContents(event.target?.value || '');
+ };
+
+ const handleChangeRating = (event: React.ChangeEvent<Record<string, unknown>>, newScore: number | null): void => {
+ setScore(newScore || 0);
+ };
+
+ return (
+ <div className={classes.root}>
+ <Rating value={score} onChange={handleChangeRating} size="large" />
+ <TextField
+ value={contents}
+ onChange={handleChange}
+ label="Feedback"
+ variant="outlined"
+ className={classes.textField}
+ rows={4}
+ multiline
+ />
+ <div>
+ <Button
+ variant="contained"
+ color="primary"
+ size="large"
+ onClick={handleSubmit}
+ >
+ submit
+ </Button>
+ </div>
+ </div>
+ );
+};
+
+export default ReviewForm;
diff --git a/src/containers/LoginPage/LoginPage.tsx b/src/containers/LoginPage/LoginPage.tsx
new file mode 100644
index 0000000..335cbb1
--- /dev/null
+++ b/src/containers/LoginPage/LoginPage.tsx
@@ -0,0 +1,103 @@
+import React, { useState, useRef } from 'react';
+import { useHistory } from 'react-router-dom';
+import { makeStyles } from '@material-ui/core/styles';
+import {
+ TextField,
+ Button,
+ FormControlLabel,
+ Switch
+} from '@material-ui/core';
+import { useAuth } from '../../hooks/useAuth';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ '& > *': {
+ margin: theme.spacing(1),
+ width: theme.spacing(35)
+ },
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ textAlign: 'center'
+ },
+ formHeader: {
+ textAlign: 'center',
+ fontSize: 25
+ },
+ formTransfer: {
+ display: 'flex',
+ justifyContent: 'center'
+ },
+ transferButton: {
+ marginLeft: 10,
+ color: 'green',
+ cursor: 'pointer'
+ }
+}));
+
+const LoginPage: React.FC = () => {
+ const [error, setError] = useState<boolean>(false);
+ const [remember, setRemember] = useState<boolean>(true);
+ const classes = useStyles();
+ const nameRef = useRef<HTMLInputElement>();
+ const passwordRef = useRef<HTMLInputElement>();
+ const { login } = useAuth();
+ const history = useHistory();
+
+ const handleCheck = () => {
+ setRemember(!remember);
+ };
+
+ const handleSubmit = async () => {
+ const name = nameRef.current?.value?.toLowerCase();
+ const password = passwordRef.current?.value;
+ if (name && password) {
+ login(name, password, remember).then(success => {
+ if (success) history.push(`/profile/${name}`);
+ else setError(true);
+ });
+ }
+ };
+
+ const handleRegistration = () => {
+ history.push('/registration');
+ };
+
+ return (
+ <>
+ <div className={classes.formHeader}>Sign In</div>
+ <form className={classes.root} noValidate autoComplete="off">
+ <TextField
+ inputRef={nameRef}
+ error={error}
+ label="Login"
+ />
+ <TextField
+ inputRef={passwordRef}
+ error={error}
+ helperText={error && 'Invalid credentials'}
+ label="Password"
+ type="password"
+ />
+ <FormControlLabel
+ control={<Switch color="primary" onClick={handleCheck} checked={remember} size="small" />}
+ label="Remember me"
+ />
+ <Button variant="contained" onClick={handleSubmit}>submit</Button>
+ </form>
+ <div className={classes.formTransfer}>
+ <div>{'Don\'t have an account?'}</div>
+ <span
+ onClick={handleRegistration}
+ className={classes.transferButton}
+ role="presentation"
+ >
+ Sign up
+ </span>
+ </div>
+ </>
+ );
+};
+
+export default LoginPage;
+
diff --git a/src/containers/NotificationsPage/NotificationsPage.tsx b/src/containers/NotificationsPage/NotificationsPage.tsx
new file mode 100644
index 0000000..064fbd4
--- /dev/null
+++ b/src/containers/NotificationsPage/NotificationsPage.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import { Typography } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ marginTop: theme.spacing(25),
+ textAlign: 'center'
+ }
+}));
+
+const NotificationsPage: React.FC = () => {
+ const classes = useStyles();
+
+ return (
+ <Typography variant="h4" className={classes.root}>
+ Sorry, this page is being constructed yet.
+ </Typography>
+ );
+};
+
+export default NotificationsPage;
+
diff --git a/src/containers/Page/Page.tsx b/src/containers/Page/Page.tsx
new file mode 100644
index 0000000..8a39636
--- /dev/null
+++ b/src/containers/Page/Page.tsx
@@ -0,0 +1,59 @@
+import React, { Suspense } from 'react';
+import { makeStyles, useTheme } from '@material-ui/core/styles';
+import { useMediaQuery } from '@material-ui/core';
+import { SnackbarProvider } from 'notistack';
+import { Switch, Route } from 'react-router-dom';
+import Loading from '../../components/Loading/Loading';
+
+const ProfilePage = React.lazy(() => import('../ProfilePage/ProfilePage'));
+const FeedPage = React.lazy(() => import('../FeedPage/FeedPage'));
+const LoginPage = React.lazy(() => import('../LoginPage/LoginPage'));
+const RegistrationPage = React.lazy(() => import('../RegistrationPage/RegistrationPage'));
+const HomePage = React.lazy(() => import('../HomePage/HomePage'));
+const NotificationsPage = React.lazy(() => import('../NotificationsPage/NotificationsPage'));
+
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ [theme.breakpoints.down('sm')]: {
+ margin: theme.spacing(2, 0, 12, 0)
+ },
+ [theme.breakpoints.up('md')]: {
+ margin: theme.spacing(15, 5, 8, 5)
+ }
+ }
+}));
+
+
+const Page: React.FC = () => {
+ const classes = useStyles();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
+
+ return (
+ <SnackbarProvider
+ maxSnack={3}
+ anchorOrigin={{
+ vertical: isMobile ? 'top' : 'bottom',
+ horizontal: 'right'
+ }}
+ >
+ <div className={classes.root}>
+ <Suspense fallback={<Loading />}>
+ <Switch>
+ <Route exact path="/" component={HomePage} />
+ <Route exact path="/login" component={LoginPage} />
+ <Route exact path="/registration" component={RegistrationPage} />
+ <Route exact path="/feed" component={FeedPage} />
+ <Route exact path="/notifications" component={NotificationsPage} />
+ <Route path="/profile/:username" component={ProfilePage} />
+ </Switch>
+ </Suspense>
+ </div>
+ </SnackbarProvider>
+ );
+};
+
+
+export default Page;
+
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;
diff --git a/src/containers/RegistrationPage/RegistrationPage.tsx b/src/containers/RegistrationPage/RegistrationPage.tsx
new file mode 100644
index 0000000..18a9379
--- /dev/null
+++ b/src/containers/RegistrationPage/RegistrationPage.tsx
@@ -0,0 +1,96 @@
+import React, { useState, useRef } from 'react';
+import { useHistory } from 'react-router-dom';
+import { makeStyles } from '@material-ui/core/styles';
+import TextField from '@material-ui/core/TextField';
+import Button from '@material-ui/core/Button';
+import { post } from '../../requests';
+import { useAuth } from '../../hooks/useAuth';
+
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ '& > *': {
+ margin: theme.spacing(1),
+ width: theme.spacing(35)
+ },
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ textAlign: 'center'
+ },
+ formHeader: {
+ textAlign: 'center',
+ fontSize: 25
+ },
+ formTransfer: {
+ display: 'flex',
+ justifyContent: 'center'
+ },
+ transferButton: {
+ marginLeft: 10,
+ color: 'green',
+ cursor: 'pointer'
+ }
+}));
+
+const RegistrationPage: React.FC = () => {
+ const [error, setError] = useState<boolean>(false);
+ const classes = useStyles();
+ const usernameRef = useRef<HTMLInputElement>();
+ const emailRef = useRef<HTMLInputElement>();
+ const passwordRef = useRef<HTMLInputElement>();
+ const { login } = useAuth();
+ const history = useHistory();
+
+ const handleSubmit = () => {
+ const username = usernameRef.current?.value?.toLowerCase();
+ const password = passwordRef.current?.value;
+ const email = emailRef.current?.value;
+ if (username && password) {
+ post('/users', { username, password, email })
+ .then(() => login(username, password))
+ .then(() => history.push(`/profile/${username}`));
+ } else setError(true);
+ };
+
+ const handleLogin = () => {
+ history.push('/login');
+ };
+
+ return (
+ <>
+ <div className={classes.formHeader}>Sign Up</div>
+ <form className={classes.root} noValidate autoComplete="off">
+ <TextField
+ inputRef={usernameRef}
+ label="Username"
+ error={error}
+ helperText={error && 'This field is required!'}
+ required
+ />
+ <TextField inputRef={emailRef} label="Email" />
+ <TextField
+ inputRef={passwordRef}
+ label="Password"
+ type="password"
+ required
+ error={error}
+ helperText={error && 'This field is required!'}
+ />
+ <Button variant="contained" onClick={handleSubmit}>submit</Button>
+ </form>
+ <div className={classes.formTransfer}>
+ <div>Already have an account?</div>
+ <span
+ onClick={handleLogin}
+ className={classes.transferButton}
+ role="presentation"
+ >
+ Log in
+ </span>
+ </div>
+ </>
+ );
+};
+
+export default RegistrationPage;