Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
drewlyton committed Dec 21, 2023
2 parents aebea18 + e253817 commit baf2c73
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 100 deletions.
23 changes: 12 additions & 11 deletions app/data/Newsletter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type Story from "./Story";

export type Newsletter = {
createdAt: string;
publishedAt: string;
id: string;
issueNumber: number;
messageBody: string;
preview: boolean;
_id: string;
_createdAt: string;
_updatedAt: string;
subject: string;
updatedAt: string;
preview: string;
body: string;
sendAt: string;
shouldSend: boolean;
sendGridId: string;
sendGridDesignId: string;
sendAt: string;
story: Story;
author: {
bio: string;
name: string;
image: string;
};
};
15 changes: 15 additions & 0 deletions app/data/sanity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const sanityClient = {
async mutate(mutations: object[]) {
return await fetch(
`https://${process.env.SANITY_PROJECT_ID}.api.sanity.io/v2021-06-07/data/mutate/production`,
{
method: "post",
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${process.env.SANITY_TOKEN}`
},
body: JSON.stringify({ mutations })
}
);
}
};
5 changes: 4 additions & 1 deletion app/emails/ConfirmSubscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export function ConfirmSubscription({ recipient }: { recipient: string }) {
<Text className="text-sm font-semibold mt-6 ">Best,</Text>
<Row className="mb-8">
<Column className="w-[40px]" valign="top">
<Img src={"https://www.drewis.cool/headshot.png"} width={"100%"} />
<Img
src={"https://www.drewis.cool/static/headshot.png"}
width={"100%"}
/>
</Column>
<Column>
<Text className="text-base ml-4 my-0">Drew Lyton</Text>
Expand Down
85 changes: 36 additions & 49 deletions app/emails/NewPostNewsletter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,29 @@ import {
Button,
Column,
Img,
Preview,
Row,
Section,
Text
} from "@react-email/components";
import { getMDXComponent } from "mdx-bundler/client";
import { PropsWithChildren } from "react";
import type { PropsWithChildren } from "react";
import type { Newsletter } from "~/data/Newsletter";
import type Story from "~/data/Story";

type NewsletterEmailProps = {
messageBody?: string[];
story?: Pick<
Story,
"title" | "description" | "featuredImage" | "slug" | "author"
>;
};
type NewsletterEmailProps = Pick<Newsletter, "body" | "author" | "preview">;

export function NewPostNewsletter({
messageBody = [
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
],
story = {
title: "Latest post title",
description: "Latest post description",
featuredImage: {
url: "https://media.graphassets.com/Eq1aApzqQ2iRqhxKjEBb"
},
slug: "test-slug",
author: {
bio: "Test bio",
name: "Drew Lyton",
picture: { url: "https://www.drewis.cool/headshot.png" }
}
}
body,
author,
preview
}: NewsletterEmailProps) {
const MDXComponentAboveFold = getMDXComponent(messageBody[0]);
const MDXComponentBelowFold = getMDXComponent(messageBody[1]);
const MDXComponentAboveFold = getMDXComponent(body);
const MDXComponents = { p: MDXText, blockquote: MDXBlockQuote };

return (
<>
<Preview>{preview}</Preview>
<Section>
<Row>
<Text className="text-2xl font-serif font-semibold">
Expand All @@ -51,33 +33,17 @@ export function NewPostNewsletter({
<MDXComponentAboveFold components={MDXComponents} />
</Row>
</Section>
<Button
href={`https://www.drewis.cool/story/${story.slug}?ref=newsletter`}
className="text-black"
>
<Section>
<div className="border border-solid rounded-md border-gray-300 my-6">
<Img
src={story.featuredImage.url}
className="rounded-t-md"
width={"100%"}
/>
<Text className="text-xl px-5 font-bold">{story.title}</Text>
<Text className="px-5 text-base">{story.description}</Text>
</div>
</Section>
</Button>
<MDXComponentBelowFold components={MDXComponents} />
<Text className="text-base mb-2">Until next time,</Text>
<Row className="mt-6 mb-8">
<Column className="w-[40px]" valign="top">
<Img src={story.author.picture.url} width={"100%"} />
<Img
src="https://www.drewis.cool/static/headshot.png"
width={"100%"}
/>
</Column>
<Column valign="top">
<Text className="text-lg font-semibold ml-4 my-0">
{story.author.name}
</Text>
<Text className="text-base ml-4 my-0">{story.author.bio}</Text>
<Text className="text-lg font-semibold ml-4 my-0">{author.name}</Text>
<Text className="text-base ml-4 my-0">{author.bio}</Text>
</Column>
</Row>
</>
Expand All @@ -95,3 +61,24 @@ const MDXBlockQuote: React.FC<PropsWithChildren> = ({ children }) => {
</blockquote>
);
};

const MDXStory: React.FC<{ story: Story }> = ({ story }) => {

Check warning on line 65 in app/emails/NewPostNewsletter.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'MDXStory' is assigned a value but never used
return (
<Button
href={`https://www.drewis.cool/story/${story.slug}?ref=newsletter`}
className="text-black"
>
<Section>
<div className="border border-solid rounded-md border-gray-300 my-6">
<Img
src={story.featuredImage.url}
className="rounded-t-md"
width={"100%"}
/>
<Text className="text-xl px-5 font-bold">{story.title}</Text>
<Text className="px-5 text-base">{story.description}</Text>
</div>
</Section>
</Button>
);
};
27 changes: 16 additions & 11 deletions app/routes/newsletter/$issueNumber/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { render } from "@react-email/render";
import { LoaderArgs, json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { bundleMDX } from "mdx-bundler";
import { useEffect } from "react";
import remarkGfm from "remark-gfm";
import GetNewsletter from "~/data/GetNewsletter";
import { Newsletter } from "~/data/Newsletter";
import { client } from "~/data/client";
import { EmailLayout } from "~/emails/EmailLayout";
import { NewPostNewsletter } from "~/emails/NewPostNewsletter";
import { getMessageBodyMarkdown } from "~/helpers/getMessageBodyMarkdown";
import { useTheme } from "~/helpers/useTheme";

export async function loader({ params }: LoaderArgs) {
Expand All @@ -24,16 +25,23 @@ export async function loader({ params }: LoaderArgs) {
);
if (newsletter.preview) throw new Response("Not found", { status: 404 });

const [messageAboveLink, messageBelowLink] = await getMessageBodyMarkdown(
newsletter.messageBody
);
const { code: messageBody } = await bundleMDX({
source: newsletter.body,
mdxOptions(options, frontmatter) {
// this is the recommended way to add custom remark/rehype plugins:
// The syntax might look weird, but it protects you in case we add/remove
// plugins in the future.
options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGfm];

return options;
}
});

return json({ messageAboveLink, messageBelowLink, newsletter });
return json({ messageBody, newsletter });
}

export default function ViewNewsletter() {
const { newsletter, messageAboveLink, messageBelowLink } =
useLoaderData<typeof loader>();
const { newsletter, messageBody } = useLoaderData<typeof loader>();

const { theme, toggleTheme } = useTheme();

Expand All @@ -47,10 +55,7 @@ export default function ViewNewsletter() {
dangerouslySetInnerHTML={{
__html: render(
<EmailLayout recipient="">
<NewPostNewsletter
{...newsletter}
messageBody={[messageAboveLink, messageBelowLink]}
/>
<NewPostNewsletter {...newsletter} body={messageBody} />
</EmailLayout>
)
}}
Expand Down
71 changes: 43 additions & 28 deletions app/routes/newsletter/webhook.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import { render } from "@react-email/render";
import { ActionArgs, json } from "@remix-run/node";
import { Newsletter } from "~/data/Newsletter";
import UpdateSendGridId from "~/data/UpdateSendGridId";
import { client } from "~/data/client";
import { bundleMDX } from "mdx-bundler";
import remarkGfm from "remark-gfm";
import type { Newsletter } from "~/data/Newsletter";
import { sanityClient } from "~/data/sanity";
import { sendgridClient } from "~/data/sendgrid";
import { EmailLayout } from "~/emails/EmailLayout";
import { NewPostNewsletter } from "~/emails/NewPostNewsletter";
import { getMessageBodyMarkdown } from "~/helpers/getMessageBodyMarkdown";

type GraphCMSWebhookBody = {
data?: Newsletter;
};

export async function action(args: ActionArgs) {
// Get Newsletter body
const { data: newsletter } =
(await args.request.json()) as GraphCMSWebhookBody;
const newsletter = (await args.request.json()) as Newsletter;
console.log({ newsletter });

// Throw if doesn't exist
if (!newsletter) return new Response("No request body", { status: 401 });
const designHTML = generateHTMLEmail(newsletter);
if (!newsletter) return new Response("No request body", { status: 200 });
const designHTML = await generateHTMLEmail(newsletter);
// Throw if newsletter is in preview mode
if (newsletter.preview)
if (!newsletter.shouldSend)
return new Response(
"Can't send a nesletter that's still in preview mode.",
{
Expand All @@ -37,7 +33,7 @@ export async function action(args: ActionArgs) {
url: "/v3/designs",
method: "POST",
body: {
name: `Issue #${newsletter.issueNumber}`,
name: `Issue #${newsletter._id}`,
html_content: designHTML,
editor: "code",
subject: newsletter.subject
Expand All @@ -46,11 +42,20 @@ export async function action(args: ActionArgs) {

if (designResponse.statusCode >= 300)
return new Response("Couldn't create design in sendgrid.", {
status: 501
status: 200
});

// Update the Newsletter with the design ID
client.request(UpdateSendGridId, { sendGridDesignId: design.id });
await sanityClient.mutate([
{
patch: {
id: newsletter._id,
set: {
sendGridDesignId: design.id
}
}
}
]);
newsletter.sendGridDesignId = design.id;
}

Expand All @@ -60,7 +65,7 @@ export async function action(args: ActionArgs) {
url: "/v3/marketing/singlesends",
method: "POST",
body: {
name: `Issue #${newsletter.issueNumber}`,
name: `Issue #${newsletter._id}`,
// Send 5 minutes from now in case need to cancel it
send_at: newsletter.sendAt,
send_to: {
Expand All @@ -79,15 +84,24 @@ export async function action(args: ActionArgs) {

if (sendResponse.statusCode >= 300)
return new Response("Couldn't create single send in sendgrid.", {
status: 501
status: 200
});
client.request(UpdateSendGridId, { sendGridId: singleSend.id });
await sanityClient.mutate([
{
patch: {
id: newsletter._id,
set: {
sendGridId: singleSend.id
}
}
}
]);
} else {
const [sendResponse, singleSend] = await sendgridClient.request({
url: `/v3/marketing/singlesends/${newsletter.sendGridId}`,
method: "PATCH",
body: {
name: `Issue #${newsletter.issueNumber}`,
name: `Issue #${newsletter._id}`,
// Send 5 minutes from now in case need to cancel it
send_at: newsletter.sendAt,
send_to: {
Expand All @@ -106,26 +120,27 @@ export async function action(args: ActionArgs) {

if (sendResponse.statusCode >= 300)
return new Response("Couldn't update single send in sendgrid.", {
status: 501
status: 200
});
}

return json({ ok: true });
}

export async function generateHTMLEmail(newsletter: Newsletter) {
const [messageAboveLink, messageBelowLink] = await getMessageBodyMarkdown(
newsletter.messageBody
);
const { code: messageBody } = await bundleMDX({
source: newsletter.body,
mdxOptions(options, frontmatter) {
options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGfm];
return options;
}
});

// Render HTML of latest newsletter
// use {{email}} for the recipient field
const html = render(
<EmailLayout recipient="{{email}}">
<NewPostNewsletter
{...newsletter}
messageBody={[messageAboveLink, messageBelowLink]}
/>
<NewPostNewsletter {...newsletter} body={messageBody} />
</EmailLayout>
);

Expand Down
2 changes: 2 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ declare namespace NodeJS {
SENDGRID_TEST_LIST: string;
CONVERTKIT_API_SECRET: string;
CONVERTKIT_STORY_FORM_ID: string;
SANITY_TOKEN: string;
SANITY_PROJECT_ID: string;
}
}

0 comments on commit baf2c73

Please sign in to comment.