In the previous article, we went through how to create a react admin dashboard with the package react-admin where we were able to create the user and post module, with the ability to
- View all existing post
- Create a new post
- Edit a post
- Delete a post
- Filter post by User
- Also we're able to export our data as a CSV file
The only downside to our previous build, is we're using the provided dataProvider, given to us by react-admin, in real-life projects, we'll definitely be working with our own APIs and backend, therefore it's important we know our to integrate our own backend. By the end of the article, you'll be able to
- Integrate your own API/backend to react-admin
- Add pagination
- Add filtering and searching
- Add authentication pages
Without further ado, let's get started.
We'll first clone our previous repo, and checkout to a new branch
1.
https://github.com/okeken/react-admin-tutorial.git
2.
cd react-admin-tutorial
3.
git checkout -b v2
4.
yarn or npm install
Step 4 will install all dependencies if, everything works fine, your screen should be like below.
Before we proceed we'll quickly go-ahead to set up our backend for this demo, we'll be using json-server, json-server-auth and fakerjs.
create a new folder, and initiate a new nodejs project in it,
open a new terminal run this command one after the order
mkdir admin-backend
cd admin-backend
npm init --y
These commands will setup our nodejs project, lastly we'll go ahead to installed the needed packages,
yarn add json-server@0.16.1 @faker-js/faker@7.6.0
There are two ways to set up our database, we can create a db.json or an index.js.
But we'll be using a mix of both due to some flexibility, we need either after deployment or during development So, we'll first an index.js
Add the code below,
const { faker } = require("@faker-js/faker");
// sample brand list
const brandList = [
{
id: 1,
name: "Unbranded",
},
{
id: 2,
name: "Handmade",
},
{
id: 3,
name: "Recycled",
},
{
id: 4,
name: "Bespoke",
},
{
id: 5,
name: "Small",
},
{
id: 6,
name: "Generic",
},
{
id: 7,
name: "Intelligent",
},
{
id: 8,
name: "Licensed",
},
{
id: 9,
name: "Oriental",
},
{
id: 10,
name: "Sleek",
},
{
id: 11,
name: "Luxurious",
},
{
id: 12,
name: "Gorgeous",
},
{
id: 13,
name: "Refined",
},
{
id: 14,
name: "Awesome",
},
{
id: 15,
name: "Practical",
},
{
id: 16,
name: "Electronic",
},
{
id: 17,
name: "Fantastic",
},
{
id: 18,
name: "Modern",
},
{
id: 19,
name: "Handcrafted",
},
{
id: 20,
name: "Tasty",
},
];
module.exports = () => {
const data = { products: [], customers: [], orders: [], brands: brandList };
// Create 2000 products
for (let i = 0; i < 2000; i++) {
const title = faker.commerce.product();
const price = faker.commerce.price();
const description = faker.commerce.productDescription();
const image = faker.image.image();
const chosenBrand = Math.floor(
Math.random() * (brandList?.length ?? 10 + 1)
);
const brand = brandList[chosenBrand]; // pick a random brand from the brands array with ranging from 0 to the length of the brands array
const brandName = (id) => brandList.find((brand) => brand.id === id)?.name;
data.products.push({
id: i + 1,
title,
price,
description,
image,
brandId: brand.id,
brandName: brandName(brand.id),
});
}
// Create 50 users
for (let i = 0; i < 50; i++) {
const name = faker.name.firstName();
const email = faker.internet.email();
const address = faker.address.streetAddress();
const city = faker.address.city();
const state = faker.address.state();
const zip = faker.address.zipCode();
const phone = faker.phone.phoneNumber();
const country = faker.address.country();
data.customers.push({
id: i + 1,
name,
email,
phone,
address: `${address} ${city}, ${state} ${zip} ${country}`,
});
}
// create 300 orders
for (let i = 0; i < 500; i++) {
const customerId = faker.datatype.number({ min: 1, max: 50 });
const productId = faker.datatype.number({ min: 1, max: 2000 });
const quantity = faker.datatype.number({ min: 1, max: 10 });
const price = faker.commerce.price();
data.orders.push({
id: i + 1,
customerId,
productId,
quantity,
price,
total: price * quantity,
});
}
return data;
};
Go to the package.json, under the scripts, remove, the default
"test": "echo \"Error: no test specified\" && exit 1"
and replace it with
"dev": "json-server --watch index.js --port 5000 --no-cors",
"start": "json-server index.js --port 5000 --no-cors"
--watch -> Is to watch for file changes
--port -> to set up the port we're running our server
-no-cors -> to prevent any cors issue from the frontend.
Go ahead and save your changes and start up the server in the terminal with
yarn dev
If everything works as expected, you should see the screens below both on your terminal and browser.
We're done with the backend, let's move back to the frontend.
Let's Connect to a real API.
We'll try to model our API structure to look like the table below, based on this, we'll try to configure react-admin to consume our API.
Actions | Api Endpoints |
---|---|
get all products | GET baseUrl/products |
get a product by id | GET baseUrl/products/id |
update product | PUT baseUrl/products/id |
delete a product | DELETE baseUrl/products/id |
create a product | POST baseUrl/products/id |
get paginated products | GET baseUrl/products?_page=1&_limit=10 |
search products | GET baseUrl/products?q=search terms |
filter product | GET baseUrl/products?brandsId=2 |
Go and create a file called dataProvider.js and put the code below in it. This file is responsible for mapping our API requests to react-admin, think of it as the translator react-admin needs, to talk to our API and effectively deliver the needed manipulation to build our dashboard.
import { fetchUtils } from 'react-admin';
import { stringify } from 'query-string';
const apiUrl = 'localhost:5000';
const httpClient = fetchUtils.fetchJson;
export default {
getList: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
filter: JSON.stringify(params.filter),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
return httpClient(url).then(({ headers, json }) => ({
data: json,
total: parseInt(headers.get('content-range').split('/').pop(), 10),
}));
},
getOne: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`).then(({ json }) => ({
data: json,
})),
getMany: (resource, params) => {
const query = {
filter: JSON.stringify({ id: params.ids }),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
return httpClient(url).then(({ json }) => ({ data: json }));
},
getManyReference: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
filter: JSON.stringify({
...params.filter,
[params.target]: params.id,
}),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
return httpClient(url).then(({ headers, json }) => ({
data: json,
total: parseInt(headers.get('content-range').split('/').pop(), 10),
}));
},
update: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`, {
method: 'PUT',
body: JSON.stringify(params.data),
}).then(({ json }) => ({ data: json })),
updateMany: (resource, params) => {
const query = {
filter: JSON.stringify({ id: params.ids}),
};
return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
method: 'PUT',
body: JSON.stringify(params.data),
}).then(({ json }) => ({ data: json }));
},
create: (resource, params) =>
httpClient(`${apiUrl}/${resource}`, {
method: 'POST',
body: JSON.stringify(params.data),
}).then(({ json }) => ({
data: { ...params.data, id: json.id },
})),
delete: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`, {
method: 'DELETE',
}).then(({ json }) => ({ data: json })),
deleteMany: (resource, params) => {
const query = {
filter: JSON.stringify({ id: params.ids}),
};
return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
method: 'DELETE',
}).then(({ json }) => ({ data: json }));
}
};
Now let's begin to modify this file, based on the structure of our API.
- GetList: This returns all the items in an resource, from our api it returns array of products, orders, users, and brands. to use it, we have to first modify our
const query = { sort: JSON.stringify([field, order]), range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]), filter: JSON.stringify(params.filter), };
andreturn httpClient(url).then(({ headers, json }) => ({ data: json, total: parseInt(headers.get('content-range').split('/').pop(), 10), }));
toconst query = { _page: page, _limit: perPage, ...params.filter, };
return httpClient(url).then((resp) => { return { data: resp.json, total: +resp.headers.get("X-Total-Count"), }; });
_page, _limit are coming from our api for pagination purposes, the params.filter will return an object which we can use for sorting, filtering, ordering purpose. the total key on our return statement represents the toal number of items in our resource, json-server exposes a header "X-Total-Count" for us to use here, note the + sign in front of resp.headers, it's used to typecast from a string to an integer.
- DeleteMany: json-server do not allow us to delete multiple items at once, however, I did a workarond for this. first we'll set the header to use
const headers = { Accept: "application/json", "Content-Type": "application/json", };
we went ahead to scrap thefetchUtils.fetchJson()
and replace it withfetch
, afterall it's just a wrapper around fetch plus some little additions.
params.ids
will give us an array of item ids we want to delete, we mapped through it and make our API request, then we use promise.all to retrieve the response of all our requests. brilliant! π
const delFetch = params.ids.map((eleid) => {
return fetch(`${apiUrl}/${resource}/${eleid}`, {
method: "DELETE",
headers: headers,
});
});
const response = await Promise.all([delFetch]).then((res) => {
return {
data: params.ids,
};
});
Note: the format, we return from our response
has to be in this format, a 'data' key with the value of params.ids as an array. Else react-admin will start yelling at us.
If you have made it to this place, I'm so proud of your progress. ππΌππΌ
Now, let's go and integrate all our changes to our app and start doing stuff. π
Head over to app.js and import dataProvider.js, replace it with the previous dataProvider, and comment out our previous components.
In our components directory create a new component Filter.jsx and paste the code below into it.
//FilterPost.jsx
import React from "react";
import { Filter as FilterAdmin, ReferenceInput, TextInput, SelectInput } from "react-admin";
const Filter = ({searchLabel = 'Search', label='', reference='', source='', ...otherProps}) => (
<FilterAdmin {...otherProps}>
<TextInput
label={searchLabel}
source="q"
alwaysOn />
<ReferenceInput
label={label}
source={source}
reference={reference}
allowEmpty>
<SelectInput optionText="name" />
</ReferenceInput>
</FilterAdmin>
);
export default Filter;
Under src again, create a new folder called, "views"
create a sub-directory under it like the image below
(
in views/brands/index.jsx
add the code below in it
import * as React from "react";
import { Datagrid, List, TextField } from "react-admin";
const BrandList = props => (
<List {...props}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="name" />
</Datagrid>
</List>)
export default BrandList;
in views/products/index.jsx
import React from "react";
import { List, Datagrid, TextField, EditButton } from "react-admin";
import Filter from "../../Components/Filter";
const filterProps = {
label: "brands",
reference: "brands",
source: "brandId",
}
const ProductsList = props => (
<List filters={<Filter {...filterProps} />} {...props}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="title" />
<TextField source="brandName" />
<TextField source="price" />
<TextField source="description" />
<EditButton />
</Datagrid>
</List>
);
export default ProductsList
in views/products/components/CreateProducts.jsx
add the code below
//CreateProducts.jsx
import React from "react";
import {
Create,
SimpleForm,
ReferenceInput,
TextInput,
SelectInput,
} from "react-admin";
const ProductsCreate = props => (
<Create {...props}>
<SimpleForm>
<ReferenceInput
source="brandId"
reference="brands" label="brands">
<SelectInput optionText="name" />
</ReferenceInput>
<TextInput source="title" />
<TextInput source="price" />
<TextInput multiline source="description" />
</SimpleForm>
</Create>
);
export default ProductsCreate;
in views/components/EditProducts.jsx
add the code below;
//EditProducts.jsx
import React from "react";
import {
Edit,
SimpleForm,
ReferenceInput,
TextInput,
SelectInput,
} from "react-admin";
//
const EditProducts = props => (
<Edit {...props}>
<SimpleForm>
<ReferenceInput source="brandId" reference="brands" label="brands">
<SelectInput optionText="name" />
</ReferenceInput>
<TextInput source="title" />
<TextInput source="price" />
<TextInput multiline source="description" />
</SimpleForm>
</Edit>
);
export default EditProducts;
Go to app.js and import the newly create components, with the final code looking like the one below.
import * as React from "react";
import { Admin, Resource } from "react-admin";
import { Dashboard } from "./Components/DashBoard.jsx";
import BrandList from "./views/brands/index.jsx";
import dataProvider from "./dataProvider";
import ProductsCreate from "./views/products/components/CreateProducts.jsx";
import EditProducts from "./views/products/components/EditProducts.jsx";
import ProductList from "./views/products";
const App = () => (
<Admin dashboard={Dashboard} dataProvider={dataProvider}>
<Resource name="brands" list={BrandList} />
<Resource
name="products"
list={ProductList}
edit={EditProducts}
create={ProductsCreate}
/>
</Admin>
);
export default App;
- Open your
admin-backend
and runyarn dev
to spin up your local backend - Go back to your frontend project and run
yarn start
in your terminal. If everything works fine, you should see the gif video below.
Let's add make some modifications to our backend code, so we can deploy it on our favorite hosting server plus authentication and authorization,
run yarn add json-server-auth axios
or npm install json-server-auth axios
in your terminal, then
create a new folder src
, move our previous index.js inside, create app.js, and put the code below
json-server-auth
exposes some API for us for authentication purposes plus some guarded routes, which we did for products and brands
Register a new user
Any of the following routes registers a new user :
- POST /register
- POST /signup
- POST /users email and password are required in the request body :
POST /register
{
"email": "user@user.com",
"password": "mypassword"
}
your response should be something like this:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im9saXZpZXJAbWFpbDEyLmNvbSIsImlhdCI6MTY1NTkyMzg4NCwiZXhwIjoxNjU1OTI3NDg0LCJzdWIiOiIyIn0.eNVKi0mjOeZl7RpLPWZbpo5ggdAtB2uq1h96cuAp3eQ",
"user": {
"email": "user@user.com",
"id": 1
}
}
Login a user
Any of the following routes logs an existing user in :
POST /login
POST /signin
email and password are required fields:
POST /login
{
"email": "user@user.com",
"password": "mypassword"
}
you should a get response like the below, it contains the JWT token and the user data excluding the password:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im9saXZpZXJAbWFpbDEyLmNvbSIsImlhdCI6MTY1NTkyNzA3MSwiZXhwIjoxNjU1OTMwNjcxLCJzdWIiOiIyIn0.PixNo_fWZJ2IiCByjtePLDSrf4_Zikup6hQt_qxQbmI",
"user": {
"email": "user@user.com",
"id": 1
}
}
const path = require("path");
const jsonServer = require("json-server");
const auth = require("json-server-auth");
const server = jsonServer.create();
const router = jsonServer.router(path.join(__dirname, "db.json"));
server.db = router.db;
const middlewares = jsonServer.defaults();
server.use(middlewares);
const rules = auth.rewriter({
"/products*": "/660/products$1",
"/orders*": "/440/orders$1",
});
// You must apply the middlewares in the following order
const port = process.env.PORT || 5000;
server.use(rules);
server.use(auth);
server.use(router);
server.listen(port, () => {
console.log("JSON Server is running on port " + port);
});
create db.json and put some sample data there
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
"profile": { "name": "typicode" },
"users": [],
}
create routes.json and put the authorization route guard there
{
"/products*": "/660/products$1",
"/orders*": "/440/orders$1"
}
Notes:
Routes | Permission |
---|---|
/660/* | User must be logged in to write or read the resource. |
/440/* | No one can write the resource. User must be logged in to read the resource |
There are more multiple ways to implement route guard with json-server-auth, for extensive study you can checkout their github repo here
head over to src/index.js
and let's extract all the products, orders, and customer creation inside a function. We'll use the node js fs(file system) to dynamically modify our db.json
Go ahead and copy the modified data in your src/index.js
const { faker } = require("@faker-js/faker");
const fs = require("fs");
// sample brand list
const brandList = [
{
id: 1,
name: "Unbranded",
},
{
id: 2,
name: "Handmade",
},
{
id: 3,
name: "Recycled",
},
{
id: 4,
name: "Bespoke",
},
{
id: 5,
name: "Small",
},
{
id: 6,
name: "Generic",
},
{
id: 7,
name: "Intelligent",
},
{
id: 8,
name: "Licensed",
},
{
id: 9,
name: "Oriental",
},
{
id: 10,
name: "Sleek",
},
{
id: 11,
name: "Luxurious",
},
{
id: 12,
name: "Gorgeous",
},
{
id: 13,
name: "Refined",
},
{
id: 14,
name: "Awesome",
},
{
id: 15,
name: "Practical",
},
{
id: 16,
name: "Electronic",
},
{
id: 17,
name: "Fantastic",
},
{
id: 18,
name: "Modern",
},
{
id: 19,
name: "Handcrafted",
},
{
id: 20,
name: "Tasty",
},
];
// Get content from file
const filePath = process.cwd() + "//src/db.json";
var contents = fs.readFileSync(filePath);
// Define to JSON type
var jsonContent = JSON.parse(contents);
const products = () => {
const product = [];
for (let i = 0; i < 2000; i++) {
const title = faker.commerce.product();
const price = faker.commerce.price();
const description = faker.commerce.productDescription();
const image = faker.image.image();
const chosenBrand = Math.floor(Math.random() * brandList.length);
const brand = brandList[chosenBrand]; // pick a random brand from the brands array with ranging from 0 to the length of the brands array
const brandName = (id) => brandList.find((brand) => brand.id === id).name;
product.push({
id: i + 1,
title,
price,
description,
image,
brandId: brand.id,
brandName: brandName(brand.id),
});
}
return product;
};
const users = () => {
const user = [];
// Create 50 users
for (let i = 0; i < 50; i++) {
const name = faker.name.firstName();
const email = faker.internet.email();
const address = faker.address.streetAddress();
const city = faker.address.city();
const state = faker.address.state();
const zip = faker.address.zipCode();
const phone = faker.phone.number();
const country = faker.address.country();
user.push({
id: i + 1,
name,
email,
phone,
address: `${address} ${city}, ${state} ${zip} ${country}`,
});
}
return user;
};
const orders = () => {
const order = [];
// create 300 orders
for (let i = 0; i < 500; i++) {
const customerId = faker.datatype.number({ min: 1, max: 50 });
const productId = faker.datatype.number({ min: 1, max: 2000 });
const quantity = faker.datatype.number({ min: 1, max: 10 });
const price = faker.commerce.price();
order.push({
id: i + 1,
customerId,
productId,
quantity,
price,
total: price * quantity,
});
}
return order;
};
const modified = {
...jsonContent,
brands: brandList,
customers: users(),
orders: orders(),
products: products(),
};
// write to a new file named 2pac.txt
fs.writeFile(filePath, JSON.stringify(modified, null, 2), (err) => {
// throws an error, you could also catch it here
if (err) throw err;
});
module.exports = () => {
const data = {
products: products(),
customers: users(),
orders: orders(),
brands: brandList,
};
return data;
};
Go to package.json, let's modify our script dev and start logic;
"dev": "json-server --watch src/index.js -m ./node_modules/json-server-auth --port 5000 --no-cors -r src/routes.json",
"start2": "node src/index.js && json-server src/db.json -m ./node_modules/json-server-auth --port 5000 --no-cors -r src/routes.json",
"start":"node src/index.js && node src/app.js --no-cors"
note: the "dev" is for development purpose while start is for deployment/production purpose
Open the terminal do yarn start
or yarn dev
, and everything should still work as like before.
Except, you'll not be able to view the products again unless we login in
Add Authentication Pages
Modify the dataProvider to send the authorization header,
Just like dataProvider, we'll be implementing the auth logic in a file called authProvider.js. Go ahead and create one and paste the code below,
// src/components/authProvider.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from "react-admin";
import axios from "axios";
import { baseUrl } from "./env";
export const authProvider = async (type, params) => {
// when a user tries to log in
if (type === AUTH_LOGIN) {
const { email, password } = params;
return axios
.post(`${baseUrl}login`, {
email,
password,
})
.then(({ data }) => {
localStorage.setItem("authToken", data.accessToken);
return data;
})
.catch((e) => e);
}
// when a user tries to logout
if (type === AUTH_LOGOUT) {
localStorage.removeItem("authToken");
return Promise.resolve();
}
// when the API throws an error
if (type === AUTH_ERROR) {
const { status } = params;
if (status === 401 || status === 403) {
localStorage.removeItem("authToken");
return Promise.reject();
}
return Promise.resolve();
}
// when a user navigates to a new location
if (type === AUTH_CHECK) {
return localStorage.getItem("authToken")
? Promise.resolve()
: Promise.reject();
}
return Promise.reject("Unknown Method");
};
Header over to app.js and import authProvider.js and add a prop of authProvider ={authProvider}
to the Admin component.
import * as React from "react";
import { Admin, Resource } from "react-admin";
import { Dashboard } from "./Components/DashBoard.jsx";
import BrandList from "./views/brands/index.jsx";
import dataProvider from "./dataProvider";
import { authProvider } from "./authProvider.js";
import ProductsCreate from "./views/products/components/CreateProducts.jsx";
import EditProducts from "./views/products/components/EditProducts.jsx";
import ProductList from "./views/products";
const App = () => (
<Admin
dashboard={Dashboard}
authProvider={authProvider}
dataProvider={dataProvider}
>
<Resource name="brands" list={BrandList} />
<Resource
name="products"
list={ProductList}
edit={EditProducts}
create={ProductsCreate}
/>
</Admin>
);
export default App;
Restart your frontend server, you should have a login page automatically pop up. But we want to supply our own login and register page. Let's go ahead and install some Material UI package we need for these two pages,
yarn add @mui/material @mui/icons-material @emotion/react @emotion/styled react-admin@latest
We also want to upgrade to the latest version of react-admin, because of many breaking changes from version 3.x.x, after the installations are done, go ahead and create Login.jsx inside our views folder and paste the code below;
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import Link from '@mui/material/Link';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { useLogin, useNotify } from 'react-admin';
const theme = createTheme();
function Login() {
const login = useLogin()
const notify = useNotify()
const handleSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const userData = {
email: data.get('email'),
password: data.get('password'),
}
notify('Login successful', {type:'success'})
login(userData);
};
return (
<ThemeProvider theme={theme}>
<Grid container component="main" sx={{ height: '100vh' }}>
<CssBaseline />
<Grid
item
xs={false}
sm={4}
md={7}
sx={{
backgroundImage: 'url(https://source.unsplash.com/random)',
backgroundRepeat: 'no-repeat',
backgroundColor: (t) =>
t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
<Box
sx={{
my: 8,
mx: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
id="password"
type="password"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Sign In
</Button>
<Grid container>
<Grid item xs>
</Grid>
<Grid item>
<Link href="#/register" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Grid>
</Grid>
</ThemeProvider>
);
}
export default Login;
For registration, create Register.jsx inside the views folder and paste the code below into it;
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import Link from '@mui/material/Link';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import axios from 'axios'
import { baseUrl } from '../env';
import { useNotify } from 'react-admin';
import { useNavigate } from "react-router-dom";
const theme = createTheme();
export default function SignUp() {
const notify = useNotify()
const navigate = useNavigate()
const handleSubmit = async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
console.log({
email: data.get('email'),
password: data.get('password'),
});
const userData = {
email: data.get('email'),
password: data.get('password'),
}
try{
const response = await axios.post(`${baseUrl}register`, userData)
localStorage.setItem('authToken', response.data.accessToken)
notify(`Registration successful`, { type: 'success' });
navigate('/#')
}
catch(e){
notify(`Error registering, try again`, { type: 'error' });
}
};
return (
<ThemeProvider theme={theme}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="new-password"
/>
</Grid>
<Grid item xs={12}>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Sign Up
</Button>
<Grid container justifyContent="flex-end">
<Grid item>
<Link href="#/login" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
</ThemeProvider>
);
}
Let's go ahead and use these pages in our app.js, and import both of them. pass a prop of loginPage to the admin component just like what we did for Dashboard, import CustomRoutes
from react-admin
, and use it as shown below;
<CustomRoutes noLayout>
<Route path="/register" element={<Register />} />
</CustomRoutes>
you should import Route component from react-router-dom
, the final version should look like below
import * as React from "react";
import { Admin, Resource, CustomRoutes } from "react-admin";
import { Dashboard } from "./Components/DashBoard.jsx";
import BrandList from "./views/brands/index.jsx";
import dataProvider from "./dataProvider";
import { authProvider } from "./authProvider.js";
import ProductsCreate from "./views/products/components/CreateProducts.jsx";
import EditProducts from "./views/products/components/EditProducts.jsx";
import ProductList from "./views/products";
import Login from "./views/Login.jsx";
import { Route } from "react-router-dom";
import Register from "./views/Register";
const App = () => (
<Admin
loginPage={Login}
dashboard={Dashboard}
authProvider={authProvider}
dataProvider={dataProvider}
>
<CustomRoutes noLayout>
<Route path="/register" element={<Register />} />
</CustomRoutes>
<Resource name="brands" list={BrandList} />
<Resource
name="products"
list={ProductList}
edit={EditProducts}
create={ProductsCreate}
/>
</Admin>
);
export default App;
You might notice, that your products and brand page no more display their data, let's quickly add authorization to these requests. In dataProvider.js, let's change the getList to be as shown below
.....
getList: (resource, params) => {
const { page, perPage } = params.pagination;
const query = {
_page: page,
_limit: perPage,
...params.filter,
};
const url = `${baseUrl}${resource}?${stringify(query)}`;
const token = localStorage.getItem("authToken");
const options = {
headers: new Headers({ Accept: "application/json" }),
};
if (token) {
options.headers.set("Authorization", `Bearer ${token}`);
return httpClient(url, options).then((resp) => {
return {
data: resp.json,
total: +resp.headers.get("X-Total-Count"),
};
});
}
},
....
Logout, and create a new user, after successful registration, you'll be redirected to the dashboard page, everything should work as expected.
You can go ahead and deploy your backend api to your preferred hosting service,and point it to the baseUrl in our frontend. I'm using heroku for this tutorial.
Todo: Because this article is already long enough, there are few other things we can make to extend this further
- Customize the dashboard with our own styles
- Port the project to nextjs
- Adding form validation on the login and page registration.
That's it guys, If you make it this far, I'm rooting for you, let me know your thoughts, suggestion, and questions in the comment section.
source codes: frontend end and backend
xoxo
Top comments (5)
i got stucked with your article tutorial.
first, by the section "setting up the database" , my question is: where should i put those codes? whilst you said create a new file named index.js, where should i put it? is it in the frontend folder or in the backend folder? you didn't explain it clearly mate..
second, "--no-cors" made an error, said that 'Unknown option'
how??? please solutions
Hi Hisoka,
I am happy you're finding the article helpful! the index.js file is a backend code, you should put it at the root of your backend.
the "--no-cors" error you are getting is due to "json-server" latest version breaking changes, please use version, "0.16.1" and fakerjs version "7.6.0" I updated the article to reflect this.
Appreciated for the replied, I'll try your advice sir, furthermore another issue occured is it fine we discuss it shall we? :)
Feel free to drop any issues you're facing or create an issue on the github repo. I'll attend to it as soon as possible.
Thank you
Well explained. Thank you