diff options
author | ilyayudovin <46264063+ilyayudovin@users.noreply.github.com> | 2020-08-29 23:39:15 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-29 23:39:15 +0300 |
commit | 96f1c8f86fc973ca66c8725a86b6cf277adb9c72 (patch) | |
tree | 866380704610aa2807777c21b3971dd71a020617 | |
parent | 890141100a2e9b942a6ef2de15620fa5a01ba581 (diff) | |
parent | 3c4f51295ec4ccbed37d2faf8fd751608fc48843 (diff) | |
download | which-ui-96f1c8f86fc973ca66c8725a86b6cf277adb9c72.tar.gz |
Merge pull request #97 from which-ecosystem/validSignUp
fix input validation errors
-rw-r--r-- | .eslintrc.json | 1 | ||||
-rw-r--r-- | package-lock.json | 91 | ||||
-rw-r--r-- | package.json | 5 | ||||
-rw-r--r-- | src/containers/Login/Login.tsx | 99 | ||||
-rw-r--r-- | src/containers/Registration/Registration.tsx | 170 |
5 files changed, 243 insertions, 123 deletions
diff --git a/.eslintrc.json b/.eslintrc.json index 75a787c..479e04f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,6 +21,7 @@ "no-cond-assign": 0, "no-nested-ternary": 0, "linebreak-style": 0, + "object-curly-newline": 0, "react/prop-types": 0, "react/no-children-prop": 0, "react/no-danger": 0, diff --git a/package-lock.json b/package-lock.json index 4f71e0b..d80d20f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1771,6 +1771,12 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" }, + "@types/yup": { + "version": "0.29.6", + "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.6.tgz", + "integrity": "sha512-YPDo5L5uHyxQ4UkyJST+33stD8Z6IT9fvmKyaPAGxkZ6q19foEi6sQGkmqBvzSyRPdstFEeJiS2rKuTn8rfO5g==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz", @@ -4292,6 +4298,11 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -5860,6 +5871,11 @@ } } }, + "fn-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz", + "integrity": "sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==" + }, "follow-redirects": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", @@ -5928,6 +5944,32 @@ "mime-types": "^2.1.12" } }, + "formik": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.5.tgz", + "integrity": "sha512-bWpo3PiqVDYslvrRjTq0Isrm0mFXHiO33D8MS6t6dWcqSFGeYF52nlpCM2xwOJ6tRVRznDkL+zz/iHPL4LDuvQ==", + "requires": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.14", + "lodash-es": "^4.17.14", + "react-fast-compare": "^2.0.1", + "scheduler": "^0.18.0", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" + }, + "dependencies": { + "scheduler": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", + "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -8089,6 +8131,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -10447,6 +10494,11 @@ "react-is": "^16.8.1" } }, + "property-expr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", + "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -10906,6 +10958,11 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==" }, + "react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "react-icons": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.10.0.tgz", @@ -12935,6 +12992,11 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "synchronous-promise": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.13.tgz", + "integrity": "sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==" + }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -13247,6 +13309,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -14719,6 +14786,30 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yup": { + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.29.3.tgz", + "integrity": "sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==", + "requires": { + "@babel/runtime": "^7.10.5", + "fn-name": "~3.0.0", + "lodash": "^4.17.15", + "lodash-es": "^4.17.11", + "property-expr": "^2.0.2", + "synchronous-promise": "^2.0.13", + "toposort": "^2.0.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } } } } diff --git a/package.json b/package.json index 22c87ff..2bb2500 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "axios": "^0.19.2", "bluebird": "^3.7.2", "compressorjs": "^1.0.6", + "formik": "^2.1.5", "lodash": "^4.17.15", "notistack": "^0.9.17", "react": "^16.13.1", @@ -20,7 +21,8 @@ "react-virtualized": "^9.21.2", "swr": "^0.3.0", "typeface-roboto": "0.0.75", - "which-types": "^1.6.1" + "which-types": "^1.6.1", + "yup": "^0.29.3" }, "scripts": { "start": "react-scripts start", @@ -49,6 +51,7 @@ "@types/react-dom": "^16.9.8", "@types/react-router-dom": "^5.1.5", "@types/react-virtualized": "^9.21.10", + "@types/yup": "^0.29.6", "@typescript-eslint/eslint-plugin": "^3.1.0", "@typescript-eslint/parser": "^3.1.0", "eslint": "^6.8.0", diff --git a/src/containers/Login/Login.tsx b/src/containers/Login/Login.tsx index bec0db5..3d58c63 100644 --- a/src/containers/Login/Login.tsx +++ b/src/containers/Login/Login.tsx @@ -1,14 +1,24 @@ -import React, { useState, useRef } from 'react'; +import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; +import { Formik, Form, Field } from 'formik'; import { makeStyles } from '@material-ui/core/styles'; import { TextField, Button, FormControlLabel, - Switch + Switch, + InputAdornment, + IconButton } from '@material-ui/core'; +import { Visibility, VisibilityOff } from '@material-ui/icons'; import { useAuth } from '../../hooks/useAuth'; +interface Fields { + username: string; + password: string; + remember: boolean; +} + const useStyles = makeStyles(theme => ({ root: { '& > *': { @@ -37,54 +47,71 @@ const useStyles = makeStyles(theme => ({ const Login: React.FC = () => { const [error, setError] = useState<boolean>(false); - const [remember, setRemember] = useState<boolean>(true); + const [showPassword, setShowPassword] = useState<boolean>(false); const classes = useStyles(); - const nameRef = useRef<HTMLInputElement>(); - const passwordRef = useRef<HTMLInputElement>(); 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}`); + const handleSubmit = async ({ username, password, remember }: Fields) => { + if (username && password) { + login(username, password, remember).then(success => { + if (success) history.push(`/profile/${username}`); else setError(true); }); - } + } else setError(true); }; - const handleRegistration = () => { history.push('/registration'); }; + const toggleShowPassword = () => { + setShowPassword(prevState => !prevState); + }; + return ( <> <div className={classes.formHeader}>Sign In</div> - <form className={classes.root} noValidate autoComplete="off"> - <TextField - inputRef={nameRef} - error={error} - label="Login" - /> - <TextField - inputRef={passwordRef} - error={error} - helperText={error && 'Invalid credentials'} - label="Password" - type="password" - /> - <FormControlLabel - control={<Switch color="primary" onClick={handleCheck} checked={remember} size="small" />} - label="Remember me" - /> - <Button variant="contained" onClick={handleSubmit}>submit</Button> - </form> + <Formik + initialValues={{ username: '', password: '', remember: true }} + onSubmit={handleSubmit} + > + {({ values, isSubmitting }) => ( + <Form className={classes.root} autoComplete="off"> + <Field + name="username" + label="Login" + value={values.username} + error={error} + as={TextField} + /> + <Field + name="password" + label="Password" + as={TextField} + value={values.password} + error={error} + helperText={error && 'Invalid credentials'} + type={showPassword ? 'text' : 'password'} + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <IconButton size="small" onClick={toggleShowPassword}> + {showPassword ? <Visibility /> : <VisibilityOff />} + </IconButton> + </InputAdornment> + ) + }} + /> + <Field + name="remember" + label="Remember me" + as={FormControlLabel} + control={<Switch color="primary" checked={values.remember} size="small" />} + /> + <Button variant="contained" type="submit" disabled={isSubmitting}>submit</Button> + </Form> + )} + </Formik> <div className={classes.formTransfer}> <div>{'Don\'t have an account?'}</div> <span diff --git a/src/containers/Registration/Registration.tsx b/src/containers/Registration/Registration.tsx index 9bcea8e..a3aedb3 100644 --- a/src/containers/Registration/Registration.tsx +++ b/src/containers/Registration/Registration.tsx @@ -1,13 +1,35 @@ -import React, { useState, useRef } from 'react'; +import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; +import { Formik, Form, Field } from 'formik'; +import * as Yup from 'yup'; import { makeStyles } from '@material-ui/core/styles'; -import TextField from '@material-ui/core/TextField'; -import Button from '@material-ui/core/Button'; -import CheckCircleIcon from '@material-ui/icons/CheckCircle'; -import InputAdornment from '@material-ui/core/InputAdornment'; +import { + TextField, + Button, + InputAdornment, + IconButton +} from '@material-ui/core'; +import { Visibility, VisibilityOff } from '@material-ui/icons'; import { post } from '../../requests'; import { useAuth } from '../../hooks/useAuth'; +interface Fields { + username: string; + email: string; + password: string; +} + +const validationSchema = Yup.object({ + username: Yup.string() + .lowercase('Must be lowercase') + .required('This field is required'), + email: Yup.string() + .email('Invalid email address') + .required('This field is required'), + password: Yup.string() + .min(6, 'Should be at least 6 characters') + .required('This field is required') +}); const useStyles = makeStyles(theme => ({ root: { @@ -35,102 +57,78 @@ const useStyles = makeStyles(theme => ({ } })); -const inputStyle = { WebkitBoxShadow: '0 0 0 1000px snow inset' }; - - const Registration: React.FC = () => { - const errorOutputs = { - usernameError: 'Username is required', - emailError: 'Invalid email address', - passwordError: 'Should be at least 6 characters' - }; - const [errorPassword, setErrorPassword] = useState<boolean>(false); - const [errorEmail, setErrorEmail] = useState<boolean>(false); - const [errorUsername, setErrorUsername] = useState<boolean>(false); - const classes = useStyles(); - const usernameRef = useRef<HTMLInputElement>(); - const emailRef = useRef<HTMLInputElement>(); - const passwordRef = useRef<HTMLInputElement>(); 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}`)); - } - }; + const [showPassword, setShowPassword] = useState<boolean>(false); const handleLogin = () => { history.push('/login'); }; - const handleLoginChange = (e: React.ChangeEvent<HTMLInputElement>) => { - setErrorUsername(e.currentTarget.value.length === 0); + const handleSubmit = ({ username, email, password }: Fields) => { + post('/users', { username, email, password }) + .then(() => login(username, password)) + .then(() => history.push(`/profile/${username}`)); }; - const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { - setErrorEmail(!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(e.currentTarget.value)); - }; - const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { - setErrorPassword(e.currentTarget.value.length < 6); + + const toggleShowPassword = () => { + setShowPassword(prevState => !prevState); }; return ( <> <div className={classes.formHeader}>Sign Up</div> - <form className={classes.root} noValidate autoComplete="off"> - <TextField - inputRef={usernameRef} - label="Username" - error={errorUsername} - helperText={errorUsername && errorOutputs.usernameError} - required - onChange={handleLoginChange} - inputProps={{ style: inputStyle }} - /> - <TextField - inputRef={emailRef} - label="Email" - error={errorEmail} - helperText={errorEmail && errorOutputs.emailError} - onChange={handleEmailChange} - InputProps={errorEmail ? {} : { - endAdornment: ( - <InputAdornment position="end"> - <CheckCircleIcon color="primary" /> - </InputAdornment> - ), - inputProps: { - style: inputStyle - } - }} - /> - <TextField - inputRef={passwordRef} - label="Password" - type="password" - required - error={errorPassword} - helperText={errorPassword && errorOutputs.passwordError} - onChange={handlePasswordChange} - InputProps={errorPassword ? {} : { - endAdornment: ( - <InputAdornment position="end"> - <CheckCircleIcon color="primary" /> - </InputAdornment> - ), - inputProps: { - style: inputStyle - } - }} - /> - <Button variant="contained" onClick={handleSubmit}>submit</Button> - </form> + <Formik + initialValues={{ username: '', email: '', password: '' }} + validationSchema={validationSchema} + onSubmit={handleSubmit} + > + {({ values, errors, touched, isSubmitting }) => ( + <Form className={classes.root} autoComplete="off"> + <Field + id="username" + name="username" + label="Username" + value={values.username.toLowerCase()} + error={touched.username && !!errors.username} + helperText={touched.username && errors.username} + required + as={TextField} + /> + <Field + name="email" + label="Email" + value={values.email} + error={touched.email && !!errors.email} + helperText={touched.email && errors.email} + required + as={TextField} + /> + <Field + name="password" + label="Password" + value={values.password} + error={touched.password && !!errors.password} + helperText={touched.password && errors.password} + required + type={showPassword ? 'text' : 'password'} + as={TextField} + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <IconButton size="small" onClick={toggleShowPassword}> + {showPassword ? <Visibility /> : <VisibilityOff />} + </IconButton> + </InputAdornment> + ) + }} + /> + <Button variant="contained" type="submit" disabled={isSubmitting}>submit</Button> + </Form> + )} + </Formik> <div className={classes.formTransfer}> <div>Already have an account?</div> <span |