Add basic barcode scanner
This commit is contained in:
parent
1a611fbea1
commit
26c4406401
@ -13,7 +13,7 @@ import {
|
|||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import {Add as AddIcon} from "@material-ui/icons";
|
import {Add as AddIcon} from "@material-ui/icons";
|
||||||
import SearchInput from 'components/SearchInput'
|
import SearchInput from 'components/SearchInput'
|
||||||
import BarcodeScanner from 'components/BarcodeScanner'
|
import BarcodeScanner from 'containers/BarcodeScanner'
|
||||||
import { createStructuredSelector } from 'reselect'
|
import { createStructuredSelector } from 'reselect'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { makeSelectProducts } from 'pages/Home/selectors'
|
import { makeSelectProducts } from 'pages/Home/selectors'
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {IconButton, Grid} from '@material-ui/core'
|
|
||||||
import {CropFree as CropFreeIcon} from '@material-ui/icons'
|
|
||||||
|
|
||||||
const BarcodeScanner = () => {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Grid item xs={2}>
|
|
||||||
<IconButton color="primary">
|
|
||||||
<CropFreeIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Grid>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BarcodeScanner;
|
|
312
src/containers/BarcodeScanner/index.js
Normal file
312
src/containers/BarcodeScanner/index.js
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {IconButton, Grid} from '@material-ui/core'
|
||||||
|
import {CropFree as CropFreeIcon} from '@material-ui/icons'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
const BarcodeScanner = () => {
|
||||||
|
const [decodecBarcode, setDecodedBarcode] = useState('');
|
||||||
|
|
||||||
|
const dimensions = {
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
start: 0,
|
||||||
|
end: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
video: null,
|
||||||
|
canvas: null,
|
||||||
|
ctx: null,
|
||||||
|
canvasg: null,
|
||||||
|
ctxg: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const upc = {
|
||||||
|
'0': [3, 2, 1, 1],
|
||||||
|
'1': [2, 2, 2, 1],
|
||||||
|
'2': [2, 1, 2, 2],
|
||||||
|
'3': [1, 4, 1, 1],
|
||||||
|
'4': [1, 1, 3, 2],
|
||||||
|
'5': [1, 2, 3, 1],
|
||||||
|
'6': [1, 1, 1, 4],
|
||||||
|
'7': [1, 3, 1, 2],
|
||||||
|
'8': [1, 2, 1, 3],
|
||||||
|
'9': [3, 1, 1, 2]
|
||||||
|
};
|
||||||
|
|
||||||
|
const check = {
|
||||||
|
'oooooo': '0',
|
||||||
|
'ooeoee': '1',
|
||||||
|
'ooeeoe': '2',
|
||||||
|
'ooeeeo': '3',
|
||||||
|
'oeooee': '4',
|
||||||
|
'oeeooe': '5',
|
||||||
|
'oeeeoo': '6',
|
||||||
|
'oeoeoe': '7',
|
||||||
|
'oeoeeo': '8',
|
||||||
|
'oeeoeo': '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
strokeColor: '#f00',
|
||||||
|
start: 0.1,
|
||||||
|
end: 0.9,
|
||||||
|
threshold: 160,
|
||||||
|
quality: 0.45,
|
||||||
|
delay: 100,
|
||||||
|
video: '#barcodevideo',
|
||||||
|
canvas: '#barcodecanvas',
|
||||||
|
canvasg: '#barcodecanvasg'
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||||||
|
|
||||||
|
elements.video = document.querySelector(config.video);
|
||||||
|
elements.canvas = document.querySelector(config.canvas);
|
||||||
|
elements.ctx = elements.canvas.getContext('2d');
|
||||||
|
elements.canvasg = document.querySelector(config.canvasg);
|
||||||
|
elements.ctxg = elements.canvasg.getContext('2d');
|
||||||
|
|
||||||
|
if (navigator.getUserMedia) {
|
||||||
|
navigator.getUserMedia({audio: false, video: true}, function(stream) {
|
||||||
|
elements.video.srcObject = stream;
|
||||||
|
}, function(error) {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.video.addEventListener('canplay', () => {
|
||||||
|
|
||||||
|
dimensions.height = elements.video.videoHeight;
|
||||||
|
dimensions.width = elements.video.videoWidth;
|
||||||
|
|
||||||
|
dimensions.start = dimensions.width * config.start;
|
||||||
|
dimensions.end = dimensions.width * config.end;
|
||||||
|
|
||||||
|
elements.canvas.width = dimensions.width;
|
||||||
|
elements.canvas.height = dimensions.height;
|
||||||
|
elements.canvasg.width = dimensions.width;
|
||||||
|
elements.canvasg.height = dimensions.height;
|
||||||
|
|
||||||
|
drawLine(elements.ctxg);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const snapshot = takeSnapshot(elements.ctx, elements.video)
|
||||||
|
processSnapshot(snapshot)
|
||||||
|
}, config.delay);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const takeSnapshot = (canvasElement, videoElement) => {
|
||||||
|
canvasElement.drawImage(videoElement, 0, 0, dimensions.width, dimensions.height);
|
||||||
|
return canvasElement.getImageData(dimensions.start, dimensions.height * 0.5, dimensions.end - dimensions.start, 1).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processSnapshot = (snapshot) => {
|
||||||
|
|
||||||
|
const bars = [];
|
||||||
|
|
||||||
|
const pixels = [];
|
||||||
|
let pixelBars = [];
|
||||||
|
|
||||||
|
// convert to grayscale
|
||||||
|
for (let i = 0; i < snapshot.length; i += 4) {
|
||||||
|
pixels.push(Math.round(snapshot[i] * 0.2126 + snapshot[i + 1] * 0.7152 + snapshot[ i + 2] * 0.0722));
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize and convert to binary
|
||||||
|
const minPixel = Math.min(...pixels);
|
||||||
|
const maxPixel = Math.max(...pixels);
|
||||||
|
|
||||||
|
const binary = pixels.reduce((arr, val) => {
|
||||||
|
const binaryValue = Math.round((val - minPixel) / (maxPixel - minPixel) * 255) > config.threshold ? 1 : 0
|
||||||
|
return [...arr, binaryValue]
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// determine bar widths
|
||||||
|
let current = binary[0];
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
if (binary[i] === current) {
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
pixelBars.push(count);
|
||||||
|
count = 1;
|
||||||
|
current = binary[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixelBars.push(count);
|
||||||
|
|
||||||
|
// quality check
|
||||||
|
if (pixelBars.length < (3 + 24 + 5 + 24 + 3 + 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find starting sequence
|
||||||
|
let startIndex = 0;
|
||||||
|
const minFactor = 0.5;
|
||||||
|
const maxFactor = 1.5;
|
||||||
|
|
||||||
|
for (let i = 3; i < pixelBars.length; i++) {
|
||||||
|
const refLength = (pixelBars[i] + pixelBars[i-1] + pixelBars[i-2]) / 3;
|
||||||
|
if (
|
||||||
|
(pixelBars[i] > (minFactor * refLength) || pixelBars[i] < (maxFactor * refLength))
|
||||||
|
&& (pixelBars[i-1] > (minFactor * refLength) || pixelBars[i-1] < (maxFactor * refLength))
|
||||||
|
&& (pixelBars[i-2] > (minFactor * refLength) || pixelBars[i-2] < (maxFactor * refLength))
|
||||||
|
&& (pixelBars[i-3] > 3 * refLength)
|
||||||
|
) {
|
||||||
|
startIndex = i - 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("startIndex: " + startIndex );
|
||||||
|
|
||||||
|
// return if no starting sequence found
|
||||||
|
if (startIndex === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// discard leading and trailing patterns
|
||||||
|
|
||||||
|
pixelBars = pixelBars.slice(startIndex, startIndex + 3 + 24 + 5 + 24 + 3);
|
||||||
|
|
||||||
|
console.log("pixelBars: " + pixelBars );
|
||||||
|
|
||||||
|
// calculate relative widths
|
||||||
|
const ref = (pixelBars[0] + pixelBars[1] + pixelBars[2]) / 3;
|
||||||
|
|
||||||
|
for (let i = 0; i < pixelBars.length; i++) {
|
||||||
|
bars.push(Math.round(pixelBars[i] / ref * 100) / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyze pattern
|
||||||
|
analyzePattern(bars);
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzePattern = (bars) => {
|
||||||
|
|
||||||
|
console.clear();
|
||||||
|
|
||||||
|
console.log("analyzing");
|
||||||
|
|
||||||
|
// determine parity first digit and reverse sequence if necessary
|
||||||
|
|
||||||
|
const first = normalize(bars.slice(3, 3 + 4), 7);
|
||||||
|
if (!isOdd(Math.round(first[1] + first[3]))) {
|
||||||
|
bars = bars.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// split into digits
|
||||||
|
|
||||||
|
const digits = [
|
||||||
|
normalize(bars.slice(3, 3 + 4), 7),
|
||||||
|
normalize(bars.slice(7, 7 + 4), 7),
|
||||||
|
normalize(bars.slice(11, 11 + 4), 7),
|
||||||
|
normalize(bars.slice(15, 15 + 4), 7),
|
||||||
|
normalize(bars.slice(19, 19 + 4), 7),
|
||||||
|
normalize(bars.slice(23, 23 + 4), 7),
|
||||||
|
normalize(bars.slice(32, 32 + 4), 7),
|
||||||
|
normalize(bars.slice(36, 36 + 4), 7),
|
||||||
|
normalize(bars.slice(40, 40 + 4), 7),
|
||||||
|
normalize(bars.slice(44, 44 + 4), 7),
|
||||||
|
normalize(bars.slice(48, 48 + 4), 7),
|
||||||
|
normalize(bars.slice(52, 52 + 4), 7)
|
||||||
|
]
|
||||||
|
|
||||||
|
console.log("digits: " + digits);
|
||||||
|
|
||||||
|
// determine parity and reverse if necessary
|
||||||
|
const parities = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
if (parity(digits[i])) {
|
||||||
|
parities.push('o');
|
||||||
|
} else {
|
||||||
|
parities.push('e');
|
||||||
|
digits[i] = digits[i].reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// identify digits
|
||||||
|
const result = [];
|
||||||
|
let quality = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < digits.length; i++) {
|
||||||
|
|
||||||
|
let distance = 9;
|
||||||
|
let bestKey = '';
|
||||||
|
|
||||||
|
for (let key in upc) {
|
||||||
|
if (maxDistance(digits[i], upc[key]) < distance) {
|
||||||
|
distance = maxDistance(digits[i], upc[key]);
|
||||||
|
bestKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(bestKey);
|
||||||
|
if (distance > quality) {
|
||||||
|
quality = distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("result: " + result);
|
||||||
|
|
||||||
|
// check digit
|
||||||
|
const checkDigit = check[parities.join('')];
|
||||||
|
|
||||||
|
// output
|
||||||
|
console.log("quality: " + quality);
|
||||||
|
|
||||||
|
if(quality < config.quality) {
|
||||||
|
setDecodedBarcode(checkDigit + result.join(''))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalize = (input, total) => {
|
||||||
|
const sum = input.reduce((acc, val) => acc + val, 0);
|
||||||
|
return input.reduce((acc, val) => [...acc, val / sum * total], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOdd = (num) => num % 2;
|
||||||
|
|
||||||
|
const maxDistance = (a, b) =>
|
||||||
|
a.reduce((max, value, index) => {
|
||||||
|
const current = Math.abs(value - b[index])
|
||||||
|
return current > max ? current : max
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const parity = (digit) => isOdd(Math.round(digit[1] + digit[3]))
|
||||||
|
|
||||||
|
const drawLine = (canvasContext) => {
|
||||||
|
canvasContext.strokeStyle = config.strokeColor;
|
||||||
|
canvasContext.lineWidth = 3;
|
||||||
|
canvasContext.beginPath();
|
||||||
|
canvasContext.moveTo(dimensions.start, dimensions.height * 0.5);
|
||||||
|
canvasContext.lineTo(dimensions.end, dimensions.height * 0.5);
|
||||||
|
canvasContext.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
init()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div id="barcode">
|
||||||
|
<video id="barcodevideo" autoPlay />
|
||||||
|
<canvas id="barcodecanvasg" />
|
||||||
|
</div>
|
||||||
|
<canvas id="barcodecanvas" />
|
||||||
|
{decodecBarcode === "5449000136350"}
|
||||||
|
{decodecBarcode}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default BarcodeScanner;
|
22
src/containers/BarcodeScanner/styles.css
Normal file
22
src/containers/BarcodeScanner/styles.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#barcodevideo, #barcodecanvas, #barcodecanvasg {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#barcodecanvasg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result {
|
||||||
|
font-family: verdana;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#barcode {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#barcodecanvas {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -11,6 +11,8 @@ import { getMealsAction } from './actions'
|
|||||||
import MealCard from "components/MealCard";
|
import MealCard from "components/MealCard";
|
||||||
import { createStructuredSelector } from 'reselect'
|
import { createStructuredSelector } from 'reselect'
|
||||||
|
|
||||||
|
import BarcodeScanner from 'containers/BarcodeScanner'
|
||||||
|
|
||||||
const stateSelector = createStructuredSelector({
|
const stateSelector = createStructuredSelector({
|
||||||
meals: makeSelectMeals()
|
meals: makeSelectMeals()
|
||||||
})
|
})
|
||||||
|
@ -1,100 +1,108 @@
|
|||||||
import React from 'react';
|
// import React from 'react';
|
||||||
import { useInjectReducer, useInjectSaga } from 'redux-injectors';
|
// import { useInjectReducer, useInjectSaga } from 'redux-injectors';
|
||||||
import { useDispatch } from 'react-redux';
|
// import { useDispatch } from 'react-redux';
|
||||||
import {
|
// import {
|
||||||
Container,
|
// Container,
|
||||||
Grid,
|
// Grid,
|
||||||
Button,
|
// Button,
|
||||||
Typography,
|
// Typography,
|
||||||
FormControlLabel,
|
// FormControlLabel,
|
||||||
Checkbox,
|
// Checkbox,
|
||||||
Link,
|
// Link,
|
||||||
} from '@material-ui/core'
|
// } from '@material-ui/core'
|
||||||
import {routes} from "utils";
|
// import {routes} from "utils";
|
||||||
import InputField from 'components/InputField'
|
// import InputField from 'components/InputField'
|
||||||
import { Formik, Form } from 'formik';
|
// import { Formik, Form } from 'formik';
|
||||||
import { loginAction } from './actions'
|
// import { loginAction } from './actions'
|
||||||
import useStyles from './styles'
|
// import useStyles from './styles'
|
||||||
import validationSchema from './FormModel/validationSchema'
|
// import validationSchema from './FormModel/validationSchema'
|
||||||
import formInitialValues from './FormModel/formInitialValues'
|
// import formInitialValues from './FormModel/formInitialValues'
|
||||||
import loginFormModel from './FormModel/loginFormModel'
|
// import loginFormModel from './FormModel/loginFormModel'
|
||||||
import saga from "./saga";
|
// import saga from "./saga";
|
||||||
|
//
|
||||||
|
// const {
|
||||||
|
// formId,
|
||||||
|
// formField: {
|
||||||
|
// email,
|
||||||
|
// password
|
||||||
|
// }
|
||||||
|
// } = loginFormModel;
|
||||||
|
//
|
||||||
|
// const key = 'loginPage'
|
||||||
|
//
|
||||||
|
// const Login = () => {
|
||||||
|
// useInjectSaga({ key, saga });
|
||||||
|
// const classes = useStyles()
|
||||||
|
// const dispatch = useDispatch();
|
||||||
|
//
|
||||||
|
// const handleSubmit = (values, actions) => {
|
||||||
|
// dispatch(loginAction(values))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <Container component="main" maxWidth="xs">
|
||||||
|
// <div className={classes.paper}>
|
||||||
|
// <Typography component="h1" variant="h5">
|
||||||
|
// Login to Account
|
||||||
|
// </Typography>
|
||||||
|
// <Formik
|
||||||
|
// initialValues={formInitialValues}
|
||||||
|
// validationSchema={validationSchema}
|
||||||
|
// onSubmit={handleSubmit}
|
||||||
|
// >
|
||||||
|
// {({ isSubmitting }) => (
|
||||||
|
// <Form id={formId}>
|
||||||
|
// <InputField
|
||||||
|
// type="text"
|
||||||
|
// label={email.label}
|
||||||
|
// name={email.name}
|
||||||
|
// variant="outlined"
|
||||||
|
// fullWidth
|
||||||
|
// className={classes.input}
|
||||||
|
// />
|
||||||
|
// <InputField
|
||||||
|
// type="password"
|
||||||
|
// label={password.label}
|
||||||
|
// name={password.name}
|
||||||
|
// variant="outlined"
|
||||||
|
// fullWidth
|
||||||
|
// className={classes.input}
|
||||||
|
// />
|
||||||
|
// <FormControlLabel
|
||||||
|
// control={<Checkbox value="remember" color="primary" />}
|
||||||
|
// label="Remember me"
|
||||||
|
// />
|
||||||
|
// <Button
|
||||||
|
// type="submit"
|
||||||
|
// fullWidth
|
||||||
|
// variant="contained"
|
||||||
|
// color="primary"
|
||||||
|
// className={classes.submit}
|
||||||
|
// disabled={isSubmitting}
|
||||||
|
// >
|
||||||
|
// Sign Up
|
||||||
|
// </Button>
|
||||||
|
// </Form>
|
||||||
|
// )}
|
||||||
|
// </Formik>
|
||||||
|
// <Grid container alignItems="flex-end">
|
||||||
|
// <Grid item>
|
||||||
|
// <Link href={routes.register.path} color="secondary" variant="body2">
|
||||||
|
// Don't have an account? Sign Up
|
||||||
|
// </Link>
|
||||||
|
// </Grid>
|
||||||
|
// </Grid>
|
||||||
|
// </div>
|
||||||
|
// </Container>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// export default Login;
|
||||||
|
|
||||||
const {
|
import BarcodeScanner from "../../containers/BarcodeScanner";
|
||||||
formId,
|
import {Container} from "@material-ui/core";
|
||||||
formField: {
|
import React from "react";
|
||||||
email,
|
|
||||||
password
|
|
||||||
}
|
|
||||||
} = loginFormModel;
|
|
||||||
|
|
||||||
const key = 'loginPage'
|
export default () => (
|
||||||
|
<BarcodeScanner />
|
||||||
const Login = () => {
|
)
|
||||||
useInjectSaga({ key, saga });
|
|
||||||
const classes = useStyles()
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const handleSubmit = (values, actions) => {
|
|
||||||
dispatch(loginAction(values))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container component="main" maxWidth="xs">
|
|
||||||
<div className={classes.paper}>
|
|
||||||
<Typography component="h1" variant="h5">
|
|
||||||
Login to Account
|
|
||||||
</Typography>
|
|
||||||
<Formik
|
|
||||||
initialValues={formInitialValues}
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
{({ isSubmitting }) => (
|
|
||||||
<Form id={formId}>
|
|
||||||
<InputField
|
|
||||||
type="text"
|
|
||||||
label={email.label}
|
|
||||||
name={email.name}
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
className={classes.input}
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
type="password"
|
|
||||||
label={password.label}
|
|
||||||
name={password.name}
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
className={classes.input}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Checkbox value="remember" color="primary" />}
|
|
||||||
label="Remember me"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
className={classes.submit}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Sign Up
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
<Grid container alignItems="flex-end">
|
|
||||||
<Grid item>
|
|
||||||
<Link href={routes.register.path} color="secondary" variant="body2">
|
|
||||||
Don't have an account? Sign Up
|
|
||||||
</Link>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
|
Loading…
Reference in New Issue
Block a user