Skip to main content

Overview

The Message Scheduling feature allows you to create, update, and manage messages that will be sent to contacts after a specified number of days. This is useful for automated follow-ups, reminders, and scheduled communications.

Key Features

Scheduled Delivery

Messages are sent after a configurable delay in days

Contact Selection

Select recipients from your People list

Full CRUD Operations

Create, read, update, and delete scheduled messages

Validation

Built-in validation for phone numbers and scheduling rules

Message Data Structure

Messages contain the following fields:
interface Message {
  id: string;
  content: string;
  sendToPhone: string;
  sendAfter: number;  // Days to wait before sending
  sendTo: {
    name: string;
  };
  createdAt: Date;
}

Creating a Message

1

Open the Add Message Dialog

Click the “Add Message” button at the bottom of the Messages section to open the creation form.
<Dialog>
  <DialogTrigger asChild>
    <Button className="w-full">
      <MailPlus />
      <span>Add Message</span>
    </Button>
  </DialogTrigger>
  <DialogContent>
    <DialogTitle>Add Message</DialogTitle>
    <CreateMessageForm people={people} />
  </DialogContent>
</Dialog>
2

Fill in Message Details

The form contains three required fields:
  • Message: The content to send (textarea)
  • Send To: Select a contact from the dropdown
  • Send After: Number of days to wait before sending
<form action={createAction} className="space-y-3">
  <Label htmlFor="content">Message</Label>
  <Textarea name="content" className="h-32" />
  
  <Label htmlFor="sendToPhone">Send To</Label>
  <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>
  
  <Label htmlFor="sendAfter">Send After (in days)</Label>
  <Input type="number" step={0.0001} min={0} name="sendAfter" />
  
  <Button type="submit" disabled={createIsPending}>
    Add
  </Button>
</form>
3

Validation and Submission

When you submit the form, the data is validated using Zod schema before being sent to the backend.

Validation Rules

The application enforces strict validation rules for message creation:

Phone Number Format

Phone numbers must follow the Bangladesh format:
Pattern: 8801XXXXXXXXX (13 digits starting with 8801)
const messageSchema = z.object({
  content: z.string().min(1, "Content cannot be empty"),
  sendToPhone: z
    .string()
    .regex(/^8801\d{9}$/, "Invalid phone number format. Must be 8801XXXXXXXXX"),
  sendAfter: z
    .number()
    .min(1, "Send after is required and minimum after 1 day"),
});

Content Requirements

  • Message content cannot be empty
  • Must be a valid string with at least 1 character

Scheduling Requirements

The sendAfter field must be at least 1 day for new messages. When updating, the minimum is 0 days.

Updating a Message

Click on any message card to open the update dialog:
<Dialog>
  <DialogTrigger asChild>
    <Card className="cursor-pointer">
      <CardHeader>{message.content.slice(0, 100)} ...</CardHeader>
      <CardContent>
        <CardDescription>
          Send to: {message.sendTo.name} ({message.sendToPhone})
        </CardDescription>
        <CardDescription>
          Send after: {message.sendAfter} Days
        </CardDescription>
      </CardContent>
    </Card>
  </DialogTrigger>
  <DialogContent>
    <DialogTitle>Update Message</DialogTitle>
    <MessageUpdateForm message={message} people={people} />
  </DialogContent>
</Dialog>
The update form is pre-populated with existing values:
<Input type="text" name="id" hidden defaultValue={message.id} />
<Textarea name="content" defaultValue={message.content} />
<Select name="sendToPhone" defaultValue={message.sendToPhone}>
  {/* Options */}
</Select>
<Input name="sendAfter" defaultValue={message.sendAfter} />

Deleting a Message

Within the update dialog, there’s a separate delete button:
<form action={deleteAction} className="space-y-3">
  <Input type="text" name="id" hidden defaultValue={message.id} />
  <Button type="submit" variant="outline" disabled={deleteIsPending}>
    Delete
  </Button>
</form>

Server Actions

All message operations use Next.js Server Actions for server-side processing:
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";

const messageSchema = z.object({
  content: z.string().min(1, "Content cannot be empty"),
  sendToPhone: z
    .string()
    .regex(/^8801\\d{9}$/, "Invalid phone number format. Must be 8801XXXXXXXXX"),
  sendAfter: z
    .number()
    .min(1, "Send after is required and minimum after 1 day"),
});

export default async function createMessage(
  prevState: MessagePrevState,
  formData: FormData
): Promise<MessagePrevState> {
  const parsedData = messageSchema.safeParse({
    content: formData.get("content"),
    sendToPhone: formData.get("sendToPhone"),
    sendAfter: Number(formData.get("sendAfter")),
  });

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

  const response = await fetch(
    `${process.env.BACKEND_URL}/messages/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 };
}

Toast Notifications

The application provides real-time feedback using toast notifications:
useEffect(() => {
  if (createState.success) {
    toast.success("Message created successfully", {
      richColors: true,
    });
  }
  if (createState.error) {
    toast.error("Message creation failed", {
      description: createState.error,
      duration: 3500,
      richColors: true,
    });
  }
}, [createState]);

Backend API Endpoints

The frontend communicates with these backend endpoints:
EndpointMethodPurpose
/messages/allGETFetch all messages
/messages/create-onePOSTCreate a new message
/messages/update-one/:idPOSTUpdate an existing message
/messages/delete-one/:idPOSTDelete a message
All endpoints require Basic Authentication using USERNAME and PASSWORD environment variables.

Build docs developers (and LLMs) love