diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..97d6ee9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ + +{ + "plugins": ["prettier-plugin-tailwindcss"], + "bracketSameLine": true +} \ No newline at end of file diff --git a/README.md b/README.md index c403366..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/app/api/mail/route.ts b/app/api/mail/route.ts index d5fc0de..8ac6b55 100644 --- a/app/api/mail/route.ts +++ b/app/api/mail/route.ts @@ -1,28 +1,29 @@ -import { render } from "@react-email/render" +import { render } from "@react-email/render"; -import WelcomeTemplate from "../../../emails" +import WelcomeTemplate from "../../../emails"; -import { Resend } from "resend" -import { NextRequest, NextResponse } from "next/server" -import { Redis } from "@upstash/redis" -import { Ratelimit } from "@upstash/ratelimit" +import { Resend } from "resend"; +import { NextRequest, NextResponse } from "next/server"; +import { Redis } from "@upstash/redis"; +import { Ratelimit } from "@upstash/ratelimit"; -const resend = new Resend(process.env.RESEND_API_KEY) +const resend = new Resend(process.env.RESEND_API_KEY); const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, -}) +}); const ratelimit = new Ratelimit({ redis, - limiter: Ratelimit.fixedWindow(3, "1 m"), -}) + // 2 requests per minute from the same IP address in a sliding window of 1 minute duration which means that the window slides forward every second and the rate limit is reset every minute for each IP address. + limiter: Ratelimit.slidingWindow(2, "1 m"), +}); export async function POST(request: NextRequest, response: NextResponse) { - const ip = request.ip ?? "127.0.0.1" + const ip = request.ip ?? "127.0.0.1"; - const result = await ratelimit.limit(ip) + const result = await ratelimit.limit(ip); if (!result.success) { return Response.json( @@ -31,11 +32,11 @@ export async function POST(request: NextRequest, response: NextResponse) { }, { status: 429, - } - ) + }, + ); } - const { email, firstname } = await request.json() + const { email, firstname } = await request.json(); const { data, error } = await resend.emails.send({ from: "morph2json ", @@ -43,15 +44,17 @@ export async function POST(request: NextRequest, response: NextResponse) { subject: "Thankyou for waitlisting morph2json!", reply_to: "lakshb.work@gmail.com", html: render(WelcomeTemplate({ userFirstname: firstname })), - }) + }); + + // const { data, error } = { data: true, error: null } if (error) { - return NextResponse.json(error) + return NextResponse.json(error); } if (!data) { - return NextResponse.json({ message: "Failed to send email" }) + return NextResponse.json({ message: "Failed to send email" }); } - return NextResponse.json({ message: "Email sent successfully" }) + return NextResponse.json({ message: "Email sent successfully" }); } diff --git a/app/api/notion/waitlist/route.ts b/app/api/notion/route.ts similarity index 81% rename from app/api/notion/waitlist/route.ts rename to app/api/notion/route.ts index fdc1ad9..4c1dabd 100644 --- a/app/api/notion/waitlist/route.ts +++ b/app/api/notion/route.ts @@ -1,10 +1,10 @@ -import { Client } from "@notionhq/client" -import { NextResponse } from "next/server" +import { Client } from "@notionhq/client"; +import { NextResponse } from "next/server"; export async function POST(request: Request) { - const body = await request.json() + const body = await request.json(); try { - const notion = new Client({ auth: process.env.NOTION_SECRET }) + const notion = new Client({ auth: process.env.NOTION_SECRET }); const response = await notion.pages.create({ parent: { database_id: `${process.env.NOTION_DB}`, @@ -31,14 +31,14 @@ export async function POST(request: Request) { ], }, }, - }) + }); if (!response) { - throw new Error("Failed to add email to Notion") + throw new Error("Failed to add email to Notion"); } - return NextResponse.json({ success: true }, { status: 200 }) + return NextResponse.json({ success: true }, { status: 200 }); } catch (error) { - return NextResponse.json({ success: false }, { status: 500 }) + return NextResponse.json({ success: false }, { status: 500 }); } } diff --git a/app/favicon.ico b/app/favicon.ico index 13753a7..ee9550c 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/layout.tsx b/app/layout.tsx index e29f397..96ec2fd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,22 +1,21 @@ -import "./globals.css" -import type { Metadata } from "next" -import { Figtree } from "next/font/google" -import { Toaster } from "@/components/ui/sonner" -import { Analytics } from "@vercel/analytics/react" +import "./globals.css"; +import type { Metadata } from "next"; +import { Figtree } from "next/font/google"; +import { Toaster } from "@/components/ui/sonner"; +import { Analytics } from "@vercel/analytics/react"; -const FigtreeFont = Figtree({ subsets: ["latin"] }) +const FigtreeFont = Figtree({ subsets: ["latin"] }); export const metadata: Metadata = { - title: - "morph2json | Effortlessly Convert your raw data into a well structured JSON", + title: "Next.js + Notion — Waitlist", description: - "Join the waitlist to get early access to morph2json and get notified when it's ready for you to use.", -} + "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.", +}; export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { return ( @@ -26,5 +25,5 @@ export default function RootLayout({ - ) + ); } diff --git a/app/page.tsx b/app/page.tsx index 605a8ac..a26c419 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,57 +1,52 @@ -"use client" - -import { EnhancedButton } from "@/components/ui/enhanced-btn" -import { Input } from "@/components/ui/input" -import { FaArrowRightLong } from "react-icons/fa6" -import { useState } from "react" -import Link from "next/link" -import Particles from "@/components/ui/particles" -import TextBlur from "@/components/ui/text-blur" -import { motion } from "framer-motion" -import { toast } from "sonner" -import { FaXTwitter } from "react-icons/fa6" -import AnimatedShinyText from "@/components/ui/shimmer-text" +"use client"; + +import { toast } from "sonner"; +import { useState } from "react"; +import CTA from "@/components/cta"; +import Form from "@/components/form"; +import Logos from "@/components/logos"; +import Particles from "@/components/ui/particles"; export default function Home() { - const [name, setName] = useState("") - const [email, setEmail] = useState("") - const [loading, setLoading] = useState(false) + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); const handleEmailChange = (event: React.ChangeEvent) => { - setEmail(event.target.value) - } + setEmail(event.target.value); + }; const handleNameChange = (event: React.ChangeEvent) => { - setName(event.target.value) - } + setName(event.target.value); + }; const isValidEmail = (email: string) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - return emailRegex.test(email) - } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; const handleSubmit = async () => { if (!name || !email) { - toast.error("Please fill in all fields 😠") - return + toast.error("Please fill in all fields 😠"); + return; } if (!isValidEmail(email)) { - toast.error("Please enter a valid email address 😠") - return + toast.error("Please enter a valid email address 😠"); + return; } - setLoading(true) + setLoading(true); const promise = new Promise(async (resolve, reject) => { try { - const notionResponse = await fetch("/api/notion/waitlist", { + const notionResponse = await fetch("/api/notion", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name, email }), - }) + }); const mailResponse = await fetch("/api/mail", { cache: "no-store", @@ -60,140 +55,48 @@ export default function Home() { "Content-Type": "application/json", }, body: JSON.stringify({ firstname: name, email }), - }) + }); if (notionResponse.ok && mailResponse.ok) { - resolve({ name }) + resolve({ name }); } else { - reject(new Error("Failed to add to the waitlist")) + reject("Request failed"); + toast.error("Request failed. Rate limit exceeded 😢"); } } catch (error) { - reject(error) + reject(error); + toast.error("An error occurred. Please try again 😢"); } - }) + }); toast.promise(promise, { loading: "Getting you on the waitlist... 🚀", success: (data) => { - setName("") - setEmail("") - return "Thank you for joining morph2json's waitlist! 🎉" + setName(""); + setEmail(""); + return "Thank you for joining morph2json's waitlist! 🎉"; }, error: "An error occurred. Please try again 😢.", - }) + }); promise.finally(() => { - setLoading(false) - }) - } - - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1, - delayChildren: 0.3, - }, - }, - } - - const itemVariants = { - hidden: { opacity: 0, y: 20, filter: "blur(10px)" }, - visible: { - opacity: 1, - y: 0, - filter: "blur(0px)", - transition: { - duration: 0.5, - }, - }, - } + setLoading(false); + }); + }; return ( -
- - -
-
- - Coming soon! - -
-
-
- - - - - - - - - - -
- - - - - - - - - - {loading ? "Loading..." : "Join Waitlist!"} - - - -

For any queries, reach out at

- - - -
-
+
+ +
+ +
- ) + ); } diff --git a/bun.lockb b/bun.lockb index 8104240..8f25f5e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/cta.tsx b/components/cta.tsx new file mode 100644 index 0000000..0cf3b4e --- /dev/null +++ b/components/cta.tsx @@ -0,0 +1,46 @@ +import { motion } from "framer-motion"; +import AnimatedShinyText from "@/components/ui/shimmer-text"; +import TextBlur from "@/components/ui/text-blur"; +import { containerVariants, itemVariants } from "@/lib/animation-variants"; + +export default function CTA() { + return ( + + +
+
+ + Coming soon! + +
+
+
+ + + + + + + + + + +
+ ); +} diff --git a/components/form.tsx b/components/form.tsx new file mode 100644 index 0000000..243e991 --- /dev/null +++ b/components/form.tsx @@ -0,0 +1,73 @@ +import Link from "next/link"; +import { ChangeEvent } from "react"; +import { motion } from "framer-motion"; +import { FaXTwitter } from "react-icons/fa6"; +import { Input } from "@/components/ui/input"; +import { FaArrowRightLong } from "react-icons/fa6"; +import { EnhancedButton } from "@/components/ui/enhanced-btn"; +import { containerVariants, itemVariants } from "@/lib/animation-variants"; + +interface FormProps { + name: string; + email: string; + handleNameChange: (e: ChangeEvent) => void; + handleEmailChange: (e: ChangeEvent) => void; + handleSubmit: () => void; + loading: boolean; +} + +export default function Form({ + name, + email, + handleNameChange, + handleEmailChange, + handleSubmit, + loading, +}: FormProps) { + return ( + + + + + + + + + + {loading ? "Loading..." : "Join Waitlist!"} + + + +

For any queries, reach out at

+ + + +
+
+ ); +} diff --git a/components/logos.tsx b/components/logos.tsx new file mode 100644 index 0000000..e31071d --- /dev/null +++ b/components/logos.tsx @@ -0,0 +1,44 @@ +import Link from "next/link"; +import { motion } from "framer-motion"; +import { containerVariants, itemVariants } from "@/lib/animation-variants"; + +const logos = [ + { href: "https://nextjs.org", src: "/nextjs.svg", alt: "Next.js Logo" }, + { href: "https://notion.so", src: "/notion.svg", alt: "Notion Logo" }, + { href: "https://upstash.com", src: "/upstash.svg", alt: "Upstash Logo" }, + { href: "https://vercel.com", src: "/vercel.svg", alt: "Vercel Logo" }, +]; + +export default function Logos() { + return ( + + +

+ Powered by +

+
+ + {logos.map((logo, index) => ( + + {logo.alt} + + ))} + +
+ ); +} diff --git a/emails/index.tsx b/emails/index.tsx index 5bcf9af..a8fadd3 100644 --- a/emails/index.tsx +++ b/emails/index.tsx @@ -7,106 +7,98 @@ import { Img, Preview, Text, -} from "@react-email/components" -import * as React from "react" +} from "@react-email/components"; +import * as React from "react"; -interface morph2jsonEmailProps { - userFirstname: string +interface EmailProps { + userFirstname: string; } -export const morph2jsonWaitlistEmail = ({ - userFirstname, -}: morph2jsonEmailProps) => ( +export const NotionWaitlistEmail = ({ userFirstname }: EmailProps) => ( - Thankyou for signing up for the morph2json waitlist! + + Thank you for signing up for the Next.js + Notion waitlist template! + morph2json logo Hi {userFirstname}, - Thanks for joining the waitlist for morph2json! I'm Lakshay, the - developer working on this tool. I'm excited you're interested and I'm - putting in the effort to make morph2json as useful as possible for - you. + Thanks for joining the waitlist for our Next.js + Notion CMS waitlist + template! I'm Lakshay, the developer behind this project. I'm glad + that you're interested in using it. - - I'll keep you updated on progress and let you know the second it's - ready to use. If you have any questions or feedback, feel free to - reply directly to{" "} - - this email + I'll keep you posted on the progress and notify you as soon as it's + ready for you to use. In the meantime, if you have any questions or + feedback, don't hesitate to reach out by replying directly to{" "} + + this email{" "} - — I'm all ears! + — I'm here to listen! - - For updates, you can also follow me on X/Twitter: {""} + You can also follow me on X/Twitter for updates:{" "} @blakssh - - Best, + Best regards,
Lakshay

- You are receiving this email because you signed up for the morph2json - waitlist. If you believe this is a mistake, please ignore this email. + You received this email because you signed up for the Notion waitlist. + If you believe this is a mistake, feel free to ignore this email.
-) +); -morph2jsonWaitlistEmail.PreviewProps = { +NotionWaitlistEmail.PreviewProps = { userFirstname: "Tyler", -} as morph2jsonEmailProps +} as EmailProps; -export default morph2jsonWaitlistEmail +export default NotionWaitlistEmail; const main = { backgroundColor: "#FCFFD5", - fontFamily: - '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', + fontFamily: 'figtree, "Helvetica Neue", Helvetica, Arial, sans-serif', color: "#cccccc", -} +}; const container = { margin: "0 auto", padding: "20px 24px 48px", backgroundColor: "#1a1a1a", borderRadius: "8px", -} +}; const logo = { - margin: "0 auto", -} + margin: "0 auto", +}; const paragraph = { fontSize: "16px", lineHeight: "26px", -} +}; const hr = { borderColor: "#cccccc", margin: "20px 0", -} +}; const footer = { color: "#8c8c8c", fontSize: "12px", -} +}; diff --git a/lib/animation-variants.ts b/lib/animation-variants.ts new file mode 100644 index 0000000..2665907 --- /dev/null +++ b/lib/animation-variants.ts @@ -0,0 +1,22 @@ +export const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.3, + }, + }, +}; + +export const itemVariants = { + hidden: { opacity: 0, y: 20, filter: "blur(10px)" }, + visible: { + opacity: 1, + y: 0, + filter: "blur(0px)", + transition: { + duration: 0.5, + }, + }, +}; diff --git a/lib/utils.ts b/lib/utils.ts index d084cca..365058c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/package.json b/package.json index 6737855..8bf7fac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "morph2json-waitlist", - "version": "0.1.0", + "name": "nextjs-notion-waitlist-template", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", @@ -33,13 +33,15 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.4", "postcss": "^8", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.4" + "typescript": "^5" } } diff --git a/public/logo.svg b/public/logo.svg index 7daabe3..3b53bba 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/public/nextjs.svg b/public/nextjs.svg new file mode 100644 index 0000000..981a3af --- /dev/null +++ b/public/nextjs.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/notion.svg b/public/notion.svg new file mode 100644 index 0000000..5d05544 --- /dev/null +++ b/public/notion.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/upstash.svg b/public/upstash.svg new file mode 100644 index 0000000..6c0dbe3 --- /dev/null +++ b/public/upstash.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..bd1d0c7 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/waitlist-logo.png b/public/waitlist-logo.png new file mode 100644 index 0000000..f5e53ab Binary files /dev/null and b/public/waitlist-logo.png differ