Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💄 Modal pop-ups for Subscriptions #1756

Merged
merged 12 commits into from
May 13, 2024
150 changes: 120 additions & 30 deletions components/forms/EmailForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import React, { useState } from 'react'
import styled, { css } from 'styled-components'
import { addToMailchimp } from '../../utils'
import { Input, Button } from '../ui'
import ModalConfirmation from 'components/ui/ModalConfirmation'
import useToggle from '../../utils/useToggle';
import { useRouter } from 'next/router'


interface EmailFormProps {
isFooter: boolean
Expand All @@ -10,24 +14,37 @@ interface EmailFormProps {
export const EmailForm = (props: EmailFormProps) => {
const [email, setEmail] = useState('')
const [isEntering, setIsEntering] = useState(false)
const [isProcessing, setIsProcessing] = useState(false);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
addToMailchimp(email)
.then((data: any) => {
alert(data.msg)
})
.catch((error: Error) => {
// Errors in here are client side
// Mailchimp always returns a 200
if (error.message === 'Timeout') {
alert(
'Looks like your browser is blocking this. Try to disable any tracker-blocking feature and resubmit.'
)
}
console.error(error)
})
const [isSuccessOpen, toggleSuccess] = useToggle(false);
const [isErrorOpen, toggleError] = useToggle(false);
const [isDuplicateOpen, toggleDuplicate] = useToggle(false);


const {push} = useRouter()

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsProcessing(true);
try {
const result = await addToMailchimp(email);
if (result.result === 'success') {
toggleSuccess();
} else {
if(result.message === 'Bad Request'){
toggleDuplicate();
}
else
{
toggleError();
}
}
} catch (error) {
console.error('Error submitting email:', error);
toggleError();
}
setIsProcessing(false);
};

const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsEntering(true)
Expand All @@ -47,46 +64,119 @@ export const EmailForm = (props: EmailFormProps) => {
type="text"
onChange={handleEmailChange}
onFocus={handleEmailChange}
disabled= {isProcessing}
/>
{props.isFooter ? (
isEntering && (
<Button type="submit" color="orange" size="small">
Subscribe
{isEntering && (
<Button
type="submit"
color="orange"
disabled={isProcessing}
>
{isProcessing ? (
<>
<svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="white" strokeWidth="4" fill="none" strokeDasharray="80" strokeDashoffset="60" />
</svg>
Processing...
</>
) : 'Subscribe'}
</Button>
)
) : (
<Button type="submit" color="orange" size="small">
Subscribe
</Button>
)}
{isSuccessOpen && (
<ModalConfirmation
isOpen={isSuccessOpen}
onClose={toggleSuccess}
body={
<div className="p-6 m-0">
<h1 className="font-tuner inline-block text-4xl lg:text-3xl lg:leading-tight bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 bg-clip-text text-transparent m-0">
Welcome Aboard!
</h1>
<p className="text-base lg:text-md mb-4">
You've been added to the llama list.
</p>
<div className="flex justify-end items-center m-0 py-2">
<Button color="blue" size="medium" onClick={toggleSuccess}>
OK
</Button>
</div>
</div>
}
/>
)}

{isErrorOpen && (
<ModalConfirmation
isOpen={isErrorOpen}
onClose={toggleError}
body={
<div className="p-6">
<h1 className="font-tuner inline-block text-4xl lg:text-3xl lg:leading-tight bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 bg-clip-text text-transparent m-0">
Hold your llamas!
</h1>
<p className="text-base lg:text-md mb-4">
Couldn't sign you up. Please retry or contact us!
</p>
<div className="flex justify-end">
<Button color="orange" size="medium" onClick={toggleError}>
GO BACK
</Button>
</div>
</div>
}
/>
)}

{isDuplicateOpen && (
<ModalConfirmation
isOpen={isDuplicateOpen}
onClose={toggleDuplicate}
body={
<div className="p-6">
<h1 className="font-tuner inline-block text-4xl lg:text-3xl lg:leading-tight bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 bg-clip-text text-transparent m-0">
Already Subscribed
</h1>
<p className="text-base lg:text-md mb-4">
You're already in our herd! Missing our emails? Let's fix that!
</p>
<div className="flex justify-end">
<Button color="white" size="medium" onClick={toggleDuplicate} className="mr-4">
GO BACK
</Button>
<Button color="blue" size="medium" onClick={ () =>
{
push('docs/support');
toggleDuplicate();
}}>
CONTACT US
</Button>
</div>
</div>
}
/>
)}
</StyledEmailForm>
)
}

EmailForm.defaultProps = {
isFooter: false,
}

interface StyledEmailFormProps {
isFooter?: boolean
isEntering: boolean
}

const StyledEmailForm = styled.form<StyledEmailFormProps>`
display: flex;
align-items: center;
width: 100%;
max-width: 38rem;
padding: 0;
gap: 0.5rem;

${(props) =>
props.isEntering &&
css`
grid-gap: 0.5rem;
align-items: center;
`}

${(props) =>
props.isFooter &&
css`
Expand Down
1 change: 1 addition & 0 deletions components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
href?: string
type?: 'button' | 'submit' | 'reset'
children: React.ReactNode | React.ReactNode[]
disabled?: boolean
}

const baseClasses =
Expand Down
27 changes: 27 additions & 0 deletions components/ui/ModalConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useState } from 'react';
import Modal from 'react-responsive-modal';
import 'react-responsive-modal/styles.css';

interface ModalConfirmationProps {
isOpen: boolean;
onClose: () => void;
body: React.ReactNode;
}

const ModalConfirmation: React.FC<ModalConfirmationProps> = ({ isOpen, onClose, body }) => {
return (
<Modal
open={isOpen}
onClose={onClose}
center
classNames={{
overlay: 'bg-gray-400 bg-opacity-80',
modal: 'bg-white w-11/12 sm:w-3/4 md:w-2/3 lg:w-1/3 max-w-5xl rounded-2xl p-4 text-left',
}}
>
<div>{body}</div>
</Modal>
);
};

export default ModalConfirmation;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"analytics:export:search": "node ./scripts/analytics/exportSearch.js"
},
"dependencies": {
"@mailchimp/mailchimp_marketing": "^3.0.80",
"@next/bundle-analyzer": "^10.0.0",
"@react-hook/window-size": "^3.1.1",
"@tailwindcss/aspect-ratio": "^0.4.2",
Expand Down Expand Up @@ -73,6 +74,7 @@
"react-instantsearch-dom": "^6.3.0",
"react-intersection-observer": "^9.4.0",
"react-markdown": "^8.0.3",
"react-responsive-modal": "^6.4.2",
"react-tinacms-editor": "^0.53.26",
"react-use-size": "^3.0.1",
"rehype-raw": "^6.1.1",
Expand Down
23 changes: 23 additions & 0 deletions pages/api/mailchimp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mailchimp from '@mailchimp/mailchimp_marketing'

export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}

mailchimp.setConfig({
apiKey: process.env.MAILCHIMP_API_KEY,
server: process.env.MAILCHIMP_SERVER_PREFIX,
})

const { email_address, status, merge_fields } = req.body
try {
const response = await mailchimp.lists.addListMember(
process.env.MAILCHIMP_AUDIENCE_ID,
{ email_address, status, merge_fields }
)
res.status(200).json({ success: true, response })
} catch (err) {
res.status(500).json({ error: true, message: err.message })
}
}
2 changes: 1 addition & 1 deletion tina/tina-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,5 @@
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
"**/*.tsx" ]
}
92 changes: 35 additions & 57 deletions utils/mailchimp_helper.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,43 @@
/*
** Adapted from @benjaminhoffman's gatsby-plugin-mailchimp
*/

import jsonp from 'jsonp'
import { validate } from 'email-validator'

/**
* Make a jsonp request to user's mailchimp list
* `param` object avoids CORS issues
* timeout to 3.5s so user isn't waiting forever
* usually occurs w/ privacy plugins enabled
* 3.5s is a bit longer than the time it would take on a Slow 3G connection
*
* @param {String} url - concatenated string of user's gatsby-config.js
* options, along with any MC list fields as query params.
*
* @return {Promise} - a promise that resolves a data object
* or rejects an error object
*/

const subscribeEmailToMailchimp = url =>
new Promise((resolve, reject) =>
jsonp(url, { param: 'c', timeout: 3500 }, (err, data) => {
if (err) reject(err)
if (data) resolve(data)
})
)

/**
* Subscribe an email address to a Mailchimp email list.
* We use ES5 function syntax (instead of arrow) because we need `arguments.length`
*
* @param {String} email - required; the email address you want to subscribe
* NOTE: For the EmailForm, I removed fields and endpointOverride since we
* weren't using them. check the original source code if they are needed
* https://github.com/benjaminhoffman/gatsby-plugin-mailchimp/blob/master/src/index.js
* @return {Object} -
* {
* result: <String>(`success` || `error`)
* msg: <String>(`Thank you for subscribing!` || `The email you entered is not valid.`),
* }
*/
interface SubscriptionResult {
result: 'success' | 'error'
message: string
}

export const addToMailchimp = function addToMailchimp(email) {
const isEmailValid = validate(email)
const emailEncoded = encodeURIComponent(email)
if (!isEmailValid) {
return Promise.resolve({
export async function addToMailchimp(
email: string
): Promise<SubscriptionResult> {
if (!validate(email)) {
return {
result: 'error',
msg: 'The email you entered is not valid.',
})
message: 'The email you entered is not valid.',
}
}

// eslint-disable-next-line no-undef
let endpoint = process.env.MAILCHIMP_ADDRESS

// Generates MC endpoint for our jsonp request. We have to
// change `/post` to `/post-json` otherwise, MC returns an error
endpoint = endpoint.replace(/\/post/g, '/post-json')
const queryParams = `&EMAIL=${emailEncoded}`
const url = `${endpoint}${queryParams}`
try {
const response = await fetch('/api/mailchimp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email_address: email,
status: 'subscribed',
merge_fields: {},
}),
})

return subscribeEmailToMailchimp(url)
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || 'Failed to add email to the list.')
}
return {
result: 'success',
message: 'Email successfully added to the list.',
}
} catch (error) {
return {
result: 'error',
message: error.message || 'Failed to add email to the list.',
}
}
}
10 changes: 10 additions & 0 deletions utils/useToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Reducer, useReducer } from 'react';

const toggleReducer = (state: boolean, nextValue?: any) =>
typeof nextValue === 'boolean' ? nextValue : !state;

const useToggle = (initialValue: boolean): [boolean, (nextValue?: any) => void] => {
return useReducer<Reducer<boolean, any>>(toggleReducer, initialValue);
};

export default useToggle;