diff options
-rw-r--r-- | package-lock.json | 6 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/components/PollCard/PollCard.tsx | 60 | ||||
-rw-r--r-- | src/components/UserStrip/UserStrip.tsx | 65 | ||||
-rw-r--r-- | src/index.tsx | 8 | ||||
-rw-r--r-- | src/pages/AuthPage/AuthPage.tsx | 6 | ||||
-rw-r--r-- | src/pages/AuthPage/SignInForm.tsx | 25 | ||||
-rw-r--r-- | src/pages/AuthPage/SignUpForm.tsx | 33 | ||||
-rw-r--r-- | src/pages/FeedPage/FeedPage.tsx | 2 | ||||
-rw-r--r-- | src/requests.ts | 8 |
10 files changed, 149 insertions, 66 deletions
diff --git a/package-lock.json b/package-lock.json index f9311e4..84e7815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14118,9 +14118,9 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "which-types": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/which-types/-/which-types-1.4.1.tgz", - "integrity": "sha512-ZtN3cDwz/fQbJBwrItsZ0jpGafReTd/fIffHNQtFW4THrZqi8z4qnFTbyu1M6LnAmPlwU/FaRLZPfd67ZQ4mFw==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/which-types/-/which-types-1.4.2.tgz", + "integrity": "sha512-nwcohvhH+VEA11cReLi/BgeuKHJYH7VM2BWe9OIX89CB+iaZ0+wb6oLFcIP6Vp6jw3k93yoPMe9pMBsOi4kj6w==" }, "word-wrap": { "version": "1.2.3", diff --git a/package.json b/package.json index 98f44df..ca1967a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "react-icons": "^3.10.0", "react-scripts": "3.4.1", "typeface-roboto": "0.0.75", - "which-types": "^1.4.1" + "which-types": "^1.4.2" }, "scripts": { "start": "react-scripts start", diff --git a/src/components/PollCard/PollCard.tsx b/src/components/PollCard/PollCard.tsx index 40f5fd7..d3b4fc2 100644 --- a/src/components/PollCard/PollCard.tsx +++ b/src/components/PollCard/PollCard.tsx @@ -3,13 +3,12 @@ import { makeStyles } from '@material-ui/core/styles'; import { Card, CardActionArea, - CardMedia, - Avatar, - CardHeader + CardMedia } from '@material-ui/core/'; import { Which, Poll } from 'which-types'; import PercentageBar from './PercentageBar'; +import UserStrip from '../UserStrip/UserStrip'; import { post } from '../../requests'; interface PropTypes { @@ -17,6 +16,14 @@ interface PropTypes { navigate: (prefix: string, id: string) => void; } +const DATE_FORMAT = { + month: 'long', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' +}; + const useStyles = makeStyles(theme => ({ root: { maxWidth: theme.spacing(75), @@ -30,18 +37,19 @@ const useStyles = makeStyles(theme => ({ imagesBlock: { display: 'flex' }, - avatar: { - cursor: 'pointer' - }, rateLine: { position: 'relative', width: '100%', height: theme.spacing(2), - backgroundColor: theme.palette.primary.light + backgroundColor: theme.palette.primary.light, + transitionDuration: '0.5s' + }, + highlight: { + backgroundColor: `${theme.palette.primary.main} !important` }, fillRateLine: { height: theme.spacing(2), - backgroundColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, transitionDuration: '0.5s' } })); @@ -50,10 +58,7 @@ const PollCard: React.FC<PropTypes> = ({ initialPoll, navigate }) => { const [poll, setPoll] = useState<Poll>(initialPoll); const classes = useStyles(); const { author, contents: { left, right }, userChoice } = poll; - - const handleNavigate = () => { - navigate('profile', poll.author._id); - }; + const date: string = new Date(poll.createdAt).toLocaleString('default', DATE_FORMAT); const vote = (which: Which) => { if (userChoice) return; @@ -68,50 +73,33 @@ const PollCard: React.FC<PropTypes> = ({ initialPoll, navigate }) => { const handleRight = () => vote('right'); const leftPercentage = Math.round(100 * (left.votes / (left.votes + right.votes))); + const rightPercentage = 100 - leftPercentage; - const percentage = { - left: leftPercentage, - right: 100 - leftPercentage - }; const dominant: Which = left.votes >= right.votes ? 'left' : 'right'; return ( <Card className={classes.root}> - <CardHeader - avatar={( - <Avatar - aria-label="avatar" - src={author.avatarUrl} - alt={author.username[0].toUpperCase()} - onClick={handleNavigate} - className={classes.avatar} - /> - )} - title={author.username} - /> + <UserStrip user={author} info={date} navigate={navigate} /> <div className={classes.imagesBlock}> <CardActionArea onDoubleClick={handleLeft}> <CardMedia className={classes.images} image={left.url} /> - <PercentageBar value={percentage.left} which="left" like={userChoice === 'left'} /> + <PercentageBar value={leftPercentage} which="left" like={userChoice === 'left'} /> </CardActionArea> <CardActionArea onDoubleClick={handleRight}> <CardMedia className={classes.images} image={right.url} /> - <PercentageBar value={percentage.right} which="right" like={userChoice === 'right'} /> + <PercentageBar value={rightPercentage} which="right" like={userChoice === 'right'} /> </CardActionArea> </div> - <div className={classes.rateLine}> + <div className={`${classes.rateLine} ${dominant === 'right' ? classes.highlight : ''}`}> <div - className={classes.fillRateLine} - style={{ - width: `${percentage[dominant]}%`, - float: dominant - }} + className={`${classes.fillRateLine} ${dominant === 'left' ? classes.highlight : ''}`} + style={{ width: `${leftPercentage}%` }} /> </div> </Card> diff --git a/src/components/UserStrip/UserStrip.tsx b/src/components/UserStrip/UserStrip.tsx new file mode 100644 index 0000000..6e84768 --- /dev/null +++ b/src/components/UserStrip/UserStrip.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import VerifiedIcon from '@material-ui/icons/CheckCircleOutline'; +import { + Avatar, + CardHeader +} from '@material-ui/core/'; +import { User } from 'which-types'; + + +interface PropTypes { + user: User; + info: string | JSX.Element + navigate: (prefix: string, id: string) => void; +} + + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + alignItems: 'center' + }, + verified: { + marginLeft: theme.spacing(0.5), + width: theme.spacing(2), + height: theme.spacing(2) + }, + avatar: { + cursor: 'pointer' + } +})); + + +const UserStrip: React.FC<PropTypes> = ({ user, info, navigate }) => { + const classes = useStyles(); + const { + username, + avatarUrl, + verified + } = user; + + const handleNavigate = () => { + navigate('profile', user._id); + }; + + const avatar = ( + <Avatar + src={avatarUrl} + alt={username[0].toUpperCase()} + onClick={handleNavigate} + className={classes.avatar} + /> + ); + + const title = ( + <div className={classes.root}> + {username} + {verified && <VerifiedIcon color="primary" className={classes.verified} />} + </div> + ); + + return <CardHeader avatar={avatar} title={title} subheader={info} />; +}; + +export default UserStrip; diff --git a/src/index.tsx b/src/index.tsx index 9139e4b..49c177b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -55,7 +55,7 @@ const App: React.FC = () => { } }; - const logIn = (username: string, password: string): Promise<boolean> => { + const logIn = (username: string, password: string, remember = true): Promise<boolean> => { return post('/authentication', { strategy: 'local', username, @@ -64,9 +64,10 @@ const App: React.FC = () => { const me = response.data.user; const token = response.data.accessToken; setUser(me); + navigate('profile', me._id); localStorage.setItem('userId', me._id); localStorage.setItem('token', token); - navigate('profile', me._id); + if (!remember) localStorage.setItem('shouldClear', 'true'); return true; }).catch(() => false); }; @@ -79,6 +80,9 @@ const App: React.FC = () => { }; useEffect(() => { + if (localStorage.getItem('shouldClear')) { + localStorage.clear(); + } const userId = localStorage.getItem('userId'); if (userId) { get(`/users/${userId}`).then(response => { diff --git a/src/pages/AuthPage/AuthPage.tsx b/src/pages/AuthPage/AuthPage.tsx index dc90c01..d2c2eec 100644 --- a/src/pages/AuthPage/AuthPage.tsx +++ b/src/pages/AuthPage/AuthPage.tsx @@ -5,7 +5,7 @@ import SignUpForm from './SignUpForm'; interface PropTypes { - logIn: (name: string, password: string) => Promise<boolean>; + logIn: (name: string, password: string, remember?: boolean) => Promise<boolean>; } const useStyles = makeStyles({ @@ -29,8 +29,8 @@ const AuthPage: React.FC<PropTypes> = ({ logIn }) => { }; const footerInfo = { - signIn: ['Don\'t have an account?', 'Sign in'], - signUp: ['Already have an account?', 'Sign up'] + signIn: ['Don\'t have an account?', 'Sign up'], + signUp: ['Already have an account?', 'Sign in'] }; return ( diff --git a/src/pages/AuthPage/SignInForm.tsx b/src/pages/AuthPage/SignInForm.tsx index c521abf..1dad153 100644 --- a/src/pages/AuthPage/SignInForm.tsx +++ b/src/pages/AuthPage/SignInForm.tsx @@ -1,10 +1,14 @@ import React, { useState, useRef } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import TextField from '@material-ui/core/TextField'; -import Button from '@material-ui/core/Button'; +import { + TextField, + Button, + FormControlLabel, + Switch +} from '@material-ui/core'; interface PropTypes { - logIn: (name: string, password: string) => Promise<boolean>; + logIn: (name: string, password: string, remember?: boolean) => Promise<boolean>; } const useStyles = makeStyles(theme => ({ @@ -26,15 +30,20 @@ const useStyles = makeStyles(theme => ({ const SignInForm: React.FC<PropTypes> = ({ logIn }) => { const [error, setError] = useState<boolean>(false); + const [remember, setRemember] = useState<boolean>(true); const classes = useStyles(); const nameRef = useRef<HTMLInputElement>(); const passwordRef = useRef<HTMLInputElement>(); - const onClick = async () => { + const handleCheck = () => { + setRemember(!remember); + }; + + const handleSubmit = async () => { const name = nameRef.current?.value; const password = passwordRef.current?.value; if (name && password) { - logIn(name, password).then(success => { + logIn(name, password, remember).then(success => { if (!success) setError(true); }); } @@ -56,7 +65,11 @@ const SignInForm: React.FC<PropTypes> = ({ logIn }) => { label="Password" type="password" /> - <Button variant="contained" onClick={onClick}>submit</Button> + <FormControlLabel + control={<Switch color="primary" onClick={handleCheck} checked={remember} size="small" />} + label="Remember me" + /> + <Button variant="contained" onClick={handleSubmit}>submit</Button> </form> </> ); diff --git a/src/pages/AuthPage/SignUpForm.tsx b/src/pages/AuthPage/SignUpForm.tsx index 0e3d0c7..25b79ff 100644 --- a/src/pages/AuthPage/SignUpForm.tsx +++ b/src/pages/AuthPage/SignUpForm.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useState, useRef } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button'; @@ -26,31 +26,42 @@ const useStyles = makeStyles(theme => ({ })); const SignUpForm: React.FC<PropTypes> = ({ logIn }) => { + const [error, setError] = useState<boolean>(false); const classes = useStyles(); - const inputRef = useRef<HTMLInputElement>(); - const inputRefPassword = useRef<HTMLInputElement>(); + const usernameRef = useRef<HTMLInputElement>(); + const emailRef = useRef<HTMLInputElement>(); + const passwordRef = useRef<HTMLInputElement>(); const onClick = () => { - const username = inputRef.current?.value; - const password = inputRefPassword.current?.value; + const username = usernameRef.current?.value; + const password = passwordRef.current?.value; + const email = emailRef.current?.value; if (username && password) { - post('/users', { username, password }).then(() => { + post('/users', { username, password, email }).then(() => { logIn(username, password); }); - } + } else setError(true); }; return ( <> <div className={classes.formHeader}>Sign Up</div> <form className={classes.root} noValidate autoComplete="off"> - <TextField inputRef={inputRef} id="standard-basic" label="Name" /> - <TextField id="standard-basic" label="Email" /> <TextField - inputRef={inputRefPassword} - id="standard-password-input" + inputRef={usernameRef} + label="Username" + error={error} + helperText={error && 'This field is required!'} + required + /> + <TextField inputRef={emailRef} label="Email" /> + <TextField + inputRef={passwordRef} label="Password" type="password" + required + error={error} + helperText={error && 'This field is required!'} /> <Button variant="contained" onClick={onClick}>submit</Button> </form> diff --git a/src/pages/FeedPage/FeedPage.tsx b/src/pages/FeedPage/FeedPage.tsx index 937b0a9..b7d719e 100644 --- a/src/pages/FeedPage/FeedPage.tsx +++ b/src/pages/FeedPage/FeedPage.tsx @@ -12,7 +12,7 @@ const FeedPage: React.FC<PropTypes> = ({ navigate }) => { const [polls, setPolls] = useState<Poll[]>([]); useEffect(() => { - get('/polls').then(response => { + get('/feed').then(response => { setPolls(response.data); }); }, []); diff --git a/src/requests.ts b/src/requests.ts index e18a056..b02329d 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -1,9 +1,11 @@ import axios from 'axios'; import _ from 'lodash'; -const requests = axios.create({ - baseURL: 'http://localhost:3030' -}); +const baseURL = process.env.NODE_ENV === 'production' + ? 'https://which-api.herokuapp.com' + : 'http://localhost:3030'; + +const requests = axios.create({ baseURL }); requests.interceptors.request.use(config => { const token = localStorage.getItem('token'); |