Build a Task Assignment App with Twilio Whatsapp, Strapi, and Next.js

November 10, 2022
Written by
Ravgeet Dhillon
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Task assignment app header

In a working environment, each and every individual is assigned a task. Task assignment is one of the most important aspects in the successful completion of a project. However, it is also very important to communicate the tasks assignment duties to the concerned person. Hence, you need a way to send a message to the assignee that a new task has been assigned to them.

In this tutorial, you’ll learn to create a task assignment app using Next.js, Strapi, and Twilio. You’ll learn to use Next.js for building the frontend UI, Strapi for building the backend, and Twilio for sending WhatsApp notifications.

Prerequisites

To follow this tutorial you need the following items:

  • Node v16 and NPM v8 - If not installed on your system, you can go to nodejs.org to download it.
  • A Twilio account - If you are new to Twilio click here to create a free account.
  • A smartphone with an active WhatsApp account (for testing).

The entire code for this repository is available in this GitHub repository.

Setting Up Project

You’ll need a master directory that holds the code for both the frontend (Next.js) and the backend (Strapi).

To do so, first, open up your terminal, navigate to a path of your choice, and create a project directory by running the following command:

mkdir task-assignment

In the task-assignment directory, you’ll install both Strapi and Next.js projects.

Setting Up Strapi

Let’s start by implementing the backend. Strapi is a headless CMS that allows you to create APIs through its intuitive GUI and also allows you to customize the default behavior via code.

To create a Strapi project, execute the following command in your terminal:

npx create-strapi-app@latest backend --quickstart

This command will create a Strapi project with quickstart settings in the backend directory. You may be asked to install additional packages, if so select yes to proceed.

Once the execution completes for the above command, your Strapi project will start on port 1337. Open up localhost:1337/admin/auth/register-admin in your browser and set up your administrative user:

Admin Sign Up page in Strapi

Enter your details and click the Let’s start button and you'll be taken to the Strapi dashboard:

Strapi dashboard

Creating Collections in Strapi

Under the Plugins header in the left sidebar, click the Content-Types Builder tab and then click Create new collection type to create a new Strapi collection.

Create Strapi Collection type

In the modal that appears, create a new collection type; enter Task as the Display name and click Continue.

Create Task Collection type

Next, add the following fields for your collection type:

  • title - Text field with Short text type and title as the Name
  • description - Text field with Long text type and description as the Name
  • dueDate - Date field with date type and dueDate as the Name
  • user - Relation field with has one relation with the User (from: users-permissions) collection. Change the Field name under Task to user.

Once the fields are added your Task collection page should look like the following:

Fields for Task Collection type

Click the Finish button and click on the Save button to save your collection type.

Next, update the Users collection type by adding the following field to it:

  • phoneNumber - Text field with Short text type and phoneNumber as the Name

Fields for User Collection type

Finally, save your collection type by clicking the Save button.

Sending Task Assignment Messages using Twilio

To send task assignment messages using Twilio WhatsApp API, you need to add lifecycle hooks in Strapi for your collection types and install the twilio NPM package. For this application, the user will receive a WhatsApp message whenever they are assigned/unassigned a task.

First, log in to your Twilio account and visit the Twilio console. On the console, look out for the Account Info section and obtain the Account SID and Auth Token.

Twilio Credentials

Next, activate your Twilio Sandbox to send and receive WhatsApp messages.

Next, open up your project directory in your preferred IDE and add them as environment variables to the .env file:

TWILIO_ACCOUNT_SID=<YOUR_TWILIO_ACCOUNT_SID>
TWILIO_AUTH_TOKEN=<YOUR_TWILIO_AUTH_TOKEN>

Note: Replace <YOUR_TWILIO_ACCOUNT_SID> and <YOUR_TWILIO_AUTH_TOKEN> with your respective values.

Next, install Twilio’s Node library by running the following command in your terminal:

npm i twilio

Next, create a lifecycles.js file in the /src/api/task/content-types/task directory path and add the following code to it:

module.exports = {
  // 1
  async beforeUpdate(event) {
    // 2
    const { params } = event;

    // 3
    const entry = await strapi.entityService.findOne(
      "api::task.task",
      params.where.id,
      {
        populate: { user: true },
      }
    );

    // 4
    if (entry.user && entry.user.phoneNumber && entry.user.id !== params.data.user) {
      const body = `You have been unassigned from task - "${entry.title}".`;
      strapi.service("api::task.whatsapp").sendWhatsappMessage({
        to: entry.user.phoneNumber,
        body,
      });
    }
  },

  // 5
  afterUpdate(event) {
    // 6
    const { result } = event;

    // 7
    if (result.user && result.user.phoneNumber) {
      const body = `You have been assigned a new task - "${result.title}" which is due on ${result.dueDate}.`;
      strapi.service("api::task.whatsapp").sendWhatsappMessage({
        to: result.user.phoneNumber,
        body,
      });
    }
  },

  // 8
  async afterCreate(event) {
    const { result } = event;

    if (result.user && result.user.phoneNumber) {
      const body = `You have been assigned a new task - "${result.title}" which is due on ${result.dueDate}.`;
      strapi.service("api::task.whatsapp").sendWhatsappMessage({
        to: result.user.phoneNumber,
        body,
      });
    }
  },
};

In the above code:

  1. You add the beforeUpdate lifecycle hook for the Task collection type.
  2. You get the params property from the event object.
  3. You use Strapi’s Service API to query the data (strapi.entityService.findOne) from the Task collection type to get the details for the task getting updated.
  4. If the user in the update request is not the same as the one currently assigned to the task, then you send a Whatsapp notification to the unassigned user using the sendWhatsappMessage service method, which you’ll implement in the next section.
  5. You add the afterUpdate lifecycle hook for the Task collection type.
  6. You get the result property from the event object.
  7. If there is an assigned user, then you send a Whatsapp notification to the assigned user using the sendWhatsappMessage service method.
  8. You add the afterCreate lifecycle hook for the Task collection type which sends the assignment message to the assigned user after the task is created.

You can read more about model lifecycles hooks from the Strapi docs.

Next, you need to create a service function to send WhatsApp messages. For that, create a whatsapp.js file in the /src/api/task/services directory path and add the following code to it:

// 1
const twilio = require("twilio");

module.exports = {
  // 2
  sendWhatsappMessage({ to, body }) {
    const accountSid = process.env.TWILIO_ACCOUNT_SID;
    const authToken = process.env.TWILIO_AUTH_TOKEN;
    // 3
    const client = new twilio(accountSid, authToken);

    // 4
    client.messages
      .create({
        from: "whatsapp:+14155238886",
        body: body,
        to: `whatsapp:${to}`,
      })
      .then((message) => console.log(message.sid))
      .catch((err) => console.error(err));
  },
};

In the above code:

  1. You import the twilio NPM module.
  2. You export the sendWhatsappMessage() function that takes in two props - to and body.
  3. You create a Twilio client to access different Twilio services.
  4. You send a WhatsApp message using the messages.create method while passing in the from number, to number, and the message’s body. The from number is the Twilio sandbox number.

At this point, your backend code is set up and the next thing you need to do is configure the permissions for API routes.

Setting Up Permissions for API

By default, there are two roles in Strapi - Public and Authenticated. For this application, you want to allow authenticated access to a user to view all tasks and a single task. So, you need to update the permissions for the Authenticated role.

To do so, navigate back to your Strapi Dashboard, click on the Settings tab under the General header and then select Roles under the Users & Permissions Plugin. Next, click the Edit icon to the right of the Authenticated Role.

Strapi Roles and Permissions

Next, scroll down to find the Permissions tab and check the following permissions for the following collection types and click Save once you are done:

  • For the Task collection type:
Permissions for Task collection type for Authenticated role
  • For the Users permissions collection type:
Permissions for User collection type for Authenticated role

Click Save in the top right corner and head back to the Roles page. Now click the Edit icon next to the Public role.

For the Public role, you only need to enable the login and disable the registration as the new users would be created by the admin from the Strapi Admin panel. So, edit the permissions for the Public role for the following collection types:

  • For the Users permissions collection type:
Permissions for User collection type for Public role

Once finished, click Save.

Adding Data to Strapi Collections

With permissions set up, you can now add some data in the form of users and tasks.

To do so, first, click on the Content Manager tab in the sidebar, and then add some users to the User collection type. For the phoneNumber field, enter your number (in E.164 format) for testing.

Data for User collection type

Next, add some tasks to the Task collection type and assign them to their respective users. Don’t forget to save and publish the tasks when you create them!

Data for Task collection type

As soon as you assign a task to a user, a WhatsApp notification will be sent to that user:

Task assignment message on WhatsApp

Setting Up Frontend

Since the backend is perfectly set up, let’s build the Next.js frontend application and integrate it with the Strapi backend.

To do so, first, in the task-assignment directory, run the following command to create a Next.js project:

npx create-next-app@latest

On the terminal, when you are asked about the project‘s name, set it to frontend. Next, it will install the required NPM dependencies.

For this application, you need the following NPM dependencies:

Next, navigate into the frontend directory and install the above dependencies by running the following command in your terminal:

cd frontend
npm i axios use-local-storage react-bootstrap

Finally, start the Next.js development server by running the following command in your terminal:

npm run dev

This will start the development server on port 3000 and take you to localhost:3000. The first view of the Next.js website will look like this

Next.js default home page

Writing an HTTP Service

You need an HTTP service to connect with the Strapi API and perform CRUD operations.

First, create a config directory in the frontend directory. In the config directory, create an axios.js file and add the following code to it:

// 1
import Axios from "axios";

// 2
export default Axios.create({
  baseURL: "http://localhost:1337/api",
  headers: {
    "Content-Type": "application/json",
  },
});

In the above code:

  1. You import the axios package.
  2. You define an Axios instance (axios) and pass the baseURL and headers parameters.

Next, create a services directory in the frontend directory. In the services directory, create a tasksApi.js file and add the following code to it:

// 1
import { axios } from "../config/axios";

// 2
let jwtStrapiToken;
if (typeof window !== "undefined") {
  jwtStrapiToken = JSON.parse(window.localStorage.getItem("jwtStrapiToken"));
}

// 3
export const TasksAPI = {
  find: async () => {
    const response = await axios.get("/tasks?populate=*", {
      headers: { Authorization: `Bearer ${jwtStrapiToken}` },
    });
    return JSON.parse(response.data);
  },

  findOne: async ({ id }) => {
    const response = await axios.get(`/tasks/${id}?populate=*`, {
      headers: { Authorization: `Bearer ${jwtStrapiToken}` },
    });
    return JSON.parse(response.data);
  },
};

In the above code:

  1. You import the axios config.
  2. You get the value of the Strapi JWT token from the browser’s local storage.
  3. You define and export the TasksAPI object and define the methods for the following:
    1. find - This method is used to get a list of all the tasks.
    2. findOne - This method is used to get a specific task by its ID (id).

Next, create a usersApi.js file in the services directory and add the following code to it:

// 1
import { axios } from "../config/axios";

// 2
export const UsersAPI = {
  login: async ({ email, password }) => {
    const response = await axios.post(
      "/auth/local",
      JSON.stringify({
        identifier: email,
        password: password,
      })
    );
    return JSON.parse(response.data);
  },
};

In the above code:

  1. You import the axios config.
  2. You define and export the UsersAPI object and define the methods for the following:
    1. login - This method is used to login into the app and get a JWT token from the Strapi.

Creating a Task Component

In this section, you’ll create a component for displaying a single product.

So, first, create a components directory in the frontend directory. In the components directory, create a Task.js file and add the following code to it:

import Link from "next/link";

// 1
export const Task = ({ task, hideLink, showId }) => {
  return (
    <div className="border p-3 rounded h-100 bg-light">
      {showId && (
        <>
          <small className="text-muted">ID</small>
          <p>{task.id}</p>
        </>
      )}
      <small className="text-muted">Title</small>
      <p>{task.attributes.title}</p>
      <small className="text-muted">Description</small>
      <p>{task.attributes.description ?? "-"}</p>
      <small className="text-muted">Due On</small>
      <p>{task.attributes.dueDate}</p>
      <small className="text-muted">Assigned to</small>
      <p>
        {task.attributes.user.data.attributes.username}(
        {task.attributes.user.data.attributes.email})
      </p>
      {!hideLink && <Link href={`/tasks/${task.id}`}>View</Link>}
    </div>
  );
};

In the above code:

  1. You define and export the Task component. This component takes in three props:
    1. task - A product object returned from the API.
    2. hideView - A boolean property to hide or view the Task’s view link.
    3. showId - A boolean property to hide or view the Task’s ID.

Creating Login Page

Next, open the index.js file in the pages directory and replace the existing code with the following code:

// 1
import { useRouter } from "next/router";
import React, { useState, useCallback, useEffect } from "react";
import { Alert, Button, Form } from "react-bootstrap";
import useLocalStorage from "use-local-storage";
import { UsersAPI } from "../services/usersApi";

// 2
export default function LoginPage() {
  const router = useRouter();
  // 3
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();
  // 4
  const [jwtStrapiToken, setJwtStrapiToken] = useLocalStorage(
    "jwtStrapiToken",
    ""
  );

  // 5
  useEffect(() => {
    if (jwtStrapiToken) {
      router.push("/tasks");
    }
  }, [jwtStrapiToken, router]);

  // 6
  const handleSubmit = useCallback(
    (e) => {
      e.preventDefault();
      setIsLoading(true);
      UsersAPI.login({ email, password })
        .then((result) => {
          if (result.error) {
            setError(result.error.message);
          } else {
            setJwtStrapiToken(result.jwt);
            router.push("/tasks");
          }
        })
        .catch((err) => console.error({ err }))
        .finally(() => setIsLoading(false));
    },
    [password, router, setJwtStrapiToken, email]
  );

  // 7
  return (
    <>
      <div className="row mb-4">
        <div className="col-lg-6 mx-auto">
          <h1 className="mb-4">Tasks Portal</h1>
          <Form onSubmit={handleSubmit}>
            {error && <Alert>{error}</Alert>}
            <Form.Label>Email</Form.Label>
            <Form.Control
              value={email}
              type="text"
              onChange={(e) => setEmail(e.target.value)}
              required
            />
            <Form.Group>
              <Form.Label>Password</Form.Label>
              <Form.Control
                value={password}
                type="password"
                onChange={(e) => setPassword(e.target.value)}
                required
              />
            </Form.Group>
            <Button
              type="submit"
              variant="primary"
              className="mt-4"
              disabled={isLoading}
            >
              Login
            </Button>
          </Form>
        </div>
      </div>
    </>
  );
}

In the above code:

  1. You import the required NPM packages and hooks.
  2. You define and export the LoginPage component.
  3. You define the state variables for the form’s state and API call state using the useState React hook.
  4. You define the jwtStrapiToken state variable using useLocalStorage hook. This allows you to persist the token in the browser’s local storage.
  5. When the component is mounted, if the jwtStrapiToken is set, you redirect the user to the tasks page which you’ll implement later.
  6. You define a callback function handleSubmit to handle the “submit” event for the login form.
  7. You return the UI for the LoginPage component in which you define a login form with fields for username and password.

Creating Tasks Page

Next, create a tasks directory in the pages directory. In the tasks directory, create an index.js file and add the following code to it:

// 1
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { TasksAPI } from "../../services/tasksApi";
import { Alert } from "react-bootstrap";
import { Task } from "../../components/Task";

// 2
export default function TasksPage() {
  // 3
  const [tasks, setTasks] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();

  // 4
  useEffect(() => {
    setIsLoading(true);
    TasksAPI.find()
      .then((result) => {
        if (result.error) {
          setError(result.error.message);
        } else {
          setTasks(result.data);
        }
      })
      .catch((err) => console.error(err))
      .finally(() => setIsLoading(false));
  }, []);

  // 5
  return (
    <>
      <div className="mb-4">
        <h1>All Tasks</h1>
      </div>
      {isLoading && <div>Loading...</div>}
      {!isLoading && error && <Alert>{error}</Alert>}
      {!isLoading && !error && tasks && (
        <div className="row">
          {tasks.map((task) => (
            <div key={task.id} className="col-6 mb-3">
              <Task task={task} />
            </div>
          ))}
        </div>
      )}
    </>
  );
}

In the above code:

  1. You import the required NPM packages and hooks.
  2. You define and export the TasksPage component.
  3. You define the tasks state variable using the useState React hook to store all the tasks fetched from the Strapi API and some other state variables to store the API call state.
  4. In the useEffect hook, you call the find method from the TasksAPI when the component is mounted.
  5. You return the UI for the TasksPage component by looping over the tasks list.

Creating Single Task Page

Next, create a [id].js file in the /pages/tasks directory and add the following code to it:

// 1
import React, { useState, useEffect, useMemo } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { TasksAPI } from "../../services/tasksApi";
import { Alert } from "react-bootstrap";
import { Task } from "../../components/Task";

// 2
export default function TaskPage() {
  // 3
  const router = useRouter();
  const { id } = useMemo(() => router.query, [router.query]);

  // 4
  const [task, setTask] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();

  // 5
  useEffect(() => {
    if (id) {
      setIsLoading(true);
      TasksAPI.findOne({ id })
        .then((result) => {
          if (result.error) {
            setError(result.error.message);
          } else {
            setTask(result.data);
          }
        })
        .catch((err) => console.error(err))
        .finally(() => setIsLoading(false));
    }
  }, [id]);

  // 6
  return (
    <>
      <div className="mb-4">
        <Link href="/logout" passHref>
          Logout
        </Link>
        <h1>Tasks</h1>
      </div>
      {isLoading && <div>Loading...</div>}
      {!isLoading && error && <Alert>{error}</Alert>}
      {!isLoading && !error && task && (
        <div className="row">
          <div key={task.id} className="col-12 mb-3">
            <Task task={task} hideLink showId />
          </div>
        </div>
      )}
    </>
  );
}

In the above code:

  1. You import the required NPM packages and hooks.
  2. You define and export the TaskPage component.
  3. You use the useRouter hook from Next.js to get the id query parameter.
  4. You define the tasks state variable using the useState React hook to store the product defined by id from the Strapi API.
  5. In the useEffect hook, you call the findOne method from the TasksAPI and pass to it the id of the task that you want to fetch.
  6. You return the UI for the TaskPage component.

With the code and configuration finally set up, you can now test the entire application. Before proceeding, shut down the running servers for Strapi and Next.js and restart them again in their respective terminal windows.

Testing the Application

Next, visit the localhost:3000 and you’ll see the login form:

Login Page for Task Assignment app

Next, enter the login details - email and password, for one of the users that you created earlier. Upon successful login, you’ll be redirected to the localhost:3000/tasks and see the following result based on the data you added for your tasks:

All Tasks Page for Task Assignment app

Next, click on View on any task and you’ll see the following result for an individual task:

Single Task Page for Task Assignment app

Next, visit the Strapi Admin panel and reassign an already assigned task to another user and a WhatsApp notification will be sent on doing so:

Unassignment notification sent on WhatsApp

And with that, you have successfully created a task assignment app using Next.js, Strapi, and Twilio.

Conclusion

In this tutorial, you learned to create a task assignment app using Next.js, Strapi, and Twilio. You used Next.js for building the frontend UI, Strapi for building the backend, and Twilio for sending WhatsApp notifications.

Let me know if this project helped you by reaching out to me over email! The entire source code for this tutorial is available in this GitHub repository.

Happy Building!

Ravgeet is a remote, full-time, full-stack developer and technical content writer based in India. He works with and writes about React, Vue, Flutter, Strapi, Python, and Automation. He can be reached via: