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

Newsletter Thinkmill opt-in checkboxes + fix light button spinner #1265

Merged
merged 13 commits into from
Aug 19, 2024
Merged
1 change: 1 addition & 0 deletions docs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BUTTONDOWN_API_KEY=
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
70 changes: 70 additions & 0 deletions docs/src/app/mailing-list/route.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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');
}
9 changes: 6 additions & 3 deletions docs/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,19 @@ export default function Button({
className
)}
>
{isLoading ? <Spinner /> : children}
{isLoading ? <Spinner impact={impact} /> : children}
</button>
);
}

function Spinner() {
function Spinner({ impact }: { impact: ButtonProps['impact'] }) {
return (
<div className="grid w-full place-items-center">
<svg
className="-my-0.5 h-5 w-5 animate-spin text-sand-1"
className={cx(
'-my-0.5 h-5 w-5 animate-spin',
impact === 'bold' ? 'text-sand-1' : 'text-sand-12'
)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
Expand Down
31 changes: 0 additions & 31 deletions docs/src/components/call-to-action.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
'use client';

import { useState } from 'react';

import Dialog from './dialog';
import SendMessageForm from './forms/send-message';
import { GithubIcon } from './icons/github-icon';
import { PaperAirplaneIcon } from './icons/paper-airplane-icon';

export default function CallToAction() {
const [isOpen, setIsOpen] = useState(false);

const linkLabels = {
tmLabs: 'Thinkmill Labs',
ksDiscussions: 'Join the discussion on GitHub',
Expand Down Expand Up @@ -121,33 +114,9 @@ export default function CallToAction() {
</span>
</a>
</li>
<li>
<button onClick={() => setIsOpen(true)} className="block w-full">
<span className="flex h-8 flex-row items-center gap-4 text-sm font-medium text-sand-12 transition-all duration-150 hover:gap-6 hover:text-black">
<PaperAirplaneIcon />
<span>Send us a message</span>
</span>
</button>
</li>
</ul>
</div>
</div>

<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
header={() => (
<>
<div className="flex flex-col gap-4">
<h2 className="pr-8 text-3xl font-medium">Send us a message</h2>
<p>Tell us what you think below.</p>
</div>
</>
)}
>
<h3 className="text-2xl font-medium">Tell us a bit about yourself</h3>
<SendMessageForm />
</Dialog>
</section>
);
}
71 changes: 0 additions & 71 deletions docs/src/components/dialog.tsx

This file was deleted.

125 changes: 91 additions & 34 deletions docs/src/components/forms/mailing-list.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<form
className="flex flex-col gap-4"
method="POST"
action="https://forms.keystatic.cloud/mailing-list"
onSubmit={() => {
setIsLoading(true);
}}
>
<div className="flex flex-col gap-1">
<label
className="block text-sm font-medium text-sand-11"
htmlFor="mailing-list-email"
>
Email
</label>
<input
type="email"
name="email"
id="mailing-list-email"
required
className="form-input h-8 w-full rounded-md border border-sand-6 bg-sand-1 px-3 py-0 text-sm leading-none hover:border-sand-8"
/>
</div>
<Button
className="self-start"
type="submit"
impact="light"
variant="small"
isLoading={isLoading}
<>
<form
id="mailing-list-form"
className="flex flex-col gap-4"
action="/mailing-list"
method="POST"
onSubmit={() => {
setIsSubmitting(true);
}}
>
Send me updates
</Button>
</form>
<div className="flex flex-col gap-1">
<label
className="block text-sm font-medium text-sand-11"
htmlFor="mailing-list-email"
>
Email
</label>
<input
type="email"
name="email"
id="mailing-list-email"
required
className="form-input h-8 w-full rounded-md border border-sand-6 bg-sand-1 px-3 py-0 text-sm leading-none hover:border-sand-8"
/>
</div>
<div className="flex flex-wrap gap-x-6 gap-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
name="tags"
id="mailing-list-keystatic"
className="form-checkbox size-4 rounded text-black"
value="keystatic_list"
defaultChecked
/>
<label
className="text-sm text-sand-11"
htmlFor="mailing-list-keystatic"
>
Keystatic news
</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
name="tags"
id="mailing-list-thinkmill"
className="form-checkbox size-4 rounded text-black"
value="thinkmill_list"
/>
<label
className="text-sm text-sand-11"
htmlFor="mailing-list-thinkmill"
>
Thinkmill news (
<a
href="https://www.thinkmill.com.au/newsletter/tailwind-for-designers-multi-brand-design-systems-and-a-search-tool-for-public-domain-content"
target="_blank"
className="cursor-pointer underline hover:text-thinkmill-red"
aria-label="Thinkmill (Opens in new tab)"
>
example
</a>
)
</label>
</div>
</div>
<Button
className="self-start"
type="submit"
impact="light"
variant="small"
isLoading={isSubmitting}
disabled={isSubmitting}
>
Send me updates
</Button>
</form>
<Suspense fallback={null}>
<ErrorMessage />
</Suspense>
</>
);
}

function ErrorMessage() {
const params = useSearchParams();
const error = params.get('error');
if (!error) return null;
return <p className="text-xs text-thinkmill-red">{error}</p>;
}
Loading
Loading