Contacts
Things are coming together nicely. But, we still hard coded contacts into the Job Application form. Let’s make this dynamic.
If we look at the Figma file, you’ll notice that we have a “Add a Contact” button on our sidebar:
When the user clicks on this button, we want to open a side panel that contains the contact form:
Let’s start with the UI and then we can connect it the database.
Adding a Contact
Styling the Contact Form
Again, shadcn/ui components are great for this.
This time, we’re going to reach for the Sheet
component.
You can copy and paste the boilerplate code from the Shad docs:
Import statement for the top of our ApplicationForm
component (but below the use client
directive):
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger,} from "@/app/components/ui/sheet"
Then, for our side bar:
<Sheet> <SheetTrigger>Open</SheetTrigger> <SheetContent> <SheetHeader> <SheetTitle>Are you absolutely sure?</SheetTitle> <SheetDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </SheetDescription> </SheetHeader> </SheetContent></Sheet>
This code will go below our <div>Contact Card</div>
placeholder:
<div className="box"> <h3>Contacts</h3> <p className="input-description">Invite your team members to collaborate.</p> <div> Contact Card </div> <Sheet> <SheetTrigger>Open</SheetTrigger> <SheetContent> <SheetHeader> <SheetTitle>Are you absolutely sure?</SheetTitle> <SheetDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </SheetDescription> </SheetHeader> </SheetContent> </Sheet></div>
If you take a look at this within the browser, you should see the “Open” trigger:
If you click on the trigger, the sheet should slide open:
Now, let’s replace the placeholder content with the real stuff.
You may have noticed that the Sheet code contains, a trigger, SheetTrigger
and SheetContent
. Inside, the content, there’s a SheetHeader
with a SheetTitle
and SheetDescription
.
First, let’s adjust the SheetTrigger
:
<SheetTrigger className="flex items-center gap-2 font-poppins text-sm font-bold bg-secondary py-3 px-6 rounded-md cursor-pointer"> <Icon id="plus" size={16} />Add a Contact</SheetTrigger>
- I added an
Icon
component and gave it anid
ofplus
and a size of16
. - I changed the text to
Add a Contact
. - We can add some styling to the
SheetTrigger
to make it look like a button.flex items-center gap-2
will align the icon and text next to each other, putting8px
of space between them.font-display font-bold text-sm
will use the font-familyPoppins
, make it bold, and set the font size to14px
.bg-secondary
will make the background a beige color (defined in our@theme
)py-3 px-6
will add12px
of padding to the top and bottom, and24px
of padding to the left and right.rounded-md
will round the corners of the button.cursor-pointer
will change the cursor to a pointer when hovering over the button.
Much better! Then, for our SheetContent
:
<SheetContent className="pt-[100px] px-12"> <SheetHeader> <SheetTitle>Add a Contact</SheetTitle> <SheetDescription> Add a Contact to this application. </SheetDescription> <ContactForm /> </SheetHeader></SheetContent>
- Change the
SheetTitle
toAdd a Contact
. - Change the
SheetDescription
toAdd a Contact to this application.
- For the form, I'm referencing the
ContactForm
component. (We'll need to create this.) - I added some styling to the
SheetContent
to our content some additional space.100px
of padding to the top and48px
of padding to the left and right.
Now, let’s create our ContactForm
component. Inside our src/app/components
directory, create a new file called ContactForm.tsx
.
Directorysrc/
Directoryapp/
Directorycomponents/
- ContactForm.tsx
Then, be sure to import the ContactForm
component at the top of our ApplicationForm
component:
import { ContactForm } from "@/app/components/ContactForm"
Inside our ContactForm.tsx
file, let’s start with a basic component (you should also see your linting error go away):
const ContactForm = () => { return ( <div>ContactForm</div> )}
export { ContactForm }
Now let’s replace our <div>
with our form. It should contain fields for:
- first name
- last name
- role
return ( <form> <div className="field"> <label htmlFor="firstName">First Name</label> <input type="text" id="firstName" name="firstName" required /> </div> <div className="field"> <label htmlFor="lastName">Last Name</label> <input type="text" id="lastName" name="lastName" required /> </div> <div className="field"> <label htmlFor="role">Role</label> <input type="text" id="role" name="role" required /> </div> <div className="field"> <label htmlFor="email">Email</label> <input type="email" id="email" name="email" required /> </div> <div className="field"> <Button> <Icon id="check" size={24} /> Create a Contact </Button> </div> </form>)
- This form should be pretty straightforward. It's mostly vanilla HTML. I wrapped each
label
andinput
pairing with adiv
that has a class offield
. Conveniently, we've already defined these styles within ourstyles.css
file. - At the bottom of the form, I'm using a shadcn/ui
Button
component, similar to what we've done before. I have a checkIcon
inside with the text "Create a Contact". - I've also made every field required, using the HTML
required
attribute.
At the top of our file, we need to import our Button
and Icon
components:
import { Icon } from "./Icon"import { Button } from "./ui/button"
Now, let’s test this out, again, within the browser. When you click on the “Add a Contact” button, you should see the styled form slide in:
Great! Now, let’s hotwire this together. We’ll follow a similar approach to what we did with the Job Application form.
Adding a Contact to the Database
On the ContactForm
component, let’s set an action
on the form
element:
<form action={handleSubmit}>
Now, we need to write the handleSubmit
function. We can add this code right above our return
statement:
const handleSubmit = async (formData: FormData) => { const result = await createContact(formData); if (result.success) { console.log("Contact created successfully"); } else { console.error("Error creating contact"); }}
- This function will take our form data and pass it to the
createContact
function. - The
createContact
function is a server action (we still need to write this). Since it's a server action, we can useasync
andawait
to wait for the result. - If the result is successful, we'll log a message to the console.
- If the result is not successful, we'll log an error to the console.
Now, inside our src/app/pages/applications/actions.ts
file, let’s create a new function at the very bottom of our file:
export const createContact = async (formData: FormData) => { try { await db.contact.create({ data: { firstName: formData.get("firstName") as string, lastName: formData.get("lastName") as string, email: formData.get("email") as string, role: formData.get("role") as string, }, }) return { success: true, error: null } } catch (error) { console.error(error) return { success: false, error: error as Error } }}
- We're exporting our
createContact
function so we can import it into ourContactForm
component. - The function takes a single argument,
formData
. - Inside, we have a
try
/catch
block. With thetry
block, we make a call to our database andawait
the result.- We're already importing our
db
at the top of our file. - Since we're working with the
Contact
table, we can reference it withdb.contact
. Then, we can invoke thecreate
method and pass it an object with all of our form data.
- We're already importing our
- Assuming, we were successful, we return an object with
success
set totrue
anderror
set tonull
. - If there's an error, we log it to the console and return an object with
success
set tofalse
and the error.
Let’s go back to our ContactForm
component and import the createContact
function at the top of our file:
import { createContact } from "../pages/applications/actions";
Let’s test it out! But, before you do, open up your developer tools and go to the console tab. If everything goes through successfully, you should see a success message in the console:
You can also check Prisma Studio (npx prisma studio
) to see the new contact in the database:
All the John Doe entries are from when we hardcoded the data in.
This works, kind of. — It works, but it’s not the best user experience. It would be better if we could see a success (or error) message in browser.
Fortunate for us, shadcn/ui has a toast component that we can use. This component works a little differently than the other components we’ve used. It has two parts. It needs the Toaster
and the Toast
.
The Toaster
handles where the message gets displayed. We can also cue up or show several messages at once. So, the Toaster
“holds” the Toast
.
The Toaster
component typically lives in a layout component.
Then, components nested inside, can trigger Toast
messages.
This might make more sense as we build it out.
Within our InteriorLayout
component, let’s add the Toaster
component:
import { Header } from "../components/Header"import { Toaster } from "@/app/components/ui/sonner"
const InteriorLayout = ({ children }: { children: React.ReactNode}) => { return ( <div className="page-wrapper"> <main className="page bg-white"> <Header /> {children} <Toaster position="top-right" richColors /> </main> </div> )}
export { InteriorLayout }
- We’ve imported our Toaster component at the top of our file (notice the import path is
@/app/components/ui/sonner
, this is slightly different than the official shadcn/ui documentation) - Then, we added the
Toaster
component below the{children}
- I positioned the
Toaster
to the top right of the screen using theposition
prop and appliedrichColors
Even though we already added the sonner.tsx
file through the shadcn/ui CLI, we need to make sure that our project knows it’s a client component. At the top of sonner.tsx
, let’s add the use client
directive:
"use client"
Now, on our ContactForm
component, let’s import our toast
at the top of our file:
import { toast } from "sonner"
Then, we can update our conditional statement, inside our handleSubmit
function:
if (result.success) { toast.success("Contact created successfully")} else { toast.error("Error creating contact")}
If you test te form again, within the browser, you should see a successful toast message in top right of the screen.
Sweet! Now, we could leave our “Create Contact” form the way it is:
- Click “Add a Contact” button. It triggers the
Sheet
component, with ourContactForm
nested inside. - Fill out the form and click “Create a Contact”.
- A success message is shown in the browser.
- Our
Sheet
component stays open and you can keep adding as many contacts as you’d like. - When you’ve finished adding contacts, you can close the
Sheet
component by clicking the “X” in the top right corner.
OR we could close the Sheet
component after the form is submitted.
Let’s do that.
Enhancing the User Experience
At first glance, this might seem a little tricky because it doesn’t look like this functionality exists. But, if you click on the “Docs” or “API” button at the top of the Sheet
documentation:
You’ll notice that this component is built on top of the Radix UI Dialog
component. There’s a prop called open
and onOpenChange
that we can leverage.
First, at the top of our ApplicationsForm
component, let’s add a piece of state that keeps track of whether the Sheet
is open or not:
const [isContactSheetOpen, setIsContactSheetOpen] = useState(false);
Then, be sure to import useState
at the top of our file:
import { useState } from "react"
Now, scroll down to the bottom of the ApplicationForm
component, to our Sheet
component:
<Sheet open={isContactSheetOpen} onOpenChange={setIsContactSheetOpen}> <SheetTrigger className="flex items-center gap-2 font-display text-sm font-bold bg-secondary py-3 px-6 rounded-md cursor-pointer"> <Icon id="plus" size={16} />Add a Contact </SheetTrigger> <SheetContent className="pt-[100px] px-12"> <SheetHeader> <SheetTitle>Add a Contact</SheetTitle> <SheetDescription> Add a Contact to this application. </SheetDescription> <ContactForm callback={() => setIsContactSheetOpen(false)} /> </SheetHeader> </SheetContent></Sheet>
- We'll set
open
toisContactSheetOpen
. -- This is the piece of state that keeps track of whether theSheet
is open or not. - We'll set
onOpenChange
tosetIsContactSheetOpen
. -- This updates our state when theSheet
is opened or closed. - Then, on our
ContactForm
, we'll pass in a callback function. This function will set our state tofalse
, closing theSheet
.
Now, head over to our ContactForm
component and update the handleSubmit
function.
.const ContactForm = ({ callback }: { callback: () => void }) => {
const handleSubmit = async (formData: FormData) => { const result = await createContact(formData); if (result.success) { toast.success("Contact created successfully");
callback(); } else { toast.error("Error creating contact"); } }
If you test everything out in the browser, you should still see the successful toaster message and the Sheet
will close. 😎
Now, we need a way to see the contacts we’ve added.
Displaying Contacts
If we look at the design within Figma, we have a list of all the contacts, associated with the job application:
First things first, we need to get a list of all our contacts from the database. We don’t want ALL the contacts, we only want the ones associated with this job application. We haven’t submitted the form yet and any contacts we’ve created so far, aren’t associated with a company. Meaning, we can use the following flow:
- Grab any contacts that aren’t associated with a company.
- When we submit the form, we’ll associate the contact with the company and the job application.
This does create an interesting loop hole. If:
- The user creates some new contacts
- The user doesn’t submit the form and navigates away from the page
- The user comes back to the page, those contacts are still there.
That should be OK, as long as we have a way to remove any contacts in the list. But, we need make sure they’re only seeing contacts they’ve added.
Let’s start by displaying all contacts that aren’t associated with a company.
Getting the Contacts from the Database
Even though our contacts will be displayed in the ApplicationForm
component, the ApplicationForm
is a client component. It’d be better if we could grab the data inside a server component and then pass it in. (You’ll remember, we did the ame thing with our application statuses).
Inside our New
page, let’s get all of our contacts, where the companyId
is null
:
const New = async () => { const statuses = await db.applicationStatus.findMany()
const contacts = await db.contact.findMany({ where: { companyId: null } })
return (
Now, we can pass this data into our ApplicationForm
component:
<ApplicationsForm statuses={statuses} contacts={contacts} />
Inside our ApplicationForm
component, let’s add contacts
to our list of props:
const ApplicationsForm = ({ statuses, contacts }: { statuses: ApplicationStatus[], contacts: Contact[] }) => {
We also need to import the Contact
type at the top of our file from @prisma/client
. We’re already importing ApplicationStatus
from @prisma/client
, so simply need to add it our list:
import { ApplicationStatus, Contact } from "@prisma/client"
Scroll down to our <div>Contact Card</div>
placeholder and replace it with a ul
element:
.{contacts && ( <ul>
{contacts.map((contact) => (
<li key={contact.id}>{contact.firstName} {contact.lastName} {contact.email} {contact.role}</li> ))} </ul>)}
If you take a look at this within the browser, you’ll see our contact list:
Let’s style our contact card.
Styling the Contact Card
Within the src/app/components
directory, create a new file called ContactCard.tsx
.
Inside our file, let’s start with a basic component structure:
const ContactCard = () => { return ( <div>ContactCard</div> )}
export { ContactCard }
We want to the contact object in as a prop:
import { Contact } from "@prisma/client"
const ContactCard = ({ contact }: { contact: Contact }) => {
Then, inside the return
statement:
return ( <div> <div> <Avatar> <AvatarFallback> {contact.firstName.charAt(0)} </AvatarFallback> </Avatar> </div> <div> <p>{contact.firstName} {contact.lastName}</p> <p>{contact.role}</p> </div> <div> <a href={`mailto:${contact.email}`}><Icon id="email" size={24} /></a> </div> </div>)
Then, at the top the file, import the Avatar
and Icon
components:
import { Avatar, AvatarFallback } from "./ui/avatar"import { Icon } from "./Icon"
Let’s go back through and add some styles:
return ( <div className="flex items-center gap-4 mb-6"> <div> <Avatar className="size-10"> <AvatarFallback> {contact.firstName.charAt(0)} </AvatarFallback> </Avatar> </div> <div className="flex-1"> <p className="text-sm font-medium">{contact.firstName} {contact.lastName}</p> <p className="text-sm text-zinc-500">{contact.role}</p> </div> <div> <a href={`mailto:${contact.email}`}><Icon id="email" size={24} /></a> </div> </div>)
- On line 7, we added a class of
flex items-center gap-4
to align the avatar, name, and email icon vertically. We also put16px
of space between each element. Then, we also added24px
of margin to the bottom of the card. - On line 9, a class of
size-10
makes the avatar40px
wide and40px
tall. - On line 15, the `flex-1 class ensure that the name and role take up as much horizontal space as possible.
- On line 16,
text-sm font-medium
will make the name14px
and have a font weight of medium. - On line 17, the text role will be
14px
withtext-sm
and gray withtext-zinc-500
.
Let’s swap out our placeholder content with our component. Inside the ApplicationsForm
:
{contacts.map((contact) => ( <li key={contact.id}><ContactCard contact={contact} /></li>))}
At the top our file:
import { ContactCard } from "./ContactCard"
If you take a look at this within the browser, you’ll see our styledcontact list:
😍 Lovely!
Now, we need to ensure that the user will only see the contacts that they’ve added.
Limiting the Contacts to the Current User
First, we’ll need to modify our prisma.schema
file so that we can keep track of who authored the contact.
model Contact {7 collapsed lines
id String @id @default(uuid()) // Contact ID (UUID-based) firstName String lastName String email String? role String? companyId String? company Company? @relation(fields: [companyId], references: [id]) userId String user User @relation(fields: [userId], references: [id])4 collapsed lines
createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
@@index([id])}...model User {3 collapsed lines
id String @id @default(uuid()) // User ID (UUID-based) username String @unique createdAt DateTime @default(now()) contacts Contact[]2 collapsed lines
applications Application[] credentials Credential[] // Relationship: One user can have many credentials}
On the Contact
model, we need to add a userId
column. Then, we need to establish the relationship between the Contact
and User
models, saying that the user
column is a type of User
. The relationship is connected through the userId
column on the Contact
model and references the id
column on the User
model.
Then, we need to set up the inverse relationship on the User
model establishing there are contacts
with a type Contact
. Since this is an array, we’ll also include square brackets []
.
Now, we need to create our migration. Within the terminal:
pnpm migrate:new "associate contacts with users"
When you run this, you’ll probably get an error:
The reason is because we already have contacts in our database. When we added the userId
column to the Contact
model, we said that it was a required field. This creates a problem for all existing contact entries that don’t have a user associated with them.
If you read the error message, the migration file was created, but not applied.
To fix this, you can try manually removing the existing contacts, or you can run the seed command:
pnpm seed
The seed command will also reset the database because we also clear all the tables at the top of the script:
await db.$executeRawUnsafe(`\ DELETE FROM Application; DELETE FROM ApplicationStatus; DELETE FROM Contact; DELETE FROM Company; DELETE FROM Credential; DELETE FROM User; DELETE FROM sqlite_sequence;`);
Now, try running pnpm migrate:dev
.
The only downside is you’ll need to create a new user account, by visiting http://localhost:5173/user/signup to register.
Then, you can run the application seed command to add a new application, but first, you’ll need to update the user ID within the applicationSeed.ts
file:
user: { connect: { id: "c4f35853-8909-4139-98bb-c08663e4230c", },},
If you scroll down within the applicationSeed.ts
file, you’ll notice there’s an error when creating a contact:
It’s looking for the userId
column. This is a quick fix. We can use userId
we just updated the application entry with:
contacts: { create: { firstName: "John", lastName: "Doe", role: "Hiring Manager", userId: "c4f35853-8909-4139-98bb-c08663e4230c" },},
Now, you can run:
pnpm seed:application
We also need to update our createContact
function, inside our actions.ts
file, to include the userId
column:
export const createContact = async (formData: FormData) => { try { const { ctx } = requestInfo;
if (!ctx.user) { throw new Error("User not found"); }
await db.contact.create({ data: {4 collapsed lines
firstName: formData.get("firstName") as string, lastName: formData.get("lastName") as string, email: formData.get("email") as string, role: formData.get("role") as string, user: { connect: { id: ctx.user?.id || "", }, }, }, })
- In order to get the current user ID, we can get the
ctx
from therequestInfo
object (line 58). (We already importedrequestInfo
for thecreateApplication
function.) - On lines 60-62, we can check to see if the
ctx.user
exists. If not, we'll throw an error. - If it does, we can connect the
contact
to theuser
using theconnect
method (lines 70-73)
Now, we need to update our database query on the New
page.
import { AppContext } from "@/worker"
const New = async ({ ctx }: { ctx: AppContext }) => { const statuses = await db.applicationStatus.findMany()
const contacts = await db.contact.findMany({ where: { companyId: null, userId: ctx.user?.id || "" } })
- On each page, we have access to the
ctx
object. This has a type ofAppContext
, defined insrc/worker
. - From
ctx
, we can access theuser
object and the user's ID. - On our database call, we can add another property to our
where
clause, saying that theuserId
should match the logged in user's ID.
If you try adding contacts using our form, the display will look the same as before. But, you can confirm the new functionality is working by using Prisma Studio to modify a contact’s userId
property. If the userId
doesn’t match the logged in user’s ID, the contact will not be displayed.
Associating the Contact with a Company
Now, when we add a job application, we need to associate the contact with the company. We’ll need to adjust our createApplication
function within the src/app/pages/applications/actions.ts
file slightly.
First, let’s ensure that we’re passing our contacts into our createApplication
function properly.
Within the ApplicationForm
component, go to the handleSubmit
function:
const ApplicationsForm = ({ statuses, contacts }: { statuses: ApplicationStatus[], contacts: Contact[] }) => { const [isContactSheetOpen, setIsContactSheetOpen] = useState(false);
const handleSubmit = async (formData: FormData) => { formData.append("contacts", JSON.stringify(contacts)); const result = await createApplication(formData);
- On line 21, we're already passing all of our
contacts
into ourApplicationForm
component. - Within our
handleSubmit
function, we canappend
thecontacts
to ourformData
. The only "catch" is that ourcontacts
will need to be converted into a JSON string.
Now, within our src/app/pages/applications/actions.ts
file, inside our createApplication
function, we need to parse our JSON
string:
export const createApplication = async (formData: FormData) => {7 collapsed lines
try { const { ctx } = requestInfo;
if (!ctx.user) { throw new Error("User not found"); }
const contacts = JSON.parse(formData.get("contacts") as string) as Contact[];
- We get the
contacts
off theformData
which is a string:formData.get("contacts") as string
- Then, we use
JSON.parse
to convert the string into an array ofContact
objects.
Be sure to import the Contact
type at the top of the file from @prisma/client
:
import { Contact } from "@prisma/client"
Then, when we’re creating our company, inside the Prisma create
method, we can connect our contacts
to the company:
company: { create: { name: formData.get("company") as string, contacts: { connect: { firstName: "John", lastName: "Doe", email: "[email protected]", role: "Hiring Manager", userId: ctx.user.id, } connect: contacts, }, },},
Now, when you create a new job application, any contacts you’ve listed, will be associated with the company and job application you created.
And, if you try to add a New Job Application, the contacts list will be empty:
One step remains: removing a contact from the list.
Removing Contacts
Styling the Delete Button
First, we need to modify the ContactCard
component to have a delete button:
return ( <div className="flex items-center gap-4 mb-6"> <div> <button role="button"> <Icon id="close" size={16} /> </button> </div> <div> <Avatar className="size-10"> <AvatarFallback> {contact.firstName.charAt(0)} </AvatarFallback> </Avatar> </div>
I added a div
right before the Avatar
div that contains a button
with an X icon inside. Pretty straightforward.
Now, we need to add some styles.
return ( <div className="relative flex items-center gap-4 mb-6"> <div className="absolute top-2 -left-[37px]"> <button role="button" className="rounded-full bg-destructive p-1"> <Icon id="close" size={16} /> </button> </div>
- On line 8, I'm using the wrapping
div
to position thebutton
absolutely. It will appear8px
from the top and-37px
over from the left. In order for it to be positioned, relative to the parent container, we need to set thediv
torelative
on line 7. - On line 9, I'm using a regular
button
tag, instead of the shadcn/uiButton
component because this button won't look like any of the other primary action buttons we've created so far.- I have a class of
rounded-full
to turn the button into a circle. bg-destructive
to make the background color red.p-1
to add4px
of padding on all sides.
- I have a class of
If you look at this in the browser, you should see our red X next to the contact card:
I think it’d look better if we made the X white. One of my favorite ways to do this is to make the svg fill match the current text color. We can do that by adding a class to our button
of text-white
and fill-current
.
<button role="button" className="text-white fill-current rounded-full bg-destructive p-1"> <Icon id="close" size={16} /></button>
Let’s also change the hover color so that the user has some visual feedback when rolling over the button. We can change the background to black and ensure the cursor is a pointer:
<button role="button" className="hover:bg-black cursor-pointer text-white fill-current rounded-full bg-destructive p-1"> <Icon id="close" size={16} /></button>
I don’t want a bunch of red circles lined up. If you have a bunch of contact cards, these will be eye catching and distracting from the main form. Let’s make it so that the button only appears when hovering over each respective contact card.
To do this, we’ll need to add a group
to the ContactCard
component.
<div className="relative group/card flex items-center gap-4 mb-6">
On the div
wrapping the button:
<div className="hidden group-hover/card:block pr-10 absolute top-1 -left-[37px]"> <button role="button" className="hover:bg-black cursor-pointer text-white fill-current rounded-full bg-destructive p-1"> <Icon id="close" size={16} /> </button></div>
- We can set the
div
tohidden
by default. But, when the user hovers over thegroup/card
it applies a display ofblock
withgroup-hover/card:block
Perfect! With the styling applied, let’s give this button the functionality it needs.
Adding the Delete Functionality
On the button
, let’s add an onClick
handler:
<button role="button" className="hover:bg-black cursor-pointer text-white fill-current rounded-full bg-destructive p-1"> <Icon id="close" size={16} /></button>
Now that we’ve introduced interactivity, this component needs to be a client component. At the top of the file, let’s add:
"use client"
Now, let’s create the handleDelete
function that we referenced in the onClick
handler. Right above our return
statement:
const handleDelete = async () => { const result = await deleteContact(contact.id); if (result.error) { toast.error("Yikes! Couldn't delete."); } else { toast.success("Contact deleted"); }}
- Since we're calling a server function, we can use
async
andawait
to wait for thedeleteContact
function to complete (we still need to write this function). - We'll pass over the contact's id, so that we know which contact to delete.
- We can store the result in a variable called
result
. - If there's an error, we'll display a toast error.
- If it works, we'll display a toast success message.
Now, we need to import toast
and deleteContact
at the top of our file:
import { toast } from "sonner"import { deleteContact } from "../pages/applications/actions"
Now, let’s head over to our actions.ts
file and create the deleteContact
function.
export const deleteContact = async (contactId: string) => { try { await db.contact.delete({ where: { id: contactId, }, }) return { success: true, error: null } } catch (error) { console.error(error) return { success: false, error: error as Error } }}
Hopefully, this workflow is starting to feel familiar.
- On line 82, we're accepting the
contactId
, which is a string. - We have a
try
/catch
block where we try to delete our contact. If it runs into any problems, it will catch our error (on line 90), console the error, and returnsuccess
asfalse
and the error itself. - On lines 84-88, we're using the Prisma API to delete the contact, where the
id
is equal to thecontactId
that we passed in.
Now, if you try running this in the browser, you’ll probably run into an error:
This is because when the use clicks on the button, the page tries to submit the form. To prevent the default browser behavior, we can update the onClick
handler to capture the event:
<button role="button" className="hover:bg-black cursor-pointer text-white fill-current rounded-full bg-destructive p-1" onClick={(e) => handleDelete(e)}> <Icon id="close" size={16} /></button>
Now, let’s update the handleDelete
function to accept the event and prevent the form from submitting:
const handleDelete = async (e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault();
Now, try running this in the browser again. You should see the contact deleted and a successful toast message:
Lovely!