diff --git a/lib/ModalContext.js b/lib/ModalContext.js
index 8580246..b43dfe9 100644
--- a/lib/ModalContext.js
+++ b/lib/ModalContext.js
@@ -1,6 +1,8 @@
-import { createContext, useState } from 'react';
+import { createContext, useState, useRef } from 'react';
import Modal from 'react-modal';
+import ReCAPTCHA from 'react-google-recaptcha';
import recordConversion from 'utils/recordConversion';
+import validateEmail from 'utils/validateEmail';
const customStyles = {
overlay: {
@@ -12,7 +14,7 @@ const customStyles = {
left: '50%',
right: 'auto',
bottom: 'auto',
- marginRight: '-50%',
+ margin: '0 auto',
transform: 'translate(-50%, -50%)',
border: 'none',
},
@@ -23,17 +25,52 @@ const ModalContext = createContext({
});
const ModalProvider = ({ children }) => {
+ const reCaptchaRef = useRef(null);
+
const [currentModal, setCurrentModal] = useState(null);
const modalIsOpen = currentModal !== null;
- const closeModal = () => setCurrentModal(null);
const [email, setEmail] = useState(null);
+ const [reCaptchaValidated, setReCaptchaValidated] = useState(false);
+ const [emailError, setEmailError] = useState('');
const [submitting, setSubmitting] = useState(false);
+
+ const closeModal = () => {
+ setCurrentModal(null);
+ setEmail(null);
+ setSubmitting(false);
+ setEmailError('');
+ };
+
+ const onReCaptchaSuccess = async (captchaCode) => {
+ if (!captchaCode) {
+ return;
+ }
+
+ setReCaptchaValidated(true);
+ };
+
+ const onBlur = () => {
+ if (!email) {
+ setEmailError('Please enter your email address');
+ } else if (!validateEmail(email)) {
+ setEmailError('Invalid email address');
+ } else {
+ setEmailError('');
+ }
+ };
+
const onSubmit = async (e) => {
e.preventDefault();
+
+ if (!validateEmail(email) || !reCaptchaValidated) {
+ return;
+ }
+
recordConversion();
+ setSubmitting(true);
+
try {
- setSubmitting(true);
await fetch('/api/emails', {
method: 'POST',
redirect: 'follow',
@@ -43,61 +80,82 @@ const ModalProvider = ({ children }) => {
},
body: JSON.stringify({ email }),
});
+ } finally {
+ setSubmitting(false);
+ reCaptchaRef.current.reset();
+
const url = new URL(window.location.href);
url.searchParams.set('note', 'thankyou');
window.location.replace(url.toString());
- } finally {
- setSubmitting(false);
}
};
return (
-
- {children}
-
- {currentModal === 'requestPricingInfo' && (
-
-
-
-
-
-
- )}
-
+ {emailError && (
+
+ {emailError}
+
+ )}
+
+
+
+
+
+
+ )}
+
+ >
);
};
diff --git a/package.json b/package.json
index 82fe4d5..7537e19 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"react": "17.0.2",
"react-calendly": "^4.1.1",
"react-dom": "17.0.2",
+ "react-google-recaptcha": "^3.1.0",
"react-modal": "^3.16.1",
"react-remarkable": "^1.1.3",
"react-swipeable": "^6.2.0",
diff --git a/styles/components/SignUpForm.scss b/styles/components/SignUpForm.scss
index 3048c7e..d25265c 100644
--- a/styles/components/SignUpForm.scss
+++ b/styles/components/SignUpForm.scss
@@ -7,6 +7,24 @@
background-color: transparent;
}
+ &__button:disabled {
+ color: color('gray');
+ cursor: not-allowed;
+ }
+
+ &__error {
+ color: color('error');
+ }
+
+ &__button--close {
+ color: color('black');
+ }
+
+ &__reCaptcha {
+ transform: scale(0.85);
+ transform-origin: 0 0;
+ }
+
&--overlay-mode {
&__input,
&__button {
diff --git a/styles/config.scss b/styles/config.scss
index 010dd0e..7b28b9a 100644
--- a/styles/config.scss
+++ b/styles/config.scss
@@ -3,6 +3,7 @@ $colors: (
'gray-darkest': #141414,
'gray': #757575,
'white': #ffffff,
+ 'error': #900,
);
$breakpoints: (
diff --git a/utils/validateEmail.js b/utils/validateEmail.js
new file mode 100644
index 0000000..ef40149
--- /dev/null
+++ b/utils/validateEmail.js
@@ -0,0 +1,7 @@
+const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+const validateEmail = (email) => {
+ return emailRegex.test(email);
+};
+
+export default validateEmail;
diff --git a/yarn.lock b/yarn.lock
index f6a3b58..298e343 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2519,6 +2519,13 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^3.3.0:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz"
@@ -4025,7 +4032,7 @@ promise-inflight@^1.0.1:
resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
-prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.5.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -4164,6 +4171,14 @@ rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-async-script@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
+ integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==
+ dependencies:
+ hoist-non-react-statics "^3.3.0"
+ prop-types "^15.5.0"
+
react-calendly@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/react-calendly/-/react-calendly-4.1.1.tgz#25ac2ff3e062e4c6a0aefb83b851bebf66a94a1c"
@@ -4178,7 +4193,15 @@ react-dom@17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
-react-is@^16.13.1:
+react-google-recaptcha@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab"
+ integrity sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==
+ dependencies:
+ prop-types "^15.5.0"
+ react-async-script "^1.2.0"
+
+react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==