The importance of notifications in mobile and web applications cannot be over-emphasized as they are an important and integral part of any modern application. Users want to get updated when events or activities happen so they don't miss them.
Throughout this tutorial, we will be looking at how we can achieve push notifications in our React app with the aid of Strapi, Cron Jobs, and Rest API.
To get the most out of this tutorial, you need the following:
The importance of routers in software applications cannot be over-emphasized as they are essential to how movements and data are passed from one screen or component to another.
Routers in React as a library have evolved over time; it has gotten even better with the new version of React Router v6, which is a groundbreaking update from the popular React Router v5.
React Router 6 vs React Router 5
To set up Strapi for your project, you need to run the following commands:
npx create-strapi-app@latest push-notification --quickstart
After running the command above, dependencies get installed and our browser starts up, redirecting us to a login screen to create an admin account. Input the necessary credentials to create an admin account.
After creating an admin account, you will be directed to your dashboard, where you can set up Strapi for the asset management application.
On our admin dashboard, on the left, under plugins, click on Content-Type Builder
to easily create a new collection type.
We will be creating two collection types called Assets and Logs. To achieve this, we will follow the steps below.
perishable
or non-perishable``.
null
on default until an asset has been returned.On the main navigation bar, click on the content-Type builder
. For our two collections, Log
and Assets
, we add one more field to them of type Relational
so we can relate our Log to many users. We will do the same for our Assets
, but we will relate it to our Log
collection.
Click on Add new field to this collection type
and this field type is Relational
.
After creating this content, we need to make sure that our API is allowed to perform certain actions on the system. To do this,
General
category on the main navigation pane. *Users & Permissions Plugin*
, click on Roles
. authenticated
and all our collections will show up.Assets
collection. Roles
and check all the permissions.
Note: Hit the save button in the top right corner of your screen to save your changes.
Hurray! We made it to the push notification feature; let’s dive right in. We will be creating new collections called Notification and UserNotificationKey.
Notification Collection This collection will hold two(2) fields, namely:
UserNotificationKey
The collection will hold two fields:
a. Users_permissions_user: This field will carry a type Relation
and will be related with Users
default collection as shown in the image below.
b. subscription: This field will have a type json.
We will need the following dependencies to aid in building our frontend.
React App with PWA Functions as we will be using service workers in this application. To install a pw react app, use the following commands:
npx create-react-app my-app --template cra-template-pwa
on your terminal.
yarn add axios
(if you use yarn as your package manager) or npm install axios
(if you use npm as your package manager).
yarn add react-router-dom
(if you use yarn as your package manager) or npm install react-router-dom
(if you use npm as your package manager).
This is what our front-end should look like once we are done with this tutorial.
As stated earlier, we will be using TailwindCSS majorly to style this application. We will also add some generic styles for a little more customisation. The generic styles are present in our index.css
file.
1 import React from 'react';
2 import { Link } from 'react-router-dom'
3 const Navbar = () => {
4 return (
5 <nav className="md:flex items-center justify-between p-6 md:p-12">
6 <div className="md:w-10/12">
7 <h3>ASSETS MANAGER</h3>
8 </div>
9 <div className="md:w-2/12 flex items-center justify-end md:w-4/12 -mx-4">
10 <div className="inline-block px-4">
11 <Link to="/">HOME</Link>
12 </div>
13 <div className="inline-block px-4">
14 <Link to="/assets">GUIDE</Link>
15 </div>
16 <div className="inline-block px-4">
17 <Link to="/">CONTACT</Link>
18 </div>
19 </div>
20 </nav>
21 );
22 };
23 export default Navbar;
For this project, we will be having four pages
Let’s dive into some preliminary procedures for managing states in our app. In this section, we will be making HTTP requests to Strapi to signup a new user on the app. The code block MainContext.js
below shows how we will handle requests and states on the app.
1 import React, { createContext } from 'react';
2 import axios from 'axios';
3 axios.defaults.baseURL = "http://localhost:1337/api";
4
5 const initialState = () => ({
6 user: localStorage.getItem("strapi")
7 ? JSON.parse(localStorage.getItem("strapi")).user
8 : null,
9 token: localStorage.getItem("strapi")
10 ? JSON.parse(localStorage.getItem("strapi")).token
11 : null,
12 assets:{
13 allAssets: null,
14 asset: null,
15 }
16 })
17 // create context
18 export const generalContext = createContext({})
19 export const StateAndEndpointHOC = (props) => {
20 const [state, setState] = React.useState(initialState);
21 let config = {
22 headers: {
23 'authorization': "Bearer " +state.token || null
24 }
25 }
26 const endpoints = {
27 login: async(params, callback)=> {
28 try{
29 const res = await axios.post('/auth/local', params)
30 console.log('endpoint result--login', res)
31 if(callback && typeof callback === 'function'){
32 callback(res, null)
33 }
34 console.log(res.data.user);
35 setState(() => ({...state, token: res?.data?.jwt, user: res.data.user }))
36 localStorage.setItem('strapi', JSON.stringify({
37 token: res?.data?.jwt,
38 user: res?.data?.user
39 }))
40 return res
41 }catch(err){
42 console.log(err.response.data.error.message)
43 if(callback && typeof callback === 'function'){
44 callback(null, err)
45 }
46 throw new Error(err)
47 }
48 },
49
50 signup: async(params, callback)=>{
51 try{
52 const res = await axios.post('/auth/local/register', params)
53 console.log('endpoint result--signup', res)
54 if(callback && typeof callback === 'function'){
55 callback(res, null)
56 }
57 setState(() => ({...state, token: res?.data?.jwt, user: res?.data?.user}))
58 localStorage.setItem('strapi', JSON.stringify({
59 token: res?.data?.jwt,
60 user: res?.data?.user
61 }))
62 return res
63 }catch(err){
64 if(callback && typeof callback === 'function'){
65 callback(null, err)
66 }
67 throw new Error(err)
68 }
69 },
70
71 getAssets: async(callback)=>{
72 try{
73 const res = await axios.get('/assets', config)
74 console.log('endpoint result--get', res)
75 if(callback && typeof callback === 'function'){
76 callback(res, null)
77 }
78 setState(() => ({ ...state, assets:{...state.assets, allAssets: res?.data?.data} }))
79 return res
80 }catch(err){
81 if(callback && typeof callback === 'function'){
82 callback(null, err)
83 }
84 throw new Error(err)
85 }
86 },
87
88 getSingleAsset: async(id, callback)=>{
89 try{
90 const res = await axios.get(`/assets/${id}`, config)
91 console.log('endpoint result--get single asset', res?.data?.data)
92 setState(()=> ({...state, assets:{...state.assets, asset: res?.data?.data} }))
93 if(callback && typeof callback === 'function'){
94 callback(res, null)
95 }
96 return res
97 }catch(err){
98 if(callback && typeof callback === 'function'){
99 callback(null, err)
100 }
101 throw new Error(err)
102 }
103 },
104
105 deleteSingleAsset: async(id, callback)=>{
106 try{
107 const res = await axios.delete(`/assets/${id}`, config)
108 console.log('endpoint result--get', res)
109 if(callback && typeof callback === 'function'){
110 callback(res, null)
111 }
112 return res
113 }catch(err){
114 if(callback && typeof callback === 'function'){
115 callback(null, err)
116 }
117 throw new Error(err)
118 }
119 },
120
121 createAssets: async(data, callback)=>{
122 try{
123 const res = await axios.post('/assets',{ data }, config)
124 console.log('endpoint result--create', res)
125 if(callback && typeof callback === 'function'){
126 callback(res, null)
127 }
128 return res
129 }catch(err){
130 if(callback && typeof callback === 'function'){
131 callback(null, err)
132 }
133 throw new Error(err)
134 }
135 },
136
137 editAssets: async(data, callback)=>{
138 try{
139 const res = await axios.put('/assets', { data }, config)
140 console.log('endpoint result--edit', res)
141 if(callback && typeof callback === 'function'){
142 callback(res, null)
143 }
144 return res
145 }catch(err){
146 if(callback && typeof callback === 'function'){
147 callback(null, err)
148 }
149 throw new Error(err)
150 }
151 },
152
153 logout: ()=>{
154 localStorage.removeItem('strapi')
155 setState({...initialState})
156 }
157 }
158 const allProps = {...state, setState, endpoints }
159
160 return <><props.app {...allProps } /></>
161 };
1 import React from 'react';
2 import AllRoutes from './components/Routes/AllRoutes';
3 import { BrowserRouter } from "react-router-dom";
4 import { generalContext, StateAndEndpointHOC } from './contexts/MainContext';
5 function App(props) {
6 const value = React.useMemo(() => props, [props]);
7 return (
8 <div className="App">
9 <BrowserRouter>
10 <generalContext.Provider value={value}>
11 <AllRoutes />
12 </generalContext.Provider>
13 </BrowserRouter>
14 </div>
15 );
16 }
17 // eslint-disable-next-line import/no-anonymous-default-export
18 export default () => <StateAndEndpointHOC app={App} />;
The above code in our app.js
is where we pass the state manager as a prop to our context provider, which will supply states to all components of our application.
To Sign in as a User
1 import React, { useState, useContext } from 'react';
2 import { Link, Navigate } from 'react-router-dom';
3 import { generalContext } from '../contexts/MainContext';
4 import { subscribeUser } from '../subscription'
5 const Signup = () => {
6 const StateManager = useContext(generalContext)
7 const [loading, setloading] = useState(false);
8 const [error, setError] = useState(null)
9 const [state, setstate] = useState({
10 email: null,
11 username: null,
12 password: null,
13 });
14 const handleChange = (e) => {
15 setstate({ ...state, [e.target.name]: e.target.value });
16 };
17 const handleSubmit = (e) => {
18 e.preventDefault();
19 setloading(true);
20 !StateManager?.token && StateManager?.endpoints.signup(state, (success, error) => {
21 if(error){
22 setError(error.response.data.error.message);
23 return alert(error.response.data.error.message);
24 }
25 if(success){
26 setError(null)
27 subscribeUser(success.data)
28 StateManager.endpoints.getAssets()
29 return;
30 }
31 setloading(false);
32 })
33 };
34 if (StateManager?.token) {
35 console.log('token-login', StateManager?.token)
36 return <Navigate to="/assets" />;
37 }
38 return (
39 <div>
40 <div className="flex h-screen items-center justify-center">
41 <div className="md:w-6/12">
42 <form className="px-6 py-10" onSubmit={handleSubmit}>
43 <h6 color={"#112E46"} className="mb-8 text-center" fontWeight="700">
44 Signup
45 </h6>
46 <div className="w-full mb-6">
47 <input
48 className="inline-block w-full p-6 c-black"
49 placeholder="Email address"
50 type="text"
51 name="email"
52 onChange={handleChange}
53 value={state.email}
54 required
55 />
56 </div>
57 <div className="w-full mb-6">
58 <input
59 className="inline-block w-full p-6 c-black"
60 placeholder="Username"
61 type="text"
62 name="username"
63 onChange={handleChange}
64 value={state.username}
65 required
66 />
67 </div>
68 <div className="w-full mb-2">
69 <input
70 className="inline-block w-full p-6 c-black"
71 placeholder="password"
72 type="password"
73 name="password"
74 onChange={handleChange}
75 value={state.password}
76 required
77 />
78 </div>
79 <p
80 className="mt-10 text-center"
81 >
82 <Link to="/forgotPassword">Forgot password?</Link>
83 </p>
84 <div className="w-full flex items-center justify-center pt-10">
85 <button
86 className="inline-block w-auto border px-4 py-4"
87 onClick={handleSubmit}
88 >
89 {loading ? 'loading...': 'Signup'}
90 </button>
91 </div>
92 {error ? (
93 <p
94 style={{color: "red"}}
95 className="mt-5 text-center"
96 >
97 {error}
98 </p>
99 ) : null}
100
101 <p
102 className="mt-16 text-center"
103 >
104 to Login, click {" "}
105 <Link to="/">here</Link>
106 </p>
107 </form>
108 </div>
109 </div>
110 </div>
111 );
112 };
113 export default Signup;
For the above component, we are basically extracting the state manager using the useContext hook and making a HTTP request to Strapi to sign up a new user on our app. The return of the user’s information and JWT token signifies that the user has signed up to our app.
To Log in as a User
1 import React, { useState, useContext } from 'react';
2 import { Link, Navigate } from 'react-router-dom';
3 import { generalContext } from '../contexts/MainContext';
4 import { subscribeUser } from '../subscription'
5 const Login = () => {
6 const StateManager = useContext(generalContext)
7 const [loading, setloading] = useState(false);
8 const [error, setError] = useState(null)
9 const [state, setstate] = useState({
10 identifier: "",
11 password: "",
12 });
13 const handleChange = (e) => {
14 setstate({ ...state, [e.target.name]: e.target.value });
15 };
16 const handleSubmit = (e) => {
17 e.preventDefault();
18 setloading(true);
19 !StateManager?.token && StateManager?.endpoints.login(state, async(success, error)=>{
20 if(error){
21 setError(error.response.data.error.message);
22 return alert(error.response.data.error.message);
23 }
24 if(success){
25 setError(null)
26 subscribeUser(success.data)
27 StateManager.endpoints.getAssets()
28 return;
29 }
30 setloading(false);
31 })
32 };
33 if (StateManager?.token) {
34 console.log('token-login', StateManager?.token)
35 return <Navigate to="/assets" />;
36 }
37 return (
38 <div>
39 <div className="flex h-screen items-center justify-center">
40 <div className="md:w-6/12">
41 <form className="px-6 py-10" onSubmit={handleSubmit}>
42 <h6 color={"#112E46"} className="mb-8 text-center" fontWeight="700">
43 Login
44 </h6>
45 <div className="w-full mb-6">
46 <input
47 className="inline-block w-full p-6 c-black"
48 placeholder="Email address"
49 type="text"
50 name="identifier"
51 onChange={handleChange}
52 value={state.email}
53 required
54 />
55 </div>
56 <div className="w-full mb-2">
57 <input
58 className="inline-block w-full p-6 c-black"
59 placeholder="password"
60 type="password"
61 name="password"
62 onChange={handleChange}
63 value={state.password}
64 required
65 />
66 </div>
67 <p
68 className="mt-10 text-center"
69 >
70 <Link to="/forgotPassword">Forgot password?</Link>
71 </p>
72 <div className="w-full flex items-center justify-center pt-10">
73 <button
74 className="inline-block w-auto border px-4 py-4"
75 onClick={handleSubmit}
76 >
77 {loading ? 'loading...': 'Login'}
78 </button>
79 </div>
80 {error ? (
81 <p
82 style={{color: "red"}}
83 className="mt-5 text-center"
84 >
85 {error}
86 </p>
87 ) : null}
88
89 <p
90 className="mt-16 text-center"
91 >
92 to Signup, click {" "}
93 <Link to="/signup">here</Link>
94 </p>
95
96 </form>
97 </div>
98 </div>
99 </div>
100 );
101 };
102 export default Login;
For the above component, we are basically extracting the state manager using the useContext hook and making an HTTP request to Strapi to login as an already-signed up user on our app. The return of the user’s information and JWT token signifies that the user has logged in on our assets manager app.
Assets Page
1 import React, { useContext, useEffect } from 'react';
2 import { Link } from 'react-router-dom';
3 import AssetsCard from '../../components/assets/AssetsCard';
4 import { generalContext } from '../../contexts/MainContext';
5 const AssetsPage = (props) => {
6 const StateManager = useContext(generalContext);
7 useEffect(()=>{
8 StateManager.endpoints.getAssets()
9 }, [])
10 return (
11 <div>
12 <div className="p-4 md:px-8 md:py-0">
13 <div className="flex justify-center items-center md:block">
14 <div className="w-full flex items-center md:justify-end pb-10">
15 <Link to="/asset/+"
16 className="inline-block w-auto border px-4 py-4"
17 >
18 Create a new asset
19 </Link>
20 </div>
21 <div className="md:hidden pb-10">
22 <div className="px-4 py-4">
23 <p>{StateManager?.user?.email}</p>
24 <button onClick={StateManager.endpoints.logout} className="text-capitalize c-CA9140">LOGOUT </button>
25 </div>
26 </div>
27 </div>
28
29
30 <div className="mt-8 md:flex md:flex-wrap my-2 md:-my-4">
31 {
32 StateManager?.assets?.allAssets ? StateManager.assets.allAssets.map((curr, idx)=>{
33 return (
34 <div className="md:w-4/12" key={idx}>
35 <div className="px-2 md:px-4 py-2 md:py-4">
36 <AssetsCard data={curr} />
37 {props.children}
38 </div>
39 </div>
40 )
41 }):(
42 <div className="md:w-3/12">
43 <div className="px-2 md:px-4 py-2 md:py-4">
44 <h1> NO Asset </h1>
45 </div>
46 </div>
47 )
48 }
49 </div>
50 </div>
51 </div>
52 );
53 };
54 export default AssetsPage;
From the component above, once a user is authenticated, he is redirected to the asset index page which lists or holds all the available assets and buttons to create, edit, or delete an asset. When hovering on the asset card, two buttons are displayed to delete and edit assets.
Creating an Asset
1 import React, { useState, useContext } from 'react';
2 import { Link, useLocation, useNavigate, Navigate } from 'react-router-dom';
3 import { generalContext } from '../../contexts/MainContext';
4 import { subscribeUser } from '../../subscription'
5 const CreateAssets = ({token}) => {
6 const StateManager = useContext(generalContext)
7 const location = useLocation();
8 let navigate = useNavigate();
9 const [loading, setloading] = useState(false);
10 const [error, setError] = useState(null)
11 const [state, setstate] = useState({
12 name: '',
13 description: '',
14 model_number: '',
15 validity_period: '',
16 category: '',
17 is_available: false,
18 logs: [],
19 is_expired: false,
20 });
21 const handleChange = (e) => {
22 switch (e.target.type) {
23 case "number":
24 setstate({ ...state, [e.target.name]: parseFloat(e.target.value) });
25 break;
26 case "checkbox":
27 setstate({ ...state, [e.target.name]: e.target.checked });
28 break;
29 default:
30 setstate({ ...state, [e.target.name]: e.target.value });
31 }
32 };
33 const handleSubmit = (e) => {
34 e.preventDefault();
35 setloading(true);
36 StateManager?.endpoints.createAssets(state, (success, error) => {
37 if(error){
38 setError(error.response.data.error.message);
39 alert(error.response.data.error.message);
40 }
41 if(success){
42 setError(null)
43 subscribeUser(success.data)
44 StateManager?.endpoints.getAssets()
45 navigate('/assets');
46 }
47 setloading(false);
48 })
49 };
50 return (
51 <div>
52 <div className="px-4 md:px-8">
53 <div className="md:w-10/12">
54 <form className="px-6 py-10 md:py-0" onSubmit={handleSubmit}>
55 <h6 color={"#112E46"} className="mb-8 " fontWeight="700">
56 Create a new Asset
57 </h6>
58 <div className="w-full mb-6">
59 <p>Asset Name</p>
60 <input
61 className="inline-block w-full p-6 c-black"
62 placeholder="Asset name"
63 type="text"
64 name="name"
65 onChange={handleChange}
66 value={state.name}
67 required
68 />
69 </div>
70 <div className="w-full mb-6">
71 <p>Asset description</p>
72 <input
73 className="inline-block w-full p-6 c-black"
74 placeholder="Asset name"
75 type="text"
76 name="description"
77 onChange={handleChange}
78 value={state.description}
79 required
80 />
81 </div>
82 <div className="w-full mb-2">
83 <p>Asset model number</p>
84 <input
85 className="inline-block w-full p-6 c-black"
86 placeholder="Asset model number"
87 type="text"
88 name="model_number"
89 onChange={handleChange}
90 value={state.model_number}
91 required
92 />
93 </div>
94 <div className="w-full mb-2">
95 <p>Asset validity period</p>
96 <input
97 className="inline-block w-full p-6 c-black"
98 placeholder="Asset validity period"
99 type="date"
100 name="validity_period"
101 onChange={handleChange}
102 value={state.validity_period}
103 required
104 />
105 </div>
106 <div className="w-full mb-2">
107 <p>Asset category</p>
108 <select
109 value={state.category}
110 className="inline-block w-full p-6 c-black"
111 onChange={(e)=> setstate({...state, category: e.target.value})}
112 >
113 <option value="perishable">perishable</option>
114 <option value="nonperishable">nonperishable</option>
115 </select>
116 </div>
117 <div className="w-full mb-2">
118 <p>is Asset available?</p>
119 <input
120 className="p-6 c-black"
121 type="checkbox"
122 name="is_available"
123 onChange={handleChange}
124 value={state.is_available}
125 required
126 />
127 </div>
128 <div className="w-full mb-2">
129 <p>is Asset expired?</p>
130 <input
131 className="p-6 c-black"
132 type="checkbox"
133 name="is_expired"
134 onChange={handleChange}
135 value={state.is_expired}
136 required
137 />
138 </div>
139 {/* <div className="w-full mb-2">
140 <p>logs</p>
141 <input
142 className="inline-block w-full p-6 c-black"
143 placeholder="logs"
144 type="text"
145 name="logs"
146 onChange={handleChange}
147 value={state.logs}
148 required
149 />
150 </div> */}
151 <div className="w-full flex items-center justify-center py-10">
152 <button
153 className="inline-block w-auto border px-4 py-4"
154 onClick={handleSubmit}
155 >
156 {loading ? 'loading...': 'edit asset'}
157 </button>
158 </div>
159 </form>
160 </div>
161 </div>
162 </div>
163 );
164 };
165 export default CreateAssets;
Once a user is authenticated, they can create a new asset. This is done by the user inputting the necessary information as shown in the code above.
1 import React, { useLayoutEffect, useEffect, useState, useContext, useCallback, useMemo } from 'react';
2 import { useParams, useNavigate, Navigate } from 'react-router-dom';
3 import { generalContext } from '../../contexts/MainContext';
4 import { subscribeUser } from '../../subscription'
5 const EditAssets = () => {
6 const StateManager = useContext(generalContext)
7 const { id } = useParams();
8 let navigate = useNavigate();
9 const [loading, setloading] = useState(false);
10 const [error, setError] = useState(null)
11 const [state, setstate] = useState({
12 name: null,
13 description: null,
14 model_number: null,
15 validity_period: null,
16 category: null,
17 is_available: false,
18 logs: [],
19 is_expired: false,
20 });
21 const handleChange = (e) => {
22 switch (e.target.type) {
23 case "number":
24 setstate({ ...state, [e.target.name]: parseFloat(e.target.value) });
25 break;
26 case "checkbox":
27 setstate({ ...state, [e.target.name]: e.target.checked });
28 break;
29 default:
30 setstate({ ...state, [e.target.name]: e.target.value });
31 }
32 };
33 const handleSubmit = (e) => {
34 e.preventDefault();
35 setloading(true);
36 StateManager?.endpoints.editAssets(state, (success, error) => {
37 if(error){
38 setError(error.response.data.error.message);
39 alert(error.response.data.error.message);
40 }
41 if(success){
42 setError(null)
43 subscribeUser(success.data)
44 StateManager?.endpoints.getSingleAsset(id)
45 StateManager?.endpoints.getAssets()
46 navigate('/assets');
47 }
48 setloading(false);
49 })
50 };
51
52 const getAsset = useCallback(()=>{
53 console.log('location', id)
54 setloading(true);
55 return StateManager?.endpoints.getSingleAsset(id, (success, error) => {
56 if(error){
57 setError(error.response.data.error.message);
58 alert(error.response.data.error.message);
59 }
60 if(success){
61 setError(null)
62 subscribeUser(success)
63 setstate(()=> ({
64 name: StateManager?.assets?.asset?.attributes?.name,
65 description: StateManager?.assets?.asset?.attributes?.description,
66 model_number: StateManager?.assets?.asset?.attributes?.model_number,
67 validity_period: StateManager?.assets?.asset?.attributes?.validity_period,
68 category: StateManager?.assets?.asset?.attributes?.category,
69 is_available: StateManager?.assets?.asset?.attributes?.is_available,
70 logs: [],
71 is_expired: StateManager?.assets?.asset?.attributes?.is_expired,
72 }))
73 }
74 setloading(false);
75 })
76 },[]);
77
78
79 useEffect(() => {
80 getAsset()
81 }, [StateManager.assets.asset, id])
82 return (
83 <div>
84 {!loading && StateManager?.assets?.asset ? (
85 <div className="px-4 md:px-8">
86 <div className="md:w-10/12">
87 <form className="px-6 py-10 md:py-0" onSubmit={handleSubmit}>
88 <h6 color={"#112E46"} className="mb-8 " fontWeight="700">
89 Edit an Asset
90 </h6>
91 <div className="w-full mb-6">
92 <p>Asset Name</p>
93 <input
94 className="inline-block w-full p-6 c-black"
95 placeholder="Asset name"
96 type="text"
97 name="name"
98 onChange={handleChange}
99 value={state.name}
100 required
101 />
102 </div>
103 <div className="w-full mb-6">
104 <p>Asset description</p>
105 <input
106 className="inline-block w-full p-6 c-black"
107 placeholder="Asset name"
108 type="text"
109 name="description"
110 onChange={handleChange}
111 value={state.description}
112 required
113 />
114 </div>
115 <div className="w-full mb-2">
116 <p>Asset model number</p>
117 <input
118 className="inline-block w-full p-6 c-black"
119 placeholder="Asset model number"
120 type="text"
121 name="model_number"
122 onChange={handleChange}
123 value={state.model_number}
124 required
125 />
126 </div>
127 <div className="w-full mb-2">
128 <p>Asset validity period</p>
129 <input
130 className="inline-block w-full p-6 c-black"
131 placeholder="Asset validity period"
132 type="date"
133 name="validity_period"
134 onChange={handleChange}
135 value={state.validity_period}
136 required
137 />
138 </div>
139 <div className="w-full mb-2">
140 <p>Asset category</p>
141 <select
142 value={state.category}
143 className="inline-block w-full p-6 c-black"
144 onChange={(e)=> setstate({...state, category: e.target.value})}
145 >
146 <option value="perishable">perishable</option>
147 <option value="nonperishable">nonperishable</option>
148 </select>
149 </div>
150 <div className="w-full mb-2">
151 <p>is Asset available?</p>
152 <input
153 className="p-6 c-black"
154 type="checkbox"
155 name="is_available"
156 onChange={handleChange}
157 checked={state.is_available}
158 value={state.is_available}
159 required
160 />
161 </div>
162 <div className="w-full mb-2">
163 <p>is Asset expired?</p>
164 <input
165 className="p-6 c-black"
166 type="checkbox"
167 name="is_expired"
168 onChange={handleChange}
169 checked={state.is_expired}
170 value={state.is_expired}
171 required
172 />
173 </div>
174 {/* <div className="w-full mb-2">
175 <p>logs</p>
176 <input
177 className="inline-block w-full p-6 c-black"
178 placeholder="logs"
179 type="text"
180 name="logs"
181 onChange={handleChange}
182 value={state.logs}
183 required
184 />
185 </div> */}
186 <div className="w-full flex items-center justify-center pt-10">
187 <button
188 className="inline-block w-auto border px-4 py-4"
189 onClick={handleSubmit}
190 >
191 {loading ? 'loading...': 'edit asset'}
192 </button>
193 </div>
194 </form>
195 </div>
196 </div>
197 ):!loading && error ?(
198 <div className="flex items-center justify-center min-h-screen">
199 <h1>{error}</h1>
200 </div>
201 ):(
202 <div className="flex items-center justify-center min-h-screen">
203 <h1>Loading...</h1>
204 </div>
205 )}
206
207 </div>
208 );
209 };
210 export default EditAssets;
From the code above, users can edit an asset when they hover on the asset card as two buttons will show up, of which one is to edit the asset and the other is to delete the asset.
We have finally reached the most important part of this article as we will be learning to create push notifications using cron jobs and web push.
What is Web Push?
Web Push is an npm package that helps users push real time events to clients, usually triggered from the backend and, in our case, Strapi is the backend.
To make use of this package, we need to install it using the following command:
yarn add web-push
.
Create a new web-push.js
file in the config folder of our backend and add the following code:
1 const { readFileSync } = require('fs');
2 const { join } = require('path');
3 const webpush = require('web-push')
4
5 const vapidKeys = JSON.parse(readFileSync(join(__dirname, 'vapidKeys.json')).toString());
6 webpush.setVapidDetails(
7 'mailto:example@yourdomain.org',
8 vapidKeys.publicKey,
9 vapidKeys.privateKey
10 )
11 const notify = async (strapi, data) => {
12 const users = await strapi.entityService.findMany(
13 'api::user-notification-key.user-notification-key', {
14 filters: {
15 subscription: {
16 $notNull: true
17 }
18 }
19 });
20 users.forEach(user => {
21 setTimeout(() => {
22 webpush.sendNotification(
23 user.subscription,
24 JSON.stringify(data)
25 ).then(() => console.log('notification sent')).catch(console.error)
26 }, 6000)
27 })
28 }
29 module.exports = {
30 webpush,
31 notify,
32 };
Let's dive in to what's happening in the code above.
To use the web-push package, we need to import the web-push package into our web-push.js
folder and we need vapid keys to sign the push notifications. These keys can be generated using the following code that checks if there is already a file that holds the vapid keys else it creates a new one.
Web push documentation warns against generating vapid keys more than once. To ensure we do not have our vapid keys generated twice, we find a way to reach the already generated vapid key file and make a check as follows. We then read it and pass the values of that file to our web push detail function.
1 const pathToVapidKey = join(__dirname, 'vapidKeys.json');
2
3 if (!existsSync(pathToVapidKey)) {
4 const keys = webpush.generateVAPIDKeys();
5 writeFileSync(pathToVapidKey, JSON.stringify(keys));
6 }
7 const vapidKeys = JSON.parse(readFileSync(pathToVapidKey).toString());
8 webpush.setVapidDetails(
9 'mailto:example@yourdomain.org',
10 vapidKeys.publicKey,
11 vapidKeys.privateKey
12 )
Trigger Notification
Recall that the field we gave the UserNotificationKey was a subscription field. We will fetch all subscription fields that are not empty and loop through for every user and use the webpush.sendNotification
function to trigger notifications. We have added timing to the notification to avoid queuing of notifications. The code below shows how we can achieve this:
1 const notify = async (strapi, data) => {
2 const users = await strapi.entityService.findMany(
3 'api::user-notification-key.user-notification-key', {
4 filters: {
5 subscription: {
6 $notNull: true
7 }
8 }
9 });
10 users.forEach(user => {
11 setTimeout(() => {
12 webpush.sendNotification(
13 user.subscription,
14 JSON.stringify(data)
15 ).then(() => console.log('notification sent')).catch(console.error)
16 }, 6000)
17 })
18 }
19 module.exports = {
20 webpush,
21 notify,
22 };
Cron Job is a utility program that allows users to run or execute scripts periodically. To enable Cron Jobs, set cron.enabled
to true
in the server.js
file, still in the config folder and declare the jobs as such:
1 const cronTasks = require("./cron-tasks");
2 module.exports = ({ env }) => ({
3 host: env('HOST', '0.0.0.0'),
4 port: env.int('PORT', 1337),
5 app: {
6 keys: env.array('APP_KEYS'),
7 },
8 cron: {
9 enabled: true,
10 tasks: cronTasks,
11 },
12 });
Cron Jobs will help you look out for expired assets and notify us when it expires. To set up Cron Job in our application, we created a file called cron-task.js
in our config
folder. This Cron Job will be executed every minute.
1 const { notify } = require('./web-push');
2
3 module.exports = {
4 '*/1 * * * * *': async ({ strapi }) => {
5
6 const expiredAssets = await strapi.entityService.findMany('api::asset.asset', {
7 filters: {
8 validity_period: {
9 $lte: new Date()
10 },
11 is_expired: false
12 },
13 });
14 for (const e of expiredAssets) {
15 const notice = await strapi.entityService.create('api::notification.notification', {
16 data: {
17 message: `Asset ${e.name} with model number ${e.model_number} has expired`,
18 type: 'asset',
19 metadata: {
20 id: e.id
21 }
22 }
23 })
24
25 await strapi.entityService.update('api::asset.asset', e.id, {
26 data: {
27 is_expired: true
28 }
29 });
30 await notify(strapi, notice);
31 }
32 },
33 };
Let’s explain what happening in our code:
1 module.exports = {
2 '*/1 * * * * *': async ({ strapi }) => {
3
4 const expiredAssets = await strapi.entityService.findMany('api::asset.asset', {
5 filters: {
6 validity_period: {
7 $lte: new Date()
8 },
9 is_expired: false
10 },
11 });
Based on the code above, we have a Cron Job that allows our function to run every minute as it filters through all assets and fetches all expired assets.
1 for (const e of expiredAssets) {
2 const notice = await strapi.entityService.create('api::notification.notification', {
3 data: {
4 message: `Asset ${e.name} with model number ${e.model_number} has expired`,
5 https://www.dropbox.com/scl/fi/dk7qxelfy1cibw3shkm6f/Implement-Push-Notification-Building-an-Asset-Manager-using-Strapi-Cron-jobs-and-Rest-API.paper?dl=0&rlkey=i9m7kjg6n6wipohk7prybugxf type: 'asset',
6 metadata: {
7 id: e.id
8 }
9 }
10 })
11
12 await strapi.entityService.update('api::asset.asset', e.id, {
13 data: {
14 is_expired: true
15 }
16 });
17 await notify(strapi, notice);
18 }
19 },
Next, it loops through the already fetched assets based on the filter from our first function and creates a notification body for every expired asset using their id as identifiers, after which the expired status of the assets are updated.
Add Notification on Creation and Deletion of Assets
We would like to be notified on every creation and deletion of assets in our application. To do this, we need to extend the asset controller. To access the assets controller, go to src >> api >> assets >> controllers >> asset.js
.
1 const { createCoreController } = require('@strapi/strapi').factories;
2 module.exports = createCoreController(''api::asset.asset');
The code above is what we should see in our file initially but we will extend these controllers to be able to get notified on every operation made.
Recall that we only want to be notified when an asset is created or deleted so we add a notify object from web-push package.
1 'use strict';
2 const { notify } = require('../../../../config/web-push');
3 /**
4 * asset controller
5 */
6 const { createCoreController } = require('@strapi/strapi').factories;
7 module.exports = createCoreController('api::asset.asset', ({ strapi }) => ({
8 async find(ctx) {
9 const response = await super.find(ctx);
10 return response;
11 },
12 async update(ctx) {
13 const response = await super.update(ctx);
14 return response;
15 },
16 async findOne(ctx) {
17 const response = await super.findOne(ctx);
18 return response;
19 },
20 async delete(ctx) {
21 const response = await super.delete(ctx);
22 await notify(strapi, { message: 'An asset has been deleted' });
23 return response;
24 },
25
26 async create (ctx) {
27 const response = await super.create(ctx);
28 await notify(strapi, {
29 message: `An asset ${response?.data?.attributes?.name} has been created`
30 });
31 return response;
32 }
33 }));
We have finally come to the end of this tutorial. In this tutorial, we looked at how the new react router and its benefits, and we also learnt how set up Strapi for our application. We also learned how to set up Strapi to push notifications and manage states in our asset manager in the process. We learned how to handle authentication from login to sign up and, most importantly, to push notifications from our Strapi backend using Cron Jobs and Web Push.
Software Developer based in Nigeria who works with Mobile, Web, and Blockchain technologies.