diff --git a/docs/.env.example b/docs/.env.example new file mode 100644 index 000000000..493eb5154 --- /dev/null +++ b/docs/.env.example @@ -0,0 +1 @@ +BUTTONDOWN_API_KEY= \ No newline at end of file diff --git a/docs/package.json b/docs/package.json index 7b3761ae2..e67012273 100644 --- a/docs/package.json +++ b/docs/package.json @@ -27,6 +27,7 @@ "@types/cookie": "^0.5.1", "@types/node": "16.11.13", "@types/react": "^18.2.8", + "@types/react-dom": "^18.0.11", "cookie": "^0.5.0", "emery": "^1.4.1", "next": "^14.1.3", diff --git a/docs/src/app/mailing-list/route.ts b/docs/src/app/mailing-list/route.ts new file mode 100644 index 000000000..3bb0644d5 --- /dev/null +++ b/docs/src/app/mailing-list/route.ts @@ -0,0 +1,70 @@ +import { redirect } from 'next/navigation'; +import { headers } from 'next/headers'; + +export const runtime = 'edge'; + +export async function POST(req: Request): Promise { + if (req.headers.get('origin') !== new URL(req.url).origin) { + return new Response('Invalid origin', { status: 400 }); + } + if (req.headers.get('content-type') !== 'application/x-www-form-urlencoded') { + return new Response('Invalid content type', { status: 415 }); + } + + try { + const referer = headers().get('referer'); + let pathname = '/'; + if (referer) { + try { + pathname = new URL(referer).pathname; + } catch {} + } + const formData = await req.formData(); + console.log(formData.getAll('tags')); + const data = { + email: formData.get('email'), + tags: [ + ...formData.getAll('tags'), + `keystatic website${pathname !== '/' ? `: ${pathname}` : ' homepage'}`, + ], + }; + + const buttondownResponse = await fetch( + 'https://api.buttondown.email/v1/subscribers', + { + method: 'POST', + headers: { + Authorization: `Token ${process.env.BUTTONDOWN_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email_address: data.email, + tags: data.tags, + }), + } + ); + + if (!buttondownResponse.ok) { + const error = await buttondownResponse.json(); + return Response.redirect( + new URL( + `/?${new URLSearchParams({ + error: + error?.detail || + 'Sorry, an error has occurred — please try again later.', + })}#mailing-list-form`, + req.url + ) + ); + } + buttondownResponse.body?.cancel(); + } catch (error) { + console.error('An error occurred: ', error); + redirect( + `/?${new URLSearchParams({ + error: 'Sorry, an error has occurred — please try again later.', + })}#mailing-list-form` + ); + } + redirect('/thank-you'); +} diff --git a/docs/src/components/button.tsx b/docs/src/components/button.tsx index f5e388414..3e5a126f5 100644 --- a/docs/src/components/button.tsx +++ b/docs/src/components/button.tsx @@ -59,16 +59,19 @@ export default function Button({ className )} > - {isLoading ? : children} + {isLoading ? : children} ); } -function Spinner() { +function Spinner({ impact }: { impact: ButtonProps['impact'] }) { return (
-
  • - -
  • - - setIsOpen(false)} - header={() => ( - <> -
    -

    Send us a message

    -

    Tell us what you think below.

    -
    - - )} - > -

    Tell us a bit about yourself

    - -
    ); } diff --git a/docs/src/components/dialog.tsx b/docs/src/components/dialog.tsx deleted file mode 100644 index 4dea24bd2..000000000 --- a/docs/src/components/dialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Fragment, ReactElement, ReactNode } from 'react'; -import { Dialog as DialogHui, Transition } from '@headlessui/react'; -import { XMarkIcon } from '@heroicons/react/24/outline'; - -type DialogProps = { - open: boolean; - onClose: () => void; - header: () => ReactElement; - children: ReactNode; -}; - -export default function Dialog({ - open, - onClose, - header, - children, -}: DialogProps) { - return ( - - - -
    - - -
    -
    - - - {/* Close button */} -
    - -
    -
    - {header()} -
    -
    -
    - {children} -
    -
    -
    -
    -
    -
    - - - ); -} diff --git a/docs/src/components/forms/mailing-list.tsx b/docs/src/components/forms/mailing-list.tsx index 6eb3392c0..9ec4de9e0 100644 --- a/docs/src/components/forms/mailing-list.tsx +++ b/docs/src/components/forms/mailing-list.tsx @@ -1,41 +1,98 @@ -import { useState } from 'react'; import Button from '../button'; +import { useSearchParams } from 'next/navigation'; +import { Suspense, useState } from 'react'; export default function MailingListForm() { - const [isLoading, setIsLoading] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); return ( -
    { - setIsLoading(true); - }} - > -
    - - -
    - -
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + + + + + ); } + +function ErrorMessage() { + const params = useSearchParams(); + const error = params.get('error'); + if (!error) return null; + return

    {error}

    ; +} diff --git a/docs/src/components/forms/send-message.tsx b/docs/src/components/forms/send-message.tsx deleted file mode 100644 index 8999cf449..000000000 --- a/docs/src/components/forms/send-message.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { ArrowSmallRightIcon } from '@heroicons/react/24/solid'; - -import Button from '../button'; -import { useState } from 'react'; - -export default function SendMessageForm() { - const [isLoading, setIsLoading] = useState(false); - return ( -
    { - setIsLoading(true); - }} - > -
    -
    - - -
    -
    - - -
    -
    -
    - -