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:
- Nodejs runtime installed on our local machine. Visit the homepage for the installation guide.
- A basic understanding of Strapi Headless CMS. See the get started page.
- A very basic knowledge of Reactjs.
- 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.
- We can add a task and the time we expect to finish the task.
- We should see a countdown displaying the number of minutes and time we have left on the task.
- We can be able to edit the task along with its duration.
- We can mark a task as completed before and after expiration.
- 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.
- The date or time in which the Task was created should be displayed.
- 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
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
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.
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.
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.
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.
dateCreate
d
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:
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
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);
},
}));
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
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
If the command is successful, we should see the following open up in our browser:
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)
Install Tailwind using the command below:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
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: [],
};
Lastly, add this to the index.css
file in our root folder.
// path: ./index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Installing Other Dependencies
Also, our application needs the following dependencies to run:
npm i axios moment react-icons react-simple-tooltip react-toastify
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
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;
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 asserverURL
.
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;
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
andsetTaskCompleted
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();
}, []);
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. Andline 29
updates the badge icon based on the number of tasks completed. - line 35: we call the
useEffect
which fires thegetAllTask()
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>
);
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.
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;
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 (
)
}
From the code above:
- Line 5: we import the
showFieldsdError
utility. - line 7: remember that we added some props to our
AddTaskModal
in ourApp.js
file. Here, we bring in the propsshowAddTaskModal
andsetShowAddTaskModal
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
andrealTime
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 thewindow.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>
);
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:
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 (
)
}
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>
)
Here is what our application should look like when a task is added:
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);
}
};
This function has already been passed to a button in our code. See a demo of what the function does below:
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);
}
};
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>
);
}
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 line
s
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:
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.
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"
}
}
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
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
This will generate a build folder. Now, this folder is where our index.html
resides. See image below:
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:
When we click the icon, you should see a popup like the one shown below, now click the “Manage Extensions”.
Once that is done. We will see the page below. Make sure to toggle the “developer mode” first, then click on the “load unpacked”.
Now we select the directory to the build folder of our app.
If that was successful, our extension should be ready! Hurray!
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.
Finally we should be able to see our extension on the extension bar.
Testing our Application (Demo)
Here is the demo of our application!
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!