Introduction
In this post, we go through the process of developing a React admin panel using Refine and daisyUI.
Refineis a React-based framework that helps quickly build data-heavy applications like dashboards, admin panels and storefronts. It comes with a headless core package that integrates with any UI framework and design system.
daisyUI is a component templates library built on top of TailwindCSS. It provides us with short semantic classes composed from TailwindCSS utilities and a growing collection of convenient component templates that helps quickly build React components for our app.
daisyUI can easily be integrated with Refine, and in this post we are going to see how to do that while building a dashboard and admin panel app using Refine's Fine Foods API.
Overview
The React admin panel we are going to build uses the Refine hosted Fine Foods API to display a dashboard of KPI data, and CRUD pages for products
and categories
resources. The dashboard will present various data in cards, charts and a table. And the products
and categories
resources will have list
, create
, show
and edit
pages.
We start this post with a brief discussion on Refine architecture - particularly how it works under the hood with React contexts backed by providers, hooks and components. We also talk about daisyUI, the short, semantic classes such as btn
, menu
, tab
, etc., and their variants it provides and how they facilitate rapid building of React components using a growing library of prestyled daisyUI templates.
We then initialize a Refine app, and integrate and configure daisyUI. Afterwards, we move on to building the features of the admin panel.
We first build the dashboard page where we present stats for relevant KPIs in cards, charts and a table. We use the React-based Recharts library for plotting our data.
In the later half of the post, we add CRUD pages for products
and categories
resources. We define the resources
prop on <Refine />
component, resource action paths, and their route definitions. CRUD actions covered for both resources are list
, create
, show
, update
and delete
. We then make use of Refine hooks such as useTable()
and useForm()
for entering, fetching and presenting data from the API. We build the UI with predefined daisyUI templates for buttons, menus, tabs, stats, etc.
Towards the end, we see how to customize the layout of a Refine app. We replace the default leftside navigation to adopt a top navbar by leveraging useMenu()
, useNavigation()
and useBreadcrumb()
hooks.
What is Refine?
Refine is a powerful React framework for building Enterprise web applications. It is particularly focused on creating data-heavy apps like dashboards, admin panels and internal tools. It comes with a core headless package that provides different sets of hooks and components for dealing with concerns like data fetching, authentication, authorization, etc. It also has supplmentary packages which enable rapid development of React applications by integrating with various backend services like Airtable, Supabase and Strapi as well as UI frameworks like Ant Design, Material UI, Chakra UI and Mantine.
Architecture
Refine separates app concerns such as data fetching, authentication, access control, etc., into layers of React contexts each backed by a provider object, a set of corresponding hooks as well as relevant components. For example, the data layer represents a context dependent on a dataProvider
object with a set of methods for handling CRUD actions. The data layer is accessed with a set of data hooks that help invoke the CRUD methods from UI components.
This means, we would have all CRUD related methods such as getList()
, create()
, show()
, update()
and delete()
inside a dataProvider
object and we are able to access them from a UI component using useList()
, useCreate()
, etc., data hooks. The data hooks, in turn, make use of React Query for data fetching, caching state management and error handling.
The Refine data hooks mentioned above are basically core hooks. Higher level hooks which are built top of these hooks exist, such as the useTable()
hook provided by @refinedev/react-table
support package that integrates React Table with Refine core. Higher level hooks adds additional features that increase development efficiency. For example, the useList()
hook is employed by the useTable()
hook that helps present data in a table using all the features of React Table. Similarly, the useCreate()
core data hook is utilized inside the useForm()
high level hook provided by the @refinedev/react-hook-form
package which augments form related CRUD actions with React Hook Form.
Resource Definitions
Refine's resource definitions are specified inside the resources
object. The resources
object is passed to the resources
prop of the <Refine />
component. Resource definitions, in combination with route definitions, set up a Refine app's nav menu items, their navigation URLs, as well as breadcrumbs, and help infer the default resource name of a CRUD page along a route.
Routing
Routing in Refine is supported by the react-router-dom
package. Refinev4
supports explicit routing by delegating everything related to routing to the React Router APIs.
Inferencer
Refine's Inferencer is a powerful tool for quickly scaffolding CRUD pages and automatically generating code for a resource page. The Inferencer works by first polling a particular API endpoint to get the shape of the data and then placing all the hooks and UI elements necessary to fetch and present the data on a page.
UI Framework Integration
Refine's core package is designed to be "headless" which gives the freedom to integrate it with any UI component library or framework.
What is daisyUI?
daisyUI is an open source UI library built on top of TailwindCSS. It provides short, component-oriented, semantic classes composed from regular, longer Tailwind class strings that typically contribute to clumsy markup in an application. daisyUI hosts a growing collection of pre-styled templates for components like buttons, menus, and tabs with responsive, size, shape, and color variants.
Composing responsive, color, size, and shape variant classes manually with the @apply
directive is practically inefficient with plain TailwindCSS classes. daisyUI does this out-of-the-box. On top of that, these component styles can be overridden or extended with the usual TailwindCSS utilities. As a result, daisyUI offers the convenience of using smaller class names, a smaller CSS file size, configurable number of variants, and greater customization without compromising much of the code quality.
Feel free to check out the daisyUI documentation to learn more.
Initialize a Refine App
For this app, we are going to start with Refine's headless core, using create refine-app
to scaffold our pages and generate the initial page code. We will then make necessary logic and UI adjustments and then apply daisyUI classes to our components.
So, let's get started with initializing the Refine app first.
We'll create a local repository by using the create refine-app
CLI-based app scaffolder. Run the following npm
command from the directory of your choice to interactively initialize the project.
npm create refine-app@latest refine-daisyui
Select the following options when prompted:
✔ Choose a project template · refine-react
✔ What would you like to name your project?: · refine-daisyui
✔ Choose your backend service to connect: · REST API
✔ Do you want to use a UI Framework?: · Headless
✔ Do you want to add example pages?: · no
✔ Do you need i18n (Internationalization) support?: · No
✔ Choose a package manager: · npm
✔ Would you mind sending us your choices so that we can improve create refine-app? · yes
Take a note of the Headless
choice. We are asking for Refine core package with plain JSX markup.
After completing the app initialization, let's navigate to the project folder and start our app with:
npm run dev
We should be greeted with the app's Welcome page.
Chores
We'll replace the Fake REST API with Fine Foods URL in the dataProvider
prop. Update the App.tsx
file to the following:
import { ErrorComponent, GitHubBanner, Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import routerBindings, {
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import dataProvider from "@refinedev/simple-rest";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import "./App.css";
import { Layout } from "./components/layout";
function App() {
return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
routerProvider={routerBindings}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
<Route
element={
<Layout>
<Outlet />
</Layout>
}
>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</RefineKbarProvider>
</BrowserRouter>
);
}
export default App;
With these changes, we'll start fresh towards building the dashboard page first and then later move on to the CRUD pages for products
and categories
resources. At this point, we don't have any resources or their pages.
Notice, we are now using the Fine Foods REST API in the dataProvider
prop of <Refine />
.
The Fine Foods API is an example of the REST API hosted by Refine with a collection of end points. In this app, we will be querying the /dailyRevenue
, /dailyOrders
, /newCustomers
and /orders
endpoints for fetching data for our dashboard page. Later on, we'll also be accessing its /products
and /categories
endpoints for our resource pages.
daisyUI Installation
We are using daisyUI as our UI library. In order to integrate daisyUI into our Refine app, we have to first perform a Vite installation of TailwindCSS, its dependencies, and set up their configurations.
Go ahead an follow the below steps to first add TailwindCSS, PostCSS and Autoprefixer packages and then initialize tailwind.config.js
:
Install TailwindCSS and related packages
- Run the following commands:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
- Inside
tailwind.config.js
file, add file paths for scanning and applying TailwindCSS classes:
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
- Modify the
App.css
to add TailwindCSS directives. It is important that we add them towards the top before any Tailwind style declarations. Copy over the CSS below:
Show App.css styles
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0px;
}
/* TailwindCSS layers towards the top */
@tailwind base;
@tailwind components;
@tailwind utilities;
.layout {
display: flex;
gap: 16px;
}
@media screen and (max-width: 751px) {
.layout {
display: block;
}
}
.layout .content {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.breadcrumb {
display: flex;
gap: 24px;
list-style-type: "/ ";
padding: 8px 16px;
border-bottom: 1px solid lightgray;
}
.breadcrumb a {
color: blue;
text-decoration: none;
}
.menu {
flex-shrink: 0;
padding: 8px 16px;
border-right: 1px solid lightgray;
}
.menu a {
color: black;
}
.menu .active {
font-weight: bold;
}
@media screen and (max-width: 751px) {
.menu {
border-right: none;
border-bottom: 1px solid lightgray;
}
}
.menu ul {
padding-left: 16px;
}
.page-container {
@apply mx-auto my-2 rounded border bg-slate-50 px-4 py-2 drop-shadow-md;
}
.page-title {
@apply text-xl font-bold;
}
.page-header {
@apply mb-6 flex items-center justify-between py-4;
}
We'll be using the custom classes in this App.css
, so feel free to copy it over.
If you need a hand with TailwindCSS installation, please follow this guide for installing TailwindCSS with Vite
Install and setup daisyUI
With TailwindCSS set up properly, it's now turn to install and configure daisyUI.
- Install daisyUI with the following command:
npm install -D daisyui@latest
- And then add daisyUI as a plugin to
tailwind.config.js
. Extend the daisyUIlight
theme and update theprimary
color:
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
daisyui: {
themes: [
{
light: {
...require("daisyui/src/theming/themes")["[data-theme=light]"],
primary: "#0d89ec",
},
},
],
},
theme: {
extend: {},
},
plugins: [require("daisyui")],
};
More details on customizing a daisyUI theme is available on the docs here
After these changes, with the server running, TailwindCSS watches for the use of daisyUI and TailwindCSS classes, and automatically compiles updated styles.
Other Packages
We have to install Refine's support packages for React Table and React Hook Form. We are using Tailwind Heroicons for our icons, the Day.js library for time calculations and Recharts library to plot our charts for KPI data. So, run the following and we are good to go:
npm install @refinedev/react-table @refinedev/react-hook-form @heroicons/react dayjs recharts
Interfaces
We'll be using the following interfaces throughout the app. So, feel free to copy and paste them over to src/interfaces/index.ts
or a similar location.
src/interfaces/index.ts
export interface IOrder {
id: number;
user: IUser;
createdAt: string;
status: IOrderStatus;
address: IAddress;
amount: number;
}
export interface IUser {
id: number;
fullName: string;
gender: string;
gsm: string;
createdAt: string;
addresses: IAddress[];
}
export interface IOrderStatus {
id: number;
text: "Pending" | "Ready" | "On The Way" | "Delivered" | "Cancelled";
}
export interface IAddress {
text: string;
coordinate: [string, string];
}
export interface IChartDatum {
date: string;
value: string;
}
export interface IChart {
data: IChartDatum[];
total: number;
trend: number;
}
export interface IProduct {
id: number;
name: string;
isActive: boolean;
description: string;
createdAt: string;
price: number;
category: ICategory;
stock: number;
}
export interface ICategory {
id: number;
title: string;
isActive: boolean;
}
export type TTab = {
id: number;
label: string;
content: JSX.Element;
};
Building the Dashboard
Now that we have all the set up and packages ready, it's time for us to start building the dashboard page. The dashboard page will be displayed at the index route and contain KPI stats, charts and a table of data.
The <Dashboard />
Component
We'll add a /dashboard
directory under src/pages
and add the <Dashboard />
component to the index.tsx
file under it.
Initially, it will return a dummy hero component. We'll update it in the coming sections.
import React from "react";
export const Dashboard: React.FC = () => {
return (
<div className="hero bg-base-200 min-h-screen">
<div className="hero-content text-center">
<div className="max-w-md">
<h1 className="text-5xl font-bold">Hello there...</h1>
<p className="py-6">
You're here. A deva just as dashing and daisyuing - as yourself
refined
</p>
<button className="btn btn-primary">Buckle Up</button>
</div>
</div>
</div>
);
};
We'll display the dashboard page at the /dashboard
path and make it the index route. So, let's add the necessary resource and routes to the <Refine />
component in App.tsx
.
Update the App.tsx
as below:
Show updated App.tsx file
import { ErrorComponent, GitHubBanner, Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import routerBindings, {
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import dataProvider from "@refinedev/simple-rest";
import {
BrowserRouter,
Navigate,
Outlet,
Route,
Routes,
} from "react-router-dom";
import "./App.css";
import { Layout } from "./components/layout";
import { Dashboard } from "./pages/dashboard";
function App() {
return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
<Route
element={
<Layout>
<Outlet />
</Layout>
}
>
<Route index element={<Navigate to="/dashboard" />} />
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</RefineKbarProvider>
</BrowserRouter>
);
}
export default App;
We have updated our imports and passed the resources
prop to <Refine />
. We have defined a dashboard
resource with only one page: the list
. In the route definitions, as children to <Routes />
, we have assigned the <Dashboard />
page to the /dashboard
route and set it as the index route.
With these additions and changes, when we navigate to /
or /dashboard
, we should be able to see the dashboard page. It looks somewhat dashing like this:
The <Stats />
Component
Let's now focus on implementing the features of the dashboard. Inside it, we'll have a <Stats />
component that takes in KPI data and returns a <KpiCard />
component for each. We'll create the <Stats />
component inside src/components/dashboard
. And use the following code:
src/components/dashboard/Stats.tsx
import React from "react";
import { KpiCard } from "./KpiCard";
import { IChartDatum } from "../../interfaces";
import {
CurrencyDollarIcon,
ShoppingCartIcon,
UserGroupIcon,
} from "@heroicons/react/24/outline";
import { GetListResponse } from "@refinedev/core";
type TStats = {
dailyRevenue?: GetListResponse<IChartDatum>;
dailyOrders?: GetListResponse<IChartDatum>;
newCustomers?: GetListResponse<IChartDatum>;
};
const Stats = ({ dailyRevenue, dailyOrders, newCustomers }: TStats) => {
return (
<div className="mx-auto mb-4 flex w-full flex-col items-stretch justify-center drop-shadow-md md:flex-row md:justify-between">
<div className="mx-auto w-full md:mr-2 md:flex-1">
<KpiCard
title="Weekly Revenue"
data={dailyRevenue}
formatTotal={(value: number | string) => `$ ${value}`}
icon={<CurrencyDollarIcon className="h-32 w-32" />}
colors={{
stroke: "rgb(54, 162, 235)",
fill: "rgba(54, 162, 235, 0.2)",
}}
/>
</div>
<div className="mx-auto w-full md:flex-1">
<KpiCard
title="Weekly Orders"
data={dailyOrders}
icon={<ShoppingCartIcon className="h-32 w-32" />}
colors={{
stroke: "rgb(255, 159, 64)",
fill: "rgba(255, 159, 64, 0.2)",
}}
/>
</div>
<div className="mx-auto w-full md:ml-2 md:flex-1">
<KpiCard
title="New Customers"
data={newCustomers}
icon={<UserGroupIcon className="h-32 w-32" />}
colors={{
stroke: "rgb(76, 175, 80)",
fill: "rgba(76, 175, 80, 0.2)",
}}
/>
</div>
</div>
);
};
export default Stats;
The <Stats />
relays and displays KPI data inside the <KpiCard />
component, so let's work on that now.
<KpiCard />
Component
Let's create the <KpiCard />
component inside src/components/dashboard
directory. The <KpiCard />
represents an individual stat item. It takes in a number of props and displays the KPI data with an icon. It looks like this:
src/components/dashboard/KpiCard.tsx
import React from "react";
type TKpiCardProps = {
title: string;
data: any;
icon: JSX.Element;
colors: {
stroke: string;
fill: string;
};
formatTotal?: (value: number | string) => typeof value;
};
export const KpiCard = ({
title,
data,
icon,
colors,
formatTotal = (value) => value,
}: TKpiCardProps) => {
const total = data?.data?.total;
const trend = data?.data?.trend;
const calc = Math.round((trend / total) * 100);
const percent = total > trend ? `+ ${calc}%` : `- ${calc}%`;
const textColor = total > trend ? "seagreen" : "crimson";
return (
<div
className="stat my-2 flex-1 rounded border-l-4 bg-zinc-50 py-4"
style={{ borderColor: colors?.stroke }}
>
<div
className="stat-figure text-secondary"
style={{ color: colors?.fill }}
>
{icon}
</div>
<div className="stat-title text-l">{title}</div>
<div className="stat-value" style={{ color: colors?.stroke }}>
{formatTotal(total ?? "...")}
</div>
<div className="stat-desc my-2">
<span className="text-l mx-1 font-bold" style={{ color: textColor }}>
{percent}
</span>
since last week
</div>
</div>
);
};
Note that we have started using daisyUI classes in the <KpiCard />
component. stat
, stat-figure
, stat-title
, stat-value
, stat-desc
are classes provided by the daisyUI Stats
template.
With the <Stats />
and <KpiCard />
components completed, we are ready to update the <Dashboard />
component. Inside it, we will query a number of Fine Foods end points to gather revenue, orders and customers data and then transform them. We will pass the transformed data to child components, one by one as they get built.
For now, we'll import and display the <Stats />
component and pass it three sets of KPI data in order to display the KPI cards at the top of the dashboard page.
Replace the code inside the <Dashboard />
component with the following:
src/pages/dashboard/index.tsx
import React, { useMemo } from "react";
import { CrudFilter, useList } from "@refinedev/core";
import dayjs from "dayjs";
import Stats from "../../components/dashboard/Stats";
import { IChartDatum, TTab } from "../../interfaces";
const filters: CrudFilter[] = [
{
field: "start",
operator: "eq",
value: dayjs()?.subtract(7, "days")?.startOf("day"),
},
{
field: "end",
operator: "eq",
value: dayjs().startOf("day"),
},
];
export const Dashboard: React.FC = () => {
const { data: dailyRevenue } = useList<IChartDatum>({
resource: "dailyRevenue",
filters,
});
const { data: dailyOrders } = useList<IChartDatum>({
resource: "dailyOrders",
filters,
});
const { data: newCustomers } = useList<IChartDatum>({
resource: "newCustomers",
filters,
});
return (
<>
<Stats
dailyRevenue={dailyRevenue}
dailyOrders={dailyOrders}
newCustomers={newCustomers}
/>
</>
);
};
Notice we are fetching data from three Fine Foods API end points: /dailyRevenue
, /dailyOrders
and /newCustomers
. We are fetching them with the useList()
Refine core hook. We are querying them as resources although in our Refine admin panel app they are not. The filters
object is used to get the past 7 days' data.
You can find more details in the Refine useList()
docs here.
With these changes, our dashboard page has three KPI cards displayed at the top:
Tab Components
Now we want to display three charts inside a tabs panel below the KPI cards. So, we'll create the <TabItem />
, <TabPanel />
and <TabView />
components inside the src/components/dashboard
directory. We'll also create two chart components, a <ResponsiveAreaChart />
and a <ResponsiveBarChart />
to plot KPI data. Once all components are ready, we'll add the <TabView />
component to <Dashboard />
.
The <TabView />
component will house the other two tab components, so in order to avoid linter and browser errors, we'll start with the children.
<TabItem />
Component
We need to have the <TabItem />
button for accessing a tab panel. So, create the <TabItem />
component as below:
src/components/dashboard/TabItem.tsx
import React from "react";
type TTabItem = {
label: string;
isActive: Boolean;
clickHandler: () => void;
};
export const TabItem = ({ label, isActive, clickHandler }: TTabItem) => {
return (
<a
className={`text-l tab font-bold tab-bordered${
isActive ? " tab-active" : ""
}`}
onClick={clickHandler}
>
{label}
</a>
);
};
<TabPanel />
Component
The <TabPanel />
will contain a chart which can be accessed by clicking on a <TabItem />
. Let's go ahead and create the <TabPanel />
component with the following code:
src/components/dashboard/TabPanel.tsx
import React from "react";
type TTabPanelProps = {
isActive: Boolean;
children: JSX.Element;
};
export const TabPanel = ({ isActive, children }: TTabPanelProps) => {
return isActive ? <div className="mx-auto py-6">{children}</div> : null;
};
<TabView />
Component
The <TabView />
component will contain the tab view logic and state. It will accept the tabs
object and display a <TabItem />
and <TabPanel >
as children for each item in the tabs
object. Let's create the <TabView />
component with the code below:
src/components/dashboard/TabView.tsx
import React, { useState } from "react";
import { TabItem } from "./TabItem";
import { TabPanel } from "./TabPanel";
import { TTab } from "../../interfaces";
type TTabViewProps = {
tabs: TTab[];
};
export const TabView = ({ tabs }: TTabViewProps) => {
const [activeTab, setActiveTab] = useState(0);
return (
<div className="mx-auto rounded-lg border bg-slate-50 py-4 drop-shadow-md">
<div className="tabs">
{tabs?.map((tab: TTab, index: number) => (
<TabItem
key={tab?.id}
label={tab?.label}
isActive={index === activeTab}
clickHandler={() => setActiveTab(index)}
/>
))}
</div>
<div className="mx-auto">
{tabs?.map((tab: TTab, index: number) => (
<TabPanel key={tab?.id} isActive={index === activeTab}>
{tab?.content}
</TabPanel>
))}
</div>
</div>
);
};
Recharts Plots
With the tab components ready, we need to create three charts to be displayed inside <TabView />
by mapping through the tabs
object. We want to implement them using Recharts. One <ResponsiveAreaChart />
and one <ResponsiveBarChart />
.
For plotting the data, <ResponsiveAreaChart />
will use the <AreaChart />
APIs of the Recharts library and <ResponsiveBarChart />
will use the <BarChart />
APIs. They both will use <ResponsiveContainer />
for responsiveness.
You can find all the details in the Rechats <AreaChart />
, <BarChart />
and <ResponsiveContainer />
documentations if you need to.
We'll build the charts inside the src/components/dashboard
directory.
Area Chart
Create the <ResponsiveAreaChart />
component with the code below:
src/components/dashboard/ResponsiveAreaChart.tsx
import React from "react";
import {
ResponsiveContainer,
AreaChart,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
Area,
} from "recharts";
import { ChartTooltip } from "../../components/dashboard/ChartTooltip";
import { IChartDatum } from "../../interfaces";
type TResponsiveAreaChartProps = {
kpi: string;
data: IChartDatum[];
colors: {
stroke: string;
fill: string;
};
};
export const ResponsiveAreaChart = ({
kpi,
data,
colors,
}: TResponsiveAreaChartProps) => {
return (
<ResponsiveContainer height={400}>
<AreaChart
data={data}
height={400}
margin={{
top: 10,
right: 30,
left: 0,
bottom: 0,
}}
>
<CartesianGrid strokeDasharray="0 0 0" />
<XAxis
dataKey="date"
tickCount={data?.length ?? 0}
tick={{
stroke: "light-grey",
strokeWidth: 0.5,
fontSize: "12px",
}}
/>
<YAxis
tickCount={13}
tick={{
stroke: "light-grey",
strokeWidth: 0.5,
fontSize: "12px",
}}
interval="preserveStartEnd"
domain={[0, "dataMax + 10"]}
/>
<Tooltip
content={<ChartTooltip kpi={kpi} colors={colors} />}
wrapperStyle={{
backgroundColor: "rgba(0, 0, 0, 0.7)",
border: "0 solid #000",
borderRadius: "10px",
}}
/>
<Area
type="monotone"
dataKey="value"
stroke={colors?.stroke}
strokeWidth={3}
fill={colors?.fill}
dot={{
stroke: colors?.stroke,
strokeWidth: 3,
}}
/>
</AreaChart>
</ResponsiveContainer>
);
};
Inside <ResponsiveAreaChart />
we are receiving the data
along with other props and relaying it to <AreaChart />
with its data={data}
prop. We are using <XAxis />
, <YAxis />
for ticks and axes labels. We are drawing the area and monotonic line with <Area />
component and its props. <CartesianGrid />
draws a grid in the background. We also have a custom tooltip shown inside <ToolTip />
.
Bar Chart
In a similar way, create the <ResponsiveBarChart />
component with the below code:
src/components/dashboard/ResponsiveBarChart.tsx
import React from "react";
import {
ResponsiveContainer,
BarChart,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
Bar,
} from "recharts";
import { ChartTooltip } from "../../components/dashboard/ChartTooltip";
import { IChartDatum } from "../../interfaces";
type TResponsiveBarChartProps = {
kpi: string;
data: IChartDatum[];
colors: {
stroke: string;
fill: string;
};
};
export const ResponsiveBarChart = ({
kpi,
data,
colors,
}: TResponsiveBarChartProps) => {
return (
<ResponsiveContainer height={400}>
<BarChart
data={data}
width={1200}
height={400}
margin={{
top: 10,
right: 30,
left: 0,
bottom: 0,
}}
>
<CartesianGrid strokeDasharray="0 0" />
<XAxis
dataKey="date"
tickCount={data?.length ?? 0}
tick={{
stroke: "light-grey",
strokeWidth: 0.5,
fontSize: "12px",
}}
/>
<YAxis
domain={[0, "dataMax"]}
tickCount={13}
tick={{
stroke: "light-grey",
strokeWidth: 0.5,
fontSize: "12px",
}}
/>
<Tooltip
content={<ChartTooltip colors={colors} kpi={kpi} />}
wrapperStyle={{
backgroundColor: "rgba(0, 0, 0, 0.7)",
border: "0 solid #000",
borderRadius: "10px",
}}
/>
<Bar
type="monotone"
dataKey="value"
stroke="rgb(255, 207, 159)"
strokeWidth={1}
fill="rgba(255, 207, 159, 0.7)"
/>
</BarChart>
</ResponsiveContainer>
);
};
<ResponsiveBarChart />
does the same thing as <ResponsiveAreaChart />
, except that it draws a bar chart with Recharts <BarChart />
component.
Custom Tooltip
The charts above use a custom tooltip, the <ChartTooltip />
component, to show data point details according to the mouse position.
Let's create the <ChartTooltip />
component with the following code:
src/components/dashboard/ChartTooltip.tsx
import React from "react";
export const ChartTooltip = ({
active,
payload,
label,
coordinate,
colors,
kpi,
}: any) => {
if (active && payload && payload.length) {
const dataPoint = payload[0].payload;
const tooltipStyle = {
left: coordinate.x, // Adjust positioning
top: coordinate.y, // Adjust positioning
};
return (
<div
className="flex flex-col items-start justify-center rounded-lg border border-black p-1 text-zinc-50"
style={tooltipStyle}
>
<div
style={{
position: "absolute",
width: "0",
height: "0",
borderTop: "10px solid transparent",
borderBottom: "10px solid transparent",
borderRight: "10px solid rgba(0, 0, 0, 0.7)",
left: "-10px",
}}
/>
<p className="flex text-xs font-semibold">{label}</p>
<p className="text-xs">
<span
className="mr-1"
style={{
width: "0.5px",
height: "0.5px",
border: `1px solid ${colors?.stroke}`,
backgroundColor: colors?.fill,
}}
>
</span>
{`${kpi}: ${dataPoint.value}`}
</p>
</div>
);
}
return null;
};
Now we have all the components for displaying <TabView />
ready. So, we'll import and display it inside the dashboard below <Stats />
. Let's update the <Dashboard />
component with the below code:
src/pages/dashboard/index.tsx
import React, { useMemo } from "react";
import { CrudFilter, useList } from "@refinedev/core";
import dayjs from "dayjs";
import Stats from "../../components/dashboard/Stats";
import { ResponsiveAreaChart } from "../../components/dashboard/ResponsiveAreaChart";
import { ResponsiveBarChart } from "../../components/dashboard/ResponsiveBarChart";
import { TabView } from "../../components/dashboard/TabView";
import { IChartDatum, TTab } from "../../interfaces";
const filters: CrudFilter[] = [
{
field: "start",
operator: "eq",
value: dayjs()?.subtract(7, "days")?.startOf("day"),
},
{
field: "end",
operator: "eq",
value: dayjs().startOf("day"),
},
];
export const Dashboard: React.FC = () => {
const { data: dailyRevenue } = useList<IChartDatum>({
resource: "dailyRevenue",
filters,
});
const { data: dailyOrders } = useList<IChartDatum>({
resource: "dailyOrders",
filters,
});
const { data: newCustomers } = useList<IChartDatum>({
resource: "newCustomers",
filters,
});
const useMemoizedChartData = (d: any) => {
return useMemo(() => {
return d?.data?.data?.map((item: IChartDatum) => ({
date: new Intl.DateTimeFormat("en-US", {
month: "short",
year: "numeric",
day: "numeric",
}).format(new Date(item.date)),
value: item?.value,
}));
}, [d]);
};
const memoizedRevenueData = useMemoizedChartData(dailyRevenue);
const memoizedOrdersData = useMemoizedChartData(dailyOrders);
const memoizedNewCustomersData = useMemoizedChartData(newCustomers);
const tabs: TTab[] = [
{
id: 1,
label: "Daily Revenue",
content: (
<ResponsiveAreaChart
kpi="Daily revenue"
data={memoizedRevenueData}
colors={{
stroke: "rgb(54, 162, 235)",
fill: "rgba(54, 162, 235, 0.2)",
}}
/>
),
},
{
id: 2,
label: "Daily Orders",
content: (
<ResponsiveBarChart
kpi="Daily orders"
data={memoizedOrdersData}
colors={{
stroke: "rgb(255, 159, 64)",
fill: "rgba(255, 159, 64, 0.7)",
}}
/>
),
},
{
id: 3,
label: "New Customers",
content: (
<ResponsiveAreaChart
kpi="New customers"
data={memoizedNewCustomersData}
colors={{
stroke: "rgb(76, 175, 80)",
fill: "rgba(54, 162, 235, 0.2)",
}}
/>
),
},
];
return (
<>
<Stats
dailyRevenue={dailyRevenue}
dailyOrders={dailyOrders}
newCustomers={newCustomers}
/>
<TabView tabs={tabs} />
</>
);
};
Notice we are defining a useMemoizedChartData()
hook to transform the fetched data and memoize them to make them chart ready. We are then setting the tabs
object from the data, the charts, and other properties. We eventually pass the tabs
object to the <TabView />
component.
With these changes, our dashboard page displays a panel of charts accessible from a top tabbed menu:
<RecentSales />
Component
Lastly, we want to display recent sales data in a table below the charts. We have the <RecentSales />
component that lists recent orders in a table with filtering and sorting features. Let's create the <RecentSales />
component with the following code:
src/components/dashboard/RecentSales.tsx
import React, { useMemo, useRef } from "react";
import { getDefaultFilter } from "@refinedev/core";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import {
FunnelIcon,
BarsArrowDownIcon,
BarsArrowUpIcon,
} from "@heroicons/react/24/outline";
export const RecentSales = () => {
const filterForm: any = useRef(null);
const columns = useMemo<ColumnDef<any>[]>(
() => [
{
id: "id",
accessorKey: "id",
header: "Id",
},
{
id: "amount",
accessorKey: "amount",
header: "Amount",
cell: function render({ getValue }) {
const amountCur = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(getValue() as number);
return <div>{amountCur}</div>;
},
},
{
id: "orderedBy",
accessorKey: "user.fullName",
header: "Ordered By",
},
{
id: "gender",
accessorKey: "user.gender",
header: "Gender",
},
{
id: "tel",
accessorKey: "user.gsm",
enableSorting: false,
header: "Tel",
},
{
id: "deliveryAddress",
accessorKey: "address.text",
header: "Delivery Address",
},
{
id: "deliveryStatus",
accessorKey: "status.text",
header: "Delivery Status",
cell: function render({ getValue }) {
type TSaleStatusStyleMap = {
[key: string]: string;
};
const saleStatusStyleMap: TSaleStatusStyleMap = {
Cancelled: "error",
Ready: "primary",
"On The Way": "info",
Pending: "warning",
Delivered: "success",
};
const status = getValue() as string;
const daisyBadgeClasses = () =>
"badge badge-" + saleStatusStyleMap[status];
return <div className={daisyBadgeClasses()}>{status}</div>;
},
},
{
id: "createdAt",
accessorKey: "createdAt",
header: "Created At",
cell: function render({ getValue }) {
const date = new Intl.DateTimeFormat("en-US", {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(getValue() as string));
return <div>{date}</div>;
},
},
],
[],
);
const {
refineCore: { filters, setCurrent, setFilters },
getHeaderGroups,
getRowModel,
} = useTable({
refineCoreProps: {
resource: "orders",
pagination: {
pageSize: 5,
},
},
columns,
});
const header = (
<div className="mx-auto w-full">
<div className="my-2">
<h1 className="page-title text-gray-700">Recent Sales</h1>
</div>
<div className="overflow-x-auto rounded-t-lg border bg-slate-50">
<div className="m-4 flex items-center justify-between">
<button
className="btn btn-outline btn-primary btn-sm font-light normal-case"
onClick={() => {
setCurrent(1);
setFilters([], "replace");
filterForm?.current?.reset();
}}
>
<FunnelIcon className="h-4 w-4" />
Clear
</button>
<div className="flex items-center justify-end">
<form ref={filterForm}>
<input
className="input input-bordered input-sm"
type="search"
value={getDefaultFilter("q", filters)}
onChange={(e) => {
setCurrent(1);
setFilters([
{
field: "q",
value: e.target.value,
operator: "contains",
},
]);
}}
placeholder="Search with keywords"
/>
</form>
</div>
</div>
</div>
</div>
);
return (
<div className="mx-auto my-8 w-full drop-shadow-md">
{header}
<div className="overflow-x-auto rounded-b-lg border bg-slate-50 p-4">
<table className="table-zebra table border-t">
<thead className="bg-slate-200">
{getHeaderGroups()?.map((headerGroup) => (
<tr key={headerGroup?.id}>
{headerGroup?.headers?.map((header) => (
<th
className="hover:bg-slate-300"
key={header?.id}
onClick={header?.column?.getToggleSortingHandler()}
>
<div className="flex items-center justify-start">
{!header?.isPlaceholder &&
flexRender(
header?.column?.columnDef?.header,
header?.getContext(),
)}
{{
asc: <BarsArrowUpIcon className="h-4 w-4" />,
desc: <BarsArrowDownIcon className="h-4 w-4" />,
}[header?.column?.getIsSorted() as string] ?? null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody>
{getRowModel()?.rows?.map((row) => (
<tr key={row?.id}>
{row?.getVisibleCells()?.map((cell) => (
<td key={cell?.id}>
{flexRender(
cell?.column?.columnDef?.cell,
cell?.getContext(),
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
In the <RecentSales />
component, we are using a useTable()
hook, which is a high level hook provided by Refine's React Table supported @refinedev/react-table
package. It queries the /orders
endpoint and implements a table with filtering and sorting features.
We'll come to the details of useTable()
when we create list
pages for products
and categories
resources.
With the <RecentSales />
component ready, let's import it and display it inside <Dashboard />
. Update it with the following code:
Show Dashboard code
import React, { useMemo } from "react";
import { CrudFilter, useList } from "@refinedev/core";
import dayjs from "dayjs";
import Stats from "../../components/dashboard/Stats";
import { ResponsiveAreaChart } from "../../components/dashboard/ResponsiveAreaChart";
import { ResponsiveBarChart } from "../../components/dashboard/ResponsiveBarChart";
import { TabView } from "../../components/dashboard/TabView";
import { RecentSales } from "../../components/dashboard/RecentSales";
import { IChartDatum, TTab } from "../../interfaces";
const filters: CrudFilter[] = [
{
field: "start",
operator: "eq",
value: dayjs()?.subtract(7, "days")?.startOf("day"),
},
{
field: "end",
operator: "eq",
value: dayjs().startOf("day"),
},
];
export const Dashboard: React.FC = () => {
const { data: dailyRevenue } = useList<IChartDatum>({
resource: "dailyRevenue",
filters,
});
const { data: dailyOrders } = useList<IChartDatum>({
resource: "dailyOrders",
filters,
});
const { data: newCustomers } = useList<IChartDatum>({
resource: "newCustomers",
filters,
});
const useMemoizedChartData = (d: any) => {
return useMemo(() => {
return d?.data?.data?.map((item: IChartDatum) => ({
date: new Intl.DateTimeFormat("en-US", {
month: "short",
year: "numeric",
day: "numeric",
}).format(new Date(item.date)),
value: item?.value,
}));
}, [d]);
};
const memoizedRevenueData = useMemoizedChartData(dailyRevenue);
const memoizedOrdersData = useMemoizedChartData(dailyOrders);
const memoizedNewCustomersData = useMemoizedChartData(newCustomers);
const tabs: TTab[] = [
{
id: 1,
label: "Daily Revenue",
content: (
<ResponsiveAreaChart
kpi="Daily revenue"
data={memoizedRevenueData}
colors={{
stroke: "rgb(54, 162, 235)",
fill: "rgba(54, 162, 235, 0.2)",
}}
/>
),
},
{
id: 2,
label: "Daily Orders",
content: (
<ResponsiveBarChart
kpi="Daily orders"
data={memoizedOrdersData}
colors={{
stroke: "rgb(255, 159, 64)",
fill: "rgba(255, 159, 64, 0.7)",
}}
/>
),
},
{
id: 3,
label: "New Customers",
content: (
<ResponsiveAreaChart
kpi="New customers"
data={memoizedNewCustomersData}
colors={{
stroke: "rgb(76, 175, 80)",
fill: "rgba(54, 162, 235, 0.2)",
}}
/>
),
},
];
return (
<>
<Stats
dailyRevenue={dailyRevenue}
dailyOrders={dailyOrders}
newCustomers={newCustomers}
/>
<TabView tabs={tabs} />
<RecentSales />
</>
);
};
With all these updates, we have completed implementing the dashboard page. It now looks like this:
Adding CRUD Pages
Having completed the dashboard page above, in this section, we'll add CRUD pages for products
and categories
resources. We'll start implementing the pages for the products
resource first.
Product Pages
We want list
, create
, edit
and show
pages for the products
. Since we are using Refine's headless core without any supported UI library, it helps if we use the Inferencer to generate the pages for us. We'll leverage the power of Refine's <HeadlessInferencer />
component in the CRUD pages.
There are two steps to getting the Inferencer generated page code:
Scaffold the CRUD pages by running the Inferencer to implement all the
products
pages with<HeadlessInferencer />
.<HeadlessInferencer />
then generates the actual codes for us that we can get from the page in the browser.Navigate along the
/products
path to an action route in your browser and get the code from the page by clicking on theShow the auto-generated code
button. It is graciously provided to us by Refine 😄
We'll scaffold the pages first with the following Inferencer command:
npm run refine create-resource product
This produces <ProductList />
, <ProductCreate />
, <ProductEdit />
and <ProductShow />
pages. You can find them under a pluralized /products
directory inside src/pages
.
This also automatically generates the resource definition for products
and adds it to the resources
array in App.tsx
. Resource paths for list
, create
, show
and edit
are specified:
resources={[
{
name: "products",
list: "/product",
create: "/product/create",
edit: "/product/edit/:id",
show: "/product/show/:id",
}
]}
We have to manually add the route definitions for each action individually to the App.tsx
file. So, the updated App.tsx
should have the following changes with resources
and routes definitions added for products
:
Show latest App.tsx code
import { ErrorComponent, GitHubBanner, Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import routerBindings, {
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import dataProvider from "@refinedev/simple-rest";
import {
BrowserRouter,
Navigate,
Outlet,
Route,
Routes,
} from "react-router-dom";
import "./App.css";
import { Layout } from "./components/layout";
import { Dashboard } from "./pages/dashboard";
import {
ProductList,
ProductCreate,
ProductEdit,
ProductShow,
} from "./pages/products";
function App() {
return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
{
name: "products",
list: "/products",
create: "/products/create",
edit: "/products/edit/:id",
show: "/products/show/:id",
meta: {
canDelete: true,
},
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
<Route
element={
<Layout>
<Outlet />
</Layout>
}
>
<Route index element={<Navigate to="/dashboard" />} />
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>
<Route path="/products">
<Route index element={<ProductList />} />
<Route path="create" element={<ProductCreate />} />
<Route path="edit/:id" element={<ProductEdit />} />
<Route path="show/:id" element={<ProductShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</RefineKbarProvider>
</BrowserRouter>
);
}
export default App;
Notice towards the top that we have imported the scaffolded components (the ones with <HeadlessInferencer />
component, not yet the actual page code). Towards the end, we assigned them to /products
paths for defining the routes. Routine React Router DOM stuff.
With the above changes, we have added possible actions and their routes for the products
resource. We defined the routes and pages for list
, create
, edit
and show
actions and have enabled delete
action as well. The page mapping for each route are handled with the <Route />
component.
Refine maps resource paths to page components via route definitions, and using the map infers the resource name of a page at the current URL of the browser. That way, hooks like useTable()
and useNavigation()
, and Inferencer components like <HeadlessInferencer />
are always able to infer the default resource name from inside a resource page.
You can find more information about resources and routing on the Refine documentation.
Now when we navigate along the /products
paths, we can see some clumsy looking pages in need of proper styling. So, we're interested in getting their code and modifying them according to our needs. We are going to do that one by one in the following sections.
<ProductList />
Page
To begin with, the scaffolded <ProductList />
component looks like this:
import { HeadlessInferencer } from "@refinedev/inferencer/headless";
export const ProductList = () => {
return <HeadlessInferencer />;
};
The <HeadlessInferencer />
infers the resource name and action path from the resource and routes definitions based on the current URL of the browser. It then polls the Fine Foods /products
end point to figure out the data shape, and based on the shape, it uses the necessary refine-React Table APIs and JSX markup to present the fetched data in a table.
We'll grab the generated code from the page modal by clicking on the Show the auto-generated code
button. It is pretty diligent and should look something like this:
Show auto-generated ProductList code
import React from "react";
import { useNavigation } from "@refinedev/core";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
export const ProductList = () => {
const columns = React.useMemo<ColumnDef<any>[]>(
() => [
{
id: "id",
accessorKey: "id",
header: "Id",
},
{
id: "name",
accessorKey: "name",
header: "Name",
},
{
id: "isActive",
accessorKey: "isActive",
header: "Is Active",
cell: function render({ getValue }) {
return getValue<any>() ? "yes" : "no";
},
},
{
id: "description",
accessorKey: "description",
header: "Description",
},
{
id: "images",
accessorKey: "images",
header: "Images",
cell: function render({ getValue }) {
return (
<ul>
{getValue<any[]>()?.map((item, index) => (
<li key={index}>{item?.url}</li>
))}
</ul>
);
},
},
{
id: "createdAt",
accessorKey: "createdAt",
header: "Created At",
cell: function render({ getValue }) {
return new Date(getValue<any>()).toLocaleString(undefined, {
timeZone: "UTC",
});
},
},
{
id: "price",
accessorKey: "price",
header: "Price",
},
{
id: "category",
accessorKey: "category.title",
header: "Category",
},
{
id: "actions",
accessorKey: "id",
header: "Actions",
cell: function render({ getValue }) {
return (
<div
style={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
gap: "4px",
}}
>
<button
onClick={() => {
show("products", getValue() as string);
}}
>
Show
</button>
<button
onClick={() => {
edit("products", getValue() as string);
}}
>
Edit
</button>
</div>
);
},
},
],
[],
);
const { edit, show, create } = useNavigation();
const {
getHeaderGroups,
getRowModel,
setOptions,
refineCore: {
tableQuery: { data: tableData },
},
getState,
setPageIndex,
getCanPreviousPage,
getPageCount,
getCanNextPage,
nextPage,
previousPage,
setPageSize,
getColumn,
} = useTable({
columns,
});
setOptions((prev) => ({
...prev,
meta: {
...prev.meta,
},
}));
return (
<div style={{ padding: "16px" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<h1>Products</h1>
<button onClick={() => create("products")}>Create</button>
</div>
<div style={{ maxWidth: "100%", overflowY: "scroll" }}>
<table>
<thead>
{getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{!header.isPlaceholder &&
flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div style={{ marginTop: "12px" }}>
<button
onClick={() => setPageIndex(0)}
disabled={!getCanPreviousPage()}
>
{"<<"}
</button>
<button onClick={() => previousPage()} disabled={!getCanPreviousPage()}>
{"<"}
</button>
<button onClick={() => nextPage()} disabled={!getCanNextPage()}>
{">"}
</button>
<button
onClick={() => setPageIndex(getPageCount() - 1)}
disabled={!getCanNextPage()}
>
{">>"}
</button>
<span>
<strong>
{" "}
{getState().pagination.pageIndex + 1} / {getPageCount()}{" "}
</strong>
</span>
<span>
| Go to Page:{" "}
<input
type="number"
defaultValue={getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
setPageIndex(page);
}}
/>
</span> <select
value={getState().pagination.pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
);
};
The generated code implements a handful of features, including data fetching, button actions, pagination, and JSX markup with minimal styles for presenting the data in a table. This is pretty much the skeleton of what we want in a table of data that we want to improve with daisyUI.
It uses the useTable()
hook provided by @refinedev/react-table
package, which augments Refine's useTable()
core hook with React Table's useReactTable()
hook. More on this below.
We want to keep most of it and add filter functionality at the top, modify the pagination and apply daisyUI classes for tables, buttons, and groups.
So, we'll build on top of it and make necessary logic, markup and style modifications. Replacing the old ones, we'll eventually adopt the following <ProductList />
code:
Show ProductList component code
import React, { useRef } from "react";
import { getDefaultFilter, useDelete, useNavigation } from "@refinedev/core";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import { PlusIcon } from "@heroicons/react/20/solid";
import {
FunnelIcon,
PencilSquareIcon,
EyeIcon,
TrashIcon,
BarsArrowDownIcon,
BarsArrowUpIcon,
} from "@heroicons/react/24/outline";
export const ProductList = () => {
const filterForm: any = useRef(null);
const { mutate: deleteProduct } = useDelete();
const columns = React.useMemo<ColumnDef<any>[]>(
() => [
{
id: "id",
accessorKey: "id",
header: "Id",
},
{
id: "name",
accessorKey: "name",
header: "Name",
},
{
id: "price",
accessorKey: "price",
header: "Price",
},
{
id: "category",
header: "Category",
enableSorting: false,
accessorKey: "category.title",
},
{
id: "description",
accessorKey: "description",
enableSorting: false,
header: "Description",
},
{
id: "actions",
accessorKey: "id",
header: "Actions",
enableSorting: false,
cell: function render({ getValue }) {
return (
<div className="flex items-center justify-around">
<button
className="btn btn-xs btn-circle btn-ghost m-1"
onClick={() => {
edit("products", getValue() as string);
}}
>
<PencilSquareIcon className="h-4 w-4" />
</button>
<button
className="btn btn-xs btn-circle btn-ghost m-1"
onClick={() => {
show("products", getValue() as string);
}}
>
<EyeIcon className="h-4 w-4" />
</button>
<button
className="btn btn-xs btn-circle btn-ghost m-1"
onClick={() => {
deleteProduct({
resource: "products",
id: getValue() as string,
});
}}
>
<TrashIcon className="text-error h-4 w-4" />
</button>
</div>
);
},
},
],
[],
);
const { edit, show, create } = useNavigation();
const {
getHeaderGroups,
getRowModel,
refineCore: { filters, setCurrent, setFilters },
getState,
setPageIndex,
getCanPreviousPage,
getPageCount,
getCanNextPage,
nextPage,
previousPage,
setPageSize,
} = useTable({
columns,
});
return (
<div className="page-container">
<div className="page-header">
<h1 className="page-title">Products</h1>
<button
className="btn btn-sm btn-primary font-normal normal-case text-zinc-50"
onClick={() => create("products")}
>
<PlusIcon className="h-5 w-5" />
Create
</button>
</div>
<div className="overflow-x-auto border bg-slate-50">
<div className="m-4 flex items-center justify-between">
<button
className="btn btn-outline btn-primary btn-sm font-light normal-case"
onClick={() => {
setCurrent(1);
setFilters([