Using Refine to Build an Admin Panel

Refine is a React Framework for building internal tools, admin panels, dashboards & B2B apps.

Pankod
Bits and Pieces

--

Refine ecosystem

Introduction

Admin panels are data-intensive business-to-business (B2B) applications that handle complex business logic. They integrate with various data sources, systems, and platforms, making them the hallmark of business management on the web.

In this article, we will look at how to use Refine and Material UI to build a minimal but complete React admin panel. By the end of this tutorial, you will have a fully functional dashboard that displays key performance indicators, charts, and tables for your business and pages with CRUD operations.

⭐ You can get the source code for the sample app we’re building in this tutorial here.

What is Refine?

Refine is an Open-source React meta-framework for building internal tools and enterprise applications, such as admin panels, dashboards, and other B2B applications. It is a feature-rich framework with built-in support for routing, authentication, internationalization, state management, and more.

Refine offers collections of helper hooks, components, providers, and more rather than limiting you to pre-styled components. With its decoupled business logic and UI, you can freely customize the UI. This means refine integrates seamlessly with any custom designs or UI frameworks.

Its headless architecture allows compatibility with popular CSS frameworks like TailwindCSS or even lets you craft your styles from the ground up. Additionally, it provides integrations with Ant Design, Material UI, Mantine, and Chakra UI.

Set up a Refine app

You can create a Refine application using the CLI or the Browser-based Scaffolder.
This tool enables seamless app creation right in your browser. It allows you to customize your application according to your preferences by selecting the libraries and frameworks you wish to use, and the tool will then generate a starter project for you.

For this tutorial, we’ll select the following options:

React Platform: Vite
UI Framework: Material UI
Backend: REST API
Authentication Provider: No Auth

Refine app scaffolder

Once you’ve completed the steps, you’ll have the ability to download your project.

After creating the project, you can download it to your local machine. You should sign into the platform with your GitHub or Google account to build and download the project.

Then, use the command below to install dependencies.

npm install

After installing dependencies, use the command below to launch the development server on localhost. You can then view the project in a web browser.

npm run dev
This example app is bootstraped by a scaffolder.

Why Material UI

Material UI is a flexible library comprising a wide range of pre-styled components, streamlining the process of building web applications while adhering to the best practices of user interface design.

Refine offers built-in support for Material UI. This means we do not have to go through the tedious process of configuring the library from scratch, as refine provides all the necessary tools and resources to start with Material UI in a Refine project.

Cleanup and structuring

When scaffolding a new Refine application, tool generates pre-configured pages based on the backend architecture and API the project is bootstrapped with. In this case, the project has been bootstrapped with a REST API backend with a blog endpoint. Thus, the project contains blog-post pages, which are unnecessary for this tutorial and should be removed.

To do this, navigate to the src/pages directory and delete the blog-posts and categories folders.

Next, update the src/App.tsx file by deleting the objects for the blog-posts and categories resources and replacing the endpoint on the data provider with a fake food store API, which the refine team has put together for learning purposes.

However, for the sake of simplicity, you may copy and paste the code below into the src/App.tsx file instead.

import { GitHubBanner, Refine } from "@refinedev/core";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";

import {
ErrorComponent,
notificationProvider,
RefineSnackbarProvider,
ThemedLayoutV2,
ThemedTitleV2,
} from "@refinedev/mui";

import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import routerBindings, {
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import dataProvider from "@refinedev/simple-rest";
import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import { AppIcon } from "./components/app-icon";
import { Header } from "./components/header";
import { ColorModeContextProvider } from "./contexts/color-mode";

function App() {
const { t, i18n } = useTranslation();

const i18nProvider = {
translate: (key: string, params: object) => t(key, params),
changeLocale: (lang: string) => i18n.changeLanguage(lang),
getLocale: () => i18n.language };


return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<DevtoolsProvider>
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
notificationProvider={notificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
projectId: "RjAC3m-NeoqYc-bB545U",
}}
>
<Routes>
<Route
element={
<ThemedLayoutV2
Header={() => <Header sticky />}
Title={({ collapsed }) => (
<ThemedTitleV2
collapsed={collapsed}
text="Fine-food Dashboard"
icon={<AppIcon />}
/>
)}
>
<Outlet />
</ThemedLayoutV2>
}
>
<Route
index
element={<NavigateToResource resource="dashboard" />}
/>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
<DevtoolsPanel />
</DevtoolsProvider>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}

export default App;

Install rechart and other packages

Material UI doesn’t have a dedicated component for rendering charts. Therefore, before proceeding with the tutorial, we must install two additional packages: Rechart and DayJS.

The Rechart library will help us render lightweight charts and graphs in our dashboard, while the DayJS library will assist in querying data from the API using specified dates and times.

To install these packages, run the following commands in your terminal:

npm install recharts dayjs

Building the dashboard

Now that the project has been set up and the necessary dependencies have been installed, we can build the dashboard.

The dashboard application we are developing will track sales records for a restaurant business. It will have three pages: the dashboard, categories, and products pages.

The dashboard page will contain key performance indicator (KPI) cards, charts, and a recent orders table to provide a quick business overview.

These pages also permit users to create, edit, and view products and categories.

The dashboard of the application will be as follows:

Dashboard page

Interfaces

To avoid the inconvenience of creating interfaces and types for our data as we proceed, we will declare the interfaces and types we will use throughout the application in the src/interfaces/index.ts directory beforehand.

Create the directory and add the code below.

export type KpiCardProps = {
title: string;
total: number;
trend: number;
target: number;
formatTotal?: (value: number) => number | string;
formatTarget?: (value: number) => number | string;
};

export type DeltaType =
| "error"
| "warning"
| "primary"
| "secondary"
| "success"
| "info";

export interface IOrder {
id: number;
user: IUser;
createdAt: string;
status: IOrderStatus;
address: IAddress;
amount: number;
}

export interface IOrderStatus {
id: number;
text: "Pending" | "Ready" | "On The Way" | "Delivered" | "Cancelled";
}

export interface IChartDatum {
date: string;
value: string;
}

export interface IChart {
data: IChartDatum[];
total: number;
trend: number;
}

export type IUser = {
id: number;
name: string;
avatar: string;
};

interface IAreaGraphProps {
data: IChartDatum[];
stroke: string;
fill: string;
}

interface IBarChartProps {
data: IChartDatum[];
fill: string;
}

Create a Dashboard page

To create the dashboard page, navigate to the src/pages/dahsboard/dashboard.tsx file and add the code below:

import React from "react";

function Dashboard() {
return <h1>Hello World</h1>;
}

export default Dashboard;

Next, return to the src/App.tsx file and import the newly created dashboard page. Add it to the resources prop on the <Refine/> component, and add a dashboard route to the routes component, as seen in the codes below.


import Dashboard from "./pages/dashboard/dashboard";
...

<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
notificationProvider={notificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
projectId: "RjAC3m-NeoqYc-bB545U",
}}
>
<Routes>
...
<Route index element={<NavigateToResource resource="dashboard" />} />
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>

<Route path="*" element={<ErrorComponent />} />
</Routes>
...
</Refine>;

If you go back to the browser, you should see a similar page as the one below.

Dashboard welcome page

The dashboard page for our application is now ready for use. We can add the key performance indicators (KPIs) to the page.

Add KPI cards

The KPI cards are the first components we will add to the dashboard. In this section, we will create a base for the card inside the src/component directory, then import it inside the dashboard page and reuse the component for all three cards. Each card will represent the weekly revenue, orders, and new customers.

As a first step, navigate to the src/component directory and create a new folder with a namekpi-card, and then add the following code.


import React from "react";
import { Card, Box, CardContent, Typography, Chip } from "@mui/material";
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import LinearProgress, {
LinearProgressProps,
} from "@mui/material/LinearProgress";

import { KpiCardProps, DeltaType } from "../../interfaces";

const getColor = (num: number): DeltaType => {
switch (true) {
case num < 20:
return "error";
case num < 50:
return "warning";
case num === 50:
return "info";
case num < 75:
return "primary";
case num < 90:
return "secondary";
default:
return "success";
}
};

function LinearProgressWithLabel(
props: LinearProgressProps & { value: number },
) {
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ width: "100%", mr: 1 }}>
<LinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value,
)}%`}</Typography>
</Box>
</Box>
);
}

export default function KpiCard({
title,
total,
trend,
target,
formatTotal = (value) => value,
formatTarget = (value) => value,
}: KpiCardProps) {
const percent = Math.round((trend / total) * 100);
const color = getColor(percent);
const arbitraryTarget = Math.round((total / target) * 100);

return (
<Card elevation={3}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
listStyle: "none",
p: 0.5,
m: 0,
}}
>
<CardContent>
<Typography variant="h5" sx={{ fontSize: 17, mb: 0 }}>
{title}
</Typography>
<Typography
variant="h5"
sx={{ fontSize: 40, fontWeight: 700, mb: 0 }}
color={color == "success" ? "green" : color}
>
{formatTotal(total)}
</Typography>
</CardContent>
<CardContent>
<Chip
icon={percent < 0 ? <ArrowDownwardIcon /> : <ArrowUpwardIcon />}
color={color}
label={`${percent}%`}
/>
</CardContent>
</Box>
<Box sx={{ flexGrow: 1 }}>
<CardContent>
<span>{`(Target: ${formatTarget(target)})`}</span>
<LinearProgressWithLabel
value={arbitraryTarget}
variant="determinate"
color={color}
sx={{
borderRadius: 5,
height: 10,
backgroundColor: (theme) =>
theme.palette.mode === "light" ? color : color,
}}
/>
</CardContent>
</Box>
</Card>
);
}

The code above uses the MUIs Card, Typography, Chip, and LinearProgressWithLabel components to construct a card with the expected data props.

To proceed, return to the src/pages/dashboard/Dashboard.tsx file and update it with the following code:

import React, { useState } from "react";
import { useApiUrl, useCustom } from "@refinedev/core";
import dayjs from "dayjs";
import { Box, Grid, Tab, Card, CardHeader, styled } from "@mui/material";
import KpiCard from "../../components/kpi-card";

import { IChart } from "../../interfaces";

const query = {
start: dayjs().subtract(7, "days").startOf("day"),
end: dayjs().startOf("day"),
};

const formatCurrency = Intl.NumberFormat("en", {
style: "currency",
currency: "USD",
});

export function Dashboard() {
const API_URL = useApiUrl();

const { data: revenue } = useCustom<IChart>({
url: `${API_URL}/dailyRevenue`,
method: "get",
config: {
query,
},
});

const { data: orders } = useCustom<IChart>({
url: `${API_URL}/dailyOrders`,
method: "get",
config: {
query,
},
});

const { data: customers } = useCustom<IChart>({
url: `${API_URL}/newCustomers`,
method: "get",
config: {
query,
},
});

return (
<main>
<Box mt={2} mb={5}>
<Grid container columnGap={3} rowGap={3}>
<Grid item xs>
<KpiCard
title="Weekly Revenue"
total={revenue?.data.total ?? 0}
trend={revenue?.data.trend ?? 0}
target={10000}
formatTotal={(value) => formatCurrency.format(value)}
formatTarget={(value) => formatCurrency.format(value)}
/>
</Grid>
<Grid item xs>
<KpiCard
title="Weekly Orders"
total={orders?.data.total ?? 0}
trend={orders?.data.trend ?? 0}
target={150}
/>
</Grid>
<Grid item xs>
<KpiCard
title="New Customers"
total={customers?.data.total ?? 0}
trend={customers?.data.trend ?? 0}
target={300}
/>
</Grid>
</Grid>
</Box>
</main>
);
}

Here, we use the useCustom hook from refine to query data from the dailyRevenue, dailyOrders, and newCustomers endpoints of the fine-food API. It's used to send custom query requests using the Tanstack Query advantages. The base URL for the API is made available using the useApiUrl hook.

We also use the dayJS package that we installed previously to help the useCustom component query data for the last week from the API.

After saving the code, the cards should display on the browser, as shown in the following image.

KPI cards

Add KPI charts using Rechart

To create the chart section of our dashboard, we will add two types of charts that will represent the weekly revenue, orders, and new customers.

Navigate to create a charts folder, then add AreaChart and BarChart subfolders. In AreaChart, create an index.tsx file and input the given code.


import React, { useContext } from "react";
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";

import { IAreaGraphProps } from "../../interfaces";

export const formatDate = new Intl.DateTimeFormat("en-US", {
month: "short",
year: "numeric",
day: "numeric",
});

export const AreaGraph: React.FC<IAreaGraphProps> = ({
data,
stroke,
fill,
}) => {
const transformedData = data.map(({ date, value }) => ({
date: formatDate.format(new Date(date)),
value,
}));

return (
<ResponsiveContainer width="99%" aspect={3}>
<AreaChart data={transformedData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis dataKey="value" />
<Tooltip label="Daily Revenue" />
<Area type="monotone" dataKey="value" stroke={stroke} fill={fill} />
</AreaChart>
</ResponsiveContainer>
);
};

Do the same for the BarChart folder: add an index.tsx file with the following code:

import React from "react";
import {
BarChart as Chart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";

import { IBarChartProps } from "../../interfaces";

const formatDate = new Intl.DateTimeFormat("en-US", {
month: "short",
year: "numeric",
day: "numeric",
});

export const BarChart: React.FC<IBarChartProps> = ({ data, fill }) => {
const transformedData = data.map(({ date, value }) => ({
date: formatDate.format(new Date(date)),
value,
}));

return (
<ResponsiveContainer width="99%" aspect={3}>
<Chart data={transformedData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis dataKey="value" />
<Tooltip />
<Bar dataKey="value" fill={fill} />
</Chart>
</ResponsiveContainer>
);
};

Next, navigate back to the root of the charts folder and add an index.ts file. We will use this file to export the AreaChart and Barchart components.

export { AreaGraph } from "./AreaGraph";
export { BarChart } from "./BarChart";

Finally, go to the Dashboard.tsx file, import both chart components, and update it with the highlighted code below:

import React, { useState } from "react";
import { useApiUrl, useCustom } from "@refinedev/core";
import dayjs from "dayjs";
import { Box, Grid, Tab, Card, CardHeader, styled } from "@mui/material";
import { TabContext } from "@mui/lab";
import TabList from "@mui/lab/TabList";
import TabPanel from "@mui/lab/TabPanel";
import KpiCard from "../../components/kpi-card";
import { AreaGraph, BarChart } from "../../components/charts";

import { IChart } from "../../interfaces";

export function Dashboard() {
const [value, setValue] = React.useState("1");

const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue);
};

...

return (
<main>
...
<Box my={5} sx={{ boxShadow: 5 }}>
<Card>
<CardHeader title="Sales Chart" />
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList
onChange={handleChange}
aria-label="lab API tabs example"
>
<Tab label="Revenues" value="1" />
<Tab label="Orders" value="2" />
<Tab label="Customers" value="3" />
</TabList>
</Box>
<TabPanel value="1">
<AreaGraph
data={revenue?.data?.data ?? []}
stroke="#8884d8"
fill="#cfeafc"
/>
</TabPanel>
<TabPanel value="2">
<BarChart data={orders?.data?.data ?? []} fill="#ffce90" />
</TabPanel>
<TabPanel value="3">
<AreaGraph
data={customers?.data?.data ?? []}
stroke="#00bd56"
fill="#ccf3f3"
/>
</TabPanel>
</TabContext>
</Card>
</Box>
</main>
);
}

The code above is simpler than it may appear. We are simply using the MUI TabContext component and its peer components to render the charts in three separate tabs: Revenue, Orders, and Customers. We then pass the appropriate data to each chart component declaration.

After making all these changes, your dashboard should resemble the GIF below:

Sales Chart

We are nearly finished constructing our dashboard. The only thing left to do is create the `recentSales` table.

Add a recent sales table

The recent sales table will display recent sales with the order ID, amount, ordered by, status, and more. Users will also be able to search for orders in the table and filter them in ascending or descending order.

Still in the src/components directory, create a new folder named recent-sales

import React from "react";
import InputAdornment from "@mui/material/InputAdornment";
import SearchIcon from "@mui/icons-material/Search";

import { DataGrid, GridColTypeDef } from "@mui/x-data-grid";
import { useDataGrid } from "@refinedev/mui";
import { Card, CardHeader, Chip, TextField, Stack } from "@mui/material";

import { getDefaultFilter } from "@refinedev/core";
import { IOrder, IOrderStatus } from "../../interfaces";

export function RecentSales() {
const { dataGridProps, setCurrent, setFilters, filters } =
useDataGrid<IOrder>({
resource: "orders",
initialPageSize: 5,
});

const currencyFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});

const formatDate = (value: string) => {
return new Date(value).toLocaleString("en-US", {
month: "short",
year: "numeric",
day: "numeric",
});
};

const getColor = (status: IOrderStatus["text"]) => {
switch (status) {
case "Cancelled":
return "error";

case "Ready":
return "success";

case "On The Way":
return "info";

case "Pending":
return "warning";

case "Delivered":
return "secondary";

default:
return "primary";
}
};

const columns = React.useMemo<GridColTypeDef<any>[]>(
() => [
{
field: "id",
headerName: "id",
width: 70,
},
{
field: "user.fullName",
headerName: "Ordered By",
width: 200,
renderCell: ({ row }) => <>{row\["user"\]["fullName"]}</>,
},
{
field: "amount",
headerName: "Amount",
type: "singleSelect",
width: 150,
valueFormatter: ({ value }) => currencyFormatter.format(value),
},
{
field: "user.gender",
headerName: "Gender",
width: 120,
renderCell: ({ row }) => <>{row\["user"\]["gender"]}</>,
},
{
field: "user.gsm",
headerName: "Tel",
width: 170,
renderCell: ({ row }) => <>{row\["user"\]["gsm"]}</>,
},
{
field: "status.text",
headerName: "Status",
width: 160,
type: "singleSelect",
valueOptions: [
"Cancelled",
"Ready",
"On The Way",
"Pending",
"Delivered",
],
renderCell: ({ row }) => (
<Chip
label={row\["status"\]["text"]}
color={getColor(row\["status"\]["text"])}
variant="outlined"
/>
),
},
{
field: "adress.text",
headerName: "Address",
width: 350,
headerAlign: "left",
renderCell: ({ row }) => <>{row\["adress"\]["text"]}</>,
},
{
field: "createdAt",
headerName: "Created At",
width: 200,
renderCell: ({ row }) => <>{formatDate(row["createdAt"])}</>,
},
],
[]
);

return (
<Card elevation={5}>
<Stack
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center"
paddingRight={2}
>
<CardHeader title="Recent Sales" />
<TextField
value={getDefaultFilter("q", filters, "contains")}
id="outlined-basic"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.value.trim()) {
setCurrent(1);
setFilters([], "replace");
return;
}

setCurrent(1);
setFilters([
{
field: "q",
value: e.target.value,
operator: "contains",
},
]);
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
size="small"
placeholder="Keyword Search"
/>
</Stack>
<DataGrid
{...dataGridProps}
columns={columns as any}
sx={{ pl: 3 }}
autoHeight
pageSizeOptions={[5, 10, 25, 50]}
/>
</Card>
);
}

In this code, we used the DataGrid component from MUI to render the table and Refine’s useDataGrid hook for querying data from the /orders endpoint, filtering, sorting, and pagination. We also used the columns array to define the table’s structure.

To complete this step, navigate to the pages/dashboard/Dashboard.tsx file, import the recent-sales component, and render it below the charts like this.

import React, { useState } from "react";
import { useApiUrl, useCustom } from "@refinedev/core";
import dayjs from "dayjs";
import { Box, Grid, Tab, Card, CardHeader, styled } from "@mui/material";
import { TabContext } from "@mui/lab";
import TabList from "@mui/lab/TabList";
import TabPanel from "@mui/lab/TabPanel";
import KpiCard from "../../components/kpi-card";
import { AreaGraph, BarChart } from "../../components/charts";
import { RecentSales } from "../../components/recent-sales";

import { IChart } from "../../interfaces";

const Responsive = styled("div")(({ theme }) => ({
[theme.breakpoints.down("md")]: {
width: "880px",
},
[theme.breakpoints.down("sm")]: {
width: "374px",
},
}));

// KPI card code
const query = {
start: dayjs().subtract(7, "days").startOf("day"),
end: dayjs().startOf("day"),
};

const formatCurrency = Intl.NumberFormat("en", {
style: "currency",
currency: "USD",
});

export function Dashboard() {
const API_URL = useApiUrl();

const { data: revenue } = useCustom<IChart>({
url: `${API_URL}/dailyRevenue`,
method: "get",
config: {
query,
},
});

const { data: orders } = useCustom<IChart>({
url: `${API_URL}/dailyOrders`,
method: "get",
config: {
query,
},
});

const { data: customers } = useCustom<IChart>({
url: `${API_URL}/newCustomers`,
method: "get",
config: {
query,
},
});

// Chart code
const [value, setValue] = React.useState("1");

const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue);
};

return (
<main>
<Box mt={2} mb={5}>
<Grid container columnGap={3} rowGap={3}>
<Grid item xs>
<KpiCard
title="Weekly Revenue"
total={revenue?.data.total ?? 0}
trend={revenue?.data.trend ?? 0}
target={10000}
formatTotal={(value) => formatCurrency.format(value)}
formatTarget={(value) => formatCurrency.format(value)}
/>
</Grid>
<Grid item xs>
<KpiCard
title="Weekly Orders"
total={orders?.data.total ?? 0}
trend={orders?.data.trend ?? 0}
target={150}
/>
</Grid>
<Grid item xs>
<KpiCard
title="New Customers"
total={customers?.data.total ?? 0}
trend={customers?.data.trend ?? 0}
target={300}
/>
</Grid>
</Grid>
</Box>
<Box my={5} sx={{ boxShadow: 5 }}>
<Card>
<CardHeader title="Sales Chart" />
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList
onChange={handleChange}
aria-label="lab API tabs example"
>
<Tab label="Revenues" value="1" />
<Tab label="Orders" value="2" />
<Tab label="Customers" value="3" />
</TabList>
</Box>
<TabPanel value="1">
<AreaGraph
data={revenue?.data?.data ?? []}
stroke="#8884d8"
fill="#cfeafc"
/>
</TabPanel>
<TabPanel value="2">
<BarChart data={orders?.data?.data ?? []} fill="#ffce90" />
</TabPanel>
<TabPanel value="3">
<AreaGraph
data={customers?.data?.data ?? []}
stroke="#00bd56"
fill="#ccf3f3"
/>
</TabPanel>
</TabContext>
</Card>
</Box>
<Responsive>
<RecentSales />
</Responsive>
</main>
);
}

The recent sales table concludes the dashboard page’s construction. Your dashboard should look like the GIF below when you save the changes and return to the browser.

Adding the CRUD pages

As previously discussed, we will create CRUD pages for the categories and products resources that will allow users to list, create, update, and view products and categories from the fine-food endpoint. We will begin with the products resources.

Products pages

We want to create four pages for the products resource: list, create, edit, and show. These pages will be located in the src/pages/products directory.

List Page

The product list page will have features and structure similar to the recent-sales component we created previously. However, the column structure and layout will be different.

The useDataGrid hook provides properties that seamlessly integrate with the MUI X `<DataGrid>` component, offering built-in functionalities like sorting, filtering, and pagination.

Let’s start by creating a list.tsx file in the src/page/products directory we created earlier, and adding the following code:

import React from "react";
import {
useDataGrid,
EditButton,
ShowButton,
DeleteButton,
List,
MarkdownField,
DateField,
} from "@refinedev/mui";
import { DataGrid, GridColDef } from "@mui/x-data-grid";

import {
IResourceComponentsProps,
useTranslate,
useMany,
} from "@refinedev/core";
import { Checkbox } from "@mui/material";

export const ProductList: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { dataGridProps } = useDataGrid();

const { data: categoryData, isLoading: categoryIsLoading } = useMany({
resource: "categories",
ids: dataGridProps?.rows?.map((item: any) => item?.category?.id) ?? [],
queryOptions: {
enabled: !!dataGridProps?.rows,
},
});

const columns = React.useMemo<GridColDef[]>(
() => [
{
field: "id",
headerName: translate("id"),
minWidth: 50,
},
{
field: "name",
flex: 1,
headerName: translate("Name"),
minWidth: 200,
},
{
field: "price",
flex: 0.5,
headerName: translate("Price"),
},
{
field: "category",
flex: 0.5,
headerName: translate("Category"),
valueGetter: ({ row }) => {
const value = row?.category?.id;

return value;
},
renderCell: function render({ value }) {
return categoryIsLoading ? (
<>Loading...</>
) : (
categoryData?.data?.find((item) => item.id === value)?.title
);
},
},
{
field: "description",
flex: 1,
headerName: translate("Description"),
minWidth: 500,
renderCell: function render({ value }) {
return <MarkdownField value={(value ?? "").slice(0, 80) + "..."} />;
},
},
{
field: "actions",
headerName: translate("table.actions"),
sortable: false,
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
align: "center",
headerAlign: "center",
flex: 1,
},
],
[translate, categoryData?.data]
);

return (
<List>
<DataGrid {...dataGridProps} columns={columns} autoHeight />
</List>
);
};

The useMany hook declaration in the code above fetches and renders the categories of each product in the categories column on the table.

useMany hook is useful when you want to fetch multiple records from the API. It will return the data and some functions to control the query.

It’s worth noting that the actions column in the columns array renders icons for performing CRUD operations on each product in the table. When interacted with, these icons will route users to the edit and show pages, which we will create in the subsequent sections, or invoke a modal component for deleting individual items.

Next, create a index.ts file in the src/pages/products directory and export the list.tsx file as follows:

export * from "./list";

Finally, in the src/App.tsx file, import the list component, add a products resource object to the resources array on the <Refine> component, and add a products route definition to the React Router DOM context, as highlighted in the code below:

...

import {
ProductCreate,
ProductEdit,
ProductList,
ProductShow,
} from "./pages/products";

function App() {
...

return (
<BrowserRouter>
...
<Refine
...
resources={[
{
name: "dashboard",
list: "/dashboard",
},
{
name: "products",
list: "/products",
create: "/products/create",
edit: "/products/edit/:id",
show: "/products/show/:id",
meta: {
canDelete: true,
},
},
]}
>
<Routes>
<Route>
...
<Route
index
element={<NavigateToResource resource="dashboard" />}
/>
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>
<Route path="/products">
<Route index element={<ProductList />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
</Refine>
</BrowserRouter>
);
}

export default App;

Note that we also added the create, edit, and show actions to the products resource and the products route definition. This will prevent us from going back and forth when creating the rest of the pages.

Now, if you return to the browser, you should see the products route in the sidebar and the list page as shown in the GIF below.

The create button in the upper right-hand corner of the page is an action button, as are the other action icons in the table. It redirects users to the create page, where they can create new products.

Create Page

The create page does not exist as it is, so if you try to navigate to it using the create action button on the list page, you will get redirected to a 404 page. Let us fix that.

Create a new file in the src/pages/products/create.tsx directory, name it, and add the following code:

import { Create, useAutocomplete } from "@refinedev/mui";
import {
Box,
TextField,
Checkbox,
FormControlLabel,
Autocomplete,
} from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";

export const ProductCreate: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
refineCore: { formLoading },
register,
control,
formState: { errors },
} = useForm();

const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: "categories",
});

return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("name", {
required: "This field is required",
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("Name")}
name="name"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("isActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
<TextField
{...register("description", {
required: "This field is required",
})}
error={!!(errors as any)?.description}
helperText={(errors as any)?.description?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={translate("description")}
name="description"
/>
<TextField
{...register("price", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.price}
helperText={(errors as any)?.price?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("Price")}
name="price"
/>
<Controller
control={control}
name="category"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
getOptionLabel={(item) => {
return (
categoryAutocompleteProps?.options?.find(
(p) => p?.id?.toString() === item?.id?.toString()
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) =>
value === undefined ||
option?.id?.toString() === value?.id?.toString()
}
renderInput={(params) => (
<TextField
{...params}
label={translate("Category")}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
</Box>
</Create>
);
};

In this example, we used the useForm hook and Create component from refine, in conjunction with Material UI components, to post data, create a layout, and construct a form, respectively.

The Create page’s key component is using the useForm hook from the @refinedev/react-hook-form package. This hook combines the capabilities of the useForm hook from the Refine core, which manages form submission, data retrieval, caching, state control, and server-side error handling. The integration of React Hook Form enhances form functionality by improving form field state management and error handling.

To learn more about these hooks and components, refer to refine’s documentation.

To finish the setup, go to the pages/products/index.tsx file and export the component as follows:

export * from “./list”;
export * from “./create”;

Since we have added the page resource and route to the resources and product route definition earlier, we can skip that step and view the result in the browser.

Edit Page

The <ProductEdit /> page is where users are redirected when interacting with the edit icon in the action column on the table. The edit page will have a similar code to the create page, but instead of using the Create component, we will use the Edit component from Refine.

To create the edit page, create a new file inside the src/pages/products directory and name it edit.tsx and add the following code:

import { Edit, useAutocomplete } from "@refinedev/mui";
import {
Box,
TextField,
Checkbox,
FormControlLabel,
Autocomplete,
} from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";

export const ProductEdit: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
refineCore: { queryResult },
register,
control,
formState: { errors },
} = useForm();

const productsData = queryResult?.data?.data;

const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: "categories",
defaultValue: productsData?.category?.id,
});

return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("id", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.id}
helperText={(errors as any)?.id?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("id")}
name="id"
disabled
/>
<TextField
{...register("name", {
required: "This field is required",
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("Name")}
name="name"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("IsActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
<TextField
{...register("description", {
required: "This field is required",
})}
error={!!(errors as any)?.description}
helperText={(errors as any)?.description?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={translate("Description")}
name="description"
/>
<TextField
{...register("price", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.price}
helperText={(errors as any)?.price?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("Price")}
name="price"
/>
<Controller
control={control}
name="category"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
getOptionLabel={(item) => {
return (
categoryAutocompleteProps?.options?.find(
(p) => p?.id?.toString() === item?.id?.toString()
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) =>
value === undefined ||
option?.id?.toString() === value?.id?.toString()
}
renderInput={(params) => (
<TextField
{...params}
label={translate("Category")}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
</Box>
</Edit>
);
};

Next, open the src/pages/products/.index.tsx and export the file like so:

export * from "./list";
export * from "./create";
export * from "./edit";

Now, the edit page should render properly when you click on the action button.

Show Page

Like the previous pages, refine offers a dedicated component for the show page, known as the Show component. However, unlike the other two pages, the Show component is designed solely for displaying individual products. It also incorporates action buttons that allow users to navigate to the edit page and delete the product with a click.

The show page will have a simple structure. It will fetch details of an individual product from the API using Refine’s useShow hook and render them using Material UI components, which are encompassed by the Show component.

To create this page, go back to the src/pages/products directory and create a new file, name it show.tsx and add the following code:

import {
useShow,
IResourceComponentsProps,
useTranslate,
useOne,
} from "@refinedev/core";
import {
Show,
NumberField,
TextFieldComponent as TextField,
BooleanField,
MarkdownField,
DateField,
} from "@refinedev/mui";
import { Typography, Stack } from "@mui/material";

export const ProductShow: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { queryResult } = useShow();
const { data, isLoading } = queryResult;

const record = data?.data;

const { data: categoryData, isLoading: categoryIsLoading } = useOne({
resource: "categories",
id: record?.category?.id || "",
queryOptions: {
enabled: !!record,
},
});

return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{translate("id")}
</Typography>
<NumberField value={record?.id ?? ""} />
<Typography variant="body1" fontWeight="bold">
{translate("Name")}
</Typography>
<TextField value={record?.name} />
<Typography variant="body1" fontWeight="bold">
{translate("IsActive")}
</Typography>
<BooleanField value={record?.isActive} />
<Typography variant="body1" fontWeight="bold">
{translate("Description")}
</Typography>
<MarkdownField value={record?.description} />
<Typography variant="body1" fontWeight="bold">
{translate("CreatedAt")}
</Typography>
<DateField value={record?.createdAt} />
<Typography variant="body1" fontWeight="bold">
{translate("Price")}
</Typography>
<NumberField value={record?.price ?? ""} />
<Typography variant="body1" fontWeight="bold">
{translate("Category")}
</Typography>

{categoryIsLoading ? <>Loading...</> : <>{categoryData?.data?.title}</>}
</Stack>
</Show>
);
};

Finally, go to the src/pages/products/index.tsx file and export the file as we’ve done with the previous pages.

export * from "./list";
export * from "./create";
export * from "./edit";
export * from "./show";

This concludes the products resource pages. The pages should work together seamlessly when interacting with them in the browser.

Categories pages

The categories CRUD pages are pretty similar to the products pages, sharing the same code except for the data source. In this case, we’ll query data from the API’s /categories endpoint instead of /products.

As a result, we won’t spend much time on this and will quickly go through the process of setting up the pages. First, let’s create the src/pages/categories directory and add all our files.

To do this, create the directory and add the following files:

  • CategoryList.tsx
  • CategoryCreate.tsx
  • CategoryEdit.tsx
  • CategoryShow.tsx
  • index.ts

Next, navigate to the src/App.tsx file and update the resources and routes with the highlighted code:

...

import {
CategoryCreate,
CategoryEdit,
CategoryList,
CategoryShow,
} from "./pages/categories";

import { Dashboard } from "./pages/dashboard";
import {
ProductCreate,
ProductEdit,
ProductList,
ProductShow,
} from "./pages/products";

function App() {

...

return (
<BrowserRouter>
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
notificationProvider={notificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
{
name: "categories",
list: "/categories",
create: "/categories/create",
edit: "/categories/edit/:id",
show: "/categories/show/:id",
meta: {
canDelete: true,
},
},
{
name: "products",
list: "/products",
create: "/products/create",
edit: "/products/edit/:id",
show: "/products/show/:id",
meta: {
canDelete: true,
},
},
]}
>
<Routes>
<Route>
...

<Route
index
element={<NavigateToResource resource="dashboard" />}
/>
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>
<Route path="/categories">
<Route index element={<CategoryList />} />
<Route path="create" element={<CategoryCreate />} />
<Route path="edit/:id" element={<CategoryEdit />} />
<Route path="show/:id" element={<CategoryShow />} />
</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>
</Refine>
</BrowserRouter>
);
}

export default App;

List page

We’ll start with the list page. Navigate to the CategoryList.tsx file and add the following code:

import React from "react";
import {
useDataGrid,
EditButton,
ShowButton,
DeleteButton,
List,
MarkdownField,
} from "@refinedev/mui";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";

export const CategoryList: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { dataGridProps } = useDataGrid();

const columns = React.useMemo<GridColDef[]>(
() => [
{
field: "id",
headerName: translate("categories.fields.id"),
width: 200,
},
{
field: "title",
headerName: translate("categories.fields.title"),
flex: 1,
},
{
field: "actions",
headerName: translate("table.actions"),
sortable: false,
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
align: "center",
headerAlign: "center",
flex: 1,
},
],
[translate]
);

return (
<List>
<DataGrid {...dataGridProps} columns={columns} autoHeight />
</List>
);
};

As you can see, the code is not significantly different from the code we used in the products list page earlier.

After adding the code to the page, go to the index.js file in the root directory of the categories folder and export the components like this:

export { CategoryCreate } from "./create";
export { CategoryEdit } from "./edit";
export { CategoryList } from "./list";
export { CategoryShow } from "./show";

Create Page

We’ll do the same for the CategoryCreate page. Open the file and add the following code:

import { Create } from "@refinedev/mui";
import { Box, TextField, Checkbox, FormControlLabel } from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";

export const CategoryCreate: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
refineCore: { formLoading },
register,
control,
formState: { errors },
} = useForm();

return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("categories.fields.title")}
name="title"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("isActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
<TextField
{...register("cover", {
required: "This field is required",
})}
error={!!(errors as any)?.cover}
helperText={(errors as any)?.cover?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={translate("cover")}
name="cover"
/>
</Box>
</Create>
);
};

Edit page

Likewise, the edit page. Open the CategoryEdit.tsx file and add the following code:

import { Edit } from "@refinedev/mui";
import { Box, TextField, Checkbox, FormControlLabel } from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";

export const CategoryEdit: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
register,
control,
formState: { errors },
} = useForm();

return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("id", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.id}
helperText={(errors as any)?.id?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("categories.fields.id")}
name="id"
disabled
/>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("categories.fields.title")}
name="title"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("isActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
</Box>
</Edit>
);
};

Show Page

Finally, to complete the step, copy and paste the code below into the CategoryShow.tsx file:

import {
useShow,
IResourceComponentsProps,
useTranslate,
} from "@refinedev/core";
import {
Show,
NumberField,
TextFieldComponent as TextField,
BooleanField,
} from "@refinedev/mui";
import { Typography, Stack } from "@mui/material";

export const CategoryShow: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { queryResult } = useShow();
const { data, isLoading } = queryResult;

const record = data?.data;

return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{translate("categories.fields.id")}
</Typography>
<NumberField value={record?.id ?? ""} />
<Typography variant="body1" fontWeight="bold">
{translate("categories.fields.title")}
</Typography>
<TextField value={record?.title} />
<Typography variant="body1" fontWeight="bold">
{translate("isActive")}
</Typography>
<BooleanField value={record?.isActive} />
</Stack>
</Show>
);
};

With every page in the src/pages/categories folder appropriately populated, we can go ahead and view the finished product in the browser.

What is Refine Inferencer

As previously discussed, Refine auto-generate CRUD pages based on the backend architecture and API your application is bootstrapped with. This is done using Refine’s inferencer package.

The inferencer package is a nifty tool the refine team developed to allow developers to auto-generate CRUD pages for resources, such as the categories and products pages, based on the data structure.

The package was created to help developers reduce the amount of time spent on creating pages for resources. It exports CRUD pages such as List, Show, Create, and Edit pages based on the design system scope of your application.

This means that it creates the highlighted pages using UI elements that your application is configured to use. For example, suppose you choose the Ant Design system for your project when bootstrapping it using the app scaffolder. In that case, the Inferencer will automatically generate codes for each page using Ant Design UI elements.

The Inferencer package is available for every design system refine is compatible with, such as:

  • Ant Design
  • Material UI
  • Chakra UI
  • Mantine
  • Headless

For more information on the Inferencer package, visit the refine documentation.

Conclusion

As we have seen in this article, refine makes building data-intensive applications a breeze by providing several hooks and packages that can halve the time it takes to build such applications compared to traditional tools and methods.

--

--

We mold great ideas into awesome software products. A young and innovative software development house located in the heart of beautiful İstanbul.