How to Create a Task Time Tracker Chrome Extension With Strapi and ReactJs

Strapi - Mar 23 '23 - - Dev Community

How to Create a Task Time Tracker Chrome Extension With Strapi and ReactJs

Outline

  • Prerequisites
  • Github URL
  • What is a Chrome Extension?
  • What we are Building
  • What is ReactJs?
  • What is Tailwind CSS?
  • What is Strapi - A Headless CMS?
  • Strapi Installation
  • Creating the Task Tracker Collection
  • Customizing our Strapi Create Controller
    • InstallingMoment
  • Bootstrapping our Reactjs App
  • Installing Tailwind CSS
  • Installing Other Dependencies
  • Creating the Home page
  • Adding a Task
  • Displaying a Task
  • Marking a Task as completed
  • Deleting a Task
  • Editing a Task
  • Creating the Manifest.json file
  • Generating a Build File
  • Adding our Application to Google Chrome Extension
  • Testing our Application
  • Conclusion

Chrome extensions are potent tools at the disposal of any user. It can increase effectiveness, solve problems, and lots more. In summary, it extends the browser's capabilities. A task tracker is an application that allows users to allocate time to a particular task.

The goal of this tutorial is to enable us to see another awesome use case of Strapi by building a Chrome extension. It will introduce us to Chrome extensions, ReactJs, manifest.json, Strapi controller customization, and adding a ReactJs application to the extension menu of our Chrome browser.

Prerequisites

For us to continue, we need the following:

  1. Nodejs runtime installed on our local machine. Visit the homepage for the installation guide.
  2. A basic understanding of Strapi Headless CMS. See the get started page.
  3. A very basic knowledge of Reactjs.
  4. And a basic understanding of Tailwind CSS.

Github URL

https://github.com/Theodore-Kelechukwu-Onyejiaku/Task-Tracker-Chrome-Extension

What is a Chrome Extension?

A Chrome Extension is an app built mostly with HTML, CSS, and JavaScript that performs a specific action, and that mostly customizes the Chrome browsing experience.
It adds additional functionality to your Chrome browser. They could be productivity tools, like our Task Time Tracker, a language translation tool, an ad blocker, and so on.

A production-ready extension can be found on the Chrome Web Store. Also, we can create our own extension locally and use it locally on our machine. Some popular Chrome extensions include ad blockers, password managers, language translation tools, productivity tools, and social media extensions. Some popular Chrome Extensions include JSON formatter, Grammarly, Adblock Plus, LastPass, Metamask, and even the React Developer Tool Chrome extension and so much more.

What we are Building

We are building a simple Task Time Tracker. Below are the features of our application.

  1. We can add a task and the time we expect to finish the task.
  2. We should see a countdown displaying the number of minutes and time we have left on the task.
  3. We can be able to edit the task along with its duration.
  4. We can mark a task as completed before and after expiration.
  5. Our Chrome extension badge should show the number of completed tasks.

Note: a badge here refers to the character we see on the icon of some chrome extensions.

  1. The date or time in which the Task was created should be displayed.
  2. We can then finally be able to delete a task.

What is ReactJs?

ReactJs or React is an open-source JavaScript framework developed by Facebook that is used for building powerful user interfaces. It allows developers to build dynamic, fast, and reusable components. We are using Reactjs because when we run the build command which generates the index.html file our chrome extension needs.

What is Tailwind CSS?

Tailwind CSS is a utility-first efficient CSS framework for creating unique user interfaces is called Tailwind CSS. It allows us to write class based CSS directly to our application. We need Tailwind in this project because it is fast to use and removes the need for many stylesheets.

What is Strapi - A Headless CMS?

Strapi is an open-source and powerful headless CMS based on Node.js that is used to develop and manage content using Restful APIs and GraphQL. It is built on top of Koa.
With Strapi, we can scaffold our API faster and consume the content via APIs using any HTTP client or GraphQL enabled frontend.

Strapi Installation

Installing Strapi is just the same way we install other NPM packages. We will have to open our CLI to run the command below:

    npx create-strapi-app task-time-tracker-api --quickstart
        ## OR
    yarn create strapi-app task-time-tracker-api --quick start
Enter fullscreen mode Exit fullscreen mode

The command above will have Strapi installed for us. The name of the application in this command is task-time-tracker-api.

When it has been installed, cd into the task-time-tracker folder and run the command below to start up our application:

    yarn build
    yarn develop
    ## OR
    npm run build
    npm run develop
Enter fullscreen mode Exit fullscreen mode

When this is complete, our Strapi application should be live on http://localhost:1337/.

Creating the Task Tracker Collection

Next, we have to create our application collection. A collection acts as a database. Head on and create a collection called task.

This will save the details of each task.

create-task-collection.png

Now, we have to create the fields for our application. Create the first field called title. This will serve as the title for any task we create.

adding-title-field.png

Next, click on the “Advanced settings” tab to make sure that this field is “Required field”. This is so that the field will be required when creating a record.

make-field-reqired.png

Go ahead and create the following fields:

Field Name Field Type Required Unique
title Short text true false
taskTime Number true false
realTime Number true false
completed Boolean false false
dateCreated Text true false

The taskTime represents the time in minutes allocated to the task. realTime represents the real-time in milliseconds for a task which will be useful when we start a countdown.

completed is a boolean field representing the status of the task if completed or not completed.

dateCreated will represent data in text format. This will be generated in the backend controller when we customize the create controller.

Allowing Access To Collection

Now that we have created our application, we need users to access or query our API. For this reason, we have to allow access to our task collection. We will set it to public since this extension is a personal extension.

Hence, allow the following access shown in the image below. Please go to Settings > Users & Permissions Plugin > Roles, then click on Public. Then allow the following access:

enable-bublic-access.png

Customizing our Strapi Create Controller

Now, we have to see how to customize a controller. A controller is the function called when a route is accessed. It is responsible for handling incoming requests and returning responses to clients.

Installing Moment

Before we continue we need moment.js in our application. This is so that we can save the date our task was created in a moment.js date format. This is so that our client, which uses moment.js will display the date in a calendar format.

Run the command below to install moment.js:

    npm i moment
Enter fullscreen mode Exit fullscreen mode

Now we have to open our task controller at this path: src/api/task/controllers/task.js.

Replace the content with the one shown below:

// path: ./src/api/task/controllers/task.js

"use strict";
/**
 * task controller
 */
const { createCoreController } = require("@strapi/strapi").factories;
// import moment
const moment = require("moment");
module.exports = createCoreController("api::task.task", ({ strapi }) => ({
  async create(ctx) {
    const { data } = ctx.request.body;
    // convert to date with Moment
    const dateCreated = moment(new Date());
    // convert to string
    data.dateCreated = dateCreated.toString();
    // create or save task
    let newTask = await strapi.service("api::task.task").create({ data });
    const sanitizedEntity = await this.sanitizeOutput(newTask, ctx);
    return this.transformResponse(sanitizedEntity);
  },
}));
Enter fullscreen mode Exit fullscreen mode

In the code above, we customized the controller for the creation of a task, the create() core action. This will handle POST requests to our API on the route /api/tasks/.

In line 12 , the code will take the data from the request body sent by the client and add the formatted dateCreated to it. And finally, it will save it and return in line 29.

Bootstrapping our Reactjs App

In order to create a frontend ReactJs application, we will cd into the folder of our choice through the terminal and run the command below:

    npx create-react-app task-time-tracker
Enter fullscreen mode Exit fullscreen mode

We specified that the name of our application is task-time-tracker .
Now run the following command to cd into and run our application.

    cd task-time-tracker
    npm start
Enter fullscreen mode Exit fullscreen mode

If the command is successful, we should see the following open up in our browser:

create-react-app.png

Bravo! We are now ready to create our application!

Installing Tailwind CSS

Presently, we have to install Tailwind CSS. Enter the command below to stop our application:

    command c  // (for mac)
    ## OR
    control c  // (for windows)
Enter fullscreen mode Exit fullscreen mode

Install Tailwind using the command below:

    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Next, we open our tailwind.config.js file which is in the root folder of our application and replace the content with the following code.

// path: ./tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Lastly, add this to the index.css file in our root folder.

// path: ./index.css

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Installing Other Dependencies

Also, our application needs the following dependencies to run:

    npm i axios moment react-icons react-simple-tooltip react-toastify
Enter fullscreen mode Exit fullscreen mode

axios: this will allow us to make HTTP requests to our Strapi.
moment : this will help us format the dateCreated field in our application.
react-icons: this will serve as our icons provider.
react-simple-tooltip: this will help us show tooltips
react-toastify: this will be used for showing of toasts.

Creating the Home page

Before we continue, we have to create an environment variable for our server API URL. We have to create a .env file in the root of our project and the following should be added:

    REACT_APP_STRAPI_SERVER=http://localhost:1337
Enter fullscreen mode Exit fullscreen mode

Now, Head over to the App.js file and add the following:

// path: ./src/App.js

import { useEffect, useState } from "react";
import { BiMessageSquareAdd } from "react-icons/bi";
import axios from "axios";
import { ToastContainer } from "react-toastify";
import Task from "./components/Task";
import EditModal from "./components/EditModal";
import AddTaskModal from "./components/AddTaskModal";
import "react-toastify/dist/ReactToastify.css";
const serverUrl = process.env.REACT_APP_STRAPI_SERVER;
Enter fullscreen mode Exit fullscreen mode

In the code above:

  • line 3: we import the useEffect and useState hooks.
  • line 4: we import an icon that will show a button to add a task.
  • line 5: we import axios that will help us make a HTTP request.
  • line 6: the ToastContainer use to show toast from react-toastify is imported.
  • line 7: Task component was imported. This will display a task on the page.
  • line 8; we import the EditModal component which we will create shortly. This represents a modal to edit a task.
  • line 9: AddTaskModal which is modal to add a task is also imported.
  • line 10: the css for showing toast from react-toastify is imported.
  • line 11: the URL to our Strapi API is imported from the .env file as serverURL.

Now let us add our State variables:

    // path: ./src/App.js

    ...
    function App() {
      const [tasks, setTasks] = useState([]);
      const [fetchError, setFetchError] = useState([]);
      const [tasksCompleted, setTasksCompleted] = useState([]);
      const [taskToUpdate, setTaskToUpdate] = useState({});
      const [showUpdateModal, setShowUpdateModal] = useState(false);
      const [showAddTaskModal, setShowAddTaskModal] = useState(false);

      return (
          <div>
          </div>
      )
    }
    export default App;
Enter fullscreen mode Exit fullscreen mode

From the code above:

  • line 5: we create a state variable for our tasks.
  • line 6: state variable for a fetch error and its setter function is created.
  • line 7: we create the state variable tasksCompleted and setTaskCompleted which should display the tasks that have been completed on the page and the setter function.
  • line 8: taskToUpdate will represent a task we want to update at the click of a button. And also its corresponding setter function.
  • line 9: we declare a boolean state variable for when the update modal should be visible. And its corresponding setter function.
  • line 10: also we declare a boolean state variable for making the adding of a task modal visible or invisible.

Now add the following functions before the return statement:

// path: ./src/App.js

// function to update extension badge
const updateChromeBadge = (value) => {
  const { chrome } = window;
  chrome.action?.setBadgeText({ text: value });
  chrome.action?.setBadgeBackgroundColor({ color: "##fff" });
};
// function to get all tasks
const getAllTask = async () => {
  setFetchError("");
  try {
    // fetch tasks from strapi
    const res = await axios.get(`${serverUrl}/api/tasks`);
    // get result from nested object destructuring
    const {
      data: { data },
    } = res;
    // get tasks that have not been completed
    const tasks = data
      .filter((task) => !task.attributes.completed)
      .map((task) => {
        // check if task has expired
        if (Date.now() > parseInt(task?.attributes?.realTime)) {
          task.attributes.realTime = 0;
          return task;
        }
        return task;
      });
    // get completed tasks
    const completedTasks = data.filter((task) => task.attributes.completed);
    setTasksCompleted(completedTasks);
    updateChromeBadge(completedTasks.length.toString());
    setTasks(tasks.reverse());
  } catch (err) {
    setFetchError(err.message);
  }
};
useEffect(() => {
  // get all tasks
  getAllTask();
}, []);
Enter fullscreen mode Exit fullscreen mode

In the code shown above:

  • line 4-8: this is the function that will be used to show our badge icon text like we described in the beginning.
  • line 10-34: this will be the function responsible for getting all tasks, and even completed tasks. In line 20 , it checks if the task has expired by comparing the time when the task was created and the current time. If expired, it returns the task real time as 0. line 27 filters out the completed tasks. And line 29 updates the badge icon based on the number of tasks completed.
  • line 35: we call the useEffect which fires the getAllTask() function as soon as the component is mounted.

Finally, add the following JSX to our App.js.

// path: ./src/App.js

return (
  <div>
    <div className="w-full flex justify-center rounded">
      <div className="w-96 sm:w-1/3 overflow-scroll border p-5 mb-20 relative bg-slate-50">
        <div className="">
          <h1 className="text-focus-in text-4xl font-bold text-slate-900">
            Task Time Tracker
          </h1>
          <span className="text-slate-400">Seize The Day!</span>
        </div>
        <div>
          <h1 className="font-bold text-lg my-5">Tasks</h1>
          {fetchError && (
            <div className="text-red-500 text-center">Something went wrong</div>
          )}
          <div>
            {tasks.length ? (
              tasks?.map((task) => (
                <Task
                  key={task.id}
                  updateChromeBadge={updateChromeBadge}
                  setTaskToUpdate={setTaskToUpdate}
                  setShowUpdateModal={setShowUpdateModal}
                  showUpdateModal={showUpdateModal}
                  task={task}
                />
              ))
            ) : (
              <div className="text-center">No tasks at the moment</div>
            )}
          </div>
        </div>
        <div>
          <h2 className="font-bold text-lg my-5">Completed Tasks</h2>
          {fetchError && (
            <div className="text-red-500 text-center">Something went wrong</div>
          )}
          <div>
            {tasksCompleted.length ? (
              tasksCompleted?.map((task) => <Task key={task.id} task={task} />)
            ) : (
              <div className="text-center">No tasks at the moment</div>
            )}
          </div>
        </div>
        <div className="fixed bottom-5 z-50  rounded  w-full left-0 flex flex-col justify-center items-center">
          <button
            type="button"
            onClick={() => {
              setShowAddTaskModal(true);
            }}
            className="bg-white p-3 rounded-full"
          >
            <BiMessageSquareAdd className="text-green-500 bg-white" size={50} />
          </button>
        </div>
      </div>
      {/* Toast Notification, Edit-task and Add-task modals */}
      <ToastContainer />
      <EditModal
        setShowUpdateModal={setShowUpdateModal}
        showUpdateModal={showUpdateModal}
        task={taskToUpdate}
      />
      <AddTaskModal
        showAddTaskModal={showAddTaskModal}
        setShowAddTaskModal={setShowAddTaskModal}
      />
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

In the code above, line 13 checks if there is a fetch error and displays “Something went wrong” if there is. line 32 maps through the taskCompleted state variable and displays the tasks already completed. line 43 displays our toast. line 44 displays our EditModal and line 45 displays our AddToTaskModal. Both the EditModal and AddTaskModal take some props.

Here is what our home page will look like but we still need to add the rest of the components so let's keep going.

home-page.png

The full code to our App.js can be found below:

https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/e56afcbd852fa82bbecd609cb56fc2df

Adding a Task

Now we want to be able to add task to our collection. To do this we have to first create a utility file to show a toast error when the fields required are not provided. Inside the src folder, create a folder called utils and create a file inside of it called showFieldsError.js and add the following:

// path: ./src/utils/showFieldsError.js

import { toast } from "react-toastify";
const showFieldsError = () => {
  toast.error("😭 Please enter all field(s)", {
    position: "top-right",
    autoClose: 5000,
    hideProgressBar: false,
    closeOnClick: true,
    pauseOnHover: true,
    draggable: true,
    progress: undefined,
    theme: "light",
  });
};
export default showFieldsError;
Enter fullscreen mode Exit fullscreen mode

Now, let us create the AddTaskModal which will allow us add a task. Inside the src folder, create a folder called components. And inside this new folder, create a file called AddTaskModal.js and add the following:

    // path: ./src/components/AddTaskModal.js

    import React, { useRef, useState } from 'react';
    import axios from 'axios';
    import showFieldsError from '../utils/showFieldsError';
    const serverUrl = process.env.REACT_APP_STRAPI_SERVER;
    export default function AddTaskModal({ showAddTaskModal, setShowAddTaskModal }) {
      const [task, setTask] = useState({ title: '', taskTime: 0, realTime: 0 });
      const formRef = useRef(null);
      const handleSubmit = async (e) => {
        e.preventDefault();
        try {
          // check all fields are complete
          if (task.title.trim() === '' || task.taskTime === '') {
            showFieldsError();
            return;
          }
          // convert to milliseconds
          const realTime = Date.now() + 1000 * 60 * task.taskTime;
          task.realTime = realTime;
          // create a task
          await axios.post(`${serverUrl}/api/tasks`, {
            data: task,
          });
          formRef.current.reset();
          setTask({ title: '', taskTime: 0 });
          setShowAddTaskModal(false);
          window.location.reload(false);
        } catch (err) {
          alert(err.message);
        }
      };
      const handleChange = (e) => {
        setTask((prev) => ({ ...prev, [e.target.name]: e.target.value }));
      };
      return (
      )
    }

Enter fullscreen mode Exit fullscreen mode

From the code above:

  • Line 5: we import the showFieldsdError utility.
  • line 7: remember that we added some props to our AddTaskModal in our App.js file. Here, we bring in the props showAddTaskModal and setShowAddTaskModal which basically shows this modal or hides it. Then we export the component.
  • line 8: we create the state variable for the task we want to add by setting its title , taskTime and realTime properties to nothing.
  • line 9: we create a ref for our form. This is so that we can reset the form of our modal after a task has been added.
  • line 10-32: here, we create the function to handle the form when submitted. It first checks to see if the form fields are empty and displays the corresponding toast. It then converts the time added by the user to a real time in milliseconds. In line 22 it makes a POST request to add a task by passing the task with its properties. And finally, we reset the form, set back the state task variable to nothing and modal display false and refresh our page using the window.location.refresh(false) function.
  • line 33: this is the handleChange method that will be responsible for changing the form inputs and setting the state variable properties.

Now let us add the following JSX to the return statement.

// path: ./src/components/AddTaskModal.js

return (
  <div
    className={`${
      showAddTaskModal ? "visible opacity-100 " : "invisible opacity-0  "
    } bg-black bg-opacity-40 h-screen fixed w-full flex justify-center items-center transition-all duration-500`}
  >
    <div className="p-10 shadow-md bg-white rounded relative">
      <span className="font-bold">Add Task</span>
      <form ref={formRef} onSubmit={handleSubmit}>
        <div className="relative  my-5 mb-3 ">
          <label>Title</label>
          <textarea
            minLength={0}
            maxLength={50}
            onChange={handleChange}
            value={task.title}
            name="title"
            className="w-full rounded-md border h-32 p-3 mb-5"
          />
          <span className="text-sm absolute bottom-0 right-0 font-thin text-slate-400">
            {task.title.length}
            /50
          </span>
        </div>
        <div>
          <label>duration</label>
          <div className="flex justify-between items-center">
            <input
              onChange={handleChange}
              value={task.taskTime}
              name="taskTime"
              type="number"
              className="w-2/3 border rounded-md p-2"
            />
            <span>(minutes)</span>
          </div>
        </div>
        <button
          type="submit"
          className="p-1 text-green-500 border border-green-500 bg-white rounded my-5"
        >
          Add Task
        </button>
      </form>
      <button
        type="button"
        onClick={() => {
          setShowAddTaskModal(false);
        }}
        className="absolute top-2 right-2 border border-red-500 p-1 text-red-500"
      >
        Close
      </button>
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

The full code to our AddTaskModal can be found here:

https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/f026db1ed39d2512bf068ddd775208f8

And this is what our page should look like:

adding-task-modal.gif

Displaying a Task

Now we have been able to implement the adding of task. We also want to be able to display a task.
For this reason, we have to create the file Task.js which will serve as a component to display our task. Inside the components folder, create a file Task.js.

    // path: ./src/components/Task.js

    import React, { useEffect, useState, useCallback } from 'react';
    import { BiEditAlt } from 'react-icons/bi';
    import { BsCheck2Square } from 'react-icons/bs';
    import { RxTrash } from 'react-icons/rx';
    import Tooltip from 'react-simple-tooltip';
    import axios from 'axios';
    import moment from 'moment';
    const serverUrl = process.env.REACT_APP_STRAPI_SERVER;
    export default function Task({
      task, setShowUpdateModal, setTaskToUpdate,
    }) {
      const [count, setCount] = useState(0);
      const [timeString, setTimeString] = useState('');

      // get moment time created
      let dateCreated = new Date(task?.attributes?.dateCreated);
      dateCreated = moment(dateCreated).calendar();

      // get the time in string of minutes and seconds
      const getTimeString = useCallback(() => {
        console.log('get time string do run oo');
        const mins = Math.floor((parseInt(task?.attributes?.realTime) - Date.now()) / 60000);
        const seconds = Math.round((parseInt(task?.attributes?.realTime) - Date.now()) / 1000) % 60;
        if (mins <= 0 && seconds <= 0) {
          setTimeString('done');
          return;
        }
        let timeString = 'starting counter';
        timeString = `${mins} min ${seconds} secs`;
        setTimeString(timeString);
      }, [task]);

      useEffect(() => {
        // set timer to run every second
        const timer = setInterval(() => {
          setCount((count) => count + 1);
          getTimeString();
        }, 1000);
        return () => clearInterval(timer);
      }, [getTimeString]);
      return (
      )
    }
Enter fullscreen mode Exit fullscreen mode

In the code above, line 10-13 allowed us to make use of the props task which represents the particular task. The *setShowUpdateModal* which should hide or display the modal for updating a task. And the setter function *setTaskToUpdate* that will set any task we want to update.

In line 14, we create our counter count that will be used to set a countdown for our tasks as they begin. And in line 15, we create a state variable timeString and its corresponding setter function that will display the time remaining for a task to expire in minutes and seconds.

Looking at line 18-19 , we got the calendar time using moment.calender() method after converting the dateCreated string to an object.

line 22-33 , here we create the getTimeString() function which basically sets the value for the timeString state variable. It does this by converting the real time of the task to current minutes and seconds. Note that we used the useCallback() react hook to memoize the getTimeString() function. This is so that Task.js component will not re-render for every count of our counter. Also it helps us to avoid performance issue in our application.

And lastly, in line 34-42, using the useEffect() hook, we pass the getTimeString() function as a dependency. This is so that our component will not re-render every second as regards the timer. It will only re-render if getTimeString() changes. And it won’t change unless its own dependency which is task changes. So for every second, the getTimeString() gets the time in string for a particular task.

Now add the following to our Task.js component.

    // path: ./src/components/Task.js
    ...
     return (
      <div className={`${task?.attributes?.completed ? ' bg-green-500 text-white ' : ' bg-white '} scale-in-center p-8 hover:shadow-xl rounded-3xl shadow-md my-5 relative`}>
          <div className="flex justify-center w-full">
            <span className="absolute top-0 text-sm font-thin border p-1">{dateCreated}</span>
          </div>
          <div className="flex items-center justify-between mb-5">
            {!task?.attributes?.completed ? (
              <div className="w-1/4">
                <Tooltip content="😎 Mark as completed!"><button type="button" disabled={task?.attributes?.completed} onClick={markCompleted} className=" p-2 rounded"><BsCheck2Square className="text-2xl hover:text-green-500" /></button></Tooltip>
              </div>
            ) : null}
            <div className="w-2/4 flex flex-col">
              <span className="text-focus-in font-bold text-base">{task?.attributes?.title}</span>
              {parseInt(task?.attributes?.realTime) ? <span className="font-thin">{timeString}</span> : <span className="font-thin">done</span>}
            </div>
            <div className="w-1/4 flex justify-end">
              {!task?.attributes?.completed ? (
                <Tooltip content="Edit task title!">
                  {' '}
                  <button type="button" onClick={() => { setTaskToUpdate(task); setShowUpdateModal(true); }}><BiEditAlt className="text-2xl hover:text-blue-500" /></button>
                </Tooltip>
              ) : null}
              <Tooltip content="🙀 Delete this task?!"><button type="button" onClick={handleDelete}><RxTrash className="text-2xl hover:text-red-500" /></button></Tooltip>
            </div>
          </div>
          <p className="absolute bottom-0 my-3 text-center text-sm w-full left-0">
            {task?.attributes?.taskTime}
            {' '}
            minute(s) task
          </p>
        </div>
    )
Enter fullscreen mode Exit fullscreen mode

Here is what our application should look like when a task is added:

tasks.png

Note that in line 6 of the code above, we used displayed the time formatted by moment.

Marking a Task as completed

Inside the Task.js file, we want to add a function that will allow a task to be completed when a certain button is clicked. Enter the code below inside the Task.js file.

// path: ./src/components/Task.js

// mark task as completed
const markCompleted = async () => {
  try {
    const res = await axios.put(`${serverUrl}/api/tasks/${task?.id}`, {
      data: {
        completed: true,
        realTime: 0,
      },
    });
    const { chrome } = window;
    let badgeValue = 0;
    chrome?.action?.getBadgeText({}, (badgeText) => {
      badgeValue = parseInt(badgeText);
    });
    badgeValue = (badgeValue + 1).toString();
    chrome.action?.setBadgeText({ text: badgeValue });
    window.location.reload(false);
  } catch (err) {
    alert(err.message);
  }
};
Enter fullscreen mode Exit fullscreen mode

This function has already been passed to a button in our code. See a demo of what the function does below:

mark-complete-task.gif

Deleting a Task

Along with marking a task as completed, a task can as well be deleted. Add the function below to the Task.js file.

// path: ./src/components/Taks.js

// delete task
const handleDelete = async () => {
  try {
    const res = await axios.delete(`${serverUrl}/api/tasks/${task?.id}`, {
      data: task,
    });
    window.location.reload(false);
  } catch (err) {
    alert(err.message);
  }
};
Enter fullscreen mode Exit fullscreen mode

Editing a Task

Now we want to add the option of editing a task. To do this, we finally create the EditModal.js file we have already imported inside of our App.js. This will be responsible for displaying of the modal that will allow us to edit our app. Remember also the setTaskToUpdate props added to the Task.js component. When the edit icon of a task is clicked, this setter function sets the task to the one we want to edit.

Now paste the code below:

// path: ./src/components/EditModal.js

import { useState } from "react";
import axios from "axios";
const serverUrl = process.env.REACT_APP_STRAPI_SERVER;
export default function EditModal({
  setShowUpdateModal,
  showUpdateModal,
  task,
}) {
  const [title, setTitle] = useState(task?.attributes?.title);
  const [taskTime, setTaskTime] = useState();
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      if (title === "") {
        alert("Please enter a title");
        return;
      }
      const taskTimeToNumber = parseInt(taskTime) || task?.attributes?.taskTime;
      const realTime = Date.now() + 1000 * 60 * parseInt(taskTimeToNumber);
      const res = await axios.put(`${serverUrl}/api/tasks/${task?.id}`, {
        data: {
          taskTime: taskTimeToNumber,
          realTime,
          title: title?.toString().trim(),
        },
      });
      setShowUpdateModal(false);
      window.location.reload(false);
    } catch (err) {
      alert("Somethind went wrong");
    }
  };
  return (
    <div
      className={`${
        showUpdateModal ? "visible opacity-100 " : "invisible opacity-0  "
      } bg-black bg-opacity-40 h-screen fixed w-full flex justify-center items-center transition-all duration-500`}
    >
      <div className="p-10 shadow-md bg-white rounded relative">
        <span className="font-bold">Update Task</span>
        <form onSubmit={handleSubmit}>
          <div className="relative  my-5 ">
            <label>Title</label>
            <textarea
              onChange={(e) => {
                setTitle(e.target.value);
              }}
              defaultValue={task?.attributes?.title}
              className="w-full border h-32 p-3 mb-5"
            />
            <span className="text-sm absolute bottom-0 right-0 font-thin text-slate-400">
              {title?.length ? title?.length : task?.attributes?.title?.length}
              /50
            </span>
          </div>
          <div>
            <label>duration</label>
            <div className="flex justify-between items-center">
              <input
                min={0}
                onChange={(e) => {
                  setTaskTime(e.target.value);
                }}
                defaultValue={task?.attributes?.taskTime}
                name="taskTime"
                type="number"
                className="w-2/3 border rounded-md p-2"
              />
              <span>(minutes)</span>
            </div>
          </div>
          <button
            className="p-1 text-green-500 border border-green-500 bg-white rounded my-5"
            type="submit"
          >
            Update
          </button>
        </form>
        <button
          type="button"
          onClick={() => {
            setShowUpdateModal(false);
          }}
          className="absolute top-2 right-2 border border-red-500 p-1"
        >
          Close
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In line 6, we import we make use of the setShowUpdateModal and showUpdateModal which are responsible for hiding or displaying our edit modal. Then we also make use of the task prop which represents the current task we want to update.

For lines 7 and line 8, we created state variables for the input title and the task time. These two will be what will be updated when sent to the server.

line 9 is where we create the edit function called handleSubmit(). This makes the request to edit the task. If successful, it reloads the page. If there was an error, an alert will be shown.

Here is what our EditModal component looks like:

preview-edit.png

Now our application is ready! We can now convert it to a Chrome extension!

Creating the Manifest.json file

In order for our application to work as an extension, we need to create a manifest.json file.
It is a JSON-formatted file that contains important information about the extension, such as its name, version, permissions, and other details. There are basically 3 different kinds of manifest.json file listed below:

- Manifest v1: This is the first iteration of the "manifest.json" file, which was first introduced in 2010 with the introduction of Chrome extensions. Chrome continues to support it, but there are some restrictions on its functionality and security.
- Manifest v2: Released in 2012, this version provides better security and functionality than Manifest v1. It unveiled a number of new features, including inline installation prevention and content security policies.
- Manifest v3, which was released in 2019 and promises to significantly enhance Chrome extensions' security, privacy, and performance. It brings about a number of changes, such as a declarative API model that restricts extensions' ability to directly alter web pages.
Enter fullscreen mode Exit fullscreen mode

For the purpose of our application, we will use version 3.

Now inside the public folder of our application, locate the manifest.json file and replace the contents with this:

// path: ./public/manifest.json

{
  "name": "Task Time Tracker",
  "description": "Powerful task tracker!",
  "version": "0.0.0.1",
  "manifest_version": 3,
  "action": {
    "default_popup": "index.html",
    "default_title": "Check your tasks!"
  },
  "icons": {
    "16": "logo.png",
    "48": "logo.png",
    "128": "logo.png"
  }
}
Enter fullscreen mode Exit fullscreen mode

In the file above, here is what each of the fields means.

  • “name”: this stands for the name of the Chrome extension we are building.
  • “description”: this is the description of our application.
  • “version”: this stands for the current version of our application.
  • “manifest_version”: here we specify the version of manifest to use.
  • "action": This field specifies what happens when the extension icon is clicked. In this case, it opens a popup window with the file "index.html", and sets the default title to "Check your tasks!". We set it to “index.html” because that is what React creates for us when run the build command as we will see below:
  • "icons": The icons used for the extension are specified in this field in various sizes (16, 48, and 128 pixels in this case). "logo.png" is the icon file that is utilized for all sizes.

Note: we are using the PNG image file logo.png as the logo or icon for our chrome extension. see the image below

icon.png

Generating a Build File

Now since every chrome extension requires a single HTML file, we need to tell React that we need our application in a single HTML file. To do this, we have to run the build command.

    npm run build
Enter fullscreen mode Exit fullscreen mode

This will generate a build folder. Now, this folder is where our index.html resides. See image below:

generated-build-folder.png

Adding our Application to Google Chrome Extension

Now that we have generated the build folder. We need to add our application to the Chrome extension.
Click on the extension bar icon at the top right of our Chrome browser. See the icon circled red in the image below:

extemsion-bar.png

When we click the icon, you should see a popup like the one shown below, now click the “Manage Extensions”.

manage-extension.png

Once that is done. We will see the page below. Make sure to toggle the “developer mode” first, then click on the “load unpacked”.

enable-develop-mode.png

Now we select the directory to the build folder of our app.

select-build-folder.png

If that was successful, our extension should be ready! Hurray!

extension-ready.png

Presently our extension is not displayed in the extension bar or menu. To do this, we have to pin our extension. Click the extension icon once again scroll down and you will see our new extension. Now click the pin button to pin it to the extension bar.

pinning-extension.png

Finally we should be able to see our extension on the extension bar.

display-extension.png

Testing our Application (Demo)

Here is the demo of our application!

https://youtu.be/gwTy6at6N-U

Conclusion

In this tutorial, we have been able to learn about Chrome Extensions, ReactJs, Tailwind CSS, a manifest.json file, and how to build a Chrome extension. Most importantly, we have seen the power of Strapi once again in another use case. Go ahead and build amazing extensions!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .