diff options
-rw-r--r-- | package-lock.json | 37 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | public/index.html | 1 | ||||
-rw-r--r-- | public/which-logo-512.png | bin | 0 -> 22412 bytes | |||
-rw-r--r-- | src/components/Feed/Feed.tsx | 57 | ||||
-rw-r--r-- | src/components/Header/Header.tsx | 99 | ||||
-rw-r--r-- | src/components/Header/SearchBar.tsx | 5 | ||||
-rw-r--r-- | src/components/PollCard/PollCard.tsx | 36 | ||||
-rw-r--r-- | src/components/ScrollTopArrow/ScrollTopArrow.tsx | 43 | ||||
-rw-r--r-- | src/components/UploadImage/UploadImage.tsx | 23 | ||||
-rw-r--r-- | src/hooks/useNavigate.tsx | 3 | ||||
-rw-r--r-- | src/index.tsx | 1 | ||||
-rw-r--r-- | src/pages/FeedPage/FeedPage.tsx | 10 | ||||
-rw-r--r-- | src/pages/FeedPage/PollSubmission.tsx | 12 | ||||
-rw-r--r-- | src/pages/FeedPage/PollSubmissionImage.tsx | 5 | ||||
-rw-r--r-- | src/pages/HomePage/HomePage.tsx | 144 | ||||
-rw-r--r-- | src/pages/NotificationsPage/NotificationsPage.tsx | 23 | ||||
-rw-r--r-- | src/pages/Page.tsx | 39 | ||||
-rw-r--r-- | src/pages/ProfilePage/ProfilePage.tsx | 6 |
19 files changed, 438 insertions, 109 deletions
diff --git a/package-lock.json b/package-lock.json index 3329f6f..41430ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1710,6 +1710,16 @@ "@types/react": "*" } }, + "@types/react-virtualized": { + "version": "9.21.10", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.10.tgz", + "integrity": "sha512-f5Ti3A7gGdLkPPFNHTrvKblpsPNBiQoSorOEOD+JPx72g/Ng2lOt4MYfhvQFQNgyIrAro+Z643jbcKafsMW2ag==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -8697,6 +8707,15 @@ "sort-keys": "^1.0.0" } }, + "notistack": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-0.9.17.tgz", + "integrity": "sha512-nypTN6sEe+q98wMaxF/UwatA1yAq948+bZOo9JKYR+tU65DW0ipWyx8DseJ3UJYvb6VDD+Fqo83qwayQ46bEEA==", + "requires": { + "clsx": "^1.1.0", + "hoist-non-react-statics": "^3.3.0" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -10808,6 +10827,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-scripts": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz", @@ -11177,6 +11201,19 @@ "prop-types": "^15.6.2" } }, + "react-virtualized": { + "version": "9.21.2", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz", + "integrity": "sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA==", + "requires": { + "babel-runtime": "^6.26.0", + "clsx": "^1.0.1", + "dom-helpers": "^5.0.0", + "loose-envify": "^1.3.0", + "prop-types": "^15.6.0", + "react-lifecycles-compat": "^3.0.4" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", diff --git a/package.json b/package.json index 61cc8f3..40797fe 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,12 @@ "@material-ui/lab": "^4.0.0-alpha.56", "axios": "^0.19.2", "lodash": "^4.17.15", + "notistack": "^0.9.17", "react": "^16.13.1", "react-dom": "^16.13.1", "react-icons": "^3.10.0", "react-scripts": "3.4.1", + "react-virtualized": "^9.21.2", "typeface-roboto": "0.0.75", "which-types": "^1.6.1" }, @@ -39,6 +41,7 @@ "@types/node": "^12.12.44", "@types/react": "^16.9.35", "@types/react-dom": "^16.9.8", + "@types/react-virtualized": "^9.21.10", "@typescript-eslint/eslint-plugin": "^3.1.0", "@typescript-eslint/parser": "^3.1.0", "eslint": "^6.8.0", diff --git a/public/index.html b/public/index.html index c3d52e2..001cbac 100644 --- a/public/index.html +++ b/public/index.html @@ -2,6 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8" /> + <link rel="icon" type="image/png" sizes="16x16" href="./which-logo-64.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Which</title> </head> diff --git a/public/which-logo-512.png b/public/which-logo-512.png Binary files differnew file mode 100644 index 0000000..ddcbd9f --- /dev/null +++ b/public/which-logo-512.png 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> diff --git a/src/hooks/useNavigate.tsx b/src/hooks/useNavigate.tsx index befc529..d1a433d 100644 --- a/src/hooks/useNavigate.tsx +++ b/src/hooks/useNavigate.tsx @@ -11,7 +11,7 @@ interface ContextType { navigate: (prefix: string, id?: string) => void; } -const landingPage = { prefix: 'feed' }; +const landingPage = { prefix: 'home' }; const context = createContext<ContextType>({ page: landingPage, @@ -24,6 +24,7 @@ const useProvideNavigation = () => { const navigate: ContextType['navigate'] = (prefix, id?) => { setPage({ prefix, id }); + window.scrollTo(0, 0); }; return { page, setPage, navigate }; diff --git a/src/index.tsx b/src/index.tsx index 180f80c..e8fbce1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,7 +11,6 @@ import Page from './pages/Page'; import { AuthProvider } from './hooks/useAuth'; import { NavigationProvider } from './hooks/useNavigate'; - const theme = createMuiTheme({ palette: { primary: { diff --git a/src/pages/FeedPage/FeedPage.tsx b/src/pages/FeedPage/FeedPage.tsx index d29103a..0b7d44a 100644 --- a/src/pages/FeedPage/FeedPage.tsx +++ b/src/pages/FeedPage/FeedPage.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect } from 'react'; import { Poll } from 'which-types'; +import { Container } from '@material-ui/core/'; import Feed from '../../components/Feed/Feed'; import { get } from '../../requests'; import PollSubmission from './PollSubmission'; import { useAuth } from '../../hooks/useAuth'; - const FeedPage: React.FC = () => { const [polls, setPolls] = useState<Poll[]>([]); const { isAuthenticated } = useAuth(); @@ -19,15 +19,15 @@ const FeedPage: React.FC = () => { const addPoll = (poll: Poll): void => { polls.unshift(poll); - setPolls([...polls]); + setPolls([]); + setPolls(polls); }; - return ( - <> + <Container maxWidth="sm" disableGutters> {isAuthenticated() && <PollSubmission addPoll={addPoll} />} <Feed polls={polls} /> - </> + </Container> ); }; diff --git a/src/pages/FeedPage/PollSubmission.tsx b/src/pages/FeedPage/PollSubmission.tsx index 18f029c..347eecc 100644 --- a/src/pages/FeedPage/PollSubmission.tsx +++ b/src/pages/FeedPage/PollSubmission.tsx @@ -8,6 +8,7 @@ import { 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'; @@ -20,6 +21,9 @@ interface PropTypes{ const useStyles = makeStyles(theme => ({ root: { + marginBottom: theme.spacing(4) + }, + images: { height: theme.spacing(50), display: 'flex' } @@ -34,6 +38,7 @@ 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; @@ -50,6 +55,9 @@ const PollSubmission: React.FC<PropTypes> = ({ addPoll }) => { if (expanded && readyToSubmit) { post('/polls/', { contents }).then(response => { addPoll(response.data); + enqueueSnackbar('Your poll has been successfully created!', { + variant: 'success' + }); }); setContents({ ...emptyContents }); } @@ -58,11 +66,11 @@ const PollSubmission: React.FC<PropTypes> = ({ addPoll }) => { return ( <ClickAwayListener onClickAway={handleClickAway}> - <Card> + <Card className={classes.root}> <Collapse in={expanded} timeout="auto" unmountOnExit> {user && <UserStrip user={user} info="" />} <Divider /> - <div className={classes.root}> + <div className={classes.images}> <PollSubmissionImage url={contents.left.url} setUrl={setUrl('left')} /> <PollSubmissionImage url={contents.right.url} setUrl={setUrl('right')} /> </div> diff --git a/src/pages/FeedPage/PollSubmissionImage.tsx b/src/pages/FeedPage/PollSubmissionImage.tsx index a8ec437..8835989 100644 --- a/src/pages/FeedPage/PollSubmissionImage.tsx +++ b/src/pages/FeedPage/PollSubmissionImage.tsx @@ -28,6 +28,9 @@ const useStyles = makeStyles({ display: 'flex', justifyContent: 'center', alignItems: 'center' + }, + text: { + textAlign: 'center' } }); @@ -56,7 +59,7 @@ const PollSubmissionImage: React.FC<PropTypes> = ({ url, setUrl }) => { const Upload = ( <> <CloudUploadIcon fontSize="large" color="primary" /> - <Typography variant="h5"> Upload an image </Typography> + <Typography variant="h5" className={classes.text}> Upload an image </Typography> </> ); diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx new file mode 100644 index 0000000..f00289a --- /dev/null +++ b/src/pages/HomePage/HomePage.tsx @@ -0,0 +1,144 @@ +import React, { useState, useEffect } from 'react'; +import { + Typography, + Divider, + Grid, + Button, + Link +} from '@material-ui/core/'; +import { makeStyles } from '@material-ui/core/styles'; +import TrendingUpIcon from '@material-ui/icons/TrendingUp'; +import { Rating } from '@material-ui/lab'; +import { Feedback } from 'which-types'; + +import { useNavigate } from '../../hooks/useNavigate'; +import { useAuth } from '../../hooks/useAuth'; +import { get } from '../../requests'; + +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) + } +})); + +const HomePage: React.FC = () => { + const [feedbacks, setFeedbacks] = useState<Feedback[]>([]); + const classes = useStyles(); + const { navigate } = useNavigate(); + const { isAuthenticated } = useAuth(); + + 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 = () => { + navigate('feed'); + }; + + const handleSignUp = () => { + navigate('auth'); + }; + + 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>; + + 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 value={rating} readOnly size="large" /> + </Grid> + <Grid item> + <Typography variant="h5" className={classes.score}> + User score: {rating.toFixed(1)} + </Typography> + </Grid> + </Grid> + </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> + </Grid> + </Grid> + </div> + ); +}; + +export default HomePage; + diff --git a/src/pages/NotificationsPage/NotificationsPage.tsx b/src/pages/NotificationsPage/NotificationsPage.tsx new file mode 100644 index 0000000..3c39ba3 --- /dev/null +++ b/src/pages/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(40), + 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/pages/Page.tsx b/src/pages/Page.tsx index 6d4315e..56d7372 100644 --- a/src/pages/Page.tsx +++ b/src/pages/Page.tsx @@ -1,28 +1,49 @@ import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; +import { useMediaQuery } from '@material-ui/core'; +import { SnackbarProvider } from 'notistack'; + import ProfilePage from './ProfilePage/ProfilePage'; import FeedPage from './FeedPage/FeedPage'; import AuthPage from './AuthPage/AuthPage'; +import HomePage from './HomePage/HomePage'; +import NotificationsPage from './NotificationsPage/NotificationsPage'; import { useNavigate } from '../hooks/useNavigate'; + const useStyles = makeStyles(theme => ({ root: { - width: theme.spacing(75), - marginTop: theme.spacing(15), - margin: '0 auto' + [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 { page } = useNavigate(); const classes = useStyles(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); return ( - <div className={classes.root}> - { page.prefix === 'profile' && <ProfilePage />} - { page.prefix === 'feed' && <FeedPage /> } - { page.prefix === 'auth' && <AuthPage /> } - </div> + <SnackbarProvider + maxSnack={3} + anchorOrigin={{ + vertical: isMobile ? 'top' : 'bottom', + horizontal: 'right' + }} + > + <div className={classes.root}> + { page.prefix === 'home' && <HomePage />} + { page.prefix === 'profile' && <ProfilePage />} + { page.prefix === 'feed' && <FeedPage /> } + { page.prefix === 'auth' && <AuthPage /> } + { page.prefix === 'notifications' && <NotificationsPage /> } + </div> + </SnackbarProvider> ); }; diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 82e5cd8..ba4db7d 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { User, Poll } from 'which-types'; +import { Container } from '@material-ui/core'; import ProfileInfo from './ProfileInfo'; import Feed from '../../components/Feed/Feed'; @@ -24,6 +25,7 @@ const ProfilePage: React.FC = () => { setUserInfo(response.data); }); get(`/profiles/${id}`).then(response => { + setPolls([]); setPolls(response.data); setTotalVotes(response.data.reduce( (total: number, current: Poll) => { @@ -37,7 +39,7 @@ const ProfilePage: React.FC = () => { }, [navigate, page, user]); return ( - <> + <Container maxWidth="sm" disableGutters> <ProfileInfo userInfo={userInfo} setUserInfo={setUserInfo} @@ -46,7 +48,7 @@ const ProfilePage: React.FC = () => { loading={isLoading} /> <Feed polls={[...polls]} /> - </> + </Container> ); }; |