diff options
| author | Eugene Sokolov <eug-vs@keemail.me> | 2020-08-13 17:27:32 +0300 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-13 17:27:32 +0300 | 
| commit | d1e0dcd8538a61184eca50fbf7769c6d2943ff6b (patch) | |
| tree | 9c2ba42d34e469d292fc1fe807e3f814a872a69e | |
| parent | 2dc5fc00347256982136deea98d483c444002595 (diff) | |
| parent | 52799ec4e4cd5801423ee0d2aa56039c061afdb4 (diff) | |
| download | which-ui-d1e0dcd8538a61184eca50fbf7769c6d2943ff6b.tar.gz | |
Merge pull request #78 from which-ecosystem/redesign
Move PollSubmission to separate page and add FAB
| -rw-r--r-- | src/components/Avatar/Avatar.tsx | 16 | ||||
| -rw-r--r-- | src/components/Fab/Fab.tsx | 50 | ||||
| -rw-r--r-- | src/components/Header/BottomBar.tsx | 2 | ||||
| -rw-r--r-- | src/components/Header/BrowserHeader.tsx | 24 | ||||
| -rw-r--r-- | src/components/Header/Header.tsx | 7 | ||||
| -rw-r--r-- | src/containers/Feed/Feed.tsx | 10 | ||||
| -rw-r--r-- | src/containers/Page/Page.tsx | 2 | ||||
| -rw-r--r-- | src/containers/PollCreation/PollCreation.tsx | 93 | ||||
| -rw-r--r-- | src/containers/PollCreation/PollCreationImage.tsx | 95 | ||||
| -rw-r--r-- | src/containers/PollCreation/types.ts | 7 | ||||
| -rw-r--r-- | src/containers/Profile/Profile.tsx | 2 | ||||
| -rw-r--r-- | src/index.tsx | 3 | 
12 files changed, 281 insertions, 30 deletions
diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index e445891..29754c9 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -1,35 +1,27 @@  import React from 'react';  import { useHistory } from 'react-router-dom';  import { Avatar as AvatarBase } from '@material-ui/core'; -import AccountCircle from '@material-ui/icons/AccountCircle';  import { User } from 'which-types';  interface PropTypes { -  user: User; +  user?: User;    className?: string;  }  const Avatar: React.FC<PropTypes> = ({ user, className }) => {    const history = useHistory(); -  const { username, avatarUrl } = user;    const handleClick = () => { -    history.push(`/profile/${username}`); +    if (user) history.push(`/profile/${user.username}`);    }; -  return avatarUrl ? ( +  return (      <AvatarBase -      src={avatarUrl} -      alt={username[0].toUpperCase()} +      src={user?.avatarUrl}        onClick={handleClick}        className={className}        style={{ cursor: 'pointer' }}      /> -  ) : ( -    <AccountCircle -      className={className} -      onClick={handleClick} -    />    );  }; diff --git a/src/components/Fab/Fab.tsx b/src/components/Fab/Fab.tsx new file mode 100644 index 0000000..7ca2893 --- /dev/null +++ b/src/components/Fab/Fab.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { Fab as FabBase, Slide, useScrollTrigger } from '@material-ui/core/'; +import { makeStyles } from '@material-ui/core/styles'; +import PlusIcon from '@material-ui/icons/Add'; + +interface PropTypes { +  hideOnScroll?: boolean; +} + +const useStyles = makeStyles(theme => ({ +  root: { +    zIndex: 1000, +    position: 'fixed', + +    [theme.breakpoints.down('sm')]: { +      right: theme.spacing(2), +      bottom: theme.spacing(8) +    }, +    [theme.breakpoints.up('md')]: { +      right: theme.spacing(5), +      bottom: theme.spacing(5) +    } +  } +})); + +const Fab: React.FC<PropTypes> = ({ hideOnScroll = false }) => { +  const classes = useStyles(); +  const history = useHistory(); +  const trigger = useScrollTrigger(); + +  const handleClick = () => { +    history.push('/new'); +  }; + +  return ( +    <Slide appear={false} direction="up" in={(!hideOnScroll) || !trigger}> +      <FabBase +        onClick={handleClick} +        className={classes.root} +        color="secondary" +      > +        <PlusIcon /> +      </FabBase> +    </Slide> +  ); +}; + +export default Fab; + diff --git a/src/components/Header/BottomBar.tsx b/src/components/Header/BottomBar.tsx index 67fe219..6264929 100644 --- a/src/components/Header/BottomBar.tsx +++ b/src/components/Header/BottomBar.tsx @@ -26,7 +26,7 @@ const BottomBar: React.FC<PropTypes> = React.memo(props => {    return (      <AppBar position="fixed" className={classes.root}> -      <Toolbar className={classes.toolbar}> +      <Toolbar variant="dense" className={classes.toolbar}>          {notifications}          {feed}          {profile} diff --git a/src/components/Header/BrowserHeader.tsx b/src/components/Header/BrowserHeader.tsx index 2dda717..2acb69c 100644 --- a/src/components/Header/BrowserHeader.tsx +++ b/src/components/Header/BrowserHeader.tsx @@ -5,6 +5,7 @@ import SearchBar from './SearchBar';  interface PropTypes {    logo: JSX.Element; +  menu: JSX.Element;    feed: JSX.Element;    notifications: JSX.Element;    profile: JSX.Element; @@ -18,7 +19,8 @@ const useStyles = makeStyles({      justifyContent: 'space-around'    },    group: { -    display: 'flex' +    display: 'flex', +    alignItems: 'center'    }  }); @@ -27,6 +29,7 @@ const BrowserHeader: React.FC<PropTypes> = React.memo(props => {    const classes = useStyles();    const {      logo, +    menu,      feed,      notifications,      profile @@ -34,13 +37,18 @@ const BrowserHeader: React.FC<PropTypes> = React.memo(props => {    return (      <AppBar position="fixed"> -      <Toolbar className={classes.root}> -        {logo} -        <SearchBar /> -        <div className={classes.group}> -          {feed} -          {notifications} -          {profile} +      <Toolbar> +        {menu} +        <div className={classes.root}> +          <div className={classes.group}> +            {logo} +          </div> +          <SearchBar /> +          <div className={classes.group}> +            {feed} +            {notifications} +            {profile} +          </div>          </div>        </Toolbar>      </AppBar> diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 224f6b0..93ba47d 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -89,7 +89,7 @@ const Header: React.FC = React.memo(() => {    const profile = (      <IconButton onClick={handleProfile}>        { -        user +        user?.avatarUrl            ? <Avatar className={classes.avatar} user={user} />            : <AccountCircle className={classes.avatar} />        } @@ -103,7 +103,10 @@ const Header: React.FC = React.memo(() => {        <Drawer isOpen={isDrawerOpen} setIsOpen={setIsDrawerOpen} />      </>    ) : ( -    <BrowserHeader logo={logo} profile={profile} notifications={notifications} feed={feed} /> +    <> +      <BrowserHeader menu={menu} logo={logo} profile={profile} notifications={notifications} feed={feed} /> +      <Drawer isOpen={isDrawerOpen} setIsOpen={setIsDrawerOpen} /> +    </>    );  }); diff --git a/src/containers/Feed/Feed.tsx b/src/containers/Feed/Feed.tsx index e923bdd..10b1adc 100644 --- a/src/containers/Feed/Feed.tsx +++ b/src/containers/Feed/Feed.tsx @@ -1,23 +1,19 @@  import React from 'react'; -import { Poll } from 'which-types';  import { Container } from '@material-ui/core/';  import PollsList from '../../components/PollsList/PollsList'; -import PollSubmission from './PollSubmission'; +import Fab from '../../components/Fab/Fab';  import { useAuth } from '../../hooks/useAuth';  import { useFeed } from '../../hooks/APIClient'; +  const Feed: React.FC = () => {    const { data: polls, mutate } = useFeed();    const { isAuthenticated } = useAuth(); -  const addPoll = (poll: Poll): void => { -    mutate([poll, ...polls], true); -  }; -    return (      <Container maxWidth="sm" disableGutters> -      {isAuthenticated && <PollSubmission addPoll={addPoll} />} +      {isAuthenticated && <Fab hideOnScroll />}        <PollsList polls={polls} mutate={mutate} />      </Container>    ); diff --git a/src/containers/Page/Page.tsx b/src/containers/Page/Page.tsx index d1171e6..19cf6aa 100644 --- a/src/containers/Page/Page.tsx +++ b/src/containers/Page/Page.tsx @@ -11,6 +11,7 @@ const Login = React.lazy(() => import('../Login/Login'));  const Registration = React.lazy(() => import('../Registration/Registration'));  const Home = React.lazy(() => import('../Home/Home'));  const Notifications = React.lazy(() => import('../Notifications/Notifications')); +const PollCreation = React.lazy(() => import('../PollCreation/PollCreation'));  const useStyles = makeStyles(theme => ({ @@ -47,6 +48,7 @@ const Page: React.FC = () => {              <Route exact path="/registration" component={Registration} />              <Route exact path="/feed" component={Feed} />              <Route exact path="/notifications" component={Notifications} /> +            <Route exact path="/new" component={PollCreation} />              <Route path="/profile/:username" component={Profile} />            </Switch>          </Suspense> diff --git a/src/containers/PollCreation/PollCreation.tsx b/src/containers/PollCreation/PollCreation.tsx new file mode 100644 index 0000000..7501d3a --- /dev/null +++ b/src/containers/PollCreation/PollCreation.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { +  Button, +  Card, +  Divider, +  Container +} from '@material-ui/core'; +import { Poll } from 'which-types'; +import { useSnackbar } from 'notistack'; +import axios from 'axios'; + +import PollCreationImage from './PollCreationImage'; +import UserStrip from '../../components/UserStrip/UserStrip'; +import { get, post } from '../../requests'; +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 PollCreation: React.FC<PropTypes> = ({ addPoll }) => { +  const classes = useStyles(); +  const [left, setLeft] = useState<File>(); +  const [right, setRight] = useState<File>(); +  const { enqueueSnackbar } = useSnackbar(); +  const { user } = useAuth(); + +  const readyToSubmit = left && right; + +  const uploadImage = (file?: File) => { +    const headers = { 'Content-Type': 'image/png' }; +    return get('/files') +      .then(response => response.data) +      .then(uploadUrl => axios.put(uploadUrl, file, { headers })) +      .then(response => { +        const { config: { url } } = response; +        return url && url.slice(0, url.indexOf('.png') + 4); +      }); +  }; + +  const handleClick = async () => { +    if (readyToSubmit) { +      const [leftUrl, rightUrl] = await Promise.all([uploadImage(left), uploadImage(right)]); + +      const contents = { +        left: { url: leftUrl }, +        right: { url: rightUrl } +      }; + +      post('/polls/', { contents }).then(response => { +        addPoll(response.data); +        enqueueSnackbar('Your poll has been successfully created!', { +          variant: 'success' +        }); +      }); +    } +  }; + +  return ( +    <Container maxWidth="sm" disableGutters> +      <Card className={classes.root}> +        {user && <UserStrip user={user} info="" />} +        <Divider /> +        <div className={classes.images}> +          <PollCreationImage file={left} setFile={setLeft} /> +          <PollCreationImage file={right} setFile={setRight} /> +        </div> +        <Button +          color="primary" +          disabled={!readyToSubmit} +          variant="contained" +          onClick={handleClick} +          fullWidth +        > +          Submit +        </Button> +      </Card> +    </Container> +  ); +}; + +export default PollCreation; diff --git a/src/containers/PollCreation/PollCreationImage.tsx b/src/containers/PollCreation/PollCreationImage.tsx new file mode 100644 index 0000000..d3203a6 --- /dev/null +++ b/src/containers/PollCreation/PollCreationImage.tsx @@ -0,0 +1,95 @@ +import React, { useState, useEffect } from 'react'; +import { useFilePicker, utils } from 'react-sage'; +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'; + +interface PropTypes { +  file: File | undefined; +  setFile: (file: File | undefined) => 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 PollCreationImage: React.FC<PropTypes> = ({ file, setFile }) => { +  const classes = useStyles(); +  const { files, onClick, HiddenFileInput } = useFilePicker(); +  const [url, setUrl] = useState<string>(); +  const [isMediaHover, setIsMediaHover] = useState(false); + +  const handleMouseEnter = (): void => { +    setIsMediaHover(true); +  }; + +  const handleMouseLeave = (): void => { +    setIsMediaHover(false); +  }; + +  useEffect(() => { +    if (files?.length) { +      const chosenFile = files[0]; +      setFile(chosenFile); +      utils.loadFile(chosenFile).then(result => setUrl(result)); +    } +  }, [files, setFile]); + + +  const handleClick = () => { +    if (file) { +      setFile(undefined); +    } else onClick(); +  }; + + +  const Upload = ( +    <> +      <CloudUploadIcon fontSize="large" color="primary" /> +      <Typography variant="h5" className={classes.text}> Upload an image </Typography> +      <HiddenFileInput accept=".jpg, .jpeg, .png, .gif" multiple={false} /> +    </> +  ); + +  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}> +        {file ? (url && Media) : Upload} +      </CardActionArea> +    </> +  ); +}; + +export default PollCreationImage; diff --git a/src/containers/PollCreation/types.ts b/src/containers/PollCreation/types.ts new file mode 100644 index 0000000..24ace4e --- /dev/null +++ b/src/containers/PollCreation/types.ts @@ -0,0 +1,7 @@ +export interface ImageData { +  url: string; +} +export interface Contents { +  left: ImageData; +  right: ImageData; +} diff --git a/src/containers/Profile/Profile.tsx b/src/containers/Profile/Profile.tsx index 7e929fb..33abfc2 100644 --- a/src/containers/Profile/Profile.tsx +++ b/src/containers/Profile/Profile.tsx @@ -6,6 +6,7 @@ import { Container } from '@material-ui/core';  import ProfileInfo from './ProfileInfo';  import PollsList from '../../components/PollsList/PollsList';  import Loading from '../../components/Loading/Loading'; +import Fab from '../../components/Fab/Fab';  import { useAuth } from '../../hooks/useAuth';  import { useUser, useProfile } from '../../hooks/APIClient'; @@ -46,6 +47,7 @@ const Profile: React.FC = () => {            ? <Loading />            : <PollsList polls={polls} mutate={mutatePolls} />        } +      {user?.username === username && <Fab />}      </Container>    );  }; diff --git a/src/index.tsx b/src/index.tsx index 8eb2506..1f70100 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,6 +17,9 @@ const theme = createMuiTheme({      primary: {        main: teal[700],        light: teal[100] +    }, +    secondary: { +      main: teal[900]      }    }  });  |