diff options
author | Eugene Sokolov <eug-vs@keemail.me> | 2020-08-10 13:51:11 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-10 13:51:11 +0300 |
commit | 823c82383424616bc7c2562e2a763321edb6050c (patch) | |
tree | 1d5220d68ab8ebb392c87038f2fc24cc72b28775 | |
parent | 70d20b76f042a519e8e164279dfa31b5ce027d44 (diff) | |
parent | 78218c0f3427ad79de003ac59cffb99b08f0ae7d (diff) | |
download | which-ui-823c82383424616bc7c2562e2a763321edb6050c.tar.gz |
Merge pull request #74 from which-ecosystem/fetching
SWR feat. crazy refactor
-rw-r--r-- | package-lock.json | 15 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/components/Header/Header.tsx | 4 | ||||
-rw-r--r-- | src/components/Header/SearchBar.tsx | 4 | ||||
-rw-r--r-- | src/components/Loading/Loading.tsx | 24 | ||||
-rw-r--r-- | src/components/PollCard/PollCard.tsx | 20 | ||||
-rw-r--r-- | src/components/PollsList/PollsList.tsx (renamed from src/components/Feed/Feed.tsx) | 43 | ||||
-rw-r--r-- | src/components/ScrollTopArrow/ScrollTopArrow.tsx | 4 | ||||
-rw-r--r-- | src/containers/Feed/Feed.tsx | 27 | ||||
-rw-r--r-- | src/containers/Feed/PollSubmission.tsx (renamed from src/pages/FeedPage/PollSubmission.tsx) | 0 | ||||
-rw-r--r-- | src/containers/Feed/PollSubmissionImage.tsx (renamed from src/pages/FeedPage/PollSubmissionImage.tsx) | 0 | ||||
-rw-r--r-- | src/containers/Feed/types.ts (renamed from src/pages/FeedPage/types.ts) | 0 | ||||
-rw-r--r-- | src/containers/Home/Home.tsx (renamed from src/pages/HomePage/HomePage.tsx) | 22 | ||||
-rw-r--r-- | src/containers/Home/ReviewForm.tsx (renamed from src/pages/HomePage/ReviewForm.tsx) | 0 | ||||
-rw-r--r-- | src/containers/Login/Login.tsx (renamed from src/pages/LoginPage/LoginPage.tsx) | 4 | ||||
-rw-r--r-- | src/containers/Notifications/Notifications.tsx (renamed from src/pages/NotificationsPage/NotificationsPage.tsx) | 4 | ||||
-rw-r--r-- | src/containers/Page/Page.tsx | 59 | ||||
-rw-r--r-- | src/containers/Profile/Highlight.tsx (renamed from src/pages/ProfilePage/Highlight.tsx) | 0 | ||||
-rw-r--r-- | src/containers/Profile/MoreMenu.tsx (renamed from src/pages/ProfilePage/MoreMenu.tsx) | 0 | ||||
-rw-r--r-- | src/containers/Profile/Profile.tsx | 53 | ||||
-rw-r--r-- | src/containers/Profile/ProfileInfo.tsx (renamed from src/pages/ProfilePage/ProfileInfo.tsx) | 16 | ||||
-rw-r--r-- | src/containers/Registration/Registration.tsx (renamed from src/pages/RegistrationPage/RegistrationPage.tsx) | 4 | ||||
-rw-r--r-- | src/hooks/APIClient.ts | 34 | ||||
-rw-r--r-- | src/hooks/useAuth.tsx | 69 | ||||
-rw-r--r-- | src/hooks/useLocalStorage.ts | 16 | ||||
-rw-r--r-- | src/index.tsx | 2 | ||||
-rw-r--r-- | src/pages/FeedPage/FeedPage.tsx | 35 | ||||
-rw-r--r-- | src/pages/Page.tsx | 56 | ||||
-rw-r--r-- | src/pages/ProfilePage/ProfilePage.tsx | 72 | ||||
-rw-r--r-- | src/requests.ts | 2 |
30 files changed, 318 insertions, 272 deletions
diff --git a/package-lock.json b/package-lock.json index 92f2ebd..f00eac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12872,6 +12872,21 @@ "util.promisify": "~1.0.0" } }, + "swr": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-0.3.0.tgz", + "integrity": "sha512-3p0p5TWH0qiaKAph5wBkMwqe2WjNseITfjmdVoNzjqRZGn/gnpRi6whMDjhMVb/vp/yyDtKWPlyjid8QZH+UhA==", + "requires": { + "fast-deep-equal": "2.0.1" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + } + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 57a1caa..453acc2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", "react-virtualized": "^9.21.2", + "swr": "^0.3.0", "typeface-roboto": "0.0.75", "which-types": "^1.6.1" }, diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 5aa66ba..c3a678c 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -42,7 +42,7 @@ const useStyles = makeStyles(theme => ({ })); -const Header: React.FC = () => { +const Header: React.FC = React.memo(() => { const classes = useStyles(); const { user } = useAuth(); const theme = useTheme(); @@ -118,7 +118,7 @@ const Header: React.FC = () => { ); return isMobile ? MobileVersion : BrowserVersion; -}; +}); export default Header; diff --git a/src/components/Header/SearchBar.tsx b/src/components/Header/SearchBar.tsx index f541589..8bfe0fb 100644 --- a/src/components/Header/SearchBar.tsx +++ b/src/components/Header/SearchBar.tsx @@ -37,7 +37,7 @@ const useStyles = makeStyles(theme => ({ } })); -const SearchBar: React.FC = () => { +const SearchBar: React.FC = React.memo(() => { const [results, setResults] = useState<User[]>([]); const [query, setQuery] = useState<string>(''); const [debouncedQuery, setDebouncedQuery] = useState<string>(query); @@ -104,7 +104,7 @@ const SearchBar: React.FC = () => { {results.length > 0 && SearchResults} </div> ); -}; +}); export default SearchBar; diff --git a/src/components/Loading/Loading.tsx b/src/components/Loading/Loading.tsx new file mode 100644 index 0000000..34d436b --- /dev/null +++ b/src/components/Loading/Loading.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + loader: { + width: '100%', + textAlign: 'center', + marginTop: theme.spacing(10) + } +})); + +const Loading: React.FC = React.memo(() => { + const classes = useStyles(); + + return ( + <div className={classes.loader}> + <CircularProgress color="primary" style={{ margin: '0 auto' }} /> + </div> + ); +}); + +export default Loading; + diff --git a/src/components/PollCard/PollCard.tsx b/src/components/PollCard/PollCard.tsx index 98ae001..689e872 100644 --- a/src/components/PollCard/PollCard.tsx +++ b/src/components/PollCard/PollCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { Card, @@ -14,7 +14,8 @@ import { post } from '../../requests'; import { useAuth } from '../../hooks/useAuth'; interface PropTypes { - initialPoll: Poll; + poll: Poll; + setPoll: (poll: Poll) => void; } const DATE_FORMAT = { @@ -49,8 +50,7 @@ const useStyles = makeStyles(theme => ({ } })); -const PollCard: React.FC<PropTypes> = ({ initialPoll }) => { - const [poll, setPoll] = useState<Poll>(initialPoll); +const PollCard: React.FC<PropTypes> = ({ poll, setPoll }) => { const classes = useStyles(); const { author, contents: { left, right }, vote } = poll; const { enqueueSnackbar } = useSnackbar(); @@ -58,7 +58,7 @@ const PollCard: React.FC<PropTypes> = ({ initialPoll }) => { const date: string = new Date(poll.createdAt).toLocaleString('default', DATE_FORMAT); const handleVote = (which: Which) => { - if (!isAuthenticated()) { + if (!isAuthenticated) { enqueueSnackbar('Unauthorized users can not vote in polls', { variant: 'error' }); @@ -68,15 +68,17 @@ const PollCard: React.FC<PropTypes> = ({ initialPoll }) => { }); } else { const newVote = ({ which, pollId: poll._id }); - post('votes/', newVote); - poll.contents[which].votes += 1; - poll.vote = { + const newPoll = { ...poll }; + newPoll.contents[which].votes += 1; + newPoll.vote = { _id: '', authorId: '', createdAt: new Date(), ...newVote }; - setPoll({ ...poll }); + setPoll(newPoll); + + post('votes/', newVote); } }; diff --git a/src/components/Feed/Feed.tsx b/src/components/PollsList/PollsList.tsx index 03358da..c95bfde 100644 --- a/src/components/Feed/Feed.tsx +++ b/src/components/PollsList/PollsList.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Poll } from 'which-types'; import { WindowScroller, AutoSizer, List } from 'react-virtualized'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import { makeStyles } from '@material-ui/core'; import PollCard from '../PollCard/PollCard'; + interface PropTypes { polls: Poll[]; + mutate: (polls: Poll[], refetch: boolean) => void; } interface RenderPropTypes { @@ -15,34 +15,29 @@ interface RenderPropTypes { style: React.CSSProperties; } -const useStyles = makeStyles(theme => ({ - loader: { - width: '100%', - textAlign: 'center', - marginTop: theme.spacing(10) - } -})); - -const Feed: React.FC<PropTypes> = ({ polls }) => { - const classes = useStyles(); - +const PollsList: React.FC<PropTypes> = ({ polls, mutate }) => { const RenderItem: React.FC<RenderPropTypes> = ({ index, style, key }) => { const poll = polls[index]; + + const setPoll = (newPoll: Poll) => { + const newPolls = [...polls]; + newPolls[index] = newPoll; + + // Force-update list-size so everything re-renders + mutate([], false); + mutate(newPolls, false); + }; + return ( - <div key={key} style={style}> - <PollCard initialPoll={poll} /> + // To re-render on list resize, add this info to key + <div key={`${key}-${poll._id}-${polls.length}`} style={style}> + <PollCard poll={poll} setPoll={setPoll} /> </div> ); }; - const loader = ( - <div className={classes.loader}> - <CircularProgress color="primary" style={{ margin: '0 auto' }} /> - </div> - ); - - const list = ( + return ( <WindowScroller> {({ height, @@ -73,9 +68,7 @@ const Feed: React.FC<PropTypes> = ({ polls }) => { )} </WindowScroller> ); - - return polls.length ? list : loader; }; -export default Feed; +export default PollsList; diff --git a/src/components/ScrollTopArrow/ScrollTopArrow.tsx b/src/components/ScrollTopArrow/ScrollTopArrow.tsx index 08b8591..8a5bb8f 100644 --- a/src/components/ScrollTopArrow/ScrollTopArrow.tsx +++ b/src/components/ScrollTopArrow/ScrollTopArrow.tsx @@ -23,7 +23,7 @@ const useStyles = makeStyles(theme => ({ } })); -const ScrollTopArrow: React.FC = () => { +const ScrollTopArrow: React.FC = React.memo(() => { const [showScroll, setShowScroll] = useState(false); const theme = useTheme(); const classes = useStyles(); @@ -52,6 +52,6 @@ const ScrollTopArrow: React.FC = () => { } </div> ); -}; +}); export default ScrollTopArrow; diff --git a/src/containers/Feed/Feed.tsx b/src/containers/Feed/Feed.tsx new file mode 100644 index 0000000..e923bdd --- /dev/null +++ b/src/containers/Feed/Feed.tsx @@ -0,0 +1,27 @@ +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 { 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} />} + <PollsList polls={polls} mutate={mutate} /> + </Container> + ); +}; + +export default Feed; + diff --git a/src/pages/FeedPage/PollSubmission.tsx b/src/containers/Feed/PollSubmission.tsx index 347eecc..347eecc 100644 --- a/src/pages/FeedPage/PollSubmission.tsx +++ b/src/containers/Feed/PollSubmission.tsx diff --git a/src/pages/FeedPage/PollSubmissionImage.tsx b/src/containers/Feed/PollSubmissionImage.tsx index 8835989..8835989 100644 --- a/src/pages/FeedPage/PollSubmissionImage.tsx +++ b/src/containers/Feed/PollSubmissionImage.tsx diff --git a/src/pages/FeedPage/types.ts b/src/containers/Feed/types.ts index 24ace4e..24ace4e 100644 --- a/src/pages/FeedPage/types.ts +++ b/src/containers/Feed/types.ts diff --git a/src/pages/HomePage/HomePage.tsx b/src/containers/Home/Home.tsx index 17e377a..203b380 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/containers/Home/Home.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { useHistory } from 'react-router-dom'; import { Typography, @@ -14,7 +14,7 @@ import { Rating } from '@material-ui/lab'; import { Feedback } from 'which-types'; import { useAuth } from '../../hooks/useAuth'; -import { get } from '../../requests'; +import { useFeedback } from '../../hooks/APIClient'; import ReviewCard from '../../components/ReviewCard/ReviewCard'; import ReviewForm from './ReviewForm'; @@ -40,8 +40,8 @@ const useStyles = makeStyles(theme => ({ } })); -const HomePage: React.FC = () => { - const [feedbacks, setFeedbacks] = useState<Feedback[]>([]); +const Home: React.FC = () => { + const { data: feedbacks } = useFeedback(); const classes = useStyles(); const history = useHistory(); const { isAuthenticated, user } = useAuth(); @@ -53,12 +53,6 @@ const HomePage: React.FC = () => { 0 ) / feedbacks.length; - useEffect(() => { - get('/feedback').then(response => { - setFeedbacks(response.data); - }); - }, []); - const handleLetsGo = () => { history.push('/feed'); }; @@ -76,7 +70,7 @@ const HomePage: React.FC = () => { const Reviews = ( <div className={classes.reviews}> - {feedbacks.map(feedback => <ReviewCard feedback={feedback} />)} + {feedbacks.map((feedback: Feedback) => <ReviewCard feedback={feedback} />)} </div> ); @@ -92,7 +86,7 @@ const HomePage: React.FC = () => { 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 /> : ( + {isAuthenticated ? <ReviewForm /> : ( <> <p> You must be authorized to leave feedback.</p> <Button @@ -142,7 +136,7 @@ const HomePage: React.FC = () => { <Button variant="contained" color="primary" size="large" onClick={handleLetsGo}> {'let\'s go!'} </Button> - {!isAuthenticated() && ( + {!isAuthenticated && ( <Button variant="outlined" color="primary" @@ -199,5 +193,5 @@ const HomePage: React.FC = () => { ); }; -export default HomePage; +export default Home; diff --git a/src/pages/HomePage/ReviewForm.tsx b/src/containers/Home/ReviewForm.tsx index b626ce2..b626ce2 100644 --- a/src/pages/HomePage/ReviewForm.tsx +++ b/src/containers/Home/ReviewForm.tsx diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/containers/Login/Login.tsx index 335cbb1..bec0db5 100644 --- a/src/pages/LoginPage/LoginPage.tsx +++ b/src/containers/Login/Login.tsx @@ -35,7 +35,7 @@ const useStyles = makeStyles(theme => ({ } })); -const LoginPage: React.FC = () => { +const Login: React.FC = () => { const [error, setError] = useState<boolean>(false); const [remember, setRemember] = useState<boolean>(true); const classes = useStyles(); @@ -99,5 +99,5 @@ const LoginPage: React.FC = () => { ); }; -export default LoginPage; +export default Login; diff --git a/src/pages/NotificationsPage/NotificationsPage.tsx b/src/containers/Notifications/Notifications.tsx index 064fbd4..0648eb5 100644 --- a/src/pages/NotificationsPage/NotificationsPage.tsx +++ b/src/containers/Notifications/Notifications.tsx @@ -9,7 +9,7 @@ const useStyles = makeStyles(theme => ({ } })); -const NotificationsPage: React.FC = () => { +const Notifications: React.FC = () => { const classes = useStyles(); return ( @@ -19,5 +19,5 @@ const NotificationsPage: React.FC = () => { ); }; -export default NotificationsPage; +export default Notifications; diff --git a/src/containers/Page/Page.tsx b/src/containers/Page/Page.tsx new file mode 100644 index 0000000..643e6de --- /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 Profile = React.lazy(() => import('../Profile/Profile')); +const Feed = React.lazy(() => import('../Feed/Feed')); +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 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={isMobile ? 1 : 3} + anchorOrigin={{ + vertical: isMobile ? 'top' : 'bottom', + horizontal: 'right' + }} + > + <div className={classes.root}> + <Suspense fallback={<Loading />}> + <Switch> + <Route exact path="/" component={Home} /> + <Route exact path="/login" component={Login} /> + <Route exact path="/registration" component={Registration} /> + <Route exact path="/feed" component={Feed} /> + <Route exact path="/notifications" component={Notifications} /> + <Route path="/profile/:username" component={Profile} /> + </Switch> + </Suspense> + </div> + </SnackbarProvider> + ); +}; + + +export default Page; + diff --git a/src/pages/ProfilePage/Highlight.tsx b/src/containers/Profile/Highlight.tsx index ebc3f56..ebc3f56 100644 --- a/src/pages/ProfilePage/Highlight.tsx +++ b/src/containers/Profile/Highlight.tsx diff --git a/src/pages/ProfilePage/MoreMenu.tsx b/src/containers/Profile/MoreMenu.tsx index 1f41879..1f41879 100644 --- a/src/pages/ProfilePage/MoreMenu.tsx +++ b/src/containers/Profile/MoreMenu.tsx diff --git a/src/containers/Profile/Profile.tsx b/src/containers/Profile/Profile.tsx new file mode 100644 index 0000000..7e929fb --- /dev/null +++ b/src/containers/Profile/Profile.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useMemo } 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 PollsList from '../../components/PollsList/PollsList'; +import Loading from '../../components/Loading/Loading'; +import { useAuth } from '../../hooks/useAuth'; +import { useUser, useProfile } from '../../hooks/APIClient'; + + +const Profile: React.FC = () => { + const history = useHistory(); + const { username } = useParams(); + const { user } = useAuth(); + + const { data: userInfo, mutate: setUserInfo } = useUser(username); + const { data: polls, mutate: mutatePolls, isValidating } = useProfile(username); + + useEffect(() => { + if (!username) { + if (user) history.push(`/profile/${user.username}`); + else history.push('/login'); + } + }, [username, history, user]); + + + const totalVotes = useMemo(() => 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} + /> + { + isValidating && !polls + ? <Loading /> + : <PollsList polls={polls} mutate={mutatePolls} /> + } + </Container> + ); +}; + +export default Profile; diff --git a/src/pages/ProfilePage/ProfileInfo.tsx b/src/containers/Profile/ProfileInfo.tsx index e4ef66a..9eee4c1 100644 --- a/src/pages/ProfilePage/ProfileInfo.tsx +++ b/src/containers/Profile/ProfileInfo.tsx @@ -16,7 +16,6 @@ interface PropTypes { totalVotes: number; userInfo: User | undefined; setUserInfo: (userInfo: User) => void; - isLoading: boolean; } const useStyles = makeStyles(theme => ({ @@ -83,11 +82,11 @@ const useStyles = makeStyles(theme => ({ const ProfileInfo: React.FC<PropTypes> = ({ - savedPolls, totalVotes, setUserInfo, userInfo, isLoading + savedPolls, totalVotes, setUserInfo, userInfo }) => { const classes = useStyles(); const [input, setInput] = useState(false); - const { setUser } = useAuth(); + const { user } = useAuth(); const dateSince = new Date(userInfo?.createdAt || '').toLocaleDateString(); const handleClick = () => { @@ -95,19 +94,18 @@ const ProfileInfo: React.FC<PropTypes> = ({ }; const patchAvatar = (url: string) => { - const id = localStorage.getItem('userId'); + const id = user?._id; patch(`/users/${id}`, { avatarUrl: url }).then(res => { setUserInfo(res.data); - setUser(res.data); }); }; return ( <div className={classes.root}> { - isLoading + !userInfo ? <Skeleton animation="wave" variant="circle" width={150} height={150} className={classes.avatar} /> - : userInfo?._id === localStorage.getItem('userId') + : userInfo?._id === user?._id ? ( <div> <MoreMenu /> @@ -133,7 +131,7 @@ const ProfileInfo: React.FC<PropTypes> = ({ : <Avatar className={classes.avatar} src={userInfo?.avatarUrl} /> } { - isLoading + !userInfo ? <Skeleton animation="wave" variant="rect" width={100} height={20} className={classes.skeleton} /> : ( <Typography variant="h5" className={classes.name}> @@ -144,7 +142,7 @@ const ProfileInfo: React.FC<PropTypes> = ({ } <div className={classes.profileMenu}> { - isLoading + !userInfo ? ( <> <Skeleton animation="wave" variant="rect" width={170} height={20} className={classes.skeleton} /> diff --git a/src/pages/RegistrationPage/RegistrationPage.tsx b/src/containers/Registration/Registration.tsx index 18a9379..c681329 100644 --- a/src/pages/RegistrationPage/RegistrationPage.tsx +++ b/src/containers/Registration/Registration.tsx @@ -33,7 +33,7 @@ const useStyles = makeStyles(theme => ({ } })); -const RegistrationPage: React.FC = () => { +const Registration: React.FC = () => { const [error, setError] = useState<boolean>(false); const classes = useStyles(); const usernameRef = useRef<HTMLInputElement>(); @@ -93,4 +93,4 @@ const RegistrationPage: React.FC = () => { ); }; -export default RegistrationPage; +export default Registration; diff --git a/src/hooks/APIClient.ts b/src/hooks/APIClient.ts new file mode 100644 index 0000000..9563bd6 --- /dev/null +++ b/src/hooks/APIClient.ts @@ -0,0 +1,34 @@ +import useSWR, { responseInterface } from 'swr'; +import { User, Poll, Feedback } from 'which-types'; +import { get } from '../requests'; + + +interface Response<T> extends responseInterface<T, Error> { + data: T; +} + +const fetcher = (endpoint: string) => get(endpoint).then(response => response.data); + +const arrayOptions = { + initialData: [], + revalidateOnMount: true +}; + +export const useUser = (username: string | null): Response<User> => { + return useSWR( + username && `/users?username=${username}`, + (url: string) => get(url).then(response => response.data[0]) + ) as Response<User>; +}; + +export const useProfile = (id: string): Response<Poll[]> => { + return useSWR(id && `/profiles/${id}`, fetcher, arrayOptions) as Response<Poll[]>; +}; + +export const useFeed = (): Response<Poll[]> => { + return useSWR('/feed', fetcher, { ...arrayOptions, revalidateOnFocus: false }) as Response<Poll[]>; +}; + +export const useFeedback = (): Response<Feedback[]> => { + return useSWR('/feedback', fetcher, arrayOptions) as Response<Feedback[]>; +}; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 55e142c..2f03a33 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,67 +1,60 @@ import React, { - useState, useEffect, useContext, createContext + useEffect, useCallback, useMemo, useContext, createContext } from 'react'; import { User } from 'which-types'; -import { post, get } from '../requests'; +import { post } from '../requests'; +import { useUser } from './APIClient'; +import useLocalStorage from './useLocalStorage'; interface ContextType { - user: User | null, - setUser: (user: User) => void; + user: User | undefined, login: (username: string, password: string, remember?: boolean) => Promise<boolean>; logout: () => void; - isAuthenticated: () => boolean; + isAuthenticated: boolean; } const authContext = createContext<ContextType>({ - user: null, - setUser: () => {}, + user: undefined, login: async () => false, logout: () => {}, - isAuthenticated: () => false + isAuthenticated: false }); const useProvideAuth = () => { - const [user, setUser] = useState<User | null>(null); + const [remember, setRemember] = useLocalStorage('remember'); + const [username, setUsername] = useLocalStorage('username'); + const [token, setToken] = useLocalStorage('token'); + const { data: user } = useUser(username); - const login: ContextType['login'] = (username, password, remember = true) => { + const isAuthenticated = useMemo(() => Boolean(username), [username]); + + const logout = useCallback(() => { + setToken(null); + setUsername(null); + }, [setToken, setUsername]); + + useEffect(() => { + // If should not remember, logout + if (!remember) logout(); + }, [remember, logout]); + + + const login: ContextType['login'] = (name, password, shouldRemember = true) => { return post('/authentication', { strategy: 'local', - username, + username: name, password }).then(response => { - const me = response.data.user; - const token = response.data.accessToken; - setUser(me); - localStorage.setItem('userId', me._id); - localStorage.setItem('token', token); - if (!remember) localStorage.setItem('shouldClear', 'true'); + setToken(response.data.accessToken); + setUsername(name); + setRemember(shouldRemember ? 'true' : null); return true; }).catch(() => false); }; - const logout = () => { - setUser(null); - localStorage.removeItem('userId'); - localStorage.removeItem('token'); - }; - - const isAuthenticated = () => Boolean(user); - - useEffect(() => { - if (localStorage.getItem('shouldClear')) { - localStorage.clear(); - } - const userId = localStorage.getItem('userId'); - if (userId) { - get(`/users/${userId}`).then(response => { - setUser(response.data); - }); - } - }, []); - return { - user, setUser, login, logout, isAuthenticated + user, login, logout, token, isAuthenticated }; }; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..faf1411 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,16 @@ +import { useState, useCallback } from 'react'; + +type Value = string | null; +type Setter = (value: Value) => void; + +export default (key: string): [Value, Setter] => { + const [state, setState] = useState<Value>(() => localStorage.getItem(key) || null); + + const update: Setter = useCallback(value => { + if (value) localStorage.setItem(key, value); + else localStorage.removeItem(key); + setState(value); + }, [key]); + + return [state, update]; +}; diff --git a/src/index.tsx b/src/index.tsx index 64b1760..8eb2506 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,7 +8,7 @@ import 'typeface-roboto'; import Header from './components/Header/Header'; import ScrollTopArrow from './components/ScrollTopArrow/ScrollTopArrow'; -import Page from './pages/Page'; +import Page from './containers/Page/Page'; import { AuthProvider } from './hooks/useAuth'; diff --git a/src/pages/FeedPage/FeedPage.tsx b/src/pages/FeedPage/FeedPage.tsx deleted file mode 100644 index 0b7d44a..0000000 --- a/src/pages/FeedPage/FeedPage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -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(); - - useEffect(() => { - get('/feed').then(response => { - setPolls(response.data); - }); - }, []); - - const addPoll = (poll: Poll): void => { - polls.unshift(poll); - setPolls([]); - setPolls(polls); - }; - - return ( - <Container maxWidth="sm" disableGutters> - {isAuthenticated() && <PollSubmission addPoll={addPoll} />} - <Feed polls={polls} /> - </Container> - ); -}; - -export default FeedPage; - diff --git a/src/pages/Page.tsx b/src/pages/Page.tsx deleted file mode 100644 index 668b171..0000000 --- a/src/pages/Page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React 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 ProfilePage from './ProfilePage/ProfilePage'; -import FeedPage from './FeedPage/FeedPage'; -import LoginPage from './LoginPage/LoginPage'; -import RegistrationPage from './RegistrationPage/RegistrationPage'; -import HomePage from './HomePage/HomePage'; -import NotificationsPage from './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}> - <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> - </div> - </SnackbarProvider> - ); -}; - - -export default Page; - diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx deleted file mode 100644 index ae94b9f..0000000 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; -import { User, Poll } from 'which-types'; -import { Container } from '@material-ui/core'; - -import ProfileInfo from './ProfileInfo'; -import Feed from '../../components/Feed/Feed'; -import { get } from '../../requests'; -import { useAuth } from '../../hooks/useAuth'; - - -const ProfilePage: React.FC = () => { - const [userInfo, setUserInfo] = useState<User>(); - const [polls, setPolls] = useState<Poll[]>([]); - const [totalVotes, setTotalVotes] = useState<number>(0); - const [isInfoLoading, setIsInfoLoading] = useState(false); - const [isPollsLoading, setIsPollsLoading] = useState(false); - const history = useHistory(); - const { username } = useParams(); - const { user } = useAuth(); - - useEffect(() => { - setIsInfoLoading(true); - - const redirect = () => { - if (user) history.push(`/profile/${user.username}`); - else history.push('/login'); - }; - - if (username) { - get(`/users?username=${username}`).then(response => { - if (!response.data.length) redirect(); // TODO: handle this case - setUserInfo(response.data[0]); - setIsInfoLoading(false); - }).catch(() => redirect()); - } else redirect(); - }, [username, user, history]); - - - useEffect(() => { - if (userInfo?._id) { - setIsPollsLoading(true); - - get(`/profiles/${userInfo._id}`).then(response => { - setIsPollsLoading(false); - setPolls([]); - setPolls(response.data); - setTotalVotes(response.data.reduce( - (total: number, current: Poll) => { - const { left, right } = current.contents; - return total + left.votes + right.votes; - }, 0 - )); - }); - } - }, [userInfo]); - - return ( - <Container maxWidth="sm" disableGutters> - <ProfileInfo - userInfo={userInfo} - setUserInfo={setUserInfo} - savedPolls={polls.length} - totalVotes={totalVotes} - isLoading={isInfoLoading} - /> - {isPollsLoading ? <Feed polls={[]} /> : (polls.length > 0 && <Feed polls={polls} />)} - </Container> - ); -}; - -export default ProfilePage; diff --git a/src/requests.ts b/src/requests.ts index e1f82b4..8ec8e3d 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -14,7 +14,7 @@ requests.interceptors.request.use(config => { requests.interceptors.response.use(response => response, error => { if (error.message === 'Request failed with status code 401' && localStorage.getItem('token')) { - localStorage.setItem('shouldClear', 'true'); + localStorage.removeItem('remember'); window.location.reload(); } return Promise.reject(error); |