Developing a Modern Admin Portal with React, Redux, and Ant Design (Part-3)

React Oct 18, 2020

In the part-1, I started with installing the React, dependencies and preparing the environment and showed  the implementation of Main Layout of the Admin Panel. I have demonstrated routing in the react with the SiderMenu and Content in the Layout.

In the part-2, I demonstrated using generic components with custom hooks, and created exemplary pages containing the tables in order to list the items, forms for create, update pages, and we designed the pages by invoking the reusable hooks.

In this part I would like show more visual components by designing the Dashboard page. For charting I use bizcharts, that is a chart component library with wide variety of choice. Thanks to its Example Charts gallery, it is easy to just copy the components and use in our pages.

Designing the Dashboard

We need to start with install the bizcharts library:

yarn add bizcharts

ChartCard Component

I would like have a Card component that will contain a summary info, and in the size of 1/4 of a row, so that I can add 4 of them in a row and display some tiny bit of information within. It is going to look like as follows:

The component code is as follows:

import React from 'react';
import { Card } from 'antd';
import './ChartCard.less';

function ChartCard(props) {
  const renderContent = () => {
    const {
      contentHeight,
      title,
      avatar,
      action,
      total,
      footer,
      children,
      loading,
    } = props;

    return (
      <div className="chartCard">
        <div className="chartTop">
          <div className="avatar">{avatar}</div>
          <div className="metaWrap">
            <div className="meta">
              <span className="title">{title}</span>
              <span className="action">{action}</span>
            </div>
            <div className="total">{total}</div>
          </div>
        </div>
        {children && (
          <div className="content" style={{ height: contentHeight || 'auto' }}>
            <div className="contentHeight">{children}</div>
          </div>
        )}
        {footer && <div className="footer">{footer}</div>}
      </div>
    );
  };
  return (
    <Card loading={false} bodyStyle={{ padding: '20px 24px 8px 24px' }}>
      {renderContent()}
    </Card>
  );
}

export default ChartCard;
ChartCard.js

And it has a styling file:

.chartCard {
  position: relative;
  .chartTop {
    position: relative;
    width: 100%;
    overflow: hidden;
  }
  .chartTopMargin {
    margin-bottom: 12px;
  }
  .chartTopHasMargin {
    margin-bottom: 20px;
  }
  .metaWrap {
    float: left;
  }
  .avatar {
    position: relative;
    top: 4px;
    float: left;
    margin-right: 20px;
    img {
      border-radius: 100%;
    }
  }
  .meta {
    height: 22px;
    color: fade(#000, 45%);
    font-size: 14px;
    line-height: 22px;
  }
  .action {
    position: absolute;
    top: 4px;
    right: 0;
    line-height: 1;
    cursor: pointer;
  }
  .total {
    height: 38px;
    margin-top: 4px;
    margin-bottom: 0;
    overflow: hidden;
    color: fade(#000, 85%);
    font-size: 30px;
    line-height: 38px;
    white-space: nowrap;
    text-overflow: ellipsis;
    word-break: break-all;
  }
  .content {
    position: relative;
    width: 100%;
    margin-bottom: 12px;
  }
  .contentFixed {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
  }
  .footer {
    margin-top: 8px;
    padding-top: 9px;
    border-top: 1px solid hsv(0, 0, 91%);
    & > * {
      position: relative;
    }
  }
  .footerMargin {
    margin-top: 20px;
  }
  .trendText {
    margin-left: 8px;
    margin-right: 4px;
    color: fade(#000, 85%);
  }
  .boldText {
    color: fade(#000, 85%);
  }
}
.chartCard {
  position: relative;
  .chartTop {
    position: relative;
    width: 100%;
    overflow: hidden;
  }
  .chartTopMargin {
    margin-bottom: 12px;
  }
  .chartTopHasMargin {
    margin-bottom: 20px;
  }
  .metaWrap {
    float: left;
  }
  .avatar {
    position: relative;
    top: 4px;
    float: left;
    margin-right: 20px;
    img {
      border-radius: 100%;
    }
  }
  .meta {
    height: 22px;
    color: fade(#000, 45%);
    font-size: 14px;
    line-height: 22px;
  }
  .action {
    position: absolute;
    top: 4px;
    right: 0;
    line-height: 1;
    cursor: pointer;
  }
  .total {
    height: 38px;
    margin-top: 4px;
    margin-bottom: 0;
    overflow: hidden;
    color: fade(#000, 85%);
    font-size: 30px;
    line-height: 38px;
    white-space: nowrap;
    text-overflow: ellipsis;
    word-break: break-all;
  }
  .content {
    position: relative;
    width: 100%;
    margin-bottom: 12px;
  }
  .contentFixed {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
  }
  .footer {
    margin-top: 8px;
    padding-top: 9px;
    border-top: 1px solid hsv(0, 0, 91%);
    & > * {
      position: relative;
    }
  }
  .footerMargin {
    margin-top: 20px;
  }
  .trendText {
    margin-left: 8px;
    margin-right: 4px;
    color: fade(#000, 85%);
  }
  .boldText {
    color: fade(#000, 85%);
  }
}
ChartCard.less

Mini Charting Components for ChartCard

I would like to add some bar/line charts within the ChartCard to display some visual summary info, but to be able to fit in such a small area, I need to use some styling and autoHeight method.

import React from 'react';

function computeHeight(node) {
  const { style } = node;
  style.height = '100%';
  const totalHeight = parseInt(`${getComputedStyle(node).height}`, 10);
  const padding =
    parseInt(`${getComputedStyle(node).paddingTop}`, 10) +
    parseInt(`${getComputedStyle(node).paddingBottom}`, 10);
  return totalHeight - padding;
}
function getAutoHeight(n) {
  if (!n) {
    return 0;
  }
  const node = n;
  let height = computeHeight(node);
  const { parentNode } = node;
  if (parentNode) {
    height = computeHeight(parentNode);
  }
  return height;
}
function autoHeight() {
  return WrappedComponent => {
    class AutoHeightComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          computedHeight: 0,
        };
        this.root = undefined;
        this.handleRoot = node => {
          this.root = node;
        };
      }

      componentDidMount() {
        // eslint-disable-next-line react/prop-types
        const { height } = this.props;
        if (!height) {
          let h = getAutoHeight(this.root);
          this.setState({ computedHeight: h });
          if (h < 1) {
            h = getAutoHeight(this.root);
            this.setState({ computedHeight: h });
          }
        }
      }

      render() {
        // eslint-disable-next-line react/prop-types
        const { height } = this.props;
        const { computedHeight } = this.state;
        const h = height || computedHeight;
        return (
          <div ref={this.handleRoot}>
            {/* eslint-disable-next-line react/jsx-props-no-spreading */}
            {h > 0 && <WrappedComponent {...this.props} height={h} />}
          </div>
        );
      }
    }
    return AutoHeightComponent;
  };
}

export default autoHeight;
autoHeight.js

Some general styling:

.miniChart {
  position: relative;
  width: 100%;
  .chartContent {
    position: absolute;
    bottom: -28px;
    width: 100%;
    > div {
      margin: 0 -5px;
      overflow: hidden;
    }
  }
  .chartLoading {
    position: absolute;
    top: 16px;
    left: 50%;
    margin-left: -7px;
  }
}
chart.less

MiniArea Component

import React from 'react';
import { Axis, Chart, Geom, Tooltip } from 'bizcharts';
import './chart.less';
import autoHeight from './autoHeight';

function MiniArea(props) {
  const {
    height = 1,
    data = [],
    forceFit = true,
    color = 'rgba(24, 144, 255, 0.2)',
    borderColor = '#1089ff',
    scale = { x: {}, y: {} },
    borderWidth = 2,
    line,
    xAxis,
    yAxis,
    animate = true,
  } = props;

  const padding = [36, 5, 30, 5];

  const scaleProps = {
    x: {
      type: 'cat',
      range: [0, 1],
      ...scale.x,
    },
    y: {
      min: 0,
      ...scale.y,
    },
  };

  const tooltip = [
    'x*y',
    (x, y) => ({
      name: x,
      value: y,
    }),
  ];

  const chartHeight = height + 54;

  return (
    <div className="miniChart" style={{ height }}>
      <div className="chartContent">
        {height > 0 && (
          <Chart
            animate={animate}
            scale={scaleProps}
            height={chartHeight}
            forceFit={forceFit}
            data={data}
            padding={padding}
          >
            <Axis
              key="axis-x"
              name="x"
              label={null}
              line={null}
              tickLine={null}
              grid={null}
              {...xAxis}
            />
            <Axis
              key="axis-y"
              name="y"
              label={null}
              line={null}
              tickLine={null}
              grid={null}
              {...yAxis}
            />
            <Tooltip showTitle={false} crosshairs={false} />
            <Geom
              type="area"
              position="x*y"
              color={color}
              tooltip={tooltip}
              shape="smooth"
              style={{
                fillOpacity: 1,
              }}
            />
            {line ? (
              <Geom
                type="line"
                position="x*y"
                shape="smooth"
                color={borderColor}
                size={borderWidth}
                tooltip={false}
              />
            ) : (
              <span style={{ display: 'none' }} />
            )}
          </Chart>
        )}
      </div>
    </div>
  );
}

export default autoHeight()(MiniArea);
MiniArea.js

We will use MiniArea component in ChartCard and it looks like as follows:

MiniBar Component

import React from 'react';
import { Chart, Interval, Interaction } from 'bizcharts';
import './chart.less';
import autoHeight from './autoHeight';

function MiniBar(props) {
  const data = [
    { year: '1951 year', sales: 38 },
    { year: '1952 year', sales: 52 },
    { year: '1956 year', sales: 61 },
    { year: '1957 year', sales: 45 },
    { year: '1958 year', sales: 48 },
    { year: '1959 year', sales: 38 },
    { year: '1960 year', sales: 38 },
    { year: '1962 year', sales: 38 },
    { year: '1963 year', sales: 10 },
    { year: '1965 year', sales: 90 },
    { year: '1966 year', sales: 80 },
    { year: '1967 year', sales: 20 },
    { year: '1968 year', sales: 80 },
    { year: '1970 year', sales: 50 },
  ];

  return (
    <div style={{ paddingTop: '20px' }}>
      <Chart autoFit pure data={data}>
        <Interval position="year*sales" />
        <Interaction type="element-highlight" />
        <Interaction type="active-region" />
      </Chart>
    </div>
  );
}

export default MiniBar;
Minibar.js

We will use MiniBar component in ChartCard and it looks like as follows:

Adding ChartCards to Dashboard

We can display similar information or visual charts in the ChartCards and we can display them in the Dashboard as follows:

import React from 'react';
import { Card, Col, Row, Layout, Tooltip } from 'antd';
import { InfoCircleFilled, CaretUpFilled } from '@ant-design/icons';
import ChartCard from '../../component/chart/ChartCard';
import MiniArea from '../../component/chart/MiniArea';
import MiniBar from '../../component/chart/MiniBar';
import MiniProgress from '../../component/chart/MiniProgress';
import { movementSummary, visitSummary } from './Constants';
import ProductBarChart from '../../component/chart/ProductBarChart';
import ProductPieChart from '../../component/chart/ProductPieChart';

function Dashboard() {
  const topColResponsiveProps = {
    xs: 24,
    sm: 12,
    md: 12,
    lg: 12,
    xl: 6,
    style: { marginBottom: 24 },
  };

  return (
    <>
      <Row gutter={24} type="flex">
        <Col {...topColResponsiveProps}>
          <ChartCard
            bordered={false}
            title="Total Items"
            action={
              <Tooltip title="Total number of items">
                <InfoCircleFilled />
              </Tooltip>
            }
            loading={false}
            total={12}
            footer={
              <>
                <span className="boldText">{13}</span> Items added in the last{' '}
                <span className="boldText">7</span> days
              </>
            }
            contentHeight={46}
          >
            <div style={{ position: 'absolute', bottom: 0, left: 0 }}>
              Weekly Changes
              <span className="trendText">{14}%</span>
              <CaretUpFilled style={{ color: '#52c41a' }} />
            </div>
          </ChartCard>
        </Col>
        <Col {...topColResponsiveProps}>
          <ChartCard
            bordered={false}
            title="Portal Visits"
            action={
              <Tooltip title="Total number of active users in the last month.">
                <InfoCircleFilled />
              </Tooltip>
            }
            loading={false}
            total={10}
            footer={
              <>
                <span className="boldText">{12}</span> Average daily visits per
                day
              </>
            }
            contentHeight={46}
          >
            <MiniArea color="#975FE4" data={visitSummary} />
          </ChartCard>
        </Col>
        <Col {...topColResponsiveProps}>
          <ChartCard
            bordered={false}
            title="Items Moved"
            action={
              <Tooltip title="Item movement in the last year.">
                <InfoCircleFilled />
              </Tooltip>
            }
            loading={false}
            total={124}
            footer={
              <>
                <span className="boldText">{123}</span> Items moved in the last
                month
              </>
            }
            contentHeight={46}
          >
            <MiniBar data={movementSummary} />
          </ChartCard>
        </Col>
        <Col {...topColResponsiveProps}>
          <ChartCard
            bordered={false}
            title="Item Returns"
            action={
              <Tooltip title="Percentage of returned items.">
                <InfoCircleFilled />
              </Tooltip>
            }
            loading={false}
            total={10 + ' %'}
            footer={
              <>
                <span className="boldText">{12}</span> Items in the last year
              </>
            }
            contentHeight={46}
          >
            <MiniProgress
              percent={10}
              strokeWidth={16}
              color="#13C2C2"
              target={100}
            />
          </ChartCard>
        </Col>
      </Row>
    </>
  );
}

export default Dashboard;
Dashboard.js

Now the Dashboard page looks like as follows:

BarChart Component

import React from 'react';
import { Chart, Interval, Tooltip } from 'bizcharts';
import { Card } from 'antd';

const barData = [
  { x: 'W-1', y: 44 },
  { x: 'W-2', y: 201 },
  { x: 'W-3', y: 41 },
  { x: 'W-4', y: 197 },
  { x: 'W-5', y: 173 },
  { x: 'W-6', y: 184 },
  { x: 'W-7', y: 109 },
  { x: 'W-8', y: 55 },
  { x: 'W-9', y: 28 },
  { x: 'W-10', y: 153 },
  { x: 'W-11', y: 76 },
  { x: 'W-12', y: 27 },
];

function ProductBarChart() {
  return (
    <Card bordered={false}>
      <Chart
        height={250}
        autoFit
        data={barData}
        interactions={['active-region']}
      >
        <Interval position="x*y" />
        <Tooltip shared />
      </Chart>
    </Card>
  );
}

export default ProductBarChart;
ProductBarChart.js

PieChart Component

import React from 'react';
import { Interaction, PieChart } from 'bizcharts';
import { Card } from 'antd';

const pieData = [
  {
    type: 'home',
    value: 27,
  },
  {
    type: 'living',
    value: 25,
  },
  {
    type: 'accessories',
    value: 18,
  },
  {
    type: 'jewellery',
    value: 15,
  },
  {
    type: 'clothing',
    value: 10,
  },
  {
    type: 'handmade',
    value: 5,
  },
];

function ProductPieChart() {
  return (
    <Card bordered={false}>
      <PieChart
        forceFit
        height={250}
        data={pieData}
        radius={0.8}
        angleField="value"
        colorField="type"
        label={{
          visible: true,
          type: 'outer',
          offset: 20,
          formatter: val => `${val}%`,
        }}
      >
        <Interaction type="element-single-selected" />
      </PieChart>
    </Card>
  );
}

export default ProductPieChart;
ProductPieChart.js

These components are just some example usage of chart components of Bizchart. We can display them in the dashboard with a 1/2 row size card. Right after the first row in the Dashboard page we can add these components as the second row:

<Row gutter={24} type="flex">
  <Col span={12}>
    <Card title="Weekly Sale Report">
      <ProductBarChart />
    </Card>
  </Col>
  <Col span={12}>
    <Card title="Sale Summary">
      <ProductPieChart />
    </Card>
  </Col>
</Row>
Dashboard.js

Now the Dashboard page looks like as follows:

See the commit for the changes: 22f2798.

Tags