diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 6642468cefb..7fb4c4f3419 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.NEW_CF_STREAM_URL }} VITE_ED_VIDEO_ID_1: ${{ secrets.NEW_VITE_ED_VIDEO_ID_1 }} VITE_ED_VIDEO_ID_2: ${{ secrets.NEW_VITE_ED_VIDEO_ID_2 }} diff --git a/package.json b/package.json index 5d13a61d446..71aff79a81f 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", + "@mailchimp/mailchimp_marketing": "^3.0.80", "@starknet-io/cms-data": "workspace:*", "@starknet-io/cms-utils": "workspace:*", "@types/cors": "^2.8.13", @@ -113,6 +114,8 @@ }, "devDependencies": { "@chakra-ui/cli": "^2.4.0", + "@marsidev/react-turnstile": "^0.3.2", + "@types/mailchimp__mailchimp_marketing": "^3.0.17", "@types/youtube-player": "^5.5.7", "eslint-plugin-storybook": "^0.6.11" } diff --git a/workspaces/website/src/api/constants.ts b/workspaces/website/src/api/constants.ts new file mode 100644 index 00000000000..d882fd2a901 --- /dev/null +++ b/workspaces/website/src/api/constants.ts @@ -0,0 +1,17 @@ +/** + * `projectId` constant. + */ + +export const projectId = 'ordinal-cacao-370307'; + +/** + * `mailchimpServer` constant. + */ + +export const mailchimpServer = 'us12'; + +/** + * `mailchimpList` constant. + */ + +export const mailchimpList = 'a7c9440979'; diff --git a/workspaces/website/src/api/index.ts b/workspaces/website/src/api/index.ts index 158f10b4fa2..8026807bee2 100644 --- a/workspaces/website/src/api/index.ts +++ b/workspaces/website/src/api/index.ts @@ -1,4 +1,7 @@ import { Router, createCors, error, json } from 'itty-router' +import { mailchimpList, mailchimpServer } from './constants'; +import axios from 'axios'; +import mailchimp from '@mailchimp/mailchimp_marketing' // now let's create a router (note the lack of "new") export const apiRouter = Router({ base: "/api" }); @@ -48,3 +51,50 @@ apiRouter.get( ); } ); + +/** + * Newsletter subscribe api route + */ + +apiRouter.post( + '/newsletter-subscribe', + async (req, env: PAGES_VARS) => { + await mailchimp.setConfig({ + apiKey: env.MAILCHIMP_API_KEY, + server: mailchimpServer, + }); + + try { + let formData = new FormData(); + formData.append('secret', env.CLOUDFLARE_RECAPTCHA_KEY); + formData.append('response', req.query.token as string); + + const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; + const result = await axios.post(url, formData); + + if (result?.data?.success !== true) { + return corsify(error( + 422, + { title: 'Invalid Captcha' } + )); + } + + const response = await mailchimp.lists.addListMember(mailchimpList, { + email_address: req.query.email as string, + status: 'subscribed', + }); + + return corsify(json({ + message: 'Successfully subscribed to newsletter!', + ...response + })); + } catch (err) { + return corsify(error( + (err as any)?.status ?? 500, + (err as any)?.response?.text ? JSON.parse((err as any)?.response?.text) : { + title: 'Internal server error!' + } + )); + } + } +) diff --git a/workspaces/website/src/components/Button/Button.tsx b/workspaces/website/src/components/Button/Button.tsx index 1114caa4638..66a9c26aa5b 100644 --- a/workspaces/website/src/components/Button/Button.tsx +++ b/workspaces/website/src/components/Button/Button.tsx @@ -42,6 +42,7 @@ export const Button = forwardRef( ref={ref} href={href} {...rest} + size={'sm'} > {children} diff --git a/workspaces/website/src/components/Layout/Navbar/Navbar.tsx b/workspaces/website/src/components/Layout/Navbar/Navbar.tsx index 5e16b700717..37d39fda7f1 100644 --- a/workspaces/website/src/components/Layout/Navbar/Navbar.tsx +++ b/workspaces/website/src/components/Layout/Navbar/Navbar.tsx @@ -36,6 +36,7 @@ type Props = { declare global { interface Window { gtag: any; + grecaptcha: any; } } diff --git a/workspaces/website/src/dev-server/index.ts b/workspaces/website/src/dev-server/index.ts index d72fe459fca..f4427ba1b55 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 3086bded034..f1ee25063a3 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 [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const captchaRef = useRef(null); + const handleClose = () => { setIsOpen(false); + setIsSuccess(false); }; + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + try { + setIsLoading(true) + const token = captchaRef.current?.getResponse(); + await axios.post(`/api/newsletter-subscribe?${qs.stringify({ + email: (event.target as any)[0].value, + token, + })}`) + + setIsLoading(false) + setIsSuccess(true) + } catch (error: any) { + setIsLoading(false) + if(error.response.data?.title === 'Invalid Captcha') { + toast({ + description: 'We\'re having trouble verifying you\'re a human. Please try again.', + duration: 1500, + isClosable: true, + status: 'error', + }); + + return; + } + + if(error.response.data?.title === 'Member Exists') { + toast({ + description: 'You are already subscribed to the newsletter.', + duration: 1500, + isClosable: true, + status: 'error', + }); + + return; + } + + toast({ + description: 'There was an issue subscribing you to the newsletter.', + duration: 1500, + isClosable: true, + status: 'error', + }); + } + } + return ( - + -