Skip to content

Commit

Permalink
Deploy blog email subscription functionality (#3029)
Browse files Browse the repository at this point in the history
This PR deploys https://github.com/WATonomous/mailing-list-gateway and
the associated frontend for subscribing to blog updates.

- [x] Configure SMTP (perhaps create a
`[email protected]` alias for the outgoing mail group
- [x] Add deployment manifests
- [x] Sentry Setup
- [x] Add metrics endpoint to Prometheus config
- [x] Create frontend. Some examples:
  - [substack](https://sub.thursdai.news/subscribe)
  - [couchdb](https://blog.couchdb.org/)
- [spotify](https://engineering.atspotify.com/) - can just put it in the
footer so that it shows up on every page -- "Sign up for engineering
updates"
  - [stripe](https://stripe.com/blog)
- [tailscale](https://tailscale.com/blog#blog-newsletter) - one large
centered panel at the bottom

<img width="1552" alt="image"
src="https://github.com/user-attachments/assets/ff11b084-1b90-4de3-b2e9-ded0cb470732">

Along with the following, blog infrastructure should be complete:
- Comment system: WATonomous/infra-config#2808
- Author profile: WATonomous/infra-config#2893
- Mailing list for blog updates:
  - WATonomous/infra-config#3024
  - This PR
  • Loading branch information
ben-z authored Aug 18, 2024
1 parent 60f21ed commit 7dcd1b1
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 59 deletions.
9 changes: 8 additions & 1 deletion components/blog-post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Link, useConfig } from "nextra-theme-docs";
import React from 'react';
import Picture from './picture';
import { GithubIcon, LinkIcon, LinkedinIcon, MailIcon, XIcon } from 'lucide-react';
import { Separator } from './ui/separator';
import { SubscribeDialog } from './blog';

// Reference for styling: https://github.com/vercel/turbo/blob/22585c9dcc23eb010ab01f177394358af03210d7/docs/pages/blog/turbo-1-10-0.mdx

Expand Down Expand Up @@ -131,5 +133,10 @@ export function BlogPostHeader() {
}

export function BlogPostFooter() {
return <CommentSection />
return <>
<Separator className="my-8" />
<SubscribeDialog />
<Separator className="my-8" />
<CommentSection />
</>
}
174 changes: 174 additions & 0 deletions components/blog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from "@/components/ui/alert-dialog"
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage
} from "@/components/ui/form"
import { websiteConfig } from '@/lib/data'
import { dayjsTz } from '@/lib/utils'
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from 'next/router'
import { MdxFile } from "nextra"
import { Link } from "nextra-theme-docs"
import { getPagesUnderRoute } from "nextra/context"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "./ui/button"
import { Input } from "./ui/input"

// Header and Index derived from https://github.com/vercel/turbo/blob/66196a70d02cddc8899ed1423684b1f716aa310e/docs/pages/blog.mdx
export function BlogHeader() {
return (
<div className="max-w-screen-lg mx-auto pt-4 pb-8 mb-16 border-b border-gray-400 border-opacity-20">
<h1>
<span className="font-bold leading-tight lg:text-5xl">Breadcrumbs</span>
</h1>
<p className="text-center text-gray-500 dark:text-gray-400 font-space-grotesk">
A record of the big and small things happening at WATcloud
</p>
</div>
);
}

export function BlogIndex() {
const { locale = websiteConfig.default_locale } = useRouter()

return getPagesUnderRoute("/blog").map((page) => {
const frontMatter = (page as MdxFile).frontMatter
const { date, timezone } = frontMatter || {}
const dateObj = date && timezone && dayjsTz(date, timezone).toDate()

return (
<div key={page.route} className="mb-10">
<Link href={page.route} style={{ color: "inherit", textDecoration: "none" }} className="block font-semibold mt-8 text-2xl">
{page.meta?.title || frontMatter?.title || page.name}
</Link>
<p className="opacity-80" style={{ marginTop: ".5rem" }}>
{frontMatter?.description}{" "}
<span className="inline-block">
<Link href={page.route}>{"Read more →"}</Link>
</span>
</p>
{dateObj ? (
<p className="opacity-50 text-sm">
{/* suppressHydrationWarning is used to prevent warnings due to differing server/client locales */}
<time dateTime={dateObj.toISOString()} suppressHydrationWarning>
{dateObj.toLocaleDateString(locale, {
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</time>
</p>
) : null}
</div>
);
});
}

const subscribeFormSchema = z.object({
email: z.string().email({
message: "Please enter a valid email.",
}),
});

export function SubscribeDialog() {

function subscribe(e: React.MouseEvent<HTMLButtonElement>) {
// Add your subscribe logic here
console.log("Subscribed!");
e.preventDefault();
}

const [isAlertOpen, setIsAlertOpen] = useState(false);
const [alertTitle, setAlertTitle] = useState("");
const [alertDescription, setAlertDescription] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);

const form = useForm<z.infer<typeof subscribeFormSchema>>({
resolver: zodResolver(subscribeFormSchema),
defaultValues: {
email: "",
},
});

async function onSubmit({ email }: z.infer<typeof subscribeFormSchema>) {
setIsSubmitting(true);
try {
const res = await fetch(
"https://mailing-list-gateway.watonomous.ca/sign-up",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, mailing_list: "[email protected]" }),
}
);

if (res.status === 200) {
setAlertTitle("Success!");
setAlertDescription(`Success! Please check your email inbox to confirm your subscription.`);
form.reset();
} else {
setAlertTitle("Error");
setAlertDescription(`Something went wrong! Error code: ${res.status}. Error message: \`${(await res.text())}\`.`);
}
} catch (e) {
setAlertTitle("Error");
setAlertDescription(`Something went wrong! Network request failed with error "${e}".`);
}
setIsAlertOpen(true);
setIsSubmitting(false);
}

return (
<div className="py-4 rounded-lg">
<h2 className="text-lg font-semibold">{"Subscribe to WATcloud's blog"}</h2>
<p className="text-sm mt-1">{"Get the latest posts delivered right to your inbox. We won't spam you!"}</p>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 mt-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="Email Address" {...field} />
</FormControl>
<FormMessage>
{form.formState.errors.email?.message}
</FormMessage>
</FormItem>
)}
/>
<Button className="w-full" type="submit" disabled={isSubmitting}>
{isSubmitting ? <>Submitting...</> : <>Subscribe</>}
</Button>
</form>
</Form>
<AlertDialog open={isAlertOpen} onOpenChange={setIsAlertOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{alertTitle}</AlertDialogTitle>
<AlertDialogDescription>{alertDescription}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction>OK</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
2 changes: 1 addition & 1 deletion components/giscus-comments.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Giscus from "@giscus/react";
import websiteConfig from "@/build/fixtures/website-config.json";
import { useTheme } from "nextra-theme-docs";
import { Separator } from "./ui/separator";

export default function CommentSection() {
const { theme } = useTheme();
Expand All @@ -18,7 +19,6 @@ export default function CommentSection() {

return (
<>
<hr className="mt-4 mb-4" />
{repo && repo_id && category && category_id ? (
<Giscus
repo={repo as `${string}/${string}`}
Expand Down
2 changes: 1 addition & 1 deletion components/profile-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function ProfileEditor() {
form.reset();
} else {
setAlertTitle("Error");
setAlertDescription(`Something went wrong! Error code: ${res.status}. Error message: "${res.json()}".`);
setAlertDescription(`Something went wrong! Error code: ${res.status}. Error message: \`${await res.text()}\`.`);
}
} catch (e) {
setAlertTitle("Error");
Expand Down
2 changes: 1 addition & 1 deletion components/ui/separator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Separator = React.forwardRef<
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
"shrink-0 bg-gray-400 opacity-20",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
Expand Down
59 changes: 5 additions & 54 deletions pages/blog.mdx
Original file line number Diff line number Diff line change
@@ -1,56 +1,7 @@
{/* Derived from https://github.com/vercel/turbo/blob/66196a70d02cddc8899ed1423684b1f716aa310e/docs/pages/blog.mdx */}
import { getPagesUnderRoute } from "nextra/context"
import { Link } from "nextra-theme-docs"
import { useRouter } from 'next/router'
import { websiteConfig } from '@/lib/data'
import { dayjsTz } from '@/lib/utils'

export function BlogHeader() {
return (
<div className="max-w-screen-lg mx-auto pt-4 pb-8 mb-16 border-b border-gray-400 border-opacity-20">
<h1>
<span className="font-bold leading-tight lg:text-5xl">Breadcrumbs</span>
</h1>
<p className="text-center text-gray-500 dark:text-gray-400 font-space-grotesk">
A record of the big and small things happening at WATcloud
</p>
</div>
);
}

export function BlogIndex() {
return getPagesUnderRoute("/blog").map((page) => {
const { date, timezone } = page.frontMatter || {}
const dateObj = date && timezone && dayjsTz(date, timezone).toDate()
const { locale = websiteConfig.default_locale } = useRouter()

return (
<div key={page.route} className="mb-10">
<Link href={page.route} style={{ color: "inherit", textDecoration: "none" }} className="block font-semibold mt-8 text-2xl">
{page.meta?.title || page.frontMatter?.title || page.name}
</Link>
<p className="opacity-80" style={{ marginTop: ".5rem" }}>
{page.frontMatter?.description}{" "}
<span className="inline-block">
<Link href={page.route}>{"Read more →"}</Link>
</span>
</p>
{dateObj ? (
<p className="opacity-50 text-sm">
{/* suppressHydrationWarning is used to prevent warnings due to differing server/client locales */}
<time dateTime={dateObj.toISOString()} suppressHydrationWarning>
{dateObj.toLocaleDateString(locale, {
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</time>
</p>
) : null}
</div>
);
});
}
import { BlogHeader, BlogIndex, SubscribeDialog } from '@/components/blog'
import { Separator } from "@/components/ui/separator";

<BlogHeader />
<BlogIndex />
<BlogIndex />
<Separator className="my-8" />
<SubscribeDialog />
2 changes: 1 addition & 1 deletion styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,5 @@ section.footnotes is created by the remark-gfm plugin:
- https://github.com/micromark/micromark-extension-gfm-footnote/blob/533e041238c15e7995afbffa7721b0e8d427f68e/readme.md
*/
section.footnotes {
@apply border-t mt-6;
@apply border-t mt-6 border-gray-400 border-opacity-20;
}

0 comments on commit 7dcd1b1

Please sign in to comment.