aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugene Sokolov <eug-vs@keemail.me>2020-08-10 13:51:11 +0300
committerGitHub <noreply@github.com>2020-08-10 13:51:11 +0300
commit823c82383424616bc7c2562e2a763321edb6050c (patch)
tree1d5220d68ab8ebb392c87038f2fc24cc72b28775
parent70d20b76f042a519e8e164279dfa31b5ce027d44 (diff)
parent78218c0f3427ad79de003ac59cffb99b08f0ae7d (diff)
downloadwhich-ui-823c82383424616bc7c2562e2a763321edb6050c.tar.gz
Merge pull request #74 from which-ecosystem/fetching
SWR feat. crazy refactor
-rw-r--r--package-lock.json15
-rw-r--r--package.json1
-rw-r--r--src/components/Header/Header.tsx4
-rw-r--r--src/components/Header/SearchBar.tsx4
-rw-r--r--src/components/Loading/Loading.tsx24
-rw-r--r--src/components/PollCard/PollCard.tsx20
-rw-r--r--src/components/PollsList/PollsList.tsx (renamed from src/components/Feed/Feed.tsx)43
-rw-r--r--src/components/ScrollTopArrow/ScrollTopArrow.tsx4
-rw-r--r--src/containers/Feed/Feed.tsx27
-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.tsx59
-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.tsx53
-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.ts34
-rw-r--r--src/hooks/useAuth.tsx69
-rw-r--r--src/hooks/useLocalStorage.ts16
-rw-r--r--src/index.tsx2
-rw-r--r--src/pages/FeedPage/FeedPage.tsx35
-rw-r--r--src/pages/Page.tsx56
-rw-r--r--src/pages/ProfilePage/ProfilePage.tsx72
-rw-r--r--src/requests.ts2
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);