Remix
Handy resource route that you can use to quickly let people sign up for your buttondown newsletter in Remix.
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useFetcher, useLoaderData, useActionData, useTransition } from "@remix-run/react";
import { useEffect, useRef, useState } from "react";
import { HiShieldCheck } from 'react-icons/hi'
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const email = formData.get("email");
try {
const API_KEY = process.env.BUTTONDOWN_API_KEY;
const response = await fetch(
`https://api.buttondown.email/v1/subscribers`,
{
body: JSON.stringify({ email }),
headers: {
Authorization: `Token ${API_KEY}`,
'Content-Type': 'application/json'
},
method: 'POST'
}
);
if (response.status >= 400) {
return json({
error: `There was an error subscribing to the newsletter.`
});
}
return json({
...response,
subscription: "ok",
success: "ok"
});
} catch(e) {
return json({})
}
};
function SuccessMessage({ children }) {
return (
<p className="flex items-center text-sm font-bold text-green-700 dark:text-green-400">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="mr-2 h-4 w-4"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
{children}
</p>
);
}
function ErrorMessage({ children }) {
return (
<p className="flex items-center text-sm font-bold text-red-800 dark:text-red-400">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="mr-2 h-4 w-4"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
{children}
</p>
);
}
export const Subscribe = () => {
const newsletter = useFetcher();
const ref = useRef();
const [subscribed, setSubscribed] = useState(false);
const [error, setError] = useState("");
const actionData = useActionData();
const transition = useTransition();
const state: "idle" | "success" | "error" | "submitting" =
newsletter.state === 'submitting'
? "submitting"
: newsletter?.type === 'done' && newsletter.data.success
? "success"
: newsletter?.type === 'done' && newsletter.data.error
? "error"
: "idle";
const inputRef = useRef<HTMLInputElement>(null);
const successRef = useRef<HTMLHeadingElement>(null);
const mounted = useRef<boolean>(false);
useEffect(() => {
if (state === "error") {
setSubscribed(false);
inputRef.current?.focus();
}
if (state === "idle" && mounted.current) {
inputRef.current?.select();
}
if (state === "success") {
setSubscribed(true);
successRef.current?.focus();
}
console.log(newsletter);
if (newsletter.type === "done" && newsletter.data.success) {
setSubscribed(true);
} else if (newsletter.type === "done" && newsletter.data.error) {
inputRef.current?.focus();
setError(newsletter.data.error);
setSubscribed(false);
}
mounted.current = true;
}, [state, newsletter]);
return (
<div className="border border-slate-300 rounded p-6 my-4 w-full dark:border-gray-800 bg-slate-50 dark:bg-blue-opaque drop-shadow-sm">
<p className="text-lg md:text-xl font-bold text-gray-900 dark:text-gray-100">
Subscribe to the newsletter
</p>
<p className="my-1 text-gray-800 dark:text-gray-200">
Subscribe to the newsletter to stay up to date with web development, tech, articles, writing and much more!
</p>
{/*
Get emails from me about web development, tech, and early access to new articles.
*/}
{!subscribed && <>
<newsletter.Form action="/resources/newsletter" className="relative my-4" method="post">
<input
aria-label="Email for newsletter"
ref={inputRef}
placeholder="[email protected]"
aria-describedby="error-message"
type="email"
name="email"
autoComplete="email"
required
className="px-4 py-2 mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 pr-32"
tabIndex={state === "success" ? -1 : 0}
/>
<button
className="flex items-center justify-center absolute right-1 top-1 px-4 pt-1 font-medium h-8 bg-blue-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100 rounded w-28"
type="submit"
tabIndex={state === "success" ? -1 : 0}
>
{state === "submitting" ? "Subscribing..." : 'Subscribe'}
</button>
</newsletter.Form>
<p className="my-1 text-sm text-gray-600 dark:text-gray-200">
<HiShieldCheck className="mr-1 w-6 h-6 text-green-600 inline-block " />
<strong>No spam!!</strong> I will send you at most one mail per month <i>if that</i>.
</p>
</>}
{subscribed && <SuccessMessage>
<h2 ref={successRef} tabIndex={-1}>
You're subscribed!{` `}
</h2>
<p>Please check your email to confirm your subscription.</p>
</SuccessMessage>}
{error && <ErrorMessage>{error}</ErrorMessage>}
</div>
)
}
Usage
First, create a Buttondown account.
From the settings page, retrieve your API key at the bottom.
To securely access the API, we need to include the secret with each request.
Remember: never commit secrets to git. Thus, we should use an environment variable.
Since this is a resource route, it doesn't export anything by default.
So on a page, such as app/routes/index.tsx
, for example:
import {Subscribe} from '~/routes/resources/newsletter';
export default function Index() {
return (
<Subscribe />
);
}
This will output the newsletter box, and anyone who signs up will be processed in the form itself without leaving the page.