Skip to content

Commit

Permalink
refactored the code + readme + OG
Browse files Browse the repository at this point in the history
  • Loading branch information
lakshaybhushan committed Aug 23, 2024
1 parent ce02241 commit 5efad52
Show file tree
Hide file tree
Showing 21 changed files with 449 additions and 100 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NOTION_SECRET=""
NOTION_DB=""

RESEND_API_KEY=""

UPSTASH_REDIS_REST_URL=""
UPSTASH_REDIS_REST_TOKEN=""
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<h1 align="center">Next.js + Notion — Wailtist Template</h1>

<p align="center">

<img src ="https://img.shields.io/badge/Next.js-000000.svg?style=for-the-badge&logo=nextdotjs&logoColor=white">
<img src ="https://img.shields.io/badge/Upstash-00E9A3.svg?style=for-the-badge&logo=Upstash&logoColor=white">
<img src ="https://img.shields.io/badge/Notion-000000.svg?style=for-the-badge&logo=Notion&logoColor=white">
<img src ="https://img.shields.io/badge/Resend-000000.svg?style=for-the-badge&logo=Resend&logoColor=white">
<img src ="https://img.shields.io/badge/shadcn/ui-000000.svg?style=for-the-badge&logo=shadcn/ui&logoColor=white">
<img src ="https://img.shields.io/badge/Vercel-000000.svg?style=for-the-badge&logo=Vercel&logoColor=white">

</p>

![GithubBanner](./app/opengraph-image.png)

This is a template repository for creating a waitlist using Next.js 14, Notion as a CMS, Upstash Redis for rate limiting and Resend for sending emails with a custom domain.

The UI is built using a mix of shadcn/ui, Magic UI and Tailwind CSS.

**Demo:** [https://nextjs-notion-waitlist.vercel.app](https://nextjs-notion-waitlist.vercel.app)

## Features

- **Next.js 14**: The latest stable version of Next.js.
- **Notion as a CMS**: Use Notion to manage your waitlist users.
- **Upstash Redis**: Use Upstash Redis to rate limit the number of signups in a given time period.
- **Resend**: Use Resend to send emails to users who sign up.
- **Vercel**: Deploy the app to Vercel with a single click.
- **shadcn/ui**: Use shadcn/ui for building the UI components.

## Why Notion?

Notion is used everywhere nowadays. It's a great tool for managing content and it's free to use. But a lot of people don't know that they can use Notion as a CMS for their websites which stands for Content Management System. This template is a very basic implementation of using Notion as a CMS for a waitlist.

However, You can extend it to use Notion for other types of content as well. Using Notion as a CMS is a great way to manage content without having to build a backend or a database. You can use Notion's API to fetch data from your Notion workspace and display it on your website.

## How to get started?

There are a few things you need to do before you can use this template:

### Notion

Assuming that you have a Notion account and a workspace, you can create a new database in your workspace and add the following columns:

- **Id**: A unique identifier for each record.
- **Name**: Title
- **Email**: Email
- **Timestamp**: Created Time

Now you need to get the secret key for your workspace. You can get it from the [Notion Integrations page](https://www.notion.so/my-integrations). You will need this key to fetch data from your workspace.

Now you need to get the ID of the database you created. You can get it from the URL of the database. It will look something like this:

`https://www.notion.so/{USERNAME}/{DATABASE_ID}?v={NUMBERS}&pvs={NUMBERS}`

You need to copy the `DATABASE_ID` from the URL.

### Upstash Redis

It's fairly simple to get started with Upstash Redis. You can sign up for a free account and create a new Redis database. You will get a `REST URL` and a `TOKEN` that you can use to interact with the Redis database.

### Resend

You need to sign up for a Resend account if not already. Then you need to add your domain and verify the DNS records. Once you have done that, you can generate an API key from the Resend dashboard which you will need to send emails.

## Building with this template

There are two ways to use this template:

1. **Deploy to Vercel**: Click the button below to deploy this template to Vercel with a single click.

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flakshaybhushan%2Fnextjs-notion-waitlist-template&env=NOTION_SECRET,NOTION_DB,RESEND_API_KEY,UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN)

The above button will create a new Vercel project and clone this repository into your GitHub account. You will need to provide the following environment variables:

- `NOTION_SECRET`: Your Notion secret key.
- `NOTION_DB`: The ID of the Notion database you want to use.
- `RESEND_API_KEY`: Your Resend API key.
- `UPSTASH_REDIS_REST_URL`: Your Upstash Redis REST URL.
- `UPSTASH_REDIS_REST_TOKEN`: Your Upstash Redis REST token.

2. **Manual Setup**: Fork this repository and clone it to your local machine.

Install the dependencies, this project uses `bun` as a package manager:

```bash
bun install
```

Run the development server:

```bash
bun dev
```

To run the email server:

```bash
bun email
```

Create a `.env.local` file in the root of the project and add the environment variables mentioned above. You can also have a look at the `.env.example` file for reference.

## License

You can use this template for personal or commercial projects. You can modify it as you like.

However, if you use this template for commercial projects, please consider buying me a coffee or sponsoring me on GitHub. It will help me to keep creating more templates like this.

<a href="https://www.buymeacoffee.com/lakshaybhushan" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50" width="200"></a>

---

If you have any questions or need help with this template, feel free to reach out to me on [Twitter](https://x.com/blakssh) or leave a comment on this repository.
4 changes: 2 additions & 2 deletions app/api/mail/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ export async function POST(request: NextRequest, response: NextResponse) {
const { email, firstname } = await request.json();

const { data, error } = await resend.emails.send({
from: "morph2json <[email protected]>",
from: "Lakshay<[email protected]>",
to: [email],
subject: "Thankyou for waitlisting morph2json!",
subject: "Thankyou for wailisting the Next.js + Notion CMS template!",
reply_to: "[email protected]",
html: render(WelcomeTemplate({ userFirstname: firstname })),
});
Expand Down
2 changes: 1 addition & 1 deletion app/api/notion/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function POST(request: Request) {
},
],
},
"First Name": {
"Name": {
type: "rich_text",
rich_text: [
{
Expand Down
18 changes: 17 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Analytics } from "@vercel/analytics/react";
const FigtreeFont = Figtree({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Next.js + Notion — Waitlist",
title: "Next.js + Notion — Waitlist Template",
description:
"A simple Next.js waitlist template with Notion as CMS and Resend to send emails created with React Email and Upstash Redis for rate limiting. Deployed on Vercel.",
};
Expand All @@ -19,6 +19,22 @@ export default function RootLayout({
}>) {
return (
<html lang="en" className="dark" suppressHydrationWarning>
<meta property="og:image" content="/opengraph-image.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="832" />
<meta
property="og:site_name"
content="Next.js + Notion — Waitlist Template"
/>
<meta
property="og:url"
content="https://nextjs-notion-waitlist.vercel.app/"
/>
<meta name="twitter:image" content="/twitter-image.png" />
<meta name="twitter:image:type" content="image/png" />
<meta name="twitter:image:width" content="1280" />
<meta name="twitter:image:height" content="832" />
<body className={FigtreeFont.className}>
{children}
<Toaster richColors position="top-center" />
Expand Down
Binary file added app/opengraph-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 56 additions & 25 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import CTA from "@/components/cta";
import Form from "@/components/form";
import Logos from "@/components/logos";
import Particles from "@/components/ui/particles";
import Header from "@/components/header";
import Footer from "@/components/footer";

export default function Home() {
const [name, setName] = useState<string>("");
Expand Down Expand Up @@ -40,32 +42,45 @@ export default function Home() {

const promise = new Promise(async (resolve, reject) => {
try {
const notionResponse = await fetch("/api/notion", {
// First, attempt to send the email
const mailResponse = await fetch("/api/mail", {
cache: "no-store",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, email }),
body: JSON.stringify({ firstname: name, email }),
});

const mailResponse = await fetch("/api/mail", {
cache: "no-store",
if (!mailResponse.ok) {
if (mailResponse.status === 429) {
reject("Rate limited");
} else {
reject("Email sending failed");
}
return; // Exit the promise early if mail sending fails
}

// If email sending is successful, proceed to insert into Notion
const notionResponse = await fetch("/api/notion", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ firstname: name, email }),
body: JSON.stringify({ name, email }),
});

if (notionResponse.ok && mailResponse.ok) {
resolve({ name });
if (!notionResponse.ok) {
if (notionResponse.status === 429) {
reject("Rate limited");
} else {
reject("Notion insertion failed");
}
} else {
reject("Request failed");
toast.error("Request failed. Rate limit exceeded 😢");
resolve({ name });
}
} catch (error) {
reject(error);
toast.error("An error occurred. Please try again 😢");
}
});

Expand All @@ -74,9 +89,18 @@ export default function Home() {
success: (data) => {
setName("");
setEmail("");
return "Thank you for joining morph2json's waitlist! 🎉";
return "Thank you for joining the waitlist 🎉";
},
error: (error) => {
if (error === "Rate limited") {
return "You're doing that too much. Please try again later";
} else if (error === "Email sending failed") {
return "Failed to send email. Please try again 😢.";
} else if (error === "Notion insertion failed") {
return "Failed to save your details. Please try again 😢.";
}
return "An error occurred. Please try again 😢.";
},
error: "An error occurred. Please try again 😢.",
});

promise.finally(() => {
Expand All @@ -85,22 +109,29 @@ export default function Home() {
};

return (
<main className="flex min-h-screen flex-col items-center px-4 pt-24 sm:px-6 lg:px-8">
<CTA />
<Form
name={name}
email={email}
handleNameChange={handleNameChange}
handleEmailChange={handleEmailChange}
handleSubmit={handleSubmit}
loading={loading}
/>
<main className="flex min-h-screen flex-col items-center overflow-x-clip pt-12 md:pt-24">
<section className="flex flex-col items-center px-4 sm:px-6 lg:px-8">
<Header />

<CTA />

<Form
name={name}
email={email}
handleNameChange={handleNameChange}
handleEmailChange={handleEmailChange}
handleSubmit={handleSubmit}
loading={loading}
/>

<Logos />
</section>

<Logos />
<Footer />

<Particles
className="absolute inset-0 -z-[100]"
quantity={150}
quantityDesktop={350}
quantityMobile={100}
ease={80}
color={"#F7FF9B"}
refresh
Expand Down
Binary file added app/twitter-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions components/cta.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { motion } from "framer-motion";
import AnimatedShinyText from "@/components/ui/shimmer-text";
import TextBlur from "@/components/ui/text-blur";
import AnimatedShinyText from "@/components/ui/shimmer-text";
import { containerVariants, itemVariants } from "@/lib/animation-variants";

export default function CTA() {
Expand Down Expand Up @@ -37,7 +37,7 @@ export default function CTA() {
<motion.div variants={itemVariants}>
<TextBlur
className="mx-auto max-w-[27rem] pt-1.5 text-center text-base text-zinc-300 sm:text-lg"
text="Join the waitlist to get early access to the template and recieve updates on the progress!"
text="Join the waitlist to get early access of the product and recieve updates on the progress!"
duration={0.8}
/>
</motion.div>
Expand Down
26 changes: 26 additions & 0 deletions components/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from "next/link";
import { motion } from "framer-motion";
import { containerVariants, itemVariants } from "@/lib/animation-variants";

export default function Footer() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="mt-auto flex w-full items-center justify-center gap-1 border-t bg-background p-6 text-muted-foreground md:justify-start">
<motion.div variants={itemVariants}>
Brought to you by{" "}
<Link
href="https://lakshb.dev"
rel="noopener noreferrer"
target="_blank">
<span className="text-zinc-300 underline underline-offset-2 transition-all duration-200 ease-linear hover:text-yellow-200">
lakshaybhushan
</span>
.
</Link>
</motion.div>
</motion.div>
);
}
9 changes: 8 additions & 1 deletion components/form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Link from "next/link";
import { ChangeEvent } from "react";
import { motion } from "framer-motion";
import { FaXTwitter } from "react-icons/fa6";
import { FaGithub, FaXTwitter } from "react-icons/fa6";
import { Input } from "@/components/ui/input";
import { FaArrowRightLong } from "react-icons/fa6";
import { EnhancedButton } from "@/components/ui/enhanced-btn";
Expand Down Expand Up @@ -67,6 +67,13 @@ export default function Form({
target="_blank">
<FaXTwitter className="h-4 w-4 transition-all duration-200 ease-linear hover:text-yellow-200" />
</Link>
or
<Link
href="https://github.com/lakshaybhushan"
rel="noopener noreferrer"
target="_blank">
<FaGithub className="ml-0.5 h-5 w-5 transition-all duration-200 ease-linear hover:text-yellow-200" />
</Link>
</motion.div>
</motion.div>
);
Expand Down
27 changes: 27 additions & 0 deletions components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from "next/link";
import { Button } from "./ui/button";
import { motion } from "framer-motion";
import { FaGithub } from "react-icons/fa6";
import { containerVariants, itemVariants } from "@/lib/animation-variants";

export default function Header() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="fixed right-0 top-0 z-[50] m-4">
<motion.div variants={itemVariants}>
<Link href="https://github.com/lakshaybhushan/nextjs-notion-waitlist-template/fork">
<Button
size="sm"
variant="secondary"
className="text-yellow-50 transition-all duration-150 ease-linear md:hover:text-yellow-200">
<FaGithub className="md:mr-1.5" />
<span className="hidden md:inline">Use this template</span>
</Button>
</Link>
</motion.div>
</motion.div>
);
}
Loading

0 comments on commit 5efad52

Please sign in to comment.