Skip to main content

Overview

The Person Management feature allows you to create, update, and manage contacts in your address book. These contacts can be selected as recipients when scheduling messages.

Key Features

Contact Directory

Centralized contact management system

Phone Validation

Automatic validation of phone numbers

CRUD Operations

Create, read, update, and delete contacts

Message Integration

Contacts are linked to scheduled messages

Person Data Structure

Each person record contains:
interface People {
  id: string;
  name: string;
  phone: string;
}

UI Components

The People component displays all contacts in a scrollable list:
export default async function People() {
  const data = await fetch(`${process.env.BACKEND_URL}/people/all`, {
    headers: {
      Authorization: `Basic ${Buffer.from(
        `${process.env.USERNAME}:${process.env.PASSWORD}`
      ).toString("base64")}`,
    },
  });
  
  const people: People[] = await data.json();
  
  return (
    <div className="cursor-pointer">
      <h2 className="text-xl font-bold mb-2">People : </h2>
      <ScrollArea className="max-h-[calc(100vh-180px)]">
        {people.map((person: People) => (
          <Person key={person.id} person={person} />
        ))}
      </ScrollArea>
      <Dialog>
        <DialogTrigger asChild>
          <Button className="w-full">
            <UserPlus />
            <span>Add Person</span>
          </Button>
        </DialogTrigger>
        <DialogContent>
          <DialogTitle>Add Person</DialogTitle>
          <PersonCreate />
        </DialogContent>
      </Dialog>
    </div>
  );
}

Creating a Contact

1

Open the Add Person Dialog

Click the “Add Person” button to open the creation form:
<Button className="w-full">
  <UserPlus />
  <span>Add Person</span>
</Button>
2

Fill in Contact Information

The form requires two fields:
  • Name: The contact’s full name
  • Phone: Phone number in Bangladesh format
<form action={createAction} className="space-y-3">
  <div className="space-y-2">
    <Label htmlFor="name" className="font-bold">Name</Label>
    <Input name="name" />
  </div>
  
  <div className="space-y-2">
    <Label htmlFor="phone" className="font-bold">Phone</Label>
    <Input name="phone" />
  </div>
  
  <Button type="submit" disabled={createIsPending}>
    Add
  </Button>
</form>
3

Submit and Validate

The form data is validated on the server before being saved to the database.

Phone Number Validation

The application enforces strict phone number validation using Zod schema:
const personSchema = z.object({
  name: z.string().min(1, "Name cannot be empty"),
  phone: z
    .string()
    .regex(/^8801\d{9}$/, "Invalid phone number format. Must be 8801XXXXXXXXX"),
});
Phone numbers must be in the format: 8801XXXXXXXXX
  • Must start with 8801
  • Must be exactly 13 digits long
  • Only numeric characters allowed
Example: 8801712345678

Valid Phone Number Examples

  • 8801712345678
  • 8801812345678
  • 8801912345678

Invalid Phone Number Examples

  • 01712345678 ✗ (Missing country code)
  • +8801712345678 ✗ (Contains + symbol)
  • 88017123456 ✗ (Too short)
  • 88017123456789 ✗ (Too long)

Viewing Contacts

Each contact is displayed as a card in the scrollable list:
function Person({ person }: { person: People }) {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Card className="border-1 border-dashed drop-shadow-xl hover:bg-ring-offset mb-2">
          <CardHeader className="font-bold">{person.name}</CardHeader>
          <CardContent className="space-y-2">
            <CardDescription>Phone: {person.phone}</CardDescription>
          </CardContent>
        </Card>
      </DialogTrigger>
      <DialogContent>
        <DialogTitle>Update Person</DialogTitle>
        <PersonUpdate person={person} />
      </DialogContent>
    </Dialog>
  );
}
Click on any contact card to open the update dialog.

Updating a Contact

The update form is pre-filled with existing data:
export default function PersonUpdate({ person }: { person: People }) {
  const [updateState, updateAction, updateIsPending] = useActionState(
    updatePerson,
    {}
  );
  
  return (
    <form action={updateAction} className="space-y-3">
      <Input type="text" name="id" hidden defaultValue={person.id} />
      
      <div className="space-y-2">
        <Label htmlFor="name">Name</Label>
        <Input name="name" defaultValue={person.name} />
      </div>
      
      <div className="space-y-2">
        <Label htmlFor="phone">Phone</Label>
        <Input name="phone" defaultValue={person.phone} />
      </div>
      
      <Button type="submit" disabled={updateIsPending}>
        Update
      </Button>
    </form>
  );
}

Deleting a Contact

The delete button is available in the update dialog:
<form action={deleteAction} className="space-y-3">
  <Input type="text" name="id" hidden defaultValue={person.id} />
  <Button type="submit" variant="outline" disabled={deleteIsPending}>
    Delete
  </Button>
</form>
Deleting a contact may affect existing scheduled messages that reference this contact.

Server Actions

All person management operations use Next.js Server Actions:
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";

interface PersonPrevState {
  success?: boolean;
  error?: string;
}

const personSchema = z.object({
  name: z.string().min(1, "Name cannot be empty"),
  phone: z
    .string()
    .regex(/^8801\\d{9}$/, "Invalid phone number format. Must be 8801XXXXXXXXX"),
});

export default async function createPerson(
  prevState: PersonPrevState,
  formData: FormData
): Promise<PersonPrevState> {
  try {
    const parsedData = personSchema.safeParse({
      name: formData.get("name"),
      phone: formData.get("phone"),
    });

    if (!parsedData.success) {
      return {
        error: parsedData.error.errors.map((err) => err.message).join(", "),
      };
    }

    const response = await fetch(
      `${process.env.BACKEND_URL}/people/create-one`,
      {
        method: "POST",
        headers: {
          Authorization: `Basic ${Buffer.from(
            `${process.env.USERNAME}:${process.env.PASSWORD}`
          ).toString("base64")}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(parsedData.data),
      }
    );

    if (!response.ok) {
      return {
        error: `Backend Error: ${response.status} ${response.statusText}`,
      };
    }
    
    revalidatePath("/");
    return { success: true };
  } catch (error) {
    return {
      error: error instanceof Error ? error.message : "Something went wrong",
    };
  }
}

Toast Notifications

The application provides feedback for all operations:
useEffect(() => {
  if (createState.success) {
    toast.success("Person created successfully", {
      richColors: true,
    });
  }
  if (createState.error) {
    toast.error("Person creation failed", {
      description: createState.error,
      duration: 3500,
      richColors: true,
    });
  }
}, [createState]);

useEffect(() => {
  if (updateState.success) {
    toast.success("Person updated successfully", { richColors: true });
  }
  if (deleteState.success) {
    toast.success("Person deleted successfully", { richColors: true });
  }
}, [updateState, deleteState]);

Backend API Endpoints

EndpointMethodPurpose
/people/allGETFetch all contacts
/people/create-onePOSTCreate a new contact
/people/update-one/:idPOSTUpdate an existing contact
/people/delete-one/:idPOSTDelete a contact
All API requests require Basic Authentication using environment variables USERNAME and PASSWORD.

Integration with Messages

When creating or updating a message, contacts from the People list are available in the recipient dropdown:
<Select name="sendToPhone">
  <SelectTrigger>
    <SelectValue placeholder="Send To" />
  </SelectTrigger>
  <SelectContent>
    {people.map((person) => (
      <SelectItem key={person.id} value={person.phone}>
        {person.name} ({person.phone})
      </SelectItem>
    ))}
  </SelectContent>
</Select>

Build docs developers (and LLMs) love