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' && ( - -
- -
-
- setEmail(e.target.value)} - className="SignUpForm__input SignUpForm--day-mode__input small p0 w100 inline-block" + className="SignUpForm__input SignUpForm--day-mode__input small p0 w100 mt1 inline-block" type="email" placeholder="hello@example.com" /> - -
- -
-
-
- )} -
+ {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==