Add Delete and Favorite Data Functions, Build the Favorite Toggle
Add Delete and Favorite Data Functions, Build the Favorite Toggle
Data functions
Open lib/contacts.ts and add:
export async function deleteContact(id: string): Promise<void> {
await pool.query(`DELETE FROM contacts WHERE id = $1`, [id]);
}
export async function toggleFavorite(id: string): Promise<void> {
await pool.query(
`UPDATE contacts SET favorite = NOT favorite, updated_at = now() WHERE id = $1`,
[id]
);
}
NOT favorite flips the boolean in place, right in SQL — no need to read
the current value first and write back its opposite.
Server Actions
Open lib/actions.ts and add:
import { deleteContact, toggleFavorite } from "./contacts";
export async function deleteContactAction(id: string) {
await deleteContact(id);
revalidatePath("/");
redirect("/");
}
export async function toggleFavoriteAction(formData: FormData) {
const id = String(formData.get("id"));
const returnTo = String(formData.get("returnTo") || "/");
await toggleFavorite(id);
revalidatePath("/");
revalidatePath(`/contacts/${id}`);
redirect(returnTo);
}
toggleFavoriteAction takes a returnTo field so the same action works
whether it’s clicked from the Home page or the contact’s own detail page —
either way, the redirect sends the learner right back where they were.
Build the favorite toggle
Create components/FavoriteToggleForm.tsx:
import { toggleFavoriteAction } from "@/lib/actions";
export default function FavoriteToggleForm({
id,
favorite,
returnTo,
}: {
id: string;
favorite: boolean;
returnTo: string;
}) {
return (
<form action={toggleFavoriteAction} className="inline">
<input type="hidden" name="id" value={id} />
<input type="hidden" name="returnTo" value={returnTo} />
<button type="submit" className={`fav-btn ${favorite ? "is-fav" : ""}`} title={favorite ? "Unfavorite" : "Mark as favorite"}>
{favorite ? "★" : "☆"}
</button>
</form>
);
}
This is a plain Server Component — no "use client" directive anywhere.
The id and returnTo values ride along as hidden form fields, so the
Server Action receives everything it needs from formData alone. Clicking
the star submits the form, runs the action on the server, and the page
redirects back with the new state — all without a single line of
client-side JavaScript.
Add it to the contact detail page
Open app/contacts/[id]/page.tsx and update the header:
import FavoriteToggleForm from "@/components/FavoriteToggleForm";
// ...inside the component, in detail-header:
<h1>
{contact.name}
<FavoriteToggleForm id={contact.id} favorite={contact.favorite} returnTo={`/contacts/${contact.id}`} />
</h1>
Test it
Open any contact’s detail page and click the star. The page should reload
with the star now filled in (or emptied out), and the change should also
show up back on the Home page — confirming revalidatePath("/") kept both
views in sync.
Checkpoint
You can favorite and unfavorite a contact from its detail page with a
single click, using only a Server Component and a Server Action — no
client-side state involved.