DEV Community

Cover image for How to create an admin panel in React JS - Part 2
okeken
okeken

Posted on • Edited on

How to create an admin panel in React JS - Part 2

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
Enter fullscreen mode Exit fullscreen mode

2.

cd react-admin-tutorial
Enter fullscreen mode Exit fullscreen mode

3.

git checkout -b v2
Enter fullscreen mode Exit fullscreen mode

4.

yarn  or npm install
Enter fullscreen mode Exit fullscreen mode

Step 4 will install all dependencies if, everything works fine, your screen should be like below.

how to create admin panel in react-js

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

react admin panel

react admin panel in react js

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;
};

Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

--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
Enter fullscreen mode Exit fullscreen mode

If everything works as expected, you should see the screens below both on your terminal and browser.

react-admin dashboard

admin dashboard in reactjs

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 }));
    }
};
Enter fullscreen mode Exit fullscreen mode

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), }; and return httpClient(url).then(({ headers, json }) => ({ data: json, total: parseInt(headers.get('content-range').split('/').pop(), 10), })); to const 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 the fetchUtils.fetchJson() and replace it with fetch, 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,
      };
    });
Enter fullscreen mode Exit fullscreen mode

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.

admin panel with reactjs

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;
Enter fullscreen mode Exit fullscreen mode

Under src again, create a new folder called, "views"
create a sub-directory under it like the image below
(how to create admin panel in reactjs

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • Open your admin-backend and run yarn 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.

react-admin custom backend

Let's add make some modifications to our backend code, so we can deploy it on our favorite hosting server plus authentication and authorization,

react admin custom data provider

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"
}
Enter fullscreen mode Exit fullscreen mode

your response should be something like this:

{
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im9saXZpZXJAbWFpbDEyLmNvbSIsImlhdCI6MTY1NTkyMzg4NCwiZXhwIjoxNjU1OTI3NDg0LCJzdWIiOiIyIn0.eNVKi0mjOeZl7RpLPWZbpo5ggdAtB2uq1h96cuAp3eQ",
    "user": {
        "email": "user@user.com",
        "id": 1
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode
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);
});

Enter fullscreen mode Exit fullscreen mode

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": [],

}
Enter fullscreen mode Exit fullscreen mode

create routes.json and put the authorization route guard there

{
    "/products*": "/660/products$1",
    "/orders*": "/440/orders$1"
  }

Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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");
};

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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"),
        };
      });
    }
  },
  ....
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
azrdr9n profile image
Hisoka Kurosawa

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

Collapse
 
okeken profile image
okeken

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.

yarn add json-server@0.16.1 @faker-js/faker@7.6.0
Enter fullscreen mode Exit fullscreen mode
Collapse
 
azrdr9n profile image
Hisoka Kurosawa

Appreciated for the replied, I'll try your advice sir, furthermore another issue occured is it fine we discuss it shall we? :)

Thread Thread
 
okeken profile image
okeken

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

Collapse
 
adetutu777 profile image
Adetutu Gbangbola

Well explained. Thank you