Skip to content

Commit

Permalink
send email via server action
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Dec 5, 2023
1 parent 003d276 commit b58d4b3
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 44 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"markdownlint.config": {
"MD033": {
"allowed_elements": ["img", "p", "a"]
}
},
"MD045": false
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is an open source e-commerce skateshop build with everything new in Next.js
> **Warning**
> This project is still in development and is not ready for production use.
>
> It uses new technologies (server actions, drizzle ORM) which are subject to change and may break your application.
> It uses new technologies (drizzle ORM) which are subject to change and may break your application.
## Tech Stack

Expand Down
4 changes: 2 additions & 2 deletions src/app/api/email/newsletter/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { eq } from "drizzle-orm"
import { z } from "zod"

import { resend } from "@/lib/resend"
import { subscribeToNewsletterSchema } from "@/lib/validations/email"
import { joinNewsletterSchema } from "@/lib/validations/email"
import NewsletterWelcomeEmail from "@/components/emails/newsletter-welcome-email"

export async function POST(req: Request) {
const input = subscribeToNewsletterSchema.parse(await req.json())
const input = joinNewsletterSchema.parse(await req.json())

try {
const emailPreference = await db.query.emailPreferences.findFirst({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useForm } from "react-hook-form"
import { toast } from "sonner"
import type { z } from "zod"

import { joinNewsletter } from "@/lib/actions/email"
import { catchError } from "@/lib/utils"
import { emailSchema } from "@/lib/validations/email"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -22,7 +24,7 @@ import { Icons } from "@/components/icons"

type Inputs = z.infer<typeof emailSchema>

export function SubscribeToNewsletterForm() {
export function JoinNewsletterForm() {
const [isPending, startTransition] = React.useTransition()

// react-hook-form
Expand All @@ -37,38 +39,18 @@ export function SubscribeToNewsletterForm() {
console.log(data)

startTransition(async () => {
const response = await fetch("/api/email/newsletter", {
method: "POST",
body: JSON.stringify({
try {
await joinNewsletter({
email: data.email,
// This token is used as a search param in the email preferences page to identify the subscriber.
token: crypto.randomUUID(),
subject: "Welcome to Skateshop13",
}),
})
subject: "Welcome to Skateshop",
})

if (!response.ok) {
switch (response.status) {
case 409:
toast.error("You are already subscribed to our newsletter.")
break
case 422:
toast.error("Invalid input.")
break
case 429:
toast.error("The daily email limit has been reached.")
break
case 500:
toast.error("Something went wrong. Please try again later.")
break
default:
toast.error("Something went wrong. Please try again later.")
}
return
toast.success("You have been subscribed to our newsletter.")
form.reset()
} catch (err) {
catchError(err)
}

toast.success("You have been subscribed to our newsletter.")
form.reset()
})
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/layouts/site-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Link from "next/link"
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { SubscribeToNewsletterForm } from "@/components/forms/subscribe-to-newsletter-form"
import { JoinNewsletterForm } from "@/components/forms/join-newsletter-form"
import { Icons } from "@/components/icons"
import { ModeToggle } from "@/components/layouts/mode-toggle"
import { Shell } from "@/components/shells/shell"
Expand Down Expand Up @@ -61,7 +61,7 @@ export function SiteFooter() {
<h4 className="text-base font-medium">
Subscribe to our newsletter
</h4>
<SubscribeToNewsletterForm />
<JoinNewsletterForm />
</section>
</section>
<section
Expand Down
10 changes: 5 additions & 5 deletions src/components/loading-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import * as React from "react"
import { useFormStatus } from "react-dom"

import { cn } from "@/lib/utils"
import { useMounted } from "@/hooks/use-mounted"
Expand All @@ -13,9 +14,8 @@ import { Skeleton } from "@/components/ui/skeleton"
import { Icons } from "@/components/icons"

const LoadingButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
// const { pending } = useFormStatus()
const pending = false
({ children, className, variant, size, ...props }, ref) => {
const { pending } = useFormStatus()
const mounted = useMounted()

if (!mounted)
Expand All @@ -26,7 +26,7 @@ const LoadingButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
"bg-muted text-muted-foreground"
)}
>
{props.children}
{children}
</Skeleton>
)

Expand All @@ -42,7 +42,7 @@ const LoadingButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
aria-hidden="true"
/>
)}
{props.children}
{children}
</Button>
)
}
Expand Down
64 changes: 61 additions & 3 deletions src/lib/actions/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,69 @@ import { eq } from "drizzle-orm"
import { type z } from "zod"

import { resend } from "@/lib/resend"
import { updateEmailPreferencesSchema } from "@/lib/validations/email"
import {
joinNewsletterSchema,
updateEmailPreferencesSchema,
} from "@/lib/validations/email"
import NewsletterWelcomeEmail from "@/components/emails/newsletter-welcome-email"

// Email can not be sent through a server action in production, because it is returning an email component maybe?
// So we are using the route handler /api/newsletter/subscribe instead
export async function joinNewsletter(
rawInput: z.infer<typeof joinNewsletterSchema>
) {
const input = joinNewsletterSchema.parse(rawInput)

const emailPreference = await db.query.emailPreferences.findFirst({
where: eq(emailPreferences.email, input.email),
})

if (emailPreference?.newsletter) {
throw new Error("You are already subscribed to the newsletter.")
}

const user = await currentUser()

const subject = input.subject ?? "Welcome to our newsletter"

// If email preference exists, update it and send the email
if (emailPreference) {
await db
.update(emailPreferences)
.set({
newsletter: true,
})
.where(eq(emailPreferences.email, input.email))

await resend.emails.send({
from: env.EMAIL_FROM_ADDRESS,
to: input.email,
subject,
react: NewsletterWelcomeEmail({
firstName: user?.firstName ?? undefined,
fromEmail: env.EMAIL_FROM_ADDRESS,
token: emailPreference.token,
}),
})
} else {
// If email preference does not exist, create it and send the email
await resend.emails.send({
from: env.EMAIL_FROM_ADDRESS,
to: input.email,
subject,
react: NewsletterWelcomeEmail({
firstName: user?.firstName ?? undefined,
fromEmail: env.EMAIL_FROM_ADDRESS,
token: input.token,
}),
})

await db.insert(emailPreferences).values({
email: input.email,
token: input.token,
userId: user?.id,
newsletter: true,
})
}
}

export async function updateEmailPreferences(
rawInput: z.infer<typeof updateEmailPreferencesSchema>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/validations/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const emailSchema = z.object({
}),
})

export const subscribeToNewsletterSchema = z.object({
export const joinNewsletterSchema = z.object({
email: emailSchema.shape.email,
token: z.string(),
subject: z.string().optional(),
Expand Down

1 comment on commit b58d4b3

@vercel
Copy link

@vercel vercel bot commented on b58d4b3 Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.