Remix

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.

Do you like my content?

Sponsor Me On Github