Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import { Link } from 'react-router-dom'
const Navbar = () => {
return (
<nav className="md:flex items-center justify-between p-6 md:p-12">
<div className="md:w-10/12">
<h3>ASSETS MANAGER</h3>
</div>
<div className="md:w-2/12 flex items-center justify-end md:w-4/12 -mx-4">
<div className="inline-block px-4">
<Link to="/">HOME</Link>
</div>
<div className="inline-block px-4">
<Link to="/assets">GUIDE</Link>
</div>
<div className="inline-block px-4">
<Link to="/">CONTACT</Link>
</div>
</div>
</nav>
);
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import React, { createContext } from 'react';
import axios from 'axios';
axios.defaults.baseURL = "http://localhost:1337/api";
const initialState = () => ({
user: localStorage.getItem("strapi")
? JSON.parse(localStorage.getItem("strapi")).user
: null,
token: localStorage.getItem("strapi")
? JSON.parse(localStorage.getItem("strapi")).token
: null,
assets:{
allAssets: null,
asset: null,
}
})
// create context
export const generalContext = createContext({})
export const StateAndEndpointHOC = (props) => {
const [state, setState] = React.useState(initialState);
let config = {
headers: {
'authorization': "Bearer " +state.token || null
}
}
const endpoints = {
login: async(params, callback)=> {
try{
const res = await axios.post('/auth/local', params)
console.log('endpoint result--login', res)
if(callback && typeof callback === 'function'){
callback(res, null)
}
console.log(res.data.user);
setState(() => ({...state, token: res?.data?.jwt, user: res.data.user }))
localStorage.setItem('strapi', JSON.stringify({
token: res?.data?.jwt,
user: res?.data?.user
}))
return res
}catch(err){
console.log(err.response.data.error.message)
if(callback && typeof callback === 'function'){
callback(null, err)
}
throw new Error(err)
}
},
signup: async(params, callback)=>{
try{
const res = await axios.post('/auth/local/register', params)
console.log('endpoint result--signup', res)
if(callback && typeof callback === 'function'){
callback(res, null)
}
setState(() => ({...state, token: res?.data?.jwt, user: res?.data?.user}))
localStorage.setItem('strapi', JSON.stringify({
token: res?.data?.jwt,
user: res?.data?.user
}))
return res
}catch(err){
if(callback && typeof callback === 'function'){
callback(null, err)
}
throw new Error(err)
}
},
getAssets: async(callback)=>{
try{
const res = await axios.get('/assets', config)
console.log('endpoint result--get', res)
if(callback && typeof callback === 'function'){
callback(res, null)
}
setState(() => ({ ...state, assets:{...state.assets, allAssets: res?.data?.data} }))
return res
}catch(err){
if(callback && typeof callback === 'function'){
callback(null, err)
}
throw new Error(err)
}
},
getSingleAsset: async(id, callback)=>{
try{
const res = await axios.get(`/assets/${id}`, config)
console.log('endpoint result--get single asset', res?.data?.data)
setState(()=> ({...state, assets:{...state.assets, asset: res?.data?.data} }))
if(callback && typeof callback === 'function'){
callback(res, null)
}
return res
}catch(err){
if(callback && typeof callback === 'function'){
callback(null, err)
}
throw new Error(err)
}
},
deleteSingleAsset: async(id, callback)=>{
try{
const res = await axios.delete(`/assets/${id}`, config)
console.log('endpoint result--get', res)
if(callback && typeof callback === 'function'){
callback(res, null)
}
return res
}catch(err){
if(callback && typeof callback === 'function'){
callback(null, err)
}
throw new Error(err)
}
},
createAssets: async(data, callback)=>{
try{
const res = await axios.post('/assets',{ data }, config)
console.log('endpoint result--create', res)
if(callback && typeof callback === 'function'){
callback(res, null)
}
return res
}catch(err){
if(callback && typeof callback === 'function'){
callback(null, err)
}
throw new Error(err)
}
},
editAssets: async(data, callback)=>{
try{
const res = await axios.put('/assets', { data }, config)
console.log('endpoint result--edit', res)
if(callback && typeof callback === 'function'){
callback(res, null)
}
return res
}catch(err){
if(callback && typeof callback === 'function'){
callback(null, err)
}
throw new Error(err)
}
},
logout: ()=>{
localStorage.removeItem('strapi')
setState({...initialState})
}
}
const allProps = {...state, setState, endpoints }
return <><props.app {...allProps } /></>
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import AllRoutes from './components/Routes/AllRoutes';
import { BrowserRouter } from "react-router-dom";
import { generalContext, StateAndEndpointHOC } from './contexts/MainContext';
function App(props) {
const value = React.useMemo(() => props, [props]);
return (
<div className="App">
<BrowserRouter>
<generalContext.Provider value={value}>
<AllRoutes />
</generalContext.Provider>
</BrowserRouter>
</div>
);
}
// eslint-disable-next-line import/no-anonymous-default-export
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import React, { useState, useContext } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { generalContext } from '../contexts/MainContext';
import { subscribeUser } from '../subscription'
const Signup = () => {
const StateManager = useContext(generalContext)
const [loading, setloading] = useState(false);
const [error, setError] = useState(null)
const [state, setstate] = useState({
email: null,
username: null,
password: null,
});
const handleChange = (e) => {
setstate({ ...state, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
setloading(true);
!StateManager?.token && StateManager?.endpoints.signup(state, (success, error) => {
if(error){
setError(error.response.data.error.message);
return alert(error.response.data.error.message);
}
if(success){
setError(null)
subscribeUser(success.data)
StateManager.endpoints.getAssets()
return;
}
setloading(false);
})
};
if (StateManager?.token) {
console.log('token-login', StateManager?.token)
return <Navigate to="/assets" />;
}
return (
<div>
<div className="flex h-screen items-center justify-center">
<div className="md:w-6/12">
<form className="px-6 py-10" onSubmit={handleSubmit}>
<h6 color={"#112E46"} className="mb-8 text-center" fontWeight="700">
Signup
</h6>
<div className="w-full mb-6">
<input
className="inline-block w-full p-6 c-black"
placeholder="Email address"
type="text"
name="email"
onChange={handleChange}
value={state.email}
required
/>
</div>
<div className="w-full mb-6">
<input
className="inline-block w-full p-6 c-black"
placeholder="Username"
type="text"
name="username"
onChange={handleChange}
value={state.username}
required
/>
</div>
<div className="w-full mb-2">
<input
className="inline-block w-full p-6 c-black"
placeholder="password"
type="password"
name="password"
onChange={handleChange}
value={state.password}
required
/>
</div>
<p
className="mt-10 text-center"
>
<Link to="/forgotPassword">Forgot password?</Link>
</p>
<div className="w-full flex items-center justify-center pt-10">
<button
className="inline-block w-auto border px-4 py-4"
onClick={handleSubmit}
>
{loading ? 'loading...': 'Signup'}
</button>
</div>
{error ? (
<p
style={{color: "red"}}
className="mt-5 text-center"
>
{error}
</p>
) : null}
<p
className="mt-16 text-center"
>
to Login, click {" "}
<Link to="/">here</Link>
</p>
</form>
</div>
</div>
</div>
);
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import React, { useState, useContext } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { generalContext } from '../contexts/MainContext';
import { subscribeUser } from '../subscription'
const Login = () => {
const StateManager = useContext(generalContext)
const [loading, setloading] = useState(false);
const [error, setError] = useState(null)
const [state, setstate] = useState({
identifier: "",
password: "",
});
const handleChange = (e) => {
setstate({ ...state, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
setloading(true);
!StateManager?.token && StateManager?.endpoints.login(state, async(success, error)=>{
if(error){
setError(error.response.data.error.message);
return alert(error.response.data.error.message);
}
if(success){
setError(null)
subscribeUser(success.data)
StateManager.endpoints.getAssets()
return;
}
setloading(false);
})
};
if (StateManager?.token) {
console.log('token-login', StateManager?.token)
return <Navigate to="/assets" />;
}
return (
<div>
<div className="flex h-screen items-center justify-center">
<div className="md:w-6/12">
<form className="px-6 py-10" onSubmit={handleSubmit}>
<h6 color={"#112E46"} className="mb-8 text-center" fontWeight="700">
Login
</h6>
<div className="w-full mb-6">
<input
className="inline-block w-full p-6 c-black"
placeholder="Email address"
type="text"
name="identifier"
onChange={handleChange}
value={state.email}
required
/>
</div>
<div className="w-full mb-2">
<input
className="inline-block w-full p-6 c-black"
placeholder="password"
type="password"
name="password"
onChange={handleChange}
value={state.password}
required
/>
</div>
<p
className="mt-10 text-center"
>
<Link to="/forgotPassword">Forgot password?</Link>
</p>
<div className="w-full flex items-center justify-center pt-10">
<button
className="inline-block w-auto border px-4 py-4"
onClick={handleSubmit}
>
{loading ? 'loading...': 'Login'}
</button>
</div>
{error ? (
<p
style={{color: "red"}}
className="mt-5 text-center"
>
{error}
</p>
) : null}
<p
className="mt-16 text-center"
>
to Signup, click {" "}
<Link to="/signup">here</Link>
</p>
</form>
</div>
</div>
</div>
);
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import React, { useContext, useEffect } from 'react';
import { Link } from 'react-router-dom';
import AssetsCard from '../../components/assets/AssetsCard';
import { generalContext } from '../../contexts/MainContext';
const AssetsPage = (props) => {
const StateManager = useContext(generalContext);
useEffect(()=>{
StateManager.endpoints.getAssets()
}, [])
return (
<div>
<div className="p-4 md:px-8 md:py-0">
<div className="flex justify-center items-center md:block">
<div className="w-full flex items-center md:justify-end pb-10">
<Link to="/asset/+"
className="inline-block w-auto border px-4 py-4"
>
Create a new asset
</Link>
</div>
<div className="md:hidden pb-10">
<div className="px-4 py-4">
<p>{StateManager?.user?.email}</p>
<button onClick={StateManager.endpoints.logout} className="text-capitalize c-CA9140">LOGOUT </button>
</div>
</div>
</div>
<div className="mt-8 md:flex md:flex-wrap my-2 md:-my-4">
{
StateManager?.assets?.allAssets ? StateManager.assets.allAssets.map((curr, idx)=>{
return (
<div className="md:w-4/12" key={idx}>
<div className="px-2 md:px-4 py-2 md:py-4">
<AssetsCard data={curr} />
{props.children}
</div>
</div>
)
}):(
<div className="md:w-3/12">
<div className="px-2 md:px-4 py-2 md:py-4">
<h1> NO Asset </h1>
</div>
</div>
)
}
</div>
</div>
</div>
);
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import React, { useState, useContext } from 'react';
import { Link, useLocation, useNavigate, Navigate } from 'react-router-dom';
import { generalContext } from '../../contexts/MainContext';
import { subscribeUser } from '../../subscription'
const CreateAssets = ({token}) => {
const StateManager = useContext(generalContext)
const location = useLocation();
let navigate = useNavigate();
const [loading, setloading] = useState(false);
const [error, setError] = useState(null)
const [state, setstate] = useState({
name: '',
description: '',
model_number: '',
validity_period: '',
category: '',
is_available: false,
logs: [],
is_expired: false,
});
const handleChange = (e) => {
switch (e.target.type) {
case "number":
setstate({ ...state, [e.target.name]: parseFloat(e.target.value) });
break;
case "checkbox":
setstate({ ...state, [e.target.name]: e.target.checked });
break;
default:
setstate({ ...state, [e.target.name]: e.target.value });
}
};
const handleSubmit = (e) => {
e.preventDefault();
setloading(true);
StateManager?.endpoints.createAssets(state, (success, error) => {
if(error){
setError(error.response.data.error.message);
alert(error.response.data.error.message);
}
if(success){
setError(null)
subscribeUser(success.data)
StateManager?.endpoints.getAssets()
navigate('/assets');
}
setloading(false);
})
};
return (
<div>
<div className="px-4 md:px-8">
<div className="md:w-10/12">
<form className="px-6 py-10 md:py-0" onSubmit={handleSubmit}>
<h6 color={"#112E46"} className="mb-8 " fontWeight="700">
Create a new Asset
</h6>
<div className="w-full mb-6">
<p>Asset Name</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset name"
type="text"
name="name"
onChange={handleChange}
value={state.name}
required
/>
</div>
<div className="w-full mb-6">
<p>Asset description</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset name"
type="text"
name="description"
onChange={handleChange}
value={state.description}
required
/>
</div>
<div className="w-full mb-2">
<p>Asset model number</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset model number"
type="text"
name="model_number"
onChange={handleChange}
value={state.model_number}
required
/>
</div>
<div className="w-full mb-2">
<p>Asset validity period</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset validity period"
type="date"
name="validity_period"
onChange={handleChange}
value={state.validity_period}
required
/>
</div>
<div className="w-full mb-2">
<p>Asset category</p>
<select
value={state.category}
className="inline-block w-full p-6 c-black"
onChange={(e)=> setstate({...state, category: e.target.value})}
>
<option value="perishable">perishable</option>
<option value="nonperishable">nonperishable</option>
</select>
</div>
<div className="w-full mb-2">
<p>is Asset available?</p>
<input
className="p-6 c-black"
type="checkbox"
name="is_available"
onChange={handleChange}
value={state.is_available}
required
/>
</div>
<div className="w-full mb-2">
<p>is Asset expired?</p>
<input
className="p-6 c-black"
type="checkbox"
name="is_expired"
onChange={handleChange}
value={state.is_expired}
required
/>
</div>
{/* <div className="w-full mb-2">
<p>logs</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="logs"
type="text"
name="logs"
onChange={handleChange}
value={state.logs}
required
/>
</div> */}
<div className="w-full flex items-center justify-center py-10">
<button
className="inline-block w-auto border px-4 py-4"
onClick={handleSubmit}
>
{loading ? 'loading...': 'edit asset'}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import React, { useLayoutEffect, useEffect, useState, useContext, useCallback, useMemo } from 'react';
import { useParams, useNavigate, Navigate } from 'react-router-dom';
import { generalContext } from '../../contexts/MainContext';
import { subscribeUser } from '../../subscription'
const EditAssets = () => {
const StateManager = useContext(generalContext)
const { id } = useParams();
let navigate = useNavigate();
const [loading, setloading] = useState(false);
const [error, setError] = useState(null)
const [state, setstate] = useState({
name: null,
description: null,
model_number: null,
validity_period: null,
category: null,
is_available: false,
logs: [],
is_expired: false,
});
const handleChange = (e) => {
switch (e.target.type) {
case "number":
setstate({ ...state, [e.target.name]: parseFloat(e.target.value) });
break;
case "checkbox":
setstate({ ...state, [e.target.name]: e.target.checked });
break;
default:
setstate({ ...state, [e.target.name]: e.target.value });
}
};
const handleSubmit = (e) => {
e.preventDefault();
setloading(true);
StateManager?.endpoints.editAssets(state, (success, error) => {
if(error){
setError(error.response.data.error.message);
alert(error.response.data.error.message);
}
if(success){
setError(null)
subscribeUser(success.data)
StateManager?.endpoints.getSingleAsset(id)
StateManager?.endpoints.getAssets()
navigate('/assets');
}
setloading(false);
})
};
const getAsset = useCallback(()=>{
console.log('location', id)
setloading(true);
return StateManager?.endpoints.getSingleAsset(id, (success, error) => {
if(error){
setError(error.response.data.error.message);
alert(error.response.data.error.message);
}
if(success){
setError(null)
subscribeUser(success)
setstate(()=> ({
name: StateManager?.assets?.asset?.attributes?.name,
description: StateManager?.assets?.asset?.attributes?.description,
model_number: StateManager?.assets?.asset?.attributes?.model_number,
validity_period: StateManager?.assets?.asset?.attributes?.validity_period,
category: StateManager?.assets?.asset?.attributes?.category,
is_available: StateManager?.assets?.asset?.attributes?.is_available,
logs: [],
is_expired: StateManager?.assets?.asset?.attributes?.is_expired,
}))
}
setloading(false);
})
},[]);
useEffect(() => {
getAsset()
}, [StateManager.assets.asset, id])
return (
<div>
{!loading && StateManager?.assets?.asset ? (
<div className="px-4 md:px-8">
<div className="md:w-10/12">
<form className="px-6 py-10 md:py-0" onSubmit={handleSubmit}>
<h6 color={"#112E46"} className="mb-8 " fontWeight="700">
Edit an Asset
</h6>
<div className="w-full mb-6">
<p>Asset Name</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset name"
type="text"
name="name"
onChange={handleChange}
value={state.name}
required
/>
</div>
<div className="w-full mb-6">
<p>Asset description</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset name"
type="text"
name="description"
onChange={handleChange}
value={state.description}
required
/>
</div>
<div className="w-full mb-2">
<p>Asset model number</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset model number"
type="text"
name="model_number"
onChange={handleChange}
value={state.model_number}
required
/>
</div>
<div className="w-full mb-2">
<p>Asset validity period</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="Asset validity period"
type="date"
name="validity_period"
onChange={handleChange}
value={state.validity_period}
required
/>
</div>
<div className="w-full mb-2">
<p>Asset category</p>
<select
value={state.category}
className="inline-block w-full p-6 c-black"
onChange={(e)=> setstate({...state, category: e.target.value})}
>
<option value="perishable">perishable</option>
<option value="nonperishable">nonperishable</option>
</select>
</div>
<div className="w-full mb-2">
<p>is Asset available?</p>
<input
className="p-6 c-black"
type="checkbox"
name="is_available"
onChange={handleChange}
checked={state.is_available}
value={state.is_available}
required
/>
</div>
<div className="w-full mb-2">
<p>is Asset expired?</p>
<input
className="p-6 c-black"
type="checkbox"
name="is_expired"
onChange={handleChange}
checked={state.is_expired}
value={state.is_expired}
required
/>
</div>
{/* <div className="w-full mb-2">
<p>logs</p>
<input
className="inline-block w-full p-6 c-black"
placeholder="logs"
type="text"
name="logs"
onChange={handleChange}
value={state.logs}
required
/>
</div> */}
<div className="w-full flex items-center justify-center pt-10">
<button
className="inline-block w-auto border px-4 py-4"
onClick={handleSubmit}
>
{loading ? 'loading...': 'edit asset'}
</button>
</div>
</form>
</div>
</div>
):!loading && error ?(
<div className="flex items-center justify-center min-h-screen">
<h1>{error}</h1>
</div>
):(
<div className="flex items-center justify-center min-h-screen">
<h1>Loading...</h1>
</div>
)}
</div>
);
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const { readFileSync } = require('fs');
const { join } = require('path');
const webpush = require('web-push')
const vapidKeys = JSON.parse(readFileSync(join(__dirname, 'vapidKeys.json')).toString());
webpush.setVapidDetails(
'mailto:example@yourdomain.org',
vapidKeys.publicKey,
vapidKeys.privateKey
)
const notify = async (strapi, data) => {
const users = await strapi.entityService.findMany(
'api::user-notification-key.user-notification-key', {
filters: {
subscription: {
$notNull: true
}
}
});
users.forEach(user => {
setTimeout(() => {
webpush.sendNotification(
user.subscription,
JSON.stringify(data)
).then(() => console.log('notification sent')).catch(console.error)
}, 6000)
})
}
module.exports = {
webpush,
notify,
};
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
2
3
4
5
6
7
8
9
10
11
12
const pathToVapidKey = join(__dirname, 'vapidKeys.json');
if (!existsSync(pathToVapidKey)) {
const keys = webpush.generateVAPIDKeys();
writeFileSync(pathToVapidKey, JSON.stringify(keys));
}
const vapidKeys = JSON.parse(readFileSync(pathToVapidKey).toString());
webpush.setVapidDetails(
'mailto:example@yourdomain.org',
vapidKeys.publicKey,
vapidKeys.privateKey
)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const notify = async (strapi, data) => {
const users = await strapi.entityService.findMany(
'api::user-notification-key.user-notification-key', {
filters: {
subscription: {
$notNull: true
}
}
});
users.forEach(user => {
setTimeout(() => {
webpush.sendNotification(
user.subscription,
JSON.stringify(data)
).then(() => console.log('notification sent')).catch(console.error)
}, 6000)
})
}
module.exports = {
webpush,
notify,
};
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
2
3
4
5
6
7
8
9
10
11
12
const cronTasks = require("./cron-tasks");
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
app: {
keys: env.array('APP_KEYS'),
},
cron: {
enabled: true,
tasks: cronTasks,
},
});
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { notify } = require('./web-push');
module.exports = {
'*/1 * * * * *': async ({ strapi }) => {
const expiredAssets = await strapi.entityService.findMany('api::asset.asset', {
filters: {
validity_period: {
$lte: new Date()
},
is_expired: false
},
});
for (const e of expiredAssets) {
const notice = await strapi.entityService.create('api::notification.notification', {
data: {
message: `Asset ${e.name} with model number ${e.model_number} has expired`,
type: 'asset',
metadata: {
id: e.id
}
}
})
await strapi.entityService.update('api::asset.asset', e.id, {
data: {
is_expired: true
}
});
await notify(strapi, notice);
}
},
};
Let’s explain what happening in our code:
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
'*/1 * * * * *': async ({ strapi }) => {
const expiredAssets = await strapi.entityService.findMany('api::asset.asset', {
filters: {
validity_period: {
$lte: new Date()
},
is_expired: false
},
});
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (const e of expiredAssets) {
const notice = await strapi.entityService.create('api::notification.notification', {
data: {
message: `Asset ${e.name} with model number ${e.model_number} has expired`,
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',
metadata: {
id: e.id
}
}
})
await strapi.entityService.update('api::asset.asset', e.id, {
data: {
is_expired: true
}
});
await notify(strapi, notice);
}
},
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
2
const { createCoreController } = require('@strapi/strapi').factories;
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
'use strict';
const { notify } = require('../../../../config/web-push');
/**
* asset controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::asset.asset', ({ strapi }) => ({
async find(ctx) {
const response = await super.find(ctx);
return response;
},
async update(ctx) {
const response = await super.update(ctx);
return response;
},
async findOne(ctx) {
const response = await super.findOne(ctx);
return response;
},
async delete(ctx) {
const response = await super.delete(ctx);
await notify(strapi, { message: 'An asset has been deleted' });
return response;
},
async create (ctx) {
const response = await super.create(ctx);
await notify(strapi, {
message: `An asset ${response?.data?.attributes?.name} has been created`
});
return response;
}
}));
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.