aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugene Sokolov <eug-vs@keemail.me>2020-08-13 17:27:32 +0300
committerGitHub <noreply@github.com>2020-08-13 17:27:32 +0300
commitd1e0dcd8538a61184eca50fbf7769c6d2943ff6b (patch)
tree9c2ba42d34e469d292fc1fe807e3f814a872a69e
parent2dc5fc00347256982136deea98d483c444002595 (diff)
parent52799ec4e4cd5801423ee0d2aa56039c061afdb4 (diff)
downloadwhich-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.tsx16
-rw-r--r--src/components/Fab/Fab.tsx50
-rw-r--r--src/components/Header/BottomBar.tsx2
-rw-r--r--src/components/Header/BrowserHeader.tsx24
-rw-r--r--src/components/Header/Header.tsx7
-rw-r--r--src/containers/Feed/Feed.tsx10
-rw-r--r--src/containers/Page/Page.tsx2
-rw-r--r--src/containers/PollCreation/PollCreation.tsx93
-rw-r--r--src/containers/PollCreation/PollCreationImage.tsx95
-rw-r--r--src/containers/PollCreation/types.ts7
-rw-r--r--src/containers/Profile/Profile.tsx2
-rw-r--r--src/index.tsx3
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]
}
}
});