From 236062c6c6278c4b433463fef9fa37eebf3fd760 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Sun, 9 Aug 2020 16:09:21 +0300 Subject: feat: lazy-load pages --- src/components/Feed/Feed.tsx | 20 ++------------------ src/components/Loading/Loading.tsx | 24 ++++++++++++++++++++++++ src/pages/Page.tsx | 33 ++++++++++++++++++--------------- 3 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 src/components/Loading/Loading.tsx diff --git a/src/components/Feed/Feed.tsx b/src/components/Feed/Feed.tsx index 03358da..9918a3d 100644 --- a/src/components/Feed/Feed.tsx +++ b/src/components/Feed/Feed.tsx @@ -1,9 +1,8 @@ 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'; +import Loading from '../Loading/Loading'; interface PropTypes { polls: Poll[]; @@ -15,17 +14,8 @@ interface RenderPropTypes { style: React.CSSProperties; } -const useStyles = makeStyles(theme => ({ - loader: { - width: '100%', - textAlign: 'center', - marginTop: theme.spacing(10) - } -})); const Feed: React.FC = ({ polls }) => { - const classes = useStyles(); - const RenderItem: React.FC = ({ index, style, key }) => { const poll = polls[index]; @@ -36,12 +26,6 @@ const Feed: React.FC = ({ polls }) => { ); }; - const loader = ( -
- -
- ); - const list = ( {({ @@ -74,7 +58,7 @@ const Feed: React.FC = ({ polls }) => { ); - return polls.length ? list : loader; + return polls.length ? list : ; }; export default Feed; diff --git a/src/components/Loading/Loading.tsx b/src/components/Loading/Loading.tsx new file mode 100644 index 0000000..30b8cda --- /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 = () => { + const classes = useStyles(); + + return ( +
+ +
+ ); +}; + +export default Loading; + diff --git a/src/pages/Page.tsx b/src/pages/Page.tsx index 668b171..49c941a 100644 --- a/src/pages/Page.tsx +++ b/src/pages/Page.tsx @@ -1,15 +1,16 @@ -import React from 'react'; +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'; -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 ProfilePage = React.lazy(() => import( './ProfilePage/ProfilePage')); +const FeedPage = React.lazy(() => import( './FeedPage/FeedPage')); +const LoginPage = React.lazy(() => import( './LoginPage/LoginPage')); +const RegistrationPage = React.lazy(() => import( './RegistrationPage/RegistrationPage')); +const HomePage = React.lazy(() => import( './HomePage/HomePage')); +const NotificationsPage = React.lazy(() => import( './NotificationsPage/NotificationsPage')); const useStyles = makeStyles(theme => ({ @@ -38,14 +39,16 @@ const Page: React.FC = () => { }} >
- - - - - - - - + }> + + + + + + + + +
); -- cgit v1.2.3 From 74d34ff5072d2582cf825121e901eed3401e63b9 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Sun, 9 Aug 2020 19:51:11 +0300 Subject: chore: install SWR --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) 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" }, -- cgit v1.2.3 From b6f9dbc3cbb21447fdeb22420b005da37de2af8e Mon Sep 17 00:00:00 2001 From: eug-vs Date: Sun, 9 Aug 2020 19:51:32 +0300 Subject: feat: use SWR in profile page --- src/hooks/APIClient.ts | 17 +++++++++++ src/pages/ProfilePage/ProfileInfo.tsx | 9 +++--- src/pages/ProfilePage/ProfilePage.tsx | 57 ++++++++++++----------------------- 3 files changed, 40 insertions(+), 43 deletions(-) create mode 100644 src/hooks/APIClient.ts diff --git a/src/hooks/APIClient.ts b/src/hooks/APIClient.ts new file mode 100644 index 0000000..fa23a21 --- /dev/null +++ b/src/hooks/APIClient.ts @@ -0,0 +1,17 @@ +import useSWR from 'swr'; +import { get } from '../requests'; + + +const fetcher = (endpoint: string) => get(endpoint).then(response => response.data); + + +export const useUser = (username: string) => { + return useSWR( + `/users?username=${username}`, + (url: string) => get(url).then(response => response.data[0]) + ); +}; + +export const useProfile = (id: string) => { + return useSWR(id && `/profiles/${id}`, fetcher, { initialData: [] }); +}; diff --git a/src/pages/ProfilePage/ProfileInfo.tsx b/src/pages/ProfilePage/ProfileInfo.tsx index e4ef66a..8b1447a 100644 --- a/src/pages/ProfilePage/ProfileInfo.tsx +++ b/src/pages/ProfilePage/ProfileInfo.tsx @@ -16,7 +16,6 @@ interface PropTypes { totalVotes: number; userInfo: User | undefined; setUserInfo: (userInfo: User) => void; - isLoading: boolean; } const useStyles = makeStyles(theme => ({ @@ -83,7 +82,7 @@ const useStyles = makeStyles(theme => ({ const ProfileInfo: React.FC = ({ - savedPolls, totalVotes, setUserInfo, userInfo, isLoading + savedPolls, totalVotes, setUserInfo, userInfo }) => { const classes = useStyles(); const [input, setInput] = useState(false); @@ -105,7 +104,7 @@ const ProfileInfo: React.FC = ({ return (
{ - isLoading + !userInfo ? : userInfo?._id === localStorage.getItem('userId') ? ( @@ -133,7 +132,7 @@ const ProfileInfo: React.FC = ({ : } { - isLoading + !userInfo ? : ( @@ -144,7 +143,7 @@ const ProfileInfo: React.FC = ({ }
{ - isLoading + !userInfo ? ( <> diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index ae94b9f..4769999 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -1,59 +1,41 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { useHistory, useParams } from 'react-router-dom'; -import { User, Poll } from 'which-types'; +import { 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'; +import { useUser, useProfile } from '../../hooks/APIClient'; const ProfilePage: React.FC = () => { - const [userInfo, setUserInfo] = useState(); - const [polls, setPolls] = useState([]); - const [totalVotes, setTotalVotes] = useState(0); - const [isInfoLoading, setIsInfoLoading] = useState(false); - const [isPollsLoading, setIsPollsLoading] = useState(false); const history = useHistory(); const { username } = useParams(); const { user } = useAuth(); + const { data: userInfo, mutate: setUserInfo } = useUser(username); + const { data: polls, mutate: fetchPolls } = useProfile(userInfo?._id); + useEffect(() => { - setIsInfoLoading(true); + fetchPolls(); + }, [userInfo, fetchPolls]) - const redirect = () => { + useEffect(() => { + if (!username) { if (user) history.push(`/profile/${user.username}`); else history.push('/login'); }; + }, [username, history, user]); - 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]); + const totalVotes = useCallback( + polls.reduce( + (total: number, current: Poll) => { + const { left, right } = current.contents; + return total + left.votes + right.votes; + }, 0 + ), [polls]); return ( @@ -62,9 +44,8 @@ const ProfilePage: React.FC = () => { setUserInfo={setUserInfo} savedPolls={polls.length} totalVotes={totalVotes} - isLoading={isInfoLoading} /> - {isPollsLoading ? : (polls.length > 0 && )} + ); }; -- cgit v1.2.3 From 992b12d3bab79e749bc25eb5a17981853ff3c216 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Sun, 9 Aug 2020 21:16:40 +0300 Subject: fix: re-render Feed correctly --- src/components/Feed/Feed.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Feed/Feed.tsx b/src/components/Feed/Feed.tsx index 9918a3d..b857cc0 100644 --- a/src/components/Feed/Feed.tsx +++ b/src/components/Feed/Feed.tsx @@ -20,7 +20,8 @@ const Feed: React.FC = ({ polls }) => { const RenderItem: React.FC = ({ index, style, key }) => { const poll = polls[index]; return ( -
+ // To re-render on list resize, add this info to key +
); -- cgit v1.2.3 From 359ec6a68ea92b3d1eecf020742157eb3be90b9f Mon Sep 17 00:00:00 2001 From: eug-vs Date: Sun, 9 Aug 2020 21:17:33 +0300 Subject: feat: add useFeed hook --- src/hooks/APIClient.ts | 10 +++++++++- src/pages/FeedPage/FeedPage.tsx | 18 +++++------------- src/pages/ProfilePage/ProfilePage.tsx | 6 +----- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/hooks/APIClient.ts b/src/hooks/APIClient.ts index fa23a21..ce11134 100644 --- a/src/hooks/APIClient.ts +++ b/src/hooks/APIClient.ts @@ -4,6 +4,10 @@ import { get } from '../requests'; const fetcher = (endpoint: string) => get(endpoint).then(response => response.data); +const arrayOptions = { + initialData: [], + revalidateOnMount: true +}; export const useUser = (username: string) => { return useSWR( @@ -13,5 +17,9 @@ export const useUser = (username: string) => { }; export const useProfile = (id: string) => { - return useSWR(id && `/profiles/${id}`, fetcher, { initialData: [] }); + return useSWR(id && `/profiles/${id}`, fetcher, arrayOptions); +}; + +export const useFeed = () => { + return useSWR(`/feed`, fetcher, arrayOptions); }; diff --git a/src/pages/FeedPage/FeedPage.tsx b/src/pages/FeedPage/FeedPage.tsx index 0b7d44a..8e7fb55 100644 --- a/src/pages/FeedPage/FeedPage.tsx +++ b/src/pages/FeedPage/FeedPage.tsx @@ -1,32 +1,24 @@ -import React, { useState, useEffect } from 'react'; +import React 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'; +import { useFeed } from '../../hooks/APIClient'; const FeedPage: React.FC = () => { - const [polls, setPolls] = useState([]); + const { data, mutate } = useFeed(); const { isAuthenticated } = useAuth(); - useEffect(() => { - get('/feed').then(response => { - setPolls(response.data); - }); - }, []); - const addPoll = (poll: Poll): void => { - polls.unshift(poll); - setPolls([]); - setPolls(polls); + mutate([poll, ...data], true); }; return ( {isAuthenticated() && } - + ); }; diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 4769999..293b766 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -15,11 +15,7 @@ const ProfilePage: React.FC = () => { const { user } = useAuth(); const { data: userInfo, mutate: setUserInfo } = useUser(username); - const { data: polls, mutate: fetchPolls } = useProfile(userInfo?._id); - - useEffect(() => { - fetchPolls(); - }, [userInfo, fetchPolls]) + const { data: polls } = useProfile(userInfo?._id); useEffect(() => { if (!username) { -- cgit v1.2.3 From fd6e663a1bcc43cfc49bda99ccbfab380489324b Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 00:02:24 +0300 Subject: feat!: add useLocalStorage hook --- src/components/Feed/Feed.tsx | 3 +- src/components/PollCard/PollCard.tsx | 2 +- src/hooks/APIClient.ts | 6 ++-- src/hooks/useAuth.tsx | 65 ++++++++++++++++------------------- src/hooks/useLocalStorage.ts | 16 +++++++++ src/pages/FeedPage/FeedPage.tsx | 2 +- src/pages/HomePage/HomePage.tsx | 4 +-- src/pages/Page.tsx | 12 +++---- src/pages/ProfilePage/ProfileInfo.tsx | 7 ++-- src/pages/ProfilePage/ProfilePage.tsx | 6 ++-- src/requests.ts | 2 +- 11 files changed, 67 insertions(+), 58 deletions(-) create mode 100644 src/hooks/useLocalStorage.ts diff --git a/src/components/Feed/Feed.tsx b/src/components/Feed/Feed.tsx index b857cc0..5805e35 100644 --- a/src/components/Feed/Feed.tsx +++ b/src/components/Feed/Feed.tsx @@ -16,12 +16,11 @@ interface RenderPropTypes { const Feed: React.FC = ({ polls }) => { - const RenderItem: React.FC = ({ index, style, key }) => { const poll = polls[index]; return ( // To re-render on list resize, add this info to key -
+
); diff --git a/src/components/PollCard/PollCard.tsx b/src/components/PollCard/PollCard.tsx index 98ae001..2378945 100644 --- a/src/components/PollCard/PollCard.tsx +++ b/src/components/PollCard/PollCard.tsx @@ -58,7 +58,7 @@ const PollCard: React.FC = ({ 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' }); diff --git a/src/hooks/APIClient.ts b/src/hooks/APIClient.ts index ce11134..7f7d170 100644 --- a/src/hooks/APIClient.ts +++ b/src/hooks/APIClient.ts @@ -9,9 +9,9 @@ const arrayOptions = { revalidateOnMount: true }; -export const useUser = (username: string) => { +export const useUser = (username: string | null) => { return useSWR( - `/users?username=${username}`, + username && `/users?username=${username}`, (url: string) => get(url).then(response => response.data[0]) ); }; @@ -21,5 +21,5 @@ export const useProfile = (id: string) => { }; export const useFeed = () => { - return useSWR(`/feed`, fetcher, arrayOptions); + return useSWR('/feed', fetcher, arrayOptions); }; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 55e142c..ed1e428 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; login: (username: string, password: string, remember?: boolean) => Promise; logout: () => void; - isAuthenticated: () => boolean; + isAuthenticated: boolean; } const authContext = createContext({ user: null, - setUser: () => {}, login: async () => false, logout: () => {}, - isAuthenticated: () => false + isAuthenticated: false }); const useProvideAuth = () => { - const [user, setUser] = useState(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(() => 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/pages/FeedPage/FeedPage.tsx b/src/pages/FeedPage/FeedPage.tsx index 8e7fb55..da0fb2a 100644 --- a/src/pages/FeedPage/FeedPage.tsx +++ b/src/pages/FeedPage/FeedPage.tsx @@ -17,7 +17,7 @@ const FeedPage: React.FC = () => { return ( - {isAuthenticated() && } + {isAuthenticated && } ); diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index 17e377a..b1dc506 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -92,7 +92,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).

- {isAuthenticated() ? : ( + {isAuthenticated ? : ( <>

You must be authorized to leave feedback.

- {!isAuthenticated() && ( + {!isAuthenticated && ( + + + ); +}; + +export default PollSubmission; diff --git a/src/containers/FeedPage/PollSubmissionImage.tsx b/src/containers/FeedPage/PollSubmissionImage.tsx new file mode 100644 index 0000000..8835989 --- /dev/null +++ b/src/containers/FeedPage/PollSubmissionImage.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import CloudUploadIcon from '@material-ui/icons/CloudUpload'; +import { CardActionArea, CardMedia, Typography } from '@material-ui/core'; +import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; + +import UploadImage from '../../components/UploadImage/UploadImage'; + +interface PropTypes { + url: string; + setUrl: (url: string) => void; +} + +const useStyles = makeStyles({ + root: { + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + alignItems: 'center' + }, + clearIcon: { + opacity: '.5', + fontSize: 50 + }, + media: { + height: '100%', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }, + text: { + textAlign: 'center' + } +}); + + +const PollSubmissionImage: React.FC = ({ url, setUrl }) => { + const classes = useStyles(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isMediaHover, setIsMediaHover] = useState(false); + + const handleClick = (): void => { + if (!isModalOpen) { + if (url) setUrl(''); + else setIsModalOpen(!isModalOpen); + } + }; + + const handleMouseEnter = (): void => { + setIsMediaHover(true); + }; + + const handleMouseLeave = (): void => { + setIsMediaHover(false); + }; + + + const Upload = ( + <> + + Upload an image + + ); + + const Media = ( + + {isMediaHover && } + + ); + + return ( + <> + + {url ? Media : Upload} + + + + ); +}; + +export default PollSubmissionImage; diff --git a/src/containers/FeedPage/types.ts b/src/containers/FeedPage/types.ts new file mode 100644 index 0000000..24ace4e --- /dev/null +++ b/src/containers/FeedPage/types.ts @@ -0,0 +1,7 @@ +export interface ImageData { + url: string; +} +export interface Contents { + left: ImageData; + right: ImageData; +} diff --git a/src/containers/HomePage/HomePage.tsx b/src/containers/HomePage/HomePage.tsx new file mode 100644 index 0000000..b1dc506 --- /dev/null +++ b/src/containers/HomePage/HomePage.tsx @@ -0,0 +1,203 @@ +import React, { useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + Typography, + Divider, + Grid, + Button, + Link, + useMediaQuery +} from '@material-ui/core/'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; +import TrendingUpIcon from '@material-ui/icons/TrendingUp'; +import { Rating } from '@material-ui/lab'; +import { Feedback } from 'which-types'; + +import { useAuth } from '../../hooks/useAuth'; +import { get } from '../../requests'; +import ReviewCard from '../../components/ReviewCard/ReviewCard'; +import ReviewForm from './ReviewForm'; + +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) + }, + reviews: { + [theme.breakpoints.up('md')]: { + padding: theme.spacing(0, 10) + } + } +})); + +const HomePage: React.FC = () => { + const [feedbacks, setFeedbacks] = useState([]); + const classes = useStyles(); + const history = useHistory(); + const { isAuthenticated, user } = useAuth(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + 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 = () => { + history.push('/feed'); + }; + + const handleSignUp = () => { + history.push('/registration'); + }; + + const GithubLink = GitHub; + const TypescriptLink = Typescript; + const ReactLink = React; + const FeathersLink = Feathers; + const MUILink = Material-UI; + const EmailLink = eug-vs@keemail.me; + + const Reviews = ( +
+ {feedbacks.map(feedback => )} +
+ ); + + const FeedbackSection = feedbacks.findIndex((feedback: Feedback) => feedback.author._id === user?._id) >= 0 ? ( +

+ You have already left feedback for this version. + If you have more to say, please open GitHub issue or contact us directly via email: {EmailLink}. + Alternatively, you can just wait for another application patch to come out. +

+ ) : ( + <> +

+ 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). +

+ {isAuthenticated ? : ( + <> +

You must be authorized to leave feedback.

+ + + )} + + ); + + return ( +
+ + + + + logo + + + {rating !== 0 && } + + + {rating !== 0 && ( + + User score: {rating.toFixed(1)} + + )} + + + {isMobile || Reviews} + + + + + Which one to choose? + + +

+ 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! +

+

Share your minor everyday uncertainties with the whole world and see what others think!

+ + {!isAuthenticated && ( + + )} +
+
+ + About the project + + +

+ 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)! +

+

+ We encourage any developer to check it out. Feel free to open issues and create Pull Requests! +

+

+ 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. +

+ +
+
+ + Leave feedback + + + {FeedbackSection} + + + {isMobile && ( + + {Reviews} + + )} +
+
+
+
+ ); +}; + +export default HomePage; + diff --git a/src/containers/HomePage/ReviewForm.tsx b/src/containers/HomePage/ReviewForm.tsx new file mode 100644 index 0000000..b626ce2 --- /dev/null +++ b/src/containers/HomePage/ReviewForm.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { makeStyles } from '@material-ui/core/styles'; +import { TextField, Button } from '@material-ui/core'; +import { Rating } from '@material-ui/lab'; +import { useSnackbar } from 'notistack'; + +import { post } from '../../requests'; + +const version = 'v1.0.0'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + flexDirection: 'column' + }, + textField: { + margin: theme.spacing(2, 0) + } +})); + +const ReviewForm: React.FC = () => { + const [contents, setContents] = useState(''); + const [score, setScore] = useState(0); + const classes = useStyles(); + const history = useHistory(); + const { enqueueSnackbar } = useSnackbar(); + + const handleSubmit = (): void => { + if (score) { + post('/feedback', { contents, score, version }).then(() => { + enqueueSnackbar('Your feedback has been submitted!', { + variant: 'success' + }); + history.push('/feed'); + }); + } + }; + + const handleChange = (event: React.ChangeEvent): void => { + setContents(event.target?.value || ''); + }; + + const handleChangeRating = (event: React.ChangeEvent>, newScore: number | null): void => { + setScore(newScore || 0); + }; + + return ( +
+ + +
+ +
+
+ ); +}; + +export default ReviewForm; diff --git a/src/containers/LoginPage/LoginPage.tsx b/src/containers/LoginPage/LoginPage.tsx new file mode 100644 index 0000000..335cbb1 --- /dev/null +++ b/src/containers/LoginPage/LoginPage.tsx @@ -0,0 +1,103 @@ +import React, { useState, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; +import { makeStyles } from '@material-ui/core/styles'; +import { + TextField, + Button, + FormControlLabel, + Switch +} from '@material-ui/core'; +import { useAuth } from '../../hooks/useAuth'; + +const useStyles = makeStyles(theme => ({ + root: { + '& > *': { + margin: theme.spacing(1), + width: theme.spacing(35) + }, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center' + }, + formHeader: { + textAlign: 'center', + fontSize: 25 + }, + formTransfer: { + display: 'flex', + justifyContent: 'center' + }, + transferButton: { + marginLeft: 10, + color: 'green', + cursor: 'pointer' + } +})); + +const LoginPage: React.FC = () => { + const [error, setError] = useState(false); + const [remember, setRemember] = useState(true); + const classes = useStyles(); + const nameRef = useRef(); + const passwordRef = useRef(); + const { login } = useAuth(); + const history = useHistory(); + + const handleCheck = () => { + setRemember(!remember); + }; + + const handleSubmit = async () => { + const name = nameRef.current?.value?.toLowerCase(); + const password = passwordRef.current?.value; + if (name && password) { + login(name, password, remember).then(success => { + if (success) history.push(`/profile/${name}`); + else setError(true); + }); + } + }; + + const handleRegistration = () => { + history.push('/registration'); + }; + + return ( + <> +
Sign In
+
+ + + } + label="Remember me" + /> + + +
+
{'Don\'t have an account?'}
+ + Sign up + +
+ + ); +}; + +export default LoginPage; + diff --git a/src/containers/NotificationsPage/NotificationsPage.tsx b/src/containers/NotificationsPage/NotificationsPage.tsx new file mode 100644 index 0000000..064fbd4 --- /dev/null +++ b/src/containers/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(25), + textAlign: 'center' + } +})); + +const NotificationsPage: React.FC = () => { + const classes = useStyles(); + + return ( + + Sorry, this page is being constructed yet. + + ); +}; + +export default NotificationsPage; + diff --git a/src/containers/Page/Page.tsx b/src/containers/Page/Page.tsx new file mode 100644 index 0000000..8a39636 --- /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 ProfilePage = React.lazy(() => import('../ProfilePage/ProfilePage')); +const FeedPage = React.lazy(() => import('../FeedPage/FeedPage')); +const LoginPage = React.lazy(() => import('../LoginPage/LoginPage')); +const RegistrationPage = React.lazy(() => import('../RegistrationPage/RegistrationPage')); +const HomePage = React.lazy(() => import('../HomePage/HomePage')); +const NotificationsPage = React.lazy(() => import('../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 ( + +
+ }> + + + + + + + + + +
+
+ ); +}; + + +export default Page; + diff --git a/src/containers/ProfilePage/Highlight.tsx b/src/containers/ProfilePage/Highlight.tsx new file mode 100644 index 0000000..ebc3f56 --- /dev/null +++ b/src/containers/ProfilePage/Highlight.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +interface PropTypes { + text: string; + value: string | number; +} + +const useStyles = makeStyles({ + root: { + position: 'relative' + }, + menuButton: { + width: 200, + height: 50, + textAlign: 'center' + }, + menuNumber: { + fontWeight: 800, + color: 'black' + }, + menuText: { + color: 'darkgray' + } +}); + + +const Highlight: React.FC = ({ text, value }) => { + const classes = useStyles(); + + return ( +
+
{value}
+
{text}
+
+ ); +}; + +export default Highlight; diff --git a/src/containers/ProfilePage/MoreMenu.tsx b/src/containers/ProfilePage/MoreMenu.tsx new file mode 100644 index 0000000..1f41879 --- /dev/null +++ b/src/containers/ProfilePage/MoreMenu.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import IconButton from '@material-ui/core/IconButton'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; +import { makeStyles } from '@material-ui/core'; +import { useAuth } from '../../hooks/useAuth'; + +const ITEM_HEIGHT = 48; + +const useStyles = makeStyles({ + moreMenu: { + position: 'absolute', + right: 10, + zIndex: 100 + } +}); + +const MoreMenu: React.FC = () => { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + const { logout } = useAuth(); + const history = useHistory(); + + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleLogout = () => { + logout(); + history.push('/login'); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( +
+
+ + + + + Log out + +
+
+ ); +}; + +export default MoreMenu; diff --git a/src/containers/ProfilePage/ProfileInfo.tsx b/src/containers/ProfilePage/ProfileInfo.tsx new file mode 100644 index 0000000..9eee4c1 --- /dev/null +++ b/src/containers/ProfilePage/ProfileInfo.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { Avatar, Badge, Typography } from '@material-ui/core/'; +import { makeStyles } from '@material-ui/core/styles'; +import { User } from 'which-types'; +import CameraAltIcon from '@material-ui/icons/CameraAlt'; +import VerifiedIcon from '@material-ui/icons/CheckCircleOutline'; +import Skeleton from '@material-ui/lab/Skeleton'; +import MoreMenu from './MoreMenu'; +import Highlight from './Highlight'; +import UploadImage from '../../components/UploadImage/UploadImage'; +import { patch } from '../../requests'; +import { useAuth } from '../../hooks/useAuth'; + +interface PropTypes { + savedPolls: number; + totalVotes: number; + userInfo: User | undefined; + setUserInfo: (userInfo: User) => void; +} + +const useStyles = makeStyles(theme => ({ + root: { + position: 'relative' + }, + avatar: { + width: 150, + height: 150, + margin: '0 auto' + }, + name: { + margin: theme.spacing(1, 0), + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + verified: { + marginLeft: theme.spacing(0.5), + width: theme.spacing(3), + height: theme.spacing(3) + }, + profileMenu: { + display: 'flex', + width: '100%', + height: 50, + margin: '50px 0', + borderBottom: '1px solid lightgray' + }, + menuButton: { + width: 200, + height: 50, + textAlign: 'center' + }, + badge: { + width: theme.spacing(5), + height: theme.spacing(5), + borderRadius: '50%', + cursor: 'pointer', + background: '#d3d3d3', + display: 'flex', + alignItems: 'center', + '& svg': { + margin: '0 auto' + } + }, + avatarContainer: { + position: 'relative', + textAlign: 'center' + }, + menuNumber: { + fontWeight: 800, + color: 'black' + }, + menuText: { + color: 'darkgray' + }, + skeleton: { + margin: '10px auto', + borderRadius: 2 + } + +})); + + +const ProfileInfo: React.FC = ({ + savedPolls, totalVotes, setUserInfo, userInfo +}) => { + const classes = useStyles(); + const [input, setInput] = useState(false); + const { user } = useAuth(); + const dateSince = new Date(userInfo?.createdAt || '').toLocaleDateString(); + + const handleClick = () => { + setInput(!input); + }; + + const patchAvatar = (url: string) => { + const id = user?._id; + patch(`/users/${id}`, { avatarUrl: url }).then(res => { + setUserInfo(res.data); + }); + }; + + return ( +
+ { + !userInfo + ? + : userInfo?._id === user?._id + ? ( +
+ +
+ + +
+ )} + > + + +
+ +
+ ) + : + } + { + !userInfo + ? + : ( + + {userInfo?.username} + {userInfo?.verified && } + + ) + } +
+ { + !userInfo + ? ( + <> + + + + + ) + : ( + <> + + + + + ) + } +
+
+ ); +}; + +export default ProfileInfo; diff --git a/src/containers/ProfilePage/ProfilePage.tsx b/src/containers/ProfilePage/ProfilePage.tsx new file mode 100644 index 0000000..db27d25 --- /dev/null +++ b/src/containers/ProfilePage/ProfilePage.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useCallback } 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 Feed from '../../components/Feed/Feed'; +import Loading from '../../components/Loading/Loading'; +import { useAuth } from '../../hooks/useAuth'; +import { useUser, useProfile } from '../../hooks/APIClient'; + + +const ProfilePage: React.FC = () => { + const history = useHistory(); + const { username } = useParams(); + const { user } = useAuth(); + + const { data: userInfo, mutate: setUserInfo } = useUser(username); + const { data: polls, isValidating } = useProfile(userInfo?._id); + + useEffect(() => { + if (!username) { + if (user) history.push(`/profile/${user.username}`); + else history.push('/login'); + } + }, [username, history, user]); + + + const totalVotes = useCallback( + polls.reduce( + (total: number, current: Poll) => { + const { left, right } = current.contents; + return total + left.votes + right.votes; + }, 0 + ), + [polls] + ); + + return ( + + + {!polls.length && isValidating + ? + : + } + + ); +}; + +export default ProfilePage; diff --git a/src/containers/RegistrationPage/RegistrationPage.tsx b/src/containers/RegistrationPage/RegistrationPage.tsx new file mode 100644 index 0000000..18a9379 --- /dev/null +++ b/src/containers/RegistrationPage/RegistrationPage.tsx @@ -0,0 +1,96 @@ +import React, { useState, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; +import { makeStyles } from '@material-ui/core/styles'; +import TextField from '@material-ui/core/TextField'; +import Button from '@material-ui/core/Button'; +import { post } from '../../requests'; +import { useAuth } from '../../hooks/useAuth'; + + +const useStyles = makeStyles(theme => ({ + root: { + '& > *': { + margin: theme.spacing(1), + width: theme.spacing(35) + }, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center' + }, + formHeader: { + textAlign: 'center', + fontSize: 25 + }, + formTransfer: { + display: 'flex', + justifyContent: 'center' + }, + transferButton: { + marginLeft: 10, + color: 'green', + cursor: 'pointer' + } +})); + +const RegistrationPage: React.FC = () => { + const [error, setError] = useState(false); + const classes = useStyles(); + const usernameRef = useRef(); + const emailRef = useRef(); + const passwordRef = useRef(); + const { login } = useAuth(); + const history = useHistory(); + + const handleSubmit = () => { + const username = usernameRef.current?.value?.toLowerCase(); + const password = passwordRef.current?.value; + const email = emailRef.current?.value; + if (username && password) { + post('/users', { username, password, email }) + .then(() => login(username, password)) + .then(() => history.push(`/profile/${username}`)); + } else setError(true); + }; + + const handleLogin = () => { + history.push('/login'); + }; + + return ( + <> +
Sign Up
+
+ + + + + +
+
Already have an account?
+ + Log in + +
+ + ); +}; + +export default RegistrationPage; 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 da0fb2a..0000000 --- a/src/pages/FeedPage/FeedPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { Poll } from 'which-types'; -import { Container } from '@material-ui/core/'; - -import Feed from '../../components/Feed/Feed'; -import PollSubmission from './PollSubmission'; -import { useAuth } from '../../hooks/useAuth'; -import { useFeed } from '../../hooks/APIClient'; - -const FeedPage: React.FC = () => { - const { data, mutate } = useFeed(); - const { isAuthenticated } = useAuth(); - - const addPoll = (poll: Poll): void => { - mutate([poll, ...data], true); - }; - - return ( - - {isAuthenticated && } - - - ); -}; - -export default FeedPage; - diff --git a/src/pages/FeedPage/PollSubmission.tsx b/src/pages/FeedPage/PollSubmission.tsx deleted file mode 100644 index 347eecc..0000000 --- a/src/pages/FeedPage/PollSubmission.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useState } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import Collapse from '@material-ui/core/Collapse'; -import { - Button, - Card, - ClickAwayListener, - 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'; -import { Contents } from './types'; -import { useAuth } from '../../hooks/useAuth'; - -interface PropTypes{ - addPoll: (poll: Poll) => void; -} - -const useStyles = makeStyles(theme => ({ - root: { - marginBottom: theme.spacing(4) - }, - images: { - height: theme.spacing(50), - display: 'flex' - } -})); - -const emptyContents: Contents = { - left: { url: '' }, - right: { url: '' } -}; - -const PollSubmission: React.FC = ({ addPoll }) => { - const classes = useStyles(); - const [expanded, setExpanded] = useState(false); - const [contents, setContents] = useState(emptyContents); - const { enqueueSnackbar } = useSnackbar(); - const { user } = useAuth(); - - const readyToSubmit = contents.left.url && contents.right.url; - - const setUrl = (which: Which) => (url: string): void => { - setContents({ ...contents, [which]: { url } }); - }; - - const handleClickAway = () => { - setExpanded(false); - }; - - const handleClick = () => { - if (expanded && readyToSubmit) { - post('/polls/', { contents }).then(response => { - addPoll(response.data); - enqueueSnackbar('Your poll has been successfully created!', { - variant: 'success' - }); - }); - setContents({ ...emptyContents }); - } - setExpanded(!expanded); - }; - - return ( - - - - {user && } - -
- - -
-
- -
-
- ); -}; - -export default PollSubmission; diff --git a/src/pages/FeedPage/PollSubmissionImage.tsx b/src/pages/FeedPage/PollSubmissionImage.tsx deleted file mode 100644 index 8835989..0000000 --- a/src/pages/FeedPage/PollSubmissionImage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import CloudUploadIcon from '@material-ui/icons/CloudUpload'; -import { CardActionArea, CardMedia, Typography } from '@material-ui/core'; -import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; - -import UploadImage from '../../components/UploadImage/UploadImage'; - -interface PropTypes { - url: string; - setUrl: (url: string) => void; -} - -const useStyles = makeStyles({ - root: { - display: 'flex', - justifyContent: 'center', - flexDirection: 'column', - alignItems: 'center' - }, - clearIcon: { - opacity: '.5', - fontSize: 50 - }, - media: { - height: '100%', - width: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center' - }, - text: { - textAlign: 'center' - } -}); - - -const PollSubmissionImage: React.FC = ({ url, setUrl }) => { - const classes = useStyles(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isMediaHover, setIsMediaHover] = useState(false); - - const handleClick = (): void => { - if (!isModalOpen) { - if (url) setUrl(''); - else setIsModalOpen(!isModalOpen); - } - }; - - const handleMouseEnter = (): void => { - setIsMediaHover(true); - }; - - const handleMouseLeave = (): void => { - setIsMediaHover(false); - }; - - - const Upload = ( - <> - - Upload an image - - ); - - const Media = ( - - {isMediaHover && } - - ); - - return ( - <> - - {url ? Media : Upload} - - - - ); -}; - -export default PollSubmissionImage; diff --git a/src/pages/FeedPage/types.ts b/src/pages/FeedPage/types.ts deleted file mode 100644 index 24ace4e..0000000 --- a/src/pages/FeedPage/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ImageData { - url: string; -} -export interface Contents { - left: ImageData; - right: ImageData; -} diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx deleted file mode 100644 index b1dc506..0000000 --- a/src/pages/HomePage/HomePage.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import { - Typography, - Divider, - Grid, - Button, - Link, - useMediaQuery -} from '@material-ui/core/'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import TrendingUpIcon from '@material-ui/icons/TrendingUp'; -import { Rating } from '@material-ui/lab'; -import { Feedback } from 'which-types'; - -import { useAuth } from '../../hooks/useAuth'; -import { get } from '../../requests'; -import ReviewCard from '../../components/ReviewCard/ReviewCard'; -import ReviewForm from './ReviewForm'; - -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) - }, - reviews: { - [theme.breakpoints.up('md')]: { - padding: theme.spacing(0, 10) - } - } -})); - -const HomePage: React.FC = () => { - const [feedbacks, setFeedbacks] = useState([]); - const classes = useStyles(); - const history = useHistory(); - const { isAuthenticated, user } = useAuth(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - - 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 = () => { - history.push('/feed'); - }; - - const handleSignUp = () => { - history.push('/registration'); - }; - - const GithubLink = GitHub; - const TypescriptLink = Typescript; - const ReactLink = React; - const FeathersLink = Feathers; - const MUILink = Material-UI; - const EmailLink = eug-vs@keemail.me; - - const Reviews = ( -
- {feedbacks.map(feedback => )} -
- ); - - const FeedbackSection = feedbacks.findIndex((feedback: Feedback) => feedback.author._id === user?._id) >= 0 ? ( -

- You have already left feedback for this version. - If you have more to say, please open GitHub issue or contact us directly via email: {EmailLink}. - Alternatively, you can just wait for another application patch to come out. -

- ) : ( - <> -

- 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). -

- {isAuthenticated ? : ( - <> -

You must be authorized to leave feedback.

- - - )} - - ); - - return ( -
- - - - - logo - - - {rating !== 0 && } - - - {rating !== 0 && ( - - User score: {rating.toFixed(1)} - - )} - - - {isMobile || Reviews} - - - - - Which one to choose? - - -

- 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! -

-

Share your minor everyday uncertainties with the whole world and see what others think!

- - {!isAuthenticated && ( - - )} -
-
- - About the project - - -

- 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)! -

-

- We encourage any developer to check it out. Feel free to open issues and create Pull Requests! -

-

- 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. -

- -
-
- - Leave feedback - - - {FeedbackSection} - - - {isMobile && ( - - {Reviews} - - )} -
-
-
-
- ); -}; - -export default HomePage; - diff --git a/src/pages/HomePage/ReviewForm.tsx b/src/pages/HomePage/ReviewForm.tsx deleted file mode 100644 index b626ce2..0000000 --- a/src/pages/HomePage/ReviewForm.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import { TextField, Button } from '@material-ui/core'; -import { Rating } from '@material-ui/lab'; -import { useSnackbar } from 'notistack'; - -import { post } from '../../requests'; - -const version = 'v1.0.0'; - -const useStyles = makeStyles(theme => ({ - root: { - display: 'flex', - flexDirection: 'column' - }, - textField: { - margin: theme.spacing(2, 0) - } -})); - -const ReviewForm: React.FC = () => { - const [contents, setContents] = useState(''); - const [score, setScore] = useState(0); - const classes = useStyles(); - const history = useHistory(); - const { enqueueSnackbar } = useSnackbar(); - - const handleSubmit = (): void => { - if (score) { - post('/feedback', { contents, score, version }).then(() => { - enqueueSnackbar('Your feedback has been submitted!', { - variant: 'success' - }); - history.push('/feed'); - }); - } - }; - - const handleChange = (event: React.ChangeEvent): void => { - setContents(event.target?.value || ''); - }; - - const handleChangeRating = (event: React.ChangeEvent>, newScore: number | null): void => { - setScore(newScore || 0); - }; - - return ( -
- - -
- -
-
- ); -}; - -export default ReviewForm; diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx deleted file mode 100644 index 335cbb1..0000000 --- a/src/pages/LoginPage/LoginPage.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { useHistory } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import { - TextField, - Button, - FormControlLabel, - Switch -} from '@material-ui/core'; -import { useAuth } from '../../hooks/useAuth'; - -const useStyles = makeStyles(theme => ({ - root: { - '& > *': { - margin: theme.spacing(1), - width: theme.spacing(35) - }, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - textAlign: 'center' - }, - formHeader: { - textAlign: 'center', - fontSize: 25 - }, - formTransfer: { - display: 'flex', - justifyContent: 'center' - }, - transferButton: { - marginLeft: 10, - color: 'green', - cursor: 'pointer' - } -})); - -const LoginPage: React.FC = () => { - const [error, setError] = useState(false); - const [remember, setRemember] = useState(true); - const classes = useStyles(); - const nameRef = useRef(); - const passwordRef = useRef(); - const { login } = useAuth(); - const history = useHistory(); - - const handleCheck = () => { - setRemember(!remember); - }; - - const handleSubmit = async () => { - const name = nameRef.current?.value?.toLowerCase(); - const password = passwordRef.current?.value; - if (name && password) { - login(name, password, remember).then(success => { - if (success) history.push(`/profile/${name}`); - else setError(true); - }); - } - }; - - const handleRegistration = () => { - history.push('/registration'); - }; - - return ( - <> -
Sign In
-
- - - } - label="Remember me" - /> - - -
-
{'Don\'t have an account?'}
- - Sign up - -
- - ); -}; - -export default LoginPage; - diff --git a/src/pages/NotificationsPage/NotificationsPage.tsx b/src/pages/NotificationsPage/NotificationsPage.tsx deleted file mode 100644 index 064fbd4..0000000 --- a/src/pages/NotificationsPage/NotificationsPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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(25), - textAlign: 'center' - } -})); - -const NotificationsPage: React.FC = () => { - const classes = useStyles(); - - return ( - - Sorry, this page is being constructed yet. - - ); -}; - -export default NotificationsPage; - diff --git a/src/pages/Page.tsx b/src/pages/Page.tsx deleted file mode 100644 index a77a98e..0000000 --- a/src/pages/Page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -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 ProfilePage = React.lazy(() => import('./ProfilePage/ProfilePage')); -const FeedPage = React.lazy(() => import('./FeedPage/FeedPage')); -const LoginPage = React.lazy(() => import('./LoginPage/LoginPage')); -const RegistrationPage = React.lazy(() => import('./RegistrationPage/RegistrationPage')); -const HomePage = React.lazy(() => import('./HomePage/HomePage')); -const NotificationsPage = React.lazy(() => import('./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 ( - -
- }> - - - - - - - - - -
-
- ); -}; - - -export default Page; - diff --git a/src/pages/ProfilePage/Highlight.tsx b/src/pages/ProfilePage/Highlight.tsx deleted file mode 100644 index ebc3f56..0000000 --- a/src/pages/ProfilePage/Highlight.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; - -interface PropTypes { - text: string; - value: string | number; -} - -const useStyles = makeStyles({ - root: { - position: 'relative' - }, - menuButton: { - width: 200, - height: 50, - textAlign: 'center' - }, - menuNumber: { - fontWeight: 800, - color: 'black' - }, - menuText: { - color: 'darkgray' - } -}); - - -const Highlight: React.FC = ({ text, value }) => { - const classes = useStyles(); - - return ( -
-
{value}
-
{text}
-
- ); -}; - -export default Highlight; diff --git a/src/pages/ProfilePage/MoreMenu.tsx b/src/pages/ProfilePage/MoreMenu.tsx deleted file mode 100644 index 1f41879..0000000 --- a/src/pages/ProfilePage/MoreMenu.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { useHistory } from 'react-router-dom'; -import IconButton from '@material-ui/core/IconButton'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; -import { makeStyles } from '@material-ui/core'; -import { useAuth } from '../../hooks/useAuth'; - -const ITEM_HEIGHT = 48; - -const useStyles = makeStyles({ - moreMenu: { - position: 'absolute', - right: 10, - zIndex: 100 - } -}); - -const MoreMenu: React.FC = () => { - const classes = useStyles(); - const [anchorEl, setAnchorEl] = React.useState(null); - const { logout } = useAuth(); - const history = useHistory(); - - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleLogout = () => { - logout(); - history.push('/login'); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( -
-
- - - - - Log out - -
-
- ); -}; - -export default MoreMenu; diff --git a/src/pages/ProfilePage/ProfileInfo.tsx b/src/pages/ProfilePage/ProfileInfo.tsx deleted file mode 100644 index 9eee4c1..0000000 --- a/src/pages/ProfilePage/ProfileInfo.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from 'react'; -import { Avatar, Badge, Typography } from '@material-ui/core/'; -import { makeStyles } from '@material-ui/core/styles'; -import { User } from 'which-types'; -import CameraAltIcon from '@material-ui/icons/CameraAlt'; -import VerifiedIcon from '@material-ui/icons/CheckCircleOutline'; -import Skeleton from '@material-ui/lab/Skeleton'; -import MoreMenu from './MoreMenu'; -import Highlight from './Highlight'; -import UploadImage from '../../components/UploadImage/UploadImage'; -import { patch } from '../../requests'; -import { useAuth } from '../../hooks/useAuth'; - -interface PropTypes { - savedPolls: number; - totalVotes: number; - userInfo: User | undefined; - setUserInfo: (userInfo: User) => void; -} - -const useStyles = makeStyles(theme => ({ - root: { - position: 'relative' - }, - avatar: { - width: 150, - height: 150, - margin: '0 auto' - }, - name: { - margin: theme.spacing(1, 0), - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - }, - verified: { - marginLeft: theme.spacing(0.5), - width: theme.spacing(3), - height: theme.spacing(3) - }, - profileMenu: { - display: 'flex', - width: '100%', - height: 50, - margin: '50px 0', - borderBottom: '1px solid lightgray' - }, - menuButton: { - width: 200, - height: 50, - textAlign: 'center' - }, - badge: { - width: theme.spacing(5), - height: theme.spacing(5), - borderRadius: '50%', - cursor: 'pointer', - background: '#d3d3d3', - display: 'flex', - alignItems: 'center', - '& svg': { - margin: '0 auto' - } - }, - avatarContainer: { - position: 'relative', - textAlign: 'center' - }, - menuNumber: { - fontWeight: 800, - color: 'black' - }, - menuText: { - color: 'darkgray' - }, - skeleton: { - margin: '10px auto', - borderRadius: 2 - } - -})); - - -const ProfileInfo: React.FC = ({ - savedPolls, totalVotes, setUserInfo, userInfo -}) => { - const classes = useStyles(); - const [input, setInput] = useState(false); - const { user } = useAuth(); - const dateSince = new Date(userInfo?.createdAt || '').toLocaleDateString(); - - const handleClick = () => { - setInput(!input); - }; - - const patchAvatar = (url: string) => { - const id = user?._id; - patch(`/users/${id}`, { avatarUrl: url }).then(res => { - setUserInfo(res.data); - }); - }; - - return ( -
- { - !userInfo - ? - : userInfo?._id === user?._id - ? ( -
- -
- - -
- )} - > - - -
- -
- ) - : - } - { - !userInfo - ? - : ( - - {userInfo?.username} - {userInfo?.verified && } - - ) - } -
- { - !userInfo - ? ( - <> - - - - - ) - : ( - <> - - - - - ) - } -
-
- ); -}; - -export default ProfileInfo; diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx deleted file mode 100644 index db27d25..0000000 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useEffect, useCallback } 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 Feed from '../../components/Feed/Feed'; -import Loading from '../../components/Loading/Loading'; -import { useAuth } from '../../hooks/useAuth'; -import { useUser, useProfile } from '../../hooks/APIClient'; - - -const ProfilePage: React.FC = () => { - const history = useHistory(); - const { username } = useParams(); - const { user } = useAuth(); - - const { data: userInfo, mutate: setUserInfo } = useUser(username); - const { data: polls, isValidating } = useProfile(userInfo?._id); - - useEffect(() => { - if (!username) { - if (user) history.push(`/profile/${user.username}`); - else history.push('/login'); - } - }, [username, history, user]); - - - const totalVotes = useCallback( - polls.reduce( - (total: number, current: Poll) => { - const { left, right } = current.contents; - return total + left.votes + right.votes; - }, 0 - ), - [polls] - ); - - return ( - - - {!polls.length && isValidating - ? - : - } - - ); -}; - -export default ProfilePage; diff --git a/src/pages/RegistrationPage/RegistrationPage.tsx b/src/pages/RegistrationPage/RegistrationPage.tsx deleted file mode 100644 index 18a9379..0000000 --- a/src/pages/RegistrationPage/RegistrationPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { useHistory } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import TextField from '@material-ui/core/TextField'; -import Button from '@material-ui/core/Button'; -import { post } from '../../requests'; -import { useAuth } from '../../hooks/useAuth'; - - -const useStyles = makeStyles(theme => ({ - root: { - '& > *': { - margin: theme.spacing(1), - width: theme.spacing(35) - }, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - textAlign: 'center' - }, - formHeader: { - textAlign: 'center', - fontSize: 25 - }, - formTransfer: { - display: 'flex', - justifyContent: 'center' - }, - transferButton: { - marginLeft: 10, - color: 'green', - cursor: 'pointer' - } -})); - -const RegistrationPage: React.FC = () => { - const [error, setError] = useState(false); - const classes = useStyles(); - const usernameRef = useRef(); - const emailRef = useRef(); - const passwordRef = useRef(); - const { login } = useAuth(); - const history = useHistory(); - - const handleSubmit = () => { - const username = usernameRef.current?.value?.toLowerCase(); - const password = passwordRef.current?.value; - const email = emailRef.current?.value; - if (username && password) { - post('/users', { username, password, email }) - .then(() => login(username, password)) - .then(() => history.push(`/profile/${username}`)); - } else setError(true); - }; - - const handleLogin = () => { - history.push('/login'); - }; - - return ( - <> -
Sign Up
-
- - - - - -
-
Already have an account?
- - Log in - -
- - ); -}; - -export default RegistrationPage; -- cgit v1.2.3 From 08e9e3abb9830b6c46f46aacb6a40ba32c4c665f Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 00:49:49 +0300 Subject: feat: memoize static components --- src/components/Header/Header.tsx | 4 ++-- src/components/Header/SearchBar.tsx | 4 ++-- src/components/Loading/Loading.tsx | 4 ++-- src/components/ScrollTopArrow/ScrollTopArrow.tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) 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([]); const [query, setQuery] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState(query); @@ -104,7 +104,7 @@ const SearchBar: React.FC = () => { {results.length > 0 && SearchResults}
); -}; +}); export default SearchBar; diff --git a/src/components/Loading/Loading.tsx b/src/components/Loading/Loading.tsx index 30b8cda..34d436b 100644 --- a/src/components/Loading/Loading.tsx +++ b/src/components/Loading/Loading.tsx @@ -10,7 +10,7 @@ const useStyles = makeStyles(theme => ({ } })); -const Loading: React.FC = () => { +const Loading: React.FC = React.memo(() => { const classes = useStyles(); return ( @@ -18,7 +18,7 @@ const Loading: React.FC = () => {
); -}; +}); export default Loading; 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 = () => { } ); -}; +}); export default ScrollTopArrow; -- cgit v1.2.3 From cab8de5c6b246e1aa1376fa2b8666f09b44b6469 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 11:12:06 +0300 Subject: refator: Feed -> PollList --- src/components/Feed/Feed.tsx | 63 ------------------------- src/components/PollCard/PollCard.tsx | 18 +++---- src/components/PollsList/PollsList.tsx | 75 ++++++++++++++++++++++++++++++ src/containers/FeedPage/FeedPage.tsx | 8 ++-- src/containers/ProfilePage/ProfilePage.tsx | 9 ++-- 5 files changed, 94 insertions(+), 79 deletions(-) delete mode 100644 src/components/Feed/Feed.tsx create mode 100644 src/components/PollsList/PollsList.tsx diff --git a/src/components/Feed/Feed.tsx b/src/components/Feed/Feed.tsx deleted file mode 100644 index bf3c5b7..0000000 --- a/src/components/Feed/Feed.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { Poll } from 'which-types'; -import { WindowScroller, AutoSizer, List } from 'react-virtualized'; -import PollCard from '../PollCard/PollCard'; - - -interface PropTypes { - polls: Poll[]; -} - -interface RenderPropTypes { - index: number; - key: string; - style: React.CSSProperties; -} - - -const Feed: React.FC = ({ polls }) => { - const RenderItem: React.FC = ({ index, style, key }) => { - const poll = polls[index]; - return ( - // To re-render on list resize, add this info to key -
- -
- ); - }; - - return ( - - {({ - height, - isScrolling, - registerChild, - onChildScroll, - scrollTop - }) => ( - - {({ width }) => ( -
- -
- )} -
- )} -
- ); -}; - -export default Feed; - diff --git a/src/components/PollCard/PollCard.tsx b/src/components/PollCard/PollCard.tsx index 2378945..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 = ({ initialPoll }) => { - const [poll, setPoll] = useState(initialPoll); +const PollCard: React.FC = ({ poll, setPoll }) => { const classes = useStyles(); const { author, contents: { left, right }, vote } = poll; const { enqueueSnackbar } = useSnackbar(); @@ -68,15 +68,17 @@ const PollCard: React.FC = ({ 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/PollsList/PollsList.tsx b/src/components/PollsList/PollsList.tsx new file mode 100644 index 0000000..0fd8fa4 --- /dev/null +++ b/src/components/PollsList/PollsList.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Poll } from 'which-types'; +import { WindowScroller, AutoSizer, List } from 'react-virtualized'; +import PollCard from '../PollCard/PollCard'; + + +interface PropTypes { + polls: Poll[]; + mutate: (polls: Poll[], refetch: boolean) => void; +} + +interface RenderPropTypes { + index: number; + key: string; + style: React.CSSProperties; +} + + +const PollsList: React.FC = ({ polls, mutate }) => { + + const RenderItem: React.FC = ({ index, style, key }) => { + const poll = polls[index]; + + const setPoll = (poll: Poll) => { + const newPolls = [...polls]; + newPolls[index] = poll; + + // Force-update list-size so everything re-renders + mutate([], false); + mutate(newPolls, false); + }; + + return ( + // To re-render on list resize, add this info to key +
+ +
+ ); + }; + + return ( + + {({ + height, + isScrolling, + registerChild, + onChildScroll, + scrollTop + }) => ( + + {({ width }) => ( +
+ +
+ )} +
+ )} +
+ ); +}; + +export default PollsList; + diff --git a/src/containers/FeedPage/FeedPage.tsx b/src/containers/FeedPage/FeedPage.tsx index da0fb2a..7445c25 100644 --- a/src/containers/FeedPage/FeedPage.tsx +++ b/src/containers/FeedPage/FeedPage.tsx @@ -2,23 +2,23 @@ import React from 'react'; import { Poll } from 'which-types'; import { Container } from '@material-ui/core/'; -import Feed from '../../components/Feed/Feed'; +import PollsList from '../../components/PollsList/PollsList'; import PollSubmission from './PollSubmission'; import { useAuth } from '../../hooks/useAuth'; import { useFeed } from '../../hooks/APIClient'; const FeedPage: React.FC = () => { - const { data, mutate } = useFeed(); + const { data: polls, mutate } = useFeed(); const { isAuthenticated } = useAuth(); const addPoll = (poll: Poll): void => { - mutate([poll, ...data], true); + mutate([poll, ...polls], true); }; return ( {isAuthenticated && } - + ); }; diff --git a/src/containers/ProfilePage/ProfilePage.tsx b/src/containers/ProfilePage/ProfilePage.tsx index db27d25..7a1d729 100644 --- a/src/containers/ProfilePage/ProfilePage.tsx +++ b/src/containers/ProfilePage/ProfilePage.tsx @@ -4,7 +4,7 @@ import { Poll } from 'which-types'; import { Container } from '@material-ui/core'; import ProfileInfo from './ProfileInfo'; -import Feed from '../../components/Feed/Feed'; +import PollsList from '../../components/PollsList/PollsList'; import Loading from '../../components/Loading/Loading'; import { useAuth } from '../../hooks/useAuth'; import { useUser, useProfile } from '../../hooks/APIClient'; @@ -16,7 +16,7 @@ const ProfilePage: React.FC = () => { const { user } = useAuth(); const { data: userInfo, mutate: setUserInfo } = useUser(username); - const { data: polls, isValidating } = useProfile(userInfo?._id); + const { data: polls, mutate: mutatePolls, isValidating } = useProfile(userInfo?._id); useEffect(() => { if (!username) { @@ -44,9 +44,10 @@ const ProfilePage: React.FC = () => { savedPolls={polls.length} totalVotes={totalVotes} /> - {!polls.length && isValidating + { + isValidating && !polls ? - : + : } ); -- cgit v1.2.3 From f97989967ee0b88a8c64f226a4b28a79eeef5fd2 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 11:19:53 +0300 Subject: refactor: remove "Page" from container names --- src/containers/Feed/Feed.tsx | 27 +++ src/containers/Feed/PollSubmission.tsx | 92 ++++++++++ src/containers/Feed/PollSubmissionImage.tsx | 87 +++++++++ src/containers/Feed/types.ts | 7 + src/containers/FeedPage/FeedPage.tsx | 27 --- src/containers/FeedPage/PollSubmission.tsx | 92 ---------- src/containers/FeedPage/PollSubmissionImage.tsx | 87 --------- src/containers/FeedPage/types.ts | 7 - src/containers/Home/Home.tsx | 203 +++++++++++++++++++++ src/containers/Home/ReviewForm.tsx | 74 ++++++++ src/containers/HomePage/HomePage.tsx | 203 --------------------- src/containers/HomePage/ReviewForm.tsx | 74 -------- src/containers/Login/Login.tsx | 103 +++++++++++ src/containers/LoginPage/LoginPage.tsx | 103 ----------- src/containers/Notifications/Notifications.tsx | 23 +++ .../NotificationsPage/NotificationsPage.tsx | 23 --- src/containers/Page/Page.tsx | 24 +-- src/containers/Profile/Highlight.tsx | 39 ++++ src/containers/Profile/MoreMenu.tsx | 72 ++++++++ src/containers/Profile/Profile.tsx | 56 ++++++ src/containers/Profile/ProfileInfo.tsx | 166 +++++++++++++++++ src/containers/ProfilePage/Highlight.tsx | 39 ---- src/containers/ProfilePage/MoreMenu.tsx | 72 -------- src/containers/ProfilePage/ProfileInfo.tsx | 166 ----------------- src/containers/ProfilePage/ProfilePage.tsx | 56 ------ src/containers/Registration/Registration.tsx | 96 ++++++++++ .../RegistrationPage/RegistrationPage.tsx | 96 ---------- 27 files changed, 1057 insertions(+), 1057 deletions(-) create mode 100644 src/containers/Feed/Feed.tsx create mode 100644 src/containers/Feed/PollSubmission.tsx create mode 100644 src/containers/Feed/PollSubmissionImage.tsx create mode 100644 src/containers/Feed/types.ts delete mode 100644 src/containers/FeedPage/FeedPage.tsx delete mode 100644 src/containers/FeedPage/PollSubmission.tsx delete mode 100644 src/containers/FeedPage/PollSubmissionImage.tsx delete mode 100644 src/containers/FeedPage/types.ts create mode 100644 src/containers/Home/Home.tsx create mode 100644 src/containers/Home/ReviewForm.tsx delete mode 100644 src/containers/HomePage/HomePage.tsx delete mode 100644 src/containers/HomePage/ReviewForm.tsx create mode 100644 src/containers/Login/Login.tsx delete mode 100644 src/containers/LoginPage/LoginPage.tsx create mode 100644 src/containers/Notifications/Notifications.tsx delete mode 100644 src/containers/NotificationsPage/NotificationsPage.tsx create mode 100644 src/containers/Profile/Highlight.tsx create mode 100644 src/containers/Profile/MoreMenu.tsx create mode 100644 src/containers/Profile/Profile.tsx create mode 100644 src/containers/Profile/ProfileInfo.tsx delete mode 100644 src/containers/ProfilePage/Highlight.tsx delete mode 100644 src/containers/ProfilePage/MoreMenu.tsx delete mode 100644 src/containers/ProfilePage/ProfileInfo.tsx delete mode 100644 src/containers/ProfilePage/ProfilePage.tsx create mode 100644 src/containers/Registration/Registration.tsx delete mode 100644 src/containers/RegistrationPage/RegistrationPage.tsx 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 ( + + {isAuthenticated && } + + + ); +}; + +export default Feed; + diff --git a/src/containers/Feed/PollSubmission.tsx b/src/containers/Feed/PollSubmission.tsx new file mode 100644 index 0000000..347eecc --- /dev/null +++ b/src/containers/Feed/PollSubmission.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Collapse from '@material-ui/core/Collapse'; +import { + Button, + Card, + ClickAwayListener, + 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'; +import { Contents } from './types'; +import { useAuth } from '../../hooks/useAuth'; + +interface PropTypes{ + addPoll: (poll: Poll) => void; +} + +const useStyles = makeStyles(theme => ({ + root: { + marginBottom: theme.spacing(4) + }, + images: { + height: theme.spacing(50), + display: 'flex' + } +})); + +const emptyContents: Contents = { + left: { url: '' }, + right: { url: '' } +}; + +const PollSubmission: React.FC = ({ addPoll }) => { + const classes = useStyles(); + const [expanded, setExpanded] = useState(false); + const [contents, setContents] = useState(emptyContents); + const { enqueueSnackbar } = useSnackbar(); + const { user } = useAuth(); + + const readyToSubmit = contents.left.url && contents.right.url; + + const setUrl = (which: Which) => (url: string): void => { + setContents({ ...contents, [which]: { url } }); + }; + + const handleClickAway = () => { + setExpanded(false); + }; + + const handleClick = () => { + if (expanded && readyToSubmit) { + post('/polls/', { contents }).then(response => { + addPoll(response.data); + enqueueSnackbar('Your poll has been successfully created!', { + variant: 'success' + }); + }); + setContents({ ...emptyContents }); + } + setExpanded(!expanded); + }; + + return ( + + + + {user && } + +
+ + +
+
+ +
+
+ ); +}; + +export default PollSubmission; diff --git a/src/containers/Feed/PollSubmissionImage.tsx b/src/containers/Feed/PollSubmissionImage.tsx new file mode 100644 index 0000000..8835989 --- /dev/null +++ b/src/containers/Feed/PollSubmissionImage.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import CloudUploadIcon from '@material-ui/icons/CloudUpload'; +import { CardActionArea, CardMedia, Typography } from '@material-ui/core'; +import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; + +import UploadImage from '../../components/UploadImage/UploadImage'; + +interface PropTypes { + url: string; + setUrl: (url: string) => void; +} + +const useStyles = makeStyles({ + root: { + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + alignItems: 'center' + }, + clearIcon: { + opacity: '.5', + fontSize: 50 + }, + media: { + height: '100%', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }, + text: { + textAlign: 'center' + } +}); + + +const PollSubmissionImage: React.FC = ({ url, setUrl }) => { + const classes = useStyles(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isMediaHover, setIsMediaHover] = useState(false); + + const handleClick = (): void => { + if (!isModalOpen) { + if (url) setUrl(''); + else setIsModalOpen(!isModalOpen); + } + }; + + const handleMouseEnter = (): void => { + setIsMediaHover(true); + }; + + const handleMouseLeave = (): void => { + setIsMediaHover(false); + }; + + + const Upload = ( + <> + + Upload an image + + ); + + const Media = ( + + {isMediaHover && } + + ); + + return ( + <> + + {url ? Media : Upload} + + + + ); +}; + +export default PollSubmissionImage; diff --git a/src/containers/Feed/types.ts b/src/containers/Feed/types.ts new file mode 100644 index 0000000..24ace4e --- /dev/null +++ b/src/containers/Feed/types.ts @@ -0,0 +1,7 @@ +export interface ImageData { + url: string; +} +export interface Contents { + left: ImageData; + right: ImageData; +} diff --git a/src/containers/FeedPage/FeedPage.tsx b/src/containers/FeedPage/FeedPage.tsx deleted file mode 100644 index 7445c25..0000000 --- a/src/containers/FeedPage/FeedPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 FeedPage: React.FC = () => { - const { data: polls, mutate } = useFeed(); - const { isAuthenticated } = useAuth(); - - const addPoll = (poll: Poll): void => { - mutate([poll, ...polls], true); - }; - - return ( - - {isAuthenticated && } - - - ); -}; - -export default FeedPage; - diff --git a/src/containers/FeedPage/PollSubmission.tsx b/src/containers/FeedPage/PollSubmission.tsx deleted file mode 100644 index 347eecc..0000000 --- a/src/containers/FeedPage/PollSubmission.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useState } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import Collapse from '@material-ui/core/Collapse'; -import { - Button, - Card, - ClickAwayListener, - 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'; -import { Contents } from './types'; -import { useAuth } from '../../hooks/useAuth'; - -interface PropTypes{ - addPoll: (poll: Poll) => void; -} - -const useStyles = makeStyles(theme => ({ - root: { - marginBottom: theme.spacing(4) - }, - images: { - height: theme.spacing(50), - display: 'flex' - } -})); - -const emptyContents: Contents = { - left: { url: '' }, - right: { url: '' } -}; - -const PollSubmission: React.FC = ({ addPoll }) => { - const classes = useStyles(); - const [expanded, setExpanded] = useState(false); - const [contents, setContents] = useState(emptyContents); - const { enqueueSnackbar } = useSnackbar(); - const { user } = useAuth(); - - const readyToSubmit = contents.left.url && contents.right.url; - - const setUrl = (which: Which) => (url: string): void => { - setContents({ ...contents, [which]: { url } }); - }; - - const handleClickAway = () => { - setExpanded(false); - }; - - const handleClick = () => { - if (expanded && readyToSubmit) { - post('/polls/', { contents }).then(response => { - addPoll(response.data); - enqueueSnackbar('Your poll has been successfully created!', { - variant: 'success' - }); - }); - setContents({ ...emptyContents }); - } - setExpanded(!expanded); - }; - - return ( - - - - {user && } - -
- - -
-
- -
-
- ); -}; - -export default PollSubmission; diff --git a/src/containers/FeedPage/PollSubmissionImage.tsx b/src/containers/FeedPage/PollSubmissionImage.tsx deleted file mode 100644 index 8835989..0000000 --- a/src/containers/FeedPage/PollSubmissionImage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import CloudUploadIcon from '@material-ui/icons/CloudUpload'; -import { CardActionArea, CardMedia, Typography } from '@material-ui/core'; -import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; - -import UploadImage from '../../components/UploadImage/UploadImage'; - -interface PropTypes { - url: string; - setUrl: (url: string) => void; -} - -const useStyles = makeStyles({ - root: { - display: 'flex', - justifyContent: 'center', - flexDirection: 'column', - alignItems: 'center' - }, - clearIcon: { - opacity: '.5', - fontSize: 50 - }, - media: { - height: '100%', - width: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center' - }, - text: { - textAlign: 'center' - } -}); - - -const PollSubmissionImage: React.FC = ({ url, setUrl }) => { - const classes = useStyles(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isMediaHover, setIsMediaHover] = useState(false); - - const handleClick = (): void => { - if (!isModalOpen) { - if (url) setUrl(''); - else setIsModalOpen(!isModalOpen); - } - }; - - const handleMouseEnter = (): void => { - setIsMediaHover(true); - }; - - const handleMouseLeave = (): void => { - setIsMediaHover(false); - }; - - - const Upload = ( - <> - - Upload an image - - ); - - const Media = ( - - {isMediaHover && } - - ); - - return ( - <> - - {url ? Media : Upload} - - - - ); -}; - -export default PollSubmissionImage; diff --git a/src/containers/FeedPage/types.ts b/src/containers/FeedPage/types.ts deleted file mode 100644 index 24ace4e..0000000 --- a/src/containers/FeedPage/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ImageData { - url: string; -} -export interface Contents { - left: ImageData; - right: ImageData; -} diff --git a/src/containers/Home/Home.tsx b/src/containers/Home/Home.tsx new file mode 100644 index 0000000..73fa479 --- /dev/null +++ b/src/containers/Home/Home.tsx @@ -0,0 +1,203 @@ +import React, { useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + Typography, + Divider, + Grid, + Button, + Link, + useMediaQuery +} from '@material-ui/core/'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; +import TrendingUpIcon from '@material-ui/icons/TrendingUp'; +import { Rating } from '@material-ui/lab'; +import { Feedback } from 'which-types'; + +import { useAuth } from '../../hooks/useAuth'; +import { get } from '../../requests'; +import ReviewCard from '../../components/ReviewCard/ReviewCard'; +import ReviewForm from './ReviewForm'; + +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) + }, + reviews: { + [theme.breakpoints.up('md')]: { + padding: theme.spacing(0, 10) + } + } +})); + +const Home: React.FC = () => { + const [feedbacks, setFeedbacks] = useState([]); + const classes = useStyles(); + const history = useHistory(); + const { isAuthenticated, user } = useAuth(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + 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 = () => { + history.push('/feed'); + }; + + const handleSignUp = () => { + history.push('/registration'); + }; + + const GithubLink = GitHub; + const TypescriptLink = Typescript; + const ReactLink = React; + const FeathersLink = Feathers; + const MUILink = Material-UI; + const EmailLink = eug-vs@keemail.me; + + const Reviews = ( +
+ {feedbacks.map(feedback => )} +
+ ); + + const FeedbackSection = feedbacks.findIndex((feedback: Feedback) => feedback.author._id === user?._id) >= 0 ? ( +

+ You have already left feedback for this version. + If you have more to say, please open GitHub issue or contact us directly via email: {EmailLink}. + Alternatively, you can just wait for another application patch to come out. +

+ ) : ( + <> +

+ 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). +

+ {isAuthenticated ? : ( + <> +

You must be authorized to leave feedback.

+ + + )} + + ); + + return ( +
+ + + + + logo + + + {rating !== 0 && } + + + {rating !== 0 && ( + + User score: {rating.toFixed(1)} + + )} + + + {isMobile || Reviews} + + + + + Which one to choose? + + +

+ 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! +

+

Share your minor everyday uncertainties with the whole world and see what others think!

+ + {!isAuthenticated && ( + + )} +
+
+ + About the project + + +

+ 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)! +

+

+ We encourage any developer to check it out. Feel free to open issues and create Pull Requests! +

+

+ 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. +

+ +
+
+ + Leave feedback + + + {FeedbackSection} + + + {isMobile && ( + + {Reviews} + + )} +
+
+
+
+ ); +}; + +export default Home; + diff --git a/src/containers/Home/ReviewForm.tsx b/src/containers/Home/ReviewForm.tsx new file mode 100644 index 0000000..b626ce2 --- /dev/null +++ b/src/containers/Home/ReviewForm.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { makeStyles } from '@material-ui/core/styles'; +import { TextField, Button } from '@material-ui/core'; +import { Rating } from '@material-ui/lab'; +import { useSnackbar } from 'notistack'; + +import { post } from '../../requests'; + +const version = 'v1.0.0'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + flexDirection: 'column' + }, + textField: { + margin: theme.spacing(2, 0) + } +})); + +const ReviewForm: React.FC = () => { + const [contents, setContents] = useState(''); + const [score, setScore] = useState(0); + const classes = useStyles(); + const history = useHistory(); + const { enqueueSnackbar } = useSnackbar(); + + const handleSubmit = (): void => { + if (score) { + post('/feedback', { contents, score, version }).then(() => { + enqueueSnackbar('Your feedback has been submitted!', { + variant: 'success' + }); + history.push('/feed'); + }); + } + }; + + const handleChange = (event: React.ChangeEvent): void => { + setContents(event.target?.value || ''); + }; + + const handleChangeRating = (event: React.ChangeEvent>, newScore: number | null): void => { + setScore(newScore || 0); + }; + + return ( +
+ + +
+ +
+
+ ); +}; + +export default ReviewForm; diff --git a/src/containers/HomePage/HomePage.tsx b/src/containers/HomePage/HomePage.tsx deleted file mode 100644 index b1dc506..0000000 --- a/src/containers/HomePage/HomePage.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import { - Typography, - Divider, - Grid, - Button, - Link, - useMediaQuery -} from '@material-ui/core/'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import TrendingUpIcon from '@material-ui/icons/TrendingUp'; -import { Rating } from '@material-ui/lab'; -import { Feedback } from 'which-types'; - -import { useAuth } from '../../hooks/useAuth'; -import { get } from '../../requests'; -import ReviewCard from '../../components/ReviewCard/ReviewCard'; -import ReviewForm from './ReviewForm'; - -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) - }, - reviews: { - [theme.breakpoints.up('md')]: { - padding: theme.spacing(0, 10) - } - } -})); - -const HomePage: React.FC = () => { - const [feedbacks, setFeedbacks] = useState([]); - const classes = useStyles(); - const history = useHistory(); - const { isAuthenticated, user } = useAuth(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - - 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 = () => { - history.push('/feed'); - }; - - const handleSignUp = () => { - history.push('/registration'); - }; - - const GithubLink = GitHub; - const TypescriptLink = Typescript; - const ReactLink = React; - const FeathersLink = Feathers; - const MUILink = Material-UI; - const EmailLink = eug-vs@keemail.me; - - const Reviews = ( -
- {feedbacks.map(feedback => )} -
- ); - - const FeedbackSection = feedbacks.findIndex((feedback: Feedback) => feedback.author._id === user?._id) >= 0 ? ( -

- You have already left feedback for this version. - If you have more to say, please open GitHub issue or contact us directly via email: {EmailLink}. - Alternatively, you can just wait for another application patch to come out. -

- ) : ( - <> -

- 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). -

- {isAuthenticated ? : ( - <> -

You must be authorized to leave feedback.

- - - )} - - ); - - return ( -
- - - - - logo - - - {rating !== 0 && } - - - {rating !== 0 && ( - - User score: {rating.toFixed(1)} - - )} - - - {isMobile || Reviews} - - - - - Which one to choose? - - -

- 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! -

-

Share your minor everyday uncertainties with the whole world and see what others think!

- - {!isAuthenticated && ( - - )} -
-
- - About the project - - -

- 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)! -

-

- We encourage any developer to check it out. Feel free to open issues and create Pull Requests! -

-

- 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. -

- -
-
- - Leave feedback - - - {FeedbackSection} - - - {isMobile && ( - - {Reviews} - - )} -
-
-
-
- ); -}; - -export default HomePage; - diff --git a/src/containers/HomePage/ReviewForm.tsx b/src/containers/HomePage/ReviewForm.tsx deleted file mode 100644 index b626ce2..0000000 --- a/src/containers/HomePage/ReviewForm.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import { TextField, Button } from '@material-ui/core'; -import { Rating } from '@material-ui/lab'; -import { useSnackbar } from 'notistack'; - -import { post } from '../../requests'; - -const version = 'v1.0.0'; - -const useStyles = makeStyles(theme => ({ - root: { - display: 'flex', - flexDirection: 'column' - }, - textField: { - margin: theme.spacing(2, 0) - } -})); - -const ReviewForm: React.FC = () => { - const [contents, setContents] = useState(''); - const [score, setScore] = useState(0); - const classes = useStyles(); - const history = useHistory(); - const { enqueueSnackbar } = useSnackbar(); - - const handleSubmit = (): void => { - if (score) { - post('/feedback', { contents, score, version }).then(() => { - enqueueSnackbar('Your feedback has been submitted!', { - variant: 'success' - }); - history.push('/feed'); - }); - } - }; - - const handleChange = (event: React.ChangeEvent): void => { - setContents(event.target?.value || ''); - }; - - const handleChangeRating = (event: React.ChangeEvent>, newScore: number | null): void => { - setScore(newScore || 0); - }; - - return ( -
- - -
- -
-
- ); -}; - -export default ReviewForm; diff --git a/src/containers/Login/Login.tsx b/src/containers/Login/Login.tsx new file mode 100644 index 0000000..bec0db5 --- /dev/null +++ b/src/containers/Login/Login.tsx @@ -0,0 +1,103 @@ +import React, { useState, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; +import { makeStyles } from '@material-ui/core/styles'; +import { + TextField, + Button, + FormControlLabel, + Switch +} from '@material-ui/core'; +import { useAuth } from '../../hooks/useAuth'; + +const useStyles = makeStyles(theme => ({ + root: { + '& > *': { + margin: theme.spacing(1), + width: theme.spacing(35) + }, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center' + }, + formHeader: { + textAlign: 'center', + fontSize: 25 + }, + formTransfer: { + display: 'flex', + justifyContent: 'center' + }, + transferButton: { + marginLeft: 10, + color: 'green', + cursor: 'pointer' + } +})); + +const Login: React.FC = () => { + const [error, setError] = useState(false); + const [remember, setRemember] = useState(true); + const classes = useStyles(); + const nameRef = useRef(); + const passwordRef = useRef(); + const { login } = useAuth(); + const history = useHistory(); + + const handleCheck = () => { + setRemember(!remember); + }; + + const handleSubmit = async () => { + const name = nameRef.current?.value?.toLowerCase(); + const password = passwordRef.current?.value; + if (name && password) { + login(name, password, remember).then(success => { + if (success) history.push(`/profile/${name}`); + else setError(true); + }); + } + }; + + const handleRegistration = () => { + history.push('/registration'); + }; + + return ( + <> +
Sign In
+
+ + + } + label="Remember me" + /> + + +
+
{'Don\'t have an account?'}
+ + Sign up + +
+ + ); +}; + +export default Login; + diff --git a/src/containers/LoginPage/LoginPage.tsx b/src/containers/LoginPage/LoginPage.tsx deleted file mode 100644 index 335cbb1..0000000 --- a/src/containers/LoginPage/LoginPage.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { useHistory } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import { - TextField, - Button, - FormControlLabel, - Switch -} from '@material-ui/core'; -import { useAuth } from '../../hooks/useAuth'; - -const useStyles = makeStyles(theme => ({ - root: { - '& > *': { - margin: theme.spacing(1), - width: theme.spacing(35) - }, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - textAlign: 'center' - }, - formHeader: { - textAlign: 'center', - fontSize: 25 - }, - formTransfer: { - display: 'flex', - justifyContent: 'center' - }, - transferButton: { - marginLeft: 10, - color: 'green', - cursor: 'pointer' - } -})); - -const LoginPage: React.FC = () => { - const [error, setError] = useState(false); - const [remember, setRemember] = useState(true); - const classes = useStyles(); - const nameRef = useRef(); - const passwordRef = useRef(); - const { login } = useAuth(); - const history = useHistory(); - - const handleCheck = () => { - setRemember(!remember); - }; - - const handleSubmit = async () => { - const name = nameRef.current?.value?.toLowerCase(); - const password = passwordRef.current?.value; - if (name && password) { - login(name, password, remember).then(success => { - if (success) history.push(`/profile/${name}`); - else setError(true); - }); - } - }; - - const handleRegistration = () => { - history.push('/registration'); - }; - - return ( - <> -
Sign In
-
- - - } - label="Remember me" - /> - - -
-
{'Don\'t have an account?'}
- - Sign up - -
- - ); -}; - -export default LoginPage; - diff --git a/src/containers/Notifications/Notifications.tsx b/src/containers/Notifications/Notifications.tsx new file mode 100644 index 0000000..0648eb5 --- /dev/null +++ b/src/containers/Notifications/Notifications.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(25), + textAlign: 'center' + } +})); + +const Notifications: React.FC = () => { + const classes = useStyles(); + + return ( + + Sorry, this page is being constructed yet. + + ); +}; + +export default Notifications; + diff --git a/src/containers/NotificationsPage/NotificationsPage.tsx b/src/containers/NotificationsPage/NotificationsPage.tsx deleted file mode 100644 index 064fbd4..0000000 --- a/src/containers/NotificationsPage/NotificationsPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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(25), - textAlign: 'center' - } -})); - -const NotificationsPage: React.FC = () => { - const classes = useStyles(); - - return ( - - Sorry, this page is being constructed yet. - - ); -}; - -export default NotificationsPage; - diff --git a/src/containers/Page/Page.tsx b/src/containers/Page/Page.tsx index 8a39636..df27b74 100644 --- a/src/containers/Page/Page.tsx +++ b/src/containers/Page/Page.tsx @@ -5,12 +5,12 @@ import { SnackbarProvider } from 'notistack'; import { Switch, Route } from 'react-router-dom'; import Loading from '../../components/Loading/Loading'; -const ProfilePage = React.lazy(() => import('../ProfilePage/ProfilePage')); -const FeedPage = React.lazy(() => import('../FeedPage/FeedPage')); -const LoginPage = React.lazy(() => import('../LoginPage/LoginPage')); -const RegistrationPage = React.lazy(() => import('../RegistrationPage/RegistrationPage')); -const HomePage = React.lazy(() => import('../HomePage/HomePage')); -const NotificationsPage = React.lazy(() => import('../NotificationsPage/NotificationsPage')); +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 => ({ @@ -41,12 +41,12 @@ const Page: React.FC = () => {
}> - - - - - - + + + + + +
diff --git a/src/containers/Profile/Highlight.tsx b/src/containers/Profile/Highlight.tsx new file mode 100644 index 0000000..ebc3f56 --- /dev/null +++ b/src/containers/Profile/Highlight.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +interface PropTypes { + text: string; + value: string | number; +} + +const useStyles = makeStyles({ + root: { + position: 'relative' + }, + menuButton: { + width: 200, + height: 50, + textAlign: 'center' + }, + menuNumber: { + fontWeight: 800, + color: 'black' + }, + menuText: { + color: 'darkgray' + } +}); + + +const Highlight: React.FC = ({ text, value }) => { + const classes = useStyles(); + + return ( +
+
{value}
+
{text}
+
+ ); +}; + +export default Highlight; diff --git a/src/containers/Profile/MoreMenu.tsx b/src/containers/Profile/MoreMenu.tsx new file mode 100644 index 0000000..1f41879 --- /dev/null +++ b/src/containers/Profile/MoreMenu.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import IconButton from '@material-ui/core/IconButton'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; +import { makeStyles } from '@material-ui/core'; +import { useAuth } from '../../hooks/useAuth'; + +const ITEM_HEIGHT = 48; + +const useStyles = makeStyles({ + moreMenu: { + position: 'absolute', + right: 10, + zIndex: 100 + } +}); + +const MoreMenu: React.FC = () => { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + const { logout } = useAuth(); + const history = useHistory(); + + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleLogout = () => { + logout(); + history.push('/login'); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( +
+
+ + + + + Log out + +
+
+ ); +}; + +export default MoreMenu; diff --git a/src/containers/Profile/Profile.tsx b/src/containers/Profile/Profile.tsx new file mode 100644 index 0000000..af52e5e --- /dev/null +++ b/src/containers/Profile/Profile.tsx @@ -0,0 +1,56 @@ +import React, { useEffect, useCallback } 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(userInfo?._id); + + useEffect(() => { + if (!username) { + if (user) history.push(`/profile/${user.username}`); + else history.push('/login'); + } + }, [username, history, user]); + + + const totalVotes = useCallback( + polls.reduce( + (total: number, current: Poll) => { + const { left, right } = current.contents; + return total + left.votes + right.votes; + }, 0 + ), + [polls] + ); + + return ( + + + { + isValidating && !polls + ? + : + } + + ); +}; + +export default Profile; diff --git a/src/containers/Profile/ProfileInfo.tsx b/src/containers/Profile/ProfileInfo.tsx new file mode 100644 index 0000000..9eee4c1 --- /dev/null +++ b/src/containers/Profile/ProfileInfo.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { Avatar, Badge, Typography } from '@material-ui/core/'; +import { makeStyles } from '@material-ui/core/styles'; +import { User } from 'which-types'; +import CameraAltIcon from '@material-ui/icons/CameraAlt'; +import VerifiedIcon from '@material-ui/icons/CheckCircleOutline'; +import Skeleton from '@material-ui/lab/Skeleton'; +import MoreMenu from './MoreMenu'; +import Highlight from './Highlight'; +import UploadImage from '../../components/UploadImage/UploadImage'; +import { patch } from '../../requests'; +import { useAuth } from '../../hooks/useAuth'; + +interface PropTypes { + savedPolls: number; + totalVotes: number; + userInfo: User | undefined; + setUserInfo: (userInfo: User) => void; +} + +const useStyles = makeStyles(theme => ({ + root: { + position: 'relative' + }, + avatar: { + width: 150, + height: 150, + margin: '0 auto' + }, + name: { + margin: theme.spacing(1, 0), + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + verified: { + marginLeft: theme.spacing(0.5), + width: theme.spacing(3), + height: theme.spacing(3) + }, + profileMenu: { + display: 'flex', + width: '100%', + height: 50, + margin: '50px 0', + borderBottom: '1px solid lightgray' + }, + menuButton: { + width: 200, + height: 50, + textAlign: 'center' + }, + badge: { + width: theme.spacing(5), + height: theme.spacing(5), + borderRadius: '50%', + cursor: 'pointer', + background: '#d3d3d3', + display: 'flex', + alignItems: 'center', + '& svg': { + margin: '0 auto' + } + }, + avatarContainer: { + position: 'relative', + textAlign: 'center' + }, + menuNumber: { + fontWeight: 800, + color: 'black' + }, + menuText: { + color: 'darkgray' + }, + skeleton: { + margin: '10px auto', + borderRadius: 2 + } + +})); + + +const ProfileInfo: React.FC = ({ + savedPolls, totalVotes, setUserInfo, userInfo +}) => { + const classes = useStyles(); + const [input, setInput] = useState(false); + const { user } = useAuth(); + const dateSince = new Date(userInfo?.createdAt || '').toLocaleDateString(); + + const handleClick = () => { + setInput(!input); + }; + + const patchAvatar = (url: string) => { + const id = user?._id; + patch(`/users/${id}`, { avatarUrl: url }).then(res => { + setUserInfo(res.data); + }); + }; + + return ( +
+ { + !userInfo + ? + : userInfo?._id === user?._id + ? ( +
+ +
+ + +
+ )} + > + + +
+ +
+ ) + : + } + { + !userInfo + ? + : ( + + {userInfo?.username} + {userInfo?.verified && } + + ) + } +
+ { + !userInfo + ? ( + <> + + + + + ) + : ( + <> + + + + + ) + } +
+ + ); +}; + +export default ProfileInfo; diff --git a/src/containers/ProfilePage/Highlight.tsx b/src/containers/ProfilePage/Highlight.tsx deleted file mode 100644 index ebc3f56..0000000 --- a/src/containers/ProfilePage/Highlight.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; - -interface PropTypes { - text: string; - value: string | number; -} - -const useStyles = makeStyles({ - root: { - position: 'relative' - }, - menuButton: { - width: 200, - height: 50, - textAlign: 'center' - }, - menuNumber: { - fontWeight: 800, - color: 'black' - }, - menuText: { - color: 'darkgray' - } -}); - - -const Highlight: React.FC = ({ text, value }) => { - const classes = useStyles(); - - return ( -
-
{value}
-
{text}
-
- ); -}; - -export default Highlight; diff --git a/src/containers/ProfilePage/MoreMenu.tsx b/src/containers/ProfilePage/MoreMenu.tsx deleted file mode 100644 index 1f41879..0000000 --- a/src/containers/ProfilePage/MoreMenu.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { useHistory } from 'react-router-dom'; -import IconButton from '@material-ui/core/IconButton'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; -import { makeStyles } from '@material-ui/core'; -import { useAuth } from '../../hooks/useAuth'; - -const ITEM_HEIGHT = 48; - -const useStyles = makeStyles({ - moreMenu: { - position: 'absolute', - right: 10, - zIndex: 100 - } -}); - -const MoreMenu: React.FC = () => { - const classes = useStyles(); - const [anchorEl, setAnchorEl] = React.useState(null); - const { logout } = useAuth(); - const history = useHistory(); - - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleLogout = () => { - logout(); - history.push('/login'); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( -
-
- - - - - Log out - -
-
- ); -}; - -export default MoreMenu; diff --git a/src/containers/ProfilePage/ProfileInfo.tsx b/src/containers/ProfilePage/ProfileInfo.tsx deleted file mode 100644 index 9eee4c1..0000000 --- a/src/containers/ProfilePage/ProfileInfo.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from 'react'; -import { Avatar, Badge, Typography } from '@material-ui/core/'; -import { makeStyles } from '@material-ui/core/styles'; -import { User } from 'which-types'; -import CameraAltIcon from '@material-ui/icons/CameraAlt'; -import VerifiedIcon from '@material-ui/icons/CheckCircleOutline'; -import Skeleton from '@material-ui/lab/Skeleton'; -import MoreMenu from './MoreMenu'; -import Highlight from './Highlight'; -import UploadImage from '../../components/UploadImage/UploadImage'; -import { patch } from '../../requests'; -import { useAuth } from '../../hooks/useAuth'; - -interface PropTypes { - savedPolls: number; - totalVotes: number; - userInfo: User | undefined; - setUserInfo: (userInfo: User) => void; -} - -const useStyles = makeStyles(theme => ({ - root: { - position: 'relative' - }, - avatar: { - width: 150, - height: 150, - margin: '0 auto' - }, - name: { - margin: theme.spacing(1, 0), - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - }, - verified: { - marginLeft: theme.spacing(0.5), - width: theme.spacing(3), - height: theme.spacing(3) - }, - profileMenu: { - display: 'flex', - width: '100%', - height: 50, - margin: '50px 0', - borderBottom: '1px solid lightgray' - }, - menuButton: { - width: 200, - height: 50, - textAlign: 'center' - }, - badge: { - width: theme.spacing(5), - height: theme.spacing(5), - borderRadius: '50%', - cursor: 'pointer', - background: '#d3d3d3', - display: 'flex', - alignItems: 'center', - '& svg': { - margin: '0 auto' - } - }, - avatarContainer: { - position: 'relative', - textAlign: 'center' - }, - menuNumber: { - fontWeight: 800, - color: 'black' - }, - menuText: { - color: 'darkgray' - }, - skeleton: { - margin: '10px auto', - borderRadius: 2 - } - -})); - - -const ProfileInfo: React.FC = ({ - savedPolls, totalVotes, setUserInfo, userInfo -}) => { - const classes = useStyles(); - const [input, setInput] = useState(false); - const { user } = useAuth(); - const dateSince = new Date(userInfo?.createdAt || '').toLocaleDateString(); - - const handleClick = () => { - setInput(!input); - }; - - const patchAvatar = (url: string) => { - const id = user?._id; - patch(`/users/${id}`, { avatarUrl: url }).then(res => { - setUserInfo(res.data); - }); - }; - - return ( -
- { - !userInfo - ? - : userInfo?._id === user?._id - ? ( -
- -
- - -
- )} - > - - -
- -
- ) - : - } - { - !userInfo - ? - : ( - - {userInfo?.username} - {userInfo?.verified && } - - ) - } -
- { - !userInfo - ? ( - <> - - - - - ) - : ( - <> - - - - - ) - } -
- - ); -}; - -export default ProfileInfo; diff --git a/src/containers/ProfilePage/ProfilePage.tsx b/src/containers/ProfilePage/ProfilePage.tsx deleted file mode 100644 index 7a1d729..0000000 --- a/src/containers/ProfilePage/ProfilePage.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useEffect, useCallback } 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 ProfilePage: 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(userInfo?._id); - - useEffect(() => { - if (!username) { - if (user) history.push(`/profile/${user.username}`); - else history.push('/login'); - } - }, [username, history, user]); - - - const totalVotes = useCallback( - polls.reduce( - (total: number, current: Poll) => { - const { left, right } = current.contents; - return total + left.votes + right.votes; - }, 0 - ), - [polls] - ); - - return ( - - - { - isValidating && !polls - ? - : - } - - ); -}; - -export default ProfilePage; diff --git a/src/containers/Registration/Registration.tsx b/src/containers/Registration/Registration.tsx new file mode 100644 index 0000000..c681329 --- /dev/null +++ b/src/containers/Registration/Registration.tsx @@ -0,0 +1,96 @@ +import React, { useState, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; +import { makeStyles } from '@material-ui/core/styles'; +import TextField from '@material-ui/core/TextField'; +import Button from '@material-ui/core/Button'; +import { post } from '../../requests'; +import { useAuth } from '../../hooks/useAuth'; + + +const useStyles = makeStyles(theme => ({ + root: { + '& > *': { + margin: theme.spacing(1), + width: theme.spacing(35) + }, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center' + }, + formHeader: { + textAlign: 'center', + fontSize: 25 + }, + formTransfer: { + display: 'flex', + justifyContent: 'center' + }, + transferButton: { + marginLeft: 10, + color: 'green', + cursor: 'pointer' + } +})); + +const Registration: React.FC = () => { + const [error, setError] = useState(false); + const classes = useStyles(); + const usernameRef = useRef(); + const emailRef = useRef(); + const passwordRef = useRef(); + const { login } = useAuth(); + const history = useHistory(); + + const handleSubmit = () => { + const username = usernameRef.current?.value?.toLowerCase(); + const password = passwordRef.current?.value; + const email = emailRef.current?.value; + if (username && password) { + post('/users', { username, password, email }) + .then(() => login(username, password)) + .then(() => history.push(`/profile/${username}`)); + } else setError(true); + }; + + const handleLogin = () => { + history.push('/login'); + }; + + return ( + <> +
Sign Up
+
+ + + + + +
+
Already have an account?
+ + Log in + +
+ + ); +}; + +export default Registration; diff --git a/src/containers/RegistrationPage/RegistrationPage.tsx b/src/containers/RegistrationPage/RegistrationPage.tsx deleted file mode 100644 index 18a9379..0000000 --- a/src/containers/RegistrationPage/RegistrationPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { useHistory } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; -import TextField from '@material-ui/core/TextField'; -import Button from '@material-ui/core/Button'; -import { post } from '../../requests'; -import { useAuth } from '../../hooks/useAuth'; - - -const useStyles = makeStyles(theme => ({ - root: { - '& > *': { - margin: theme.spacing(1), - width: theme.spacing(35) - }, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - textAlign: 'center' - }, - formHeader: { - textAlign: 'center', - fontSize: 25 - }, - formTransfer: { - display: 'flex', - justifyContent: 'center' - }, - transferButton: { - marginLeft: 10, - color: 'green', - cursor: 'pointer' - } -})); - -const RegistrationPage: React.FC = () => { - const [error, setError] = useState(false); - const classes = useStyles(); - const usernameRef = useRef(); - const emailRef = useRef(); - const passwordRef = useRef(); - const { login } = useAuth(); - const history = useHistory(); - - const handleSubmit = () => { - const username = usernameRef.current?.value?.toLowerCase(); - const password = passwordRef.current?.value; - const email = emailRef.current?.value; - if (username && password) { - post('/users', { username, password, email }) - .then(() => login(username, password)) - .then(() => history.push(`/profile/${username}`)); - } else setError(true); - }; - - const handleLogin = () => { - history.push('/login'); - }; - - return ( - <> -
Sign Up
-
- - - - - -
-
Already have an account?
- - Log in - -
- - ); -}; - -export default RegistrationPage; -- cgit v1.2.3 From a5c7950b65652d105c8eff22d14aeeacade6d31f Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 11:32:13 +0300 Subject: feat: useSWR for feedbacks --- src/containers/Home/Home.tsx | 14 ++++---------- src/hooks/APIClient.ts | 4 ++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/containers/Home/Home.tsx b/src/containers/Home/Home.tsx index 73fa479..203b380 100644 --- a/src/containers/Home/Home.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'; @@ -41,7 +41,7 @@ const useStyles = makeStyles(theme => ({ })); const Home: React.FC = () => { - const [feedbacks, setFeedbacks] = useState([]); + const { data: feedbacks } = useFeedback(); const classes = useStyles(); const history = useHistory(); const { isAuthenticated, user } = useAuth(); @@ -53,12 +53,6 @@ const Home: React.FC = () => { 0 ) / feedbacks.length; - useEffect(() => { - get('/feedback').then(response => { - setFeedbacks(response.data); - }); - }, []); - const handleLetsGo = () => { history.push('/feed'); }; @@ -76,7 +70,7 @@ const Home: React.FC = () => { const Reviews = (
- {feedbacks.map(feedback => )} + {feedbacks.map((feedback: Feedback) => )}
); diff --git a/src/hooks/APIClient.ts b/src/hooks/APIClient.ts index 7f7d170..59bf4fc 100644 --- a/src/hooks/APIClient.ts +++ b/src/hooks/APIClient.ts @@ -23,3 +23,7 @@ export const useProfile = (id: string) => { export const useFeed = () => { return useSWR('/feed', fetcher, arrayOptions); }; + +export const useFeedback = () => { + return useSWR('/feedback', fetcher, arrayOptions); +}; -- cgit v1.2.3 From a8424edaea142f159022506dc8c8799718469d30 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 12:00:01 +0300 Subject: feat: do not revalidate Feed on focus --- src/hooks/APIClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/APIClient.ts b/src/hooks/APIClient.ts index 59bf4fc..2322af4 100644 --- a/src/hooks/APIClient.ts +++ b/src/hooks/APIClient.ts @@ -21,7 +21,7 @@ export const useProfile = (id: string) => { }; export const useFeed = () => { - return useSWR('/feed', fetcher, arrayOptions); + return useSWR('/feed', fetcher, { ...arrayOptions, revalidateOnFocus: false }); }; export const useFeedback = () => { -- cgit v1.2.3 From 35fcdceb8f04fe333d45c5b1cb7ba395352c92d6 Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 13:22:00 +0300 Subject: feat: only allow 1 snackbar on mobile --- src/containers/Page/Page.tsx | 2 +- src/containers/Profile/Profile.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/Page/Page.tsx b/src/containers/Page/Page.tsx index df27b74..643e6de 100644 --- a/src/containers/Page/Page.tsx +++ b/src/containers/Page/Page.tsx @@ -32,7 +32,7 @@ const Page: React.FC = () => { return ( { const { user } = useAuth(); const { data: userInfo, mutate: setUserInfo } = useUser(username); - const { data: polls, mutate: mutatePolls, isValidating } = useProfile(userInfo?._id); + const { data: polls, mutate: mutatePolls, isValidating } = useProfile(username); useEffect(() => { if (!username) { -- cgit v1.2.3 From 78218c0f3427ad79de003ac59cffb99b08f0ae7d Mon Sep 17 00:00:00 2001 From: eug-vs Date: Mon, 10 Aug 2020 13:47:02 +0300 Subject: fix: resolve eslint errors --- src/components/PollsList/PollsList.tsx | 5 ++--- src/containers/Profile/Profile.tsx | 21 +++++++++------------ src/hooks/APIClient.ts | 23 ++++++++++++++--------- src/hooks/useAuth.tsx | 4 ++-- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/components/PollsList/PollsList.tsx b/src/components/PollsList/PollsList.tsx index 0fd8fa4..c95bfde 100644 --- a/src/components/PollsList/PollsList.tsx +++ b/src/components/PollsList/PollsList.tsx @@ -17,13 +17,12 @@ interface RenderPropTypes { const PollsList: React.FC = ({ polls, mutate }) => { - const RenderItem: React.FC = ({ index, style, key }) => { const poll = polls[index]; - const setPoll = (poll: Poll) => { + const setPoll = (newPoll: Poll) => { const newPolls = [...polls]; - newPolls[index] = poll; + newPolls[index] = newPoll; // Force-update list-size so everything re-renders mutate([], false); diff --git a/src/containers/Profile/Profile.tsx b/src/containers/Profile/Profile.tsx index f7678de..7e929fb 100644 --- a/src/containers/Profile/Profile.tsx +++ b/src/containers/Profile/Profile.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { Poll } from 'which-types'; import { Container } from '@material-ui/core'; @@ -26,15 +26,12 @@ const Profile: React.FC = () => { }, [username, history, user]); - const totalVotes = useCallback( - polls.reduce( - (total: number, current: Poll) => { - const { left, right } = current.contents; - return total + left.votes + right.votes; - }, 0 - ), - [polls] - ); + const totalVotes = useMemo(() => polls.reduce( + (total: number, current: Poll) => { + const { left, right } = current.contents; + return total + left.votes + right.votes; + }, 0 + ), [polls]); return ( @@ -46,8 +43,8 @@ const Profile: React.FC = () => { /> { isValidating && !polls - ? - : + ? + : } ); diff --git a/src/hooks/APIClient.ts b/src/hooks/APIClient.ts index 2322af4..9563bd6 100644 --- a/src/hooks/APIClient.ts +++ b/src/hooks/APIClient.ts @@ -1,7 +1,12 @@ -import useSWR from 'swr'; +import useSWR, { responseInterface } from 'swr'; +import { User, Poll, Feedback } from 'which-types'; import { get } from '../requests'; +interface Response extends responseInterface { + data: T; +} + const fetcher = (endpoint: string) => get(endpoint).then(response => response.data); const arrayOptions = { @@ -9,21 +14,21 @@ const arrayOptions = { revalidateOnMount: true }; -export const useUser = (username: string | null) => { +export const useUser = (username: string | null): Response => { return useSWR( username && `/users?username=${username}`, (url: string) => get(url).then(response => response.data[0]) - ); + ) as Response; }; -export const useProfile = (id: string) => { - return useSWR(id && `/profiles/${id}`, fetcher, arrayOptions); +export const useProfile = (id: string): Response => { + return useSWR(id && `/profiles/${id}`, fetcher, arrayOptions) as Response; }; -export const useFeed = () => { - return useSWR('/feed', fetcher, { ...arrayOptions, revalidateOnFocus: false }); +export const useFeed = (): Response => { + return useSWR('/feed', fetcher, { ...arrayOptions, revalidateOnFocus: false }) as Response; }; -export const useFeedback = () => { - return useSWR('/feedback', fetcher, arrayOptions); +export const useFeedback = (): Response => { + return useSWR('/feedback', fetcher, arrayOptions) as Response; }; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index ed1e428..2f03a33 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -8,14 +8,14 @@ import useLocalStorage from './useLocalStorage'; interface ContextType { - user: User | null, + user: User | undefined, login: (username: string, password: string, remember?: boolean) => Promise; logout: () => void; isAuthenticated: boolean; } const authContext = createContext({ - user: null, + user: undefined, login: async () => false, logout: () => {}, isAuthenticated: false -- cgit v1.2.3