aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Feed/Feed.tsx57
-rw-r--r--src/components/Header/Header.tsx99
-rw-r--r--src/components/Header/SearchBar.tsx5
-rw-r--r--src/components/PollCard/PollCard.tsx36
-rw-r--r--src/components/ScrollTopArrow/ScrollTopArrow.tsx43
-rw-r--r--src/components/UploadImage/UploadImage.tsx23
6 files changed, 175 insertions, 88 deletions
diff --git a/src/components/Feed/Feed.tsx b/src/components/Feed/Feed.tsx
index 7636573..afa914d 100644
--- a/src/components/Feed/Feed.tsx
+++ b/src/components/Feed/Feed.tsx
@@ -1,26 +1,59 @@
import React from 'react';
-import { makeStyles } from '@material-ui/core/styles';
import { Poll } from 'which-types';
+import { WindowScroller, AutoSizer, List } from 'react-virtualized';
+
import PollCard from '../PollCard/PollCard';
interface PropTypes {
polls: Poll[];
}
-const useStyles = makeStyles(theme => ({
- root: {
- position: 'relative',
- maxWidth: theme.spacing(75),
- margin: '0 auto'
- }
-}));
+interface RenderPropTypes {
+ index: number;
+ key: string;
+ style: React.CSSProperties;
+}
const Feed: React.FC<PropTypes> = ({ polls }) => {
- const classes = useStyles();
+ const RenderItem: React.FC<RenderPropTypes> = ({ index, style, key }) => {
+ const poll = polls[index];
+ return (
+ <div key={key} style={style}>
+ <PollCard initialPoll={poll} />
+ </div>
+ );
+ };
+
return (
- <div className={classes.root}>
- {polls.map(poll => <PollCard initialPoll={poll} key={poll._id} />)}
- </div>
+ <WindowScroller>
+ {({
+ height,
+ isScrolling,
+ registerChild,
+ onChildScroll,
+ scrollTop
+ }) => (
+ <AutoSizer disableHeight>
+ {({ width }) => (
+ <div ref={registerChild}>
+ <List
+ autoHeight
+ height={height}
+ isScrolling={isScrolling}
+ onScroll={onChildScroll}
+ rowCount={polls.length}
+ rowHeight={550}
+ rowRenderer={RenderItem}
+ scrollTop={scrollTop}
+ width={width}
+ containerStyle={{ pointerEvents: 'auto' }}
+ overscanRowCount={1}
+ />
+ </div>
+ )}
+ </AutoSizer>
+ )}
+ </WindowScroller>
);
};
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 72e40f8..41aeec7 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -3,9 +3,11 @@ import {
AppBar,
Toolbar,
IconButton,
- Typography, Avatar
+ Typography,
+ Avatar,
+ useMediaQuery
} from '@material-ui/core';
-import { makeStyles } from '@material-ui/core/styles';
+import { makeStyles, useTheme } from '@material-ui/core/styles';
import AccountCircle from '@material-ui/icons/AccountCircle';
import NotificationsIcon from '@material-ui/icons/Notifications';
import HomeIcon from '@material-ui/icons/Home';
@@ -14,28 +16,42 @@ import { useNavigate } from '../../hooks/useNavigate';
import SearchBar from './SearchBar';
-const useStyles = makeStyles({
- root: {
+const useStyles = makeStyles(theme => ({
+ mobile: {
+ top: 'auto',
+ bottom: 0
+ },
+ toolbar: {
display: 'flex',
- justifyContent: 'space-around',
+ justifyContent: 'space-around'
+ },
+ browserToolbar: {
width: '60%',
margin: 'auto'
},
logo: {
- fontWeight: 'bold'
+ fontWeight: 'bold',
+ cursor: 'pointer',
+ color: 'white'
},
- avatar: {
- width: 24,
- height: 24
+ round: {
+ width: theme.spacing(3),
+ height: theme.spacing(3)
}
-});
+}));
const Header: React.FC = () => {
const classes = useStyles();
const { user } = useAuth();
const { navigate } = useNavigate();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const handleHome = (): void => {
+ navigate('home');
+ };
+
+ const handleFeed = (): void => {
navigate('feed');
};
@@ -44,33 +60,62 @@ const Header: React.FC = () => {
else navigate('auth');
};
- const handleNotifications = (): void => {};
+ const handleNotifications = (): void => {
+ navigate('notifications');
+ };
+
+ const FeedButton = (
+ <IconButton onClick={handleFeed}>
+ <HomeIcon />
+ </IconButton>
+ );
+
+ const NotificationsButton = (
+ <IconButton onClick={handleNotifications}>
+ <NotificationsIcon />
+ </IconButton>
+ );
+
+ const ProfileButton = (
+ <IconButton onClick={handleProfile}>
+ {
+ user?.avatarUrl
+ ? <Avatar className={classes.round} src={user?.avatarUrl} />
+ : <AccountCircle />
+ }
+ </IconButton>
+ );
- return (
+ const BrowserVersion = (
<AppBar position="fixed">
- <Toolbar className={classes.root}>
- <Typography variant="h5" className={classes.logo}>
+ <Toolbar className={`${classes.toolbar} ${classes.browserToolbar}`}>
+ <Typography variant="h5" className={classes.logo} onClick={handleHome}>
Which
</Typography>
<SearchBar />
<div>
- <IconButton onClick={handleHome}>
- <HomeIcon />
- </IconButton>
- <IconButton onClick={handleNotifications}>
- <NotificationsIcon />
- </IconButton>
- <IconButton onClick={handleProfile}>
- {
- user?.avatarUrl?.match(/\.(jpeg|jpg|gif|png)$/)
- ? <Avatar className={classes.avatar} src={user?.avatarUrl} />
- : <AccountCircle />
- }
- </IconButton>
+ {FeedButton}
+ {NotificationsButton}
+ {ProfileButton}
</div>
</Toolbar>
</AppBar>
);
+
+ const MobileVersion = (
+ <AppBar position="fixed" className={classes.mobile}>
+ <Toolbar className={classes.toolbar}>
+ <IconButton onClick={handleHome}>
+ <Typography className={`${classes.logo} ${classes.round}`}>W</Typography>
+ </IconButton>
+ {FeedButton}
+ {NotificationsButton}
+ {ProfileButton}
+ </Toolbar>
+ </AppBar>
+ );
+
+ return isMobile ? MobileVersion : BrowserVersion;
};
export default Header;
diff --git a/src/components/Header/SearchBar.tsx b/src/components/Header/SearchBar.tsx
index 253e77f..ba0943b 100644
--- a/src/components/Header/SearchBar.tsx
+++ b/src/components/Header/SearchBar.tsx
@@ -31,6 +31,9 @@ const useStyles = makeStyles(theme => ({
position: 'absolute',
width: '100%',
top: theme.spacing(5)
+ },
+ listItem: {
+ padding: 0
}
}));
@@ -77,7 +80,7 @@ const SearchBar: React.FC = () => {
{
results.map((result, index) => (
<div key={result._id}>
- <ListItem button onClick={handleNavigate(index)}>
+ <ListItem button onClick={handleNavigate(index)} className={classes.listItem}>
<UserStrip user={result} />
</ListItem>
{(index < results.length - 1) && <Divider />}
diff --git a/src/components/PollCard/PollCard.tsx b/src/components/PollCard/PollCard.tsx
index 2a23522..98ae001 100644
--- a/src/components/PollCard/PollCard.tsx
+++ b/src/components/PollCard/PollCard.tsx
@@ -6,10 +6,12 @@ import {
CardMedia
} from '@material-ui/core/';
import { Which, Poll } from 'which-types';
+import { useSnackbar } from 'notistack';
import PercentageBar from './PercentageBar';
import UserStrip from '../UserStrip/UserStrip';
import { post } from '../../requests';
+import { useAuth } from '../../hooks/useAuth';
interface PropTypes {
initialPoll: Poll;
@@ -24,14 +26,8 @@ const DATE_FORMAT = {
};
const useStyles = makeStyles(theme => ({
- root: {
- maxWidth: theme.spacing(75),
- height: 488,
- margin: '40px auto'
- },
images: {
- height: theme.spacing(50),
- width: 300
+ height: theme.spacing(50)
},
imagesBlock: {
display: 'flex'
@@ -57,15 +53,31 @@ const PollCard: React.FC<PropTypes> = ({ initialPoll }) => {
const [poll, setPoll] = useState<Poll>(initialPoll);
const classes = useStyles();
const { author, contents: { left, right }, vote } = poll;
+ const { enqueueSnackbar } = useSnackbar();
+ const { isAuthenticated } = useAuth();
const date: string = new Date(poll.createdAt).toLocaleString('default', DATE_FORMAT);
const handleVote = (which: Which) => {
- if (vote) return;
- post('votes/', { which, pollId: poll._id }).then(response => {
+ if (!isAuthenticated()) {
+ enqueueSnackbar('Unauthorized users can not vote in polls', {
+ variant: 'error'
+ });
+ } else if (vote) {
+ enqueueSnackbar('You have already voted in this poll', {
+ variant: 'error'
+ });
+ } else {
+ const newVote = ({ which, pollId: poll._id });
+ post('votes/', newVote);
poll.contents[which].votes += 1;
- poll.vote = response.data;
+ poll.vote = {
+ _id: '',
+ authorId: '',
+ createdAt: new Date(),
+ ...newVote
+ };
setPoll({ ...poll });
- });
+ }
};
const handleLeft = () => handleVote('left');
@@ -85,7 +97,7 @@ const PollCard: React.FC<PropTypes> = ({ initialPoll }) => {
const dominant: Which = left.votes >= right.votes ? 'left' : 'right';
return (
- <Card className={classes.root}>
+ <Card>
<UserStrip user={author} info={date} />
<div className={classes.imagesBlock}>
<CardActionArea onDoubleClick={handleLeft}>
diff --git a/src/components/ScrollTopArrow/ScrollTopArrow.tsx b/src/components/ScrollTopArrow/ScrollTopArrow.tsx
index 6b9d5c9..08b8591 100644
--- a/src/components/ScrollTopArrow/ScrollTopArrow.tsx
+++ b/src/components/ScrollTopArrow/ScrollTopArrow.tsx
@@ -1,26 +1,33 @@
import React, { useState } from 'react';
-import { FaArrowCircleUp } from 'react-icons/fa';
-import { makeStyles } from '@material-ui/core';
-import teal from '@material-ui/core/colors/teal';
+import { makeStyles, useTheme } from '@material-ui/core/styles';
+import { useMediaQuery } from '@material-ui/core';
+import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
-const useStyles = makeStyles(() => ({
- scrollTop: {
+const useStyles = makeStyles(theme => ({
+ root: {
position: 'fixed',
- width: 50,
- bottom: 50,
- left: 50,
+ bottom: theme.spacing(10),
+ left: theme.spacing(10),
zIndex: 1000,
cursor: 'pointer',
- opacity: 0.5,
+ opacity: 0.4,
'&:hover': {
- opacity: '1'
- }
+ opacity: 1
+ },
+ background: theme.palette.primary.main,
+ borderRadius: '50%'
+ },
+ icon: {
+ fontSize: 80,
+ color: 'white'
}
}));
-const ScrollTopArrow:React.FC = () => {
+const ScrollTopArrow: React.FC = () => {
const [showScroll, setShowScroll] = useState(false);
+ const theme = useTheme();
const classes = useStyles();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const checkScrollTop = () => {
if (!showScroll && window.pageYOffset > 400) {
@@ -37,11 +44,13 @@ const ScrollTopArrow:React.FC = () => {
window.addEventListener('scroll', checkScrollTop);
return (
- <FaArrowCircleUp
- className={classes.scrollTop}
- onClick={scrollTop}
- style={{ height: 50, display: showScroll ? 'block' : 'none', color: teal[700] }}
- />
+ <div className={classes.root}>
+ {
+ showScroll
+ && !isMobile
+ && <ArrowUpwardIcon className={classes.icon} color="primary" onClick={scrollTop} />
+ }
+ </div>
);
};
diff --git a/src/components/UploadImage/UploadImage.tsx b/src/components/UploadImage/UploadImage.tsx
index f5e680d..8ad65d5 100644
--- a/src/components/UploadImage/UploadImage.tsx
+++ b/src/components/UploadImage/UploadImage.tsx
@@ -6,7 +6,6 @@ import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
-import get from 'axios';
interface PropTypes {
isOpen: boolean;
@@ -15,8 +14,7 @@ interface PropTypes {
}
const UploadImage: React.FC<PropTypes> = ({ setIsOpen, isOpen, callback }) => {
- const [url, setUrl] = useState('');
- const [isError, setIsError] = useState(false);
+ const [url, setUrl] = useState<string>('');
const handleClose = () => {
@@ -24,19 +22,8 @@ const UploadImage: React.FC<PropTypes> = ({ setIsOpen, isOpen, callback }) => {
};
const handleSubmit = () => {
- get(url).then(res => {
- if (res.headers['content-type'] === 'image/jpeg') {
- callback(url || '');
- setIsOpen(false);
- setIsError(false);
- } else {
- // console.warn(res); TODO: handle error if response status is ok but not an image
- setIsError(true);
- }
- }).catch(() => {
- // console.warn(err); TODO: handle error if resposne status is not ok
- setIsError(true);
- });
+ handleClose();
+ callback(url || '');
};
const handleChange = (event:React.ChangeEvent<HTMLInputElement>) => {
@@ -49,7 +36,7 @@ const UploadImage: React.FC<PropTypes> = ({ setIsOpen, isOpen, callback }) => {
<DialogTitle>Upload an Image</DialogTitle>
<DialogContent>
<DialogContentText>
- Unfortunetly we do not support uploading images yet. Please provide a valid URL to your image.
+ Unfortunetly we do not support uploading images yet. Please provide a valid URL to your image:
</DialogContentText>
<TextField
autoFocus
@@ -60,8 +47,6 @@ const UploadImage: React.FC<PropTypes> = ({ setIsOpen, isOpen, callback }) => {
fullWidth
autoComplete="off"
onChange={handleChange}
- error={isError}
- helperText={isError === true ? 'invalid Url!' : ''}
/>
</DialogContent>
<DialogActions>