Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversion to sanity #2

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
<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 @@
</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;
}
}
Loading