From c4fa82a6e73a85496cf55a44005beb0678e5b6d8 Mon Sep 17 00:00:00 2001 From: nuno-aac Date: Thu, 9 Nov 2023 01:25:55 +0000 Subject: [PATCH] Add mailchimp newsletter subscription --- .github/workflows/website.yml | 1 + package.json | 1 + workspaces/website/src/api/index.ts | 71 ++++++++ workspaces/website/src/dev-server/index.ts | 2 +- .../(components)/roadmap/RoadmapLayout.tsx | 6 +- .../roadmap/RoadmapSubscribeForm.tsx | 163 +++++++++++++++++- .../(components)/AnnouncementsPage.tsx | 6 +- .../pages/announcements/index.page.server.tsx | 3 + .../roadmap/(components)/RoadmapPage.tsx | 6 +- .../src/pages/roadmap/index.page.server.tsx | 3 + .../website/src/style/algolia/overrides.css | 6 +- workspaces/website/src/types.d.ts | 3 + yarn.lock | 11 ++ 13 files changed, 270 insertions(+), 12 deletions(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index de11822ade..ef59760f3c 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -64,6 +64,7 @@ jobs: VITE_ALGOLIA_INDEX: ${{ github.ref_name == 'production' && 'production' || 'dev' }} VITE_ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} VITE_ALGOLIA_SEARCH_API_KEY: ${{ secrets.ALGOLIA_SEARCH_API_KEY }} + VITE_CLOUDFLARE_RECAPTCHA_KEY: ${{ secrets.CLOUDFLARE_RECAPTCHA_KEY }} VITE_CF_STREAM_URL: ${{ secrets.CF_STREAM_URL }} VITE_ED_VIDEO_ID_1: ${{ secrets.VITE_ED_VIDEO_ID_1 }} VITE_ED_VIDEO_ID_2: ${{ secrets.VITE_ED_VIDEO_ID_2 }} diff --git a/package.json b/package.json index 5d13a61d44..9b059eddcc 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@fontsource/tajawal": "^4.5.9", "@headlessui/react": "^1.7.14", "@heroicons/react": "^2.0.17", + "@marsidev/react-turnstile": "^0.3.2", "@starknet-io/cms-data": "workspace:*", "@starknet-io/cms-utils": "workspace:*", "@types/cors": "^2.8.13", diff --git a/workspaces/website/src/api/index.ts b/workspaces/website/src/api/index.ts index 158f10b4fa..14c780bdb6 100644 --- a/workspaces/website/src/api/index.ts +++ b/workspaces/website/src/api/index.ts @@ -1,3 +1,7 @@ +/** + * Module dependencies. + */ + import { Router, createCors, error, json } from 'itty-router' // now let's create a router (note the lack of "new") @@ -48,3 +52,70 @@ apiRouter.get( ); } ); + +/** + * Newsletter subscribe api route. + */ + +apiRouter.post( + '/newsletter-subscribe', + async (req, env: PAGES_VARS) => { + try { + const formData = new FormData(); + + formData.append('secret', env.CLOUDFLARE_RECAPTCHA_KEY); + formData.append('response', req.query.token as string); + + const captchaUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; + const captchaResponse = await fetch(captchaUrl, { + body: formData, + method: 'POST', + }); + + const captchaResult = await captchaResponse.json() as { success: boolean }; + + if (!captchaResult.success) { + return corsify(error( + 422, + { title: 'Invalid Captcha' } + )); + } + + const mailchimpResponse = await fetch( + env.MAILCHIMP_NEWSLETTER_URL, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${btoa(`anystring:${env.MAILCHIMP_API_KEY}`)}` + }, + body: JSON.stringify({ + email_address: req.query.email as string, + status: 'subscribed', + }) + } + ) + + const mailchimpResult = await mailchimpResponse.json() as any; + + if(mailchimpResponse.ok !== true) { + return corsify(error( + mailchimpResponse.status, + mailchimpResult + )); + } + + return corsify(json({ + message: 'Successfully subscribed to newsletter!', + ...mailchimpResult + })); + } catch (err) { + return corsify(error( + (err as any)?.status ?? 500, + (err as any)?.response?.text ? JSON.parse((err as any)?.response?.text) : { + error: 'Internal Server Error' + } + )); + } + } +) diff --git a/workspaces/website/src/dev-server/index.ts b/workspaces/website/src/dev-server/index.ts index d72fe459fc..f4427ba1b5 100644 --- a/workspaces/website/src/dev-server/index.ts +++ b/workspaces/website/src/dev-server/index.ts @@ -38,7 +38,7 @@ app.all(/\/api(.*)/, async (req, res, next) => { res.header(key, value); }); - res.send(await httpResponse.text()); + res.status(httpResponse.status).send(await httpResponse.text()); } else { res.send("API!"); } diff --git a/workspaces/website/src/pages/(components)/roadmap/RoadmapLayout.tsx b/workspaces/website/src/pages/(components)/roadmap/RoadmapLayout.tsx index 3086bded03..f1ee25063a 100644 --- a/workspaces/website/src/pages/(components)/roadmap/RoadmapLayout.tsx +++ b/workspaces/website/src/pages/(components)/roadmap/RoadmapLayout.tsx @@ -11,9 +11,13 @@ type RoadmapLayoutProps = { mode: "ROADMAP" | "ANNOUNCEMENTS"; locale: string; roadmapSettings?: Roadmap; + env: { + CLOUDFLARE_RECAPTCHA_KEY: string; + } }; export default function RoadmapLayout({ children, + env, mode, locale, roadmapSettings @@ -31,7 +35,7 @@ export default function RoadmapLayout({ {...roadmapSettings?.show_hero_cta && { buttonText: roadmapSettings?.hero_cta_copy} } onButtonClick={() => setIsOpen(true)} />} - + {/* void; }; + +/** + * `RoadmapSubscribeForm` component. + */ + function RoadmapSubscribeForm({ + env, isOpen, setIsOpen, }: RoadmapSubscribeFormProps) { + const toast = useToast(); + const [formState, setFormState] = useState<'submitting' | 'success' | null>(null); + const captchaRef = useRef(null); + const handleClose = () => { setIsOpen(false); + setFormState(null); }; + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setFormState('submitting'); + + try { + const token = captchaRef.current?.getResponse(); + + await axios.post(`/api/newsletter-subscribe?${qs.stringify({ + email: (event.target as any)[0].value, + token, + })}`) + + captchaRef.current?.reset(); + setFormState('success'); + } catch (error: any) { + setFormState(null); + captchaRef.current?.reset(); + + const toastErrorConfig = { + duration: 1500, + isClosable: true, + status: 'error' as 'error', + }; + + if(error.response.data?.title === 'Invalid Captcha') { + toast({ + description: 'We\'re having trouble verifying you\'re a human. Please try again.', + ...toastErrorConfig + }); + + return; + } + + if(error.response.data?.title === 'Member Exists') { + toast({ + description: 'You are already subscribed to the newsletter.', + ...toastErrorConfig + }); + + return; + } + + toast({ + description: 'There was an issue subscribing you to the newsletter.', + ...toastErrorConfig + }); + } + } + return ( - + -