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 168c401 commit c4fa82a
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 12 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.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 }}
Expand Down
1 change: 1 addition & 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",
"@marsidev/react-turnstile": "^0.3.2",
"@starknet-io/cms-data": "workspace:*",
"@starknet-io/cms-utils": "workspace:*",
"@types/cors": "^2.8.13",
Expand Down
71 changes: 71 additions & 0 deletions workspaces/website/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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'
}
));
}
}
)
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,181 @@
/**
* 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 { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef, useState } from 'react';
import axios from 'axios';
import qs from 'qs';

/**
* `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 [formState, setFormState] = useState<'submitting' | 'success' | null>(null);
const captchaRef = useRef<TurnstileInstance | undefined>(null);

const handleClose = () => {
setIsOpen(false);
setFormState(null);
};

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
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 (
<Modal isOpen={isOpen} onClose={handleClose}>
<ModalOverlay />
<ModalContent maxWidth="400px">
<ModalContent
height={'342px'}
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'}
>
{formState !== 'success' ? (
<>
<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={formState === 'submitting'}
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>
</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
3 changes: 3 additions & 0 deletions workspaces/website/src/pages/roadmap/index.page.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export async function onBeforeRender(pageContext: PageContextServer) {
const roadmapSettings = await getRoadmapSettings(locale, pageContext.context);

const pageProps: RoadmapPageProps = {
env: {
CLOUDFLARE_RECAPTCHA_KEY: import.meta.env.VITE_CLOUDFLARE_RECAPTCHA_KEY,
},
roadmapPosts,
roadmapVersions,
roadmapSettings,
Expand Down
6 changes: 5 additions & 1 deletion workspaces/website/src/style/algolia/overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ body.chakra-ui-dark {
--aa-item-background-rgb: 30, 29, 29;
}

.chakra-alert {
padding: 16px 32px !important;
}

body.chakra-ui-dark .aa-Input::placeholder {
color: rgba(var(--aa-primary-color-rgb), 1);
}
Expand Down Expand Up @@ -341,4 +345,4 @@ body.chakra-ui-dark .aa-Item[aria-selected=true] svg{
.aa-DetachedSearchButton {
background-color: transparent;
}
}
}
Loading

0 comments on commit c4fa82a

Please sign in to comment.