Skip to content

Commit

Permalink
Add mailchimp newsletter subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
nuno-aac committed Nov 9, 2023
1 parent 84518f7 commit 49c9661
Show file tree
Hide file tree
Showing 16 changed files with 363 additions and 16 deletions.
1 change: 1 addition & 0 deletions .github/workflows/website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
Expand Down
17 changes: 17 additions & 0 deletions workspaces/website/src/api/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* `projectId` constant.
*/

export const projectId = 'ordinal-cacao-370307';

/**
* `mailchimpServer` constant.
*/

export const mailchimpServer = 'us12';

/**
* `mailchimpList` constant.
*/

export const mailchimpList = 'a7c9440979';
50 changes: 50 additions & 0 deletions workspaces/website/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -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" });
Expand Down Expand Up @@ -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!'
}
));
}
}
)
1 change: 1 addition & 0 deletions workspaces/website/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const Button = forwardRef<HTMLButtonElement, props>(
ref={ref}
href={href}
{...rest}
size={'sm'}
>
{children}
</ChakraButton>
Expand Down
1 change: 1 addition & 0 deletions workspaces/website/src/components/Layout/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Props = {
declare global {
interface Window {
gtag: any;
grecaptcha: any;
}
}

Expand Down
2 changes: 1 addition & 1 deletion workspaces/website/src/dev-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,7 +35,7 @@ export default function RoadmapLayout({
{...roadmapSettings?.show_hero_cta && { buttonText: roadmapSettings?.hero_cta_copy} }
onButtonClick={() => setIsOpen(true)}
/>}
<RoadmapSubscribeForm isOpen={isOpen} setIsOpen={setIsOpen} />
<RoadmapSubscribeForm env={env} isOpen={isOpen} setIsOpen={setIsOpen} />
<Box my="56px"></Box>
{/* <Flex
as="ul"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,186 @@
/**
* Module dependencies.
*/

import { Button } from '@ui/Button';
import {
FormControl,
FormLabel,
Input,
Modal,
ModalOverlay,
ModalContent,
ModalCloseButton,
} from "@chakra-ui/react";
Container,
useToast,
} from '@chakra-ui/react';

import { Heading } from '@ui/Typography/Heading';
import { Text } from '@ui/Typography/Text';
import { useRef, useState } from 'react';
import axios from 'axios';
import qs from 'qs';
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'

/**
* `RoadmapSubscribeForm` props.
*/

type RoadmapSubscribeFormProps = {
env: {
CLOUDFLARE_RECAPTCHA_KEY: string;
}
isOpen: boolean;
setIsOpen: (b: boolean) => 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<TurnstileInstance | undefined>(null);

const handleClose = () => {
setIsOpen(false);
setIsSuccess(false);
};

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
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 (
<Modal isOpen={isOpen} onClose={handleClose}>
<ModalOverlay />
<ModalContent maxWidth="400px">
<ModalContent
height={'440px'}
maxWidth={'410px'}
overflow={'auto'}
>
<ModalCloseButton />
<iframe
src="https://cdn.forms-content.sg-form.com/2fb14a4a-f24d-11ed-97a1-2255cc459392"
style={{ width: "100%", height: "420px" }}
frameBorder="0"
/>

<Container
padding={'60px 40px 40px !important'}
>
{!isSuccess ? (
<>
<Heading
variant={'h3'}
color={'heading-navy-fg'}
paddingBottom={'16px'}
>
Starknet Dev News ✨🗞️
</Heading>

<Text
variant={'cardBody'}
color={'heading-navy-fg'}
paddingBottom={'32px'}
>
Receive notifications on Starknet version updates and network status.
</Text>

<form onSubmit={handleSubmit}>
<FormControl isRequired paddingBottom={'20px'}>
<FormLabel fontWeight={'700'}>
Email
</FormLabel>

<Input
type={'email'}
name={'email'}
id={'email'}
placeholder={'Please enter your email'}
/>
</FormControl>

<Turnstile
options={{
size: 'invisible'
}}
ref={captchaRef}
siteKey={env.CLOUDFLARE_RECAPTCHA_KEY}
/>

<Button
fontSize={'14px'}
isLoading={isLoading}
marginTop={'20px'}
padding={'6px 16px'}
type={'submit'}
variant={'solid'}
>
Submit
</Button>
</form>
</>
) : (
<div style={{ textAlign: 'center' }}>
<Text
variant={'cardBody'}
color={'heading-navy-fg'}
>
Thank you and may the STARK be with you ✨🗞️
</Text>
</div>
)}
</Container>

{/*<iframe
src='https://cdn.forms-content.sg-form.com/2fb14a4a-f24d-11ed-97a1-2255cc459392'
style={{ width: '100%', height: '420px' }}
frameBorder='0'
/>*/}
</ModalContent>
</Modal>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import AnnouncementPostCard from "./AnnouncementPostCard";
export type AnnouncementsPageProps = {
announcements: readonly AnnouncementDetails[];
locale: string;
env: {
CLOUDFLARE_RECAPTCHA_KEY: string;
}
};
export default function AnnouncementsPage({
announcements,
env,
locale,
}: AnnouncementsPageProps) {
return (
<RoadmapLayout locale={locale} mode="ANNOUNCEMENTS">
<RoadmapLayout env={env} locale={locale} mode="ANNOUNCEMENTS">
<Heading variant="h3" mb="2rem">
Announcements
</Heading>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export async function onBeforeRender(pageContext: PageContextServer) {

const pageProps: AnnouncementsPageProps = {
announcements,
env: {
CLOUDFLARE_RECAPTCHA_KEY: import.meta.env.VITE_CLOUDFLARE_RECAPTCHA_KEY,
},
locale,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ export type RoadmapPageProps = {
roadmapVersions: readonly RoadmapVersion[];
roadmapSettings: Roadmap;
locale: string;
env: {
CLOUDFLARE_RECAPTCHA_KEY: string;
}
};
export default function RoadmapPage({
env,
roadmapPosts,
roadmapVersions,
roadmapSettings,
Expand All @@ -39,7 +43,7 @@ export default function RoadmapPage({
}, [roadmapPosts]);

return (
<RoadmapLayout locale={locale} mode="ROADMAP" roadmapSettings={roadmapSettings}>
<RoadmapLayout env={env} locale={locale} mode="ROADMAP" roadmapSettings={roadmapSettings}>
{roadmapStagesFields.map((stage) => {
const stagePosts = roadmapPostsByStage[stage.value] || [];

Expand Down
Loading

0 comments on commit 49c9661

Please sign in to comment.