Developer Center

Building a B2B React CRM App with Refine and Deploying It on DigitalOcean

Published on February 9, 2024
authorauthor

Salih Özdemir, Software Engineer. and Anish Singh Walia

Building a B2B React CRM App with Refine and Deploying It on DigitalOcean

Introduction

In this tutorial, we will build a B2B React CRM application with Refine Framework and deploy it to DigitalOcean App Platform.

At the end of this tutorial, we’ll have a CRM application that includes:

  • Dashboard with metric cards and charts.
  • Companies pages to list, create, edit, and show companies.
  • Contacts pages to list, create, edit and show contacts.

While doing these, we’ll use the:

  • GraphQL API to fetch the data. Refine has built-in data provider packages for both GraphQL and REST APIs, but you can also build your own to suit your specific requirements. In this guide, we’re going to use Nestjs Query as our backend service and the @refinedev/nestjs-query package as our data provider.
  • Ant Design UI library.
  • Once we’ve built the app, we’ll put it online using DigitalOcean’s App Platform. This service makes it really easy and fast to set up, launch, and grow apps and static websites. You can deploy code by simply pointing to a GitHub repository and let App Platform do the heavy lifting of managing the infrastructure, app runtimes, and dependencies.

You can get the final source code of the application on GitHub.

Slide #1Slide #2Slide #3Slide #4Slide #5Slide #6Slide #7Slide #8Slide #9

Prerequisites

Step 1 — What is Refine?

Refine is a React meta-framework for building data-intensive B2B CRUD web applications like internal tools, dashboards, and admin panels. It ships with various hooks and components to reduce the development time and increase the developer experience.

It is designed to build production-ready enterprise B2B apps. Instead of starting from scratch, it provides essential hooks and components to help with tasks such as data & state management, handling authentication, and managing permissions.

So you can focus on building the important parts of your app without getting bogged down in the technical stuff.

Refine is particularly effective in situations where managing data is key, such as:

  • Internal tools:
  • Dashboards:
  • Admin panels:
  • All type of CRUD apps

Customization and styling

Refine’s headless architecture allows the flexibility to use any UI library or custom CSS. Additionally, it has built-in support for popular open-source UI libraries, including Ant Design, Material UI, Mantine, and Chakra UI.

Step 2 — Setting Up the Project

We’ll use the npm create refine-app command to interactively initialize the project.

npm create refine-app@latest

Select the following options when prompted:

✔ Choose a project template · Vite
✔ What would you like to name your project?: · crm-app
✔ Choose your backend service to connect: · NestJS Query
✔ Do you want to use a UI Framework?: · Ant Design
✔ Do you need any Authentication logic?: · None
✔ Do you need i18n (Internationalization) support?: · No
✔ Choose a package manager: · npm

Once the setup is complete, navigate to the project folder and start your app with:

npm run dev

Open http://localhost:5173 in your browser to see the app.

Welcome Page

Step 3 — Building the Dashboard Page

This page will serve as an introduction to the CRM app, showing various metrics and charts. It will be the first page users see when they access the app.

Initially, we’ll create a <Dashboard /> component in src/pages/dashboard/index.tsx directory with the following code:

src/pages/dashboard/index.tsx
import { Button } from "antd";
export const Dashboard = () => {
    return (
        <div>
            <h1>Dashboard</h1>
            <Button type="primary">Primary Button</Button>
        </div>
    );
};

To render the component we created in the “/” path, let’s add the necessary resources and routes to the <Refine /> component in src/App.tsx.

Show <App /> code
src/App.tsx
import { Refine } from '@refinedev/core';
import { RefineKbar, RefineKbarProvider } from '@refinedev/kbar';
import { ThemedLayoutV2, useNotificationProvider } from '@refinedev/antd';
import dataProvider, {
    GraphQLClient,
    liveProvider,
} from '@refinedev/nestjs-query';
import { createClient } from 'graphql-ws';
import { BrowserRouter, Outlet, Route, Routes } from 'react-router-dom';
import routerBindings, {
    UnsavedChangesNotifier,
    DocumentTitleHandler,
} from '@refinedev/react-router-v6';
import {
    DashboardOutlined,
    ShopOutlined,
    TeamOutlined,
} from '@ant-design/icons';
import { ColorModeContextProvider } from './contexts/color-mode';
import { Dashboard } from "./pages/dashboard";
import '@refinedev/antd/dist/reset.css';
const API_URL = 'https://api.crm.refine.dev/graphql';
const WS_URL = 'wss://api.crm.refine.dev/graphql';
const ACCESS_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw';

const gqlClient = new GraphQLClient(API_URL, {
    headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
    },
});
const wsClient = createClient({
    url: WS_URL,
    connectionParams: () => ({
        headers: {
            Authorization: `Bearer ${ACCESS_TOKEN}`,
        },
    }),
});
function App() {
    return (
        <BrowserRouter>
            <RefineKbarProvider>
                <ColorModeContextProvider>
                    <Refine
                        dataProvider={dataProvider(gqlClient)}
                        liveProvider={liveProvider(wsClient)}
                        notificationProvider={useNotificationProvider}
                        routerProvider={routerBindings}
                        resources={[
                            {
                                name: 'dashboard',
                                list: '/',
                                meta: {
                                    icon: <DashboardOutlined />,
                                },
                            },
                        ]}
                        options={{
                            syncWithLocation: true,
                            warnWhenUnsavedChanges: true,
                            liveMode: 'auto',
                        }}
                    >
                        <Routes>
                            <Route
                                element={
                                    <ThemedLayoutV2>
                                        <Outlet />
                                    </ThemedLayoutV2>
                                }
                            >
                                <Route path="/">
                                    <Route index element={<Dashboard />} />
                                </Route>
                            </Route>
                        </Routes>
                        <RefineKbar />
                        <UnsavedChangesNotifier />
                        <DocumentTitleHandler />
                    </Refine>
                </ColorModeContextProvider>
            </RefineKbarProvider>
        </BrowserRouter>
    );
}
export default App;

We’ve included the dashboard resource in the resources prop of the <Refine /> component. Additionally, we assigned “/” to the list prop of the dashboard resource, which resulted in the creation of a sidebar menu item.

Additionally, we’ve added the <Dashboard /> component to the “/” path by using the <Route /> component from the react-router-dom package. We’ve also added the <ThemedLayoutV2 /> component from the @refinedev/antd package to the <Route /> component to wrap the <Dashboard /> component with the layout component.

Info: You can find more information about resources and adding routes in the React Router v6.

Also, we used the fake CRM GraphQL API to fetch the data, so we updated the values of the following constants API_URL and WS_URL to use the CRM GraphQL API endpoints. Also, this API has some authentication rules, but we won’t implement any authentication logic. We disabled the authentication rules by passing the Authorization header to the GraphQLClient constructor.

Now, if you navigate to the “/” path, you should see the <Dashboard /> page.

Creating MetricCard component

Let’s create a <MetricCard /> component to show the metrics on the dashboard page. We’ll use the Ant Design components to build the metric card and Ant Design Charts to build the chart.

First, we’ll install the specific Antd chart package.

npm install @ant-design/plots@1.2.5

Then, create a src/components/metricCard/index.tsx file with the following code:

Show <MetricCard /> component
src/components/metricCard/index.tsx
import React, { FC, PropsWithChildren } from "react";
import { Card, Skeleton, Typography } from "antd";
import { useList } from "@refinedev/core";
import { Area, AreaConfig } from "@ant-design/plots";
import { AuditOutlined, ShopOutlined, TeamOutlined } from "@ant-design/icons";
type MetricType = "companies" | "contacts" | "deals";
export const MetricCard = ({ variant }: { variant: MetricType }) => {
    const { data, isLoading, isError, error } = useList({
        resource: variant,
        liveMode: "off",
        meta: {
            fields: ["id"],
        },
    });
    if (isError) {
        console.error("Error fetching dashboard data", error);
        return null;
    }
    const { primaryColor, secondaryColor, icon, title } = variants[variant];
    const config: AreaConfig = {
        style: {
            height: "48px",
            width: "100%",
        },
        appendPadding: [1, 0, 0, 0],
        padding: 0,
        syncViewPadding: true,
        data: variants[variant].data,
        autoFit: true,
        tooltip: false,
        animation: false,
        xField: "index",
        yField: "value",
        xAxis: false,
        yAxis: {
            tickCount: 12,
            label: { style: { fill: "transparent" } },
            grid: { line: { style: { stroke: "transparent" } } },
        },
        smooth: true,
        areaStyle: () => ({
            fill: `l(270) 0:#fff 0.2:${secondaryColor} 1:${primaryColor}`,
        }),
        line: { color: primaryColor },
    };

    return (
        <Card
            bodyStyle={{
                padding: "8px 8px 8px 12px",
                height: "100%",
                display: "flex",
                alignItems: "center",
                justifyContent: "flex-start",
            }}
            size="small"
        >
            <div
                style={{
                    display: "flex",
                    flexDirection: "column",
                    justifyContent: "space-between",
                }}
            >
                <div
                    style={{
                        display: "flex",
                        alignItems: "center",
                        gap: "8px",
                        whiteSpace: "nowrap",
                    }}
                >
                    {icon}
                    <Typography.Text
                        className="secondary"
                        style={{ marginLeft: "8px" }}
                    >
                        {title}
                    </Typography.Text>
                </div>

                {isLoading ? (
                    <div
                        style={{
                            display: "flex",
                            alignItems: "center",
                            height: "60px",
                        }}
                    >
                        <Skeleton.Button
                            style={{ marginLeft: "48px", marginTop: "8px" }}
                        />
                    </div>
                ) : (
                    <Typography.Text
                        strong
                        style={{
                            fontSize: 38,
                            textAlign: "start",
                            marginLeft: "48px",
                            fontVariantNumeric: "tabular-nums",
                        }}
                    >
                        {data?.total}
                    </Typography.Text>
                )}
            </div>
            <div
                style={{
                    marginTop: "auto",
                    marginLeft: "auto",
                    width: "110px",
                }}
            >
                <Area {...config} />
            </div>
        </Card>
    );
};
const IconWrapper: FC<PropsWithChildren<{ color: string }>> = ({
    color,
    children,
}) => {
    return (
        <div
            style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                width: "32px",
                height: "32px",
                borderRadius: "50%",
                backgroundColor: color,
            }}
        >
            {children}
        </div>
    );
};
const variants: {
    [key in MetricType]: {
        primaryColor: string;
        secondaryColor?: string;
        icon: React.ReactNode;
        title: string;
        data: { index: string; value: number }[];
    };
} = {
    companies: {
        primaryColor: "#1677FF",
        secondaryColor: "#BAE0FF",
        icon: (
            <IconWrapper color="#E6F4FF">
                <ShopOutlined
                    className="md"
                    style={{
                        color: "#1677FF",
                    }}
                />
            </IconWrapper>
        ),
        title: "Number of companies",
        data: [
            { index: "1", value: 3500 },
            { index: "2", value: 2750 },
            { index: "3", value: 5000 },
            { index: "4", value: 4250 },
            { index: "5", value: 5000 },
        ],
    },
    contacts: {
        primaryColor: "#52C41A",
        secondaryColor: "#D9F7BE",
        icon: (
            <IconWrapper color="#F6FFED">
                <TeamOutlined
                    className="md"
                    style={{
                        color: "#52C41A",
                    }}
                />
            </IconWrapper>
        ),
        title: "Number of contacts",
        data: [
            { index: "1", value: 10000 },
            { index: "2", value: 19500 },
            { index: "3", value: 13000 },
            { index: "4", value: 17000 },
            { index: "5", value: 13000 },
            { index: "6", value: 20000 },
        ],
    },
    deals: {
        primaryColor: "#FA541C",
        secondaryColor: "#FFD8BF",
        icon: (
            <IconWrapper color="#FFF2E8">
                <AuditOutlined
                    className="md"
                    style={{
                        color: "#FA541C",
                    }}
                />
            </IconWrapper>
        ),
        title: "Total deals in pipeline",
        data: [
            { index: "1", value: 1000 },
            { index: "2", value: 1300 },
            { index: "3", value: 1200 },
            { index: "4", value: 2000 },
            { index: "5", value: 800 },
            { index: "6", value: 1700 },
            { index: "7", value: 1400 },
            { index: "8", value: 1800 },
        ],
    },
};

In the above code, the component fetches the data from the API and renders the metric card with the data and chart.

For fetching the data, we used the useCustom hook, and we passed the raw query using the meta.rawQuery property. When we pass the raw query by meta.rawQuery prop, @refinedev/nestjs-graphql data provider will pass it to the GraphQL API as it is. This is useful when you want to use some advanced features of the GraphQL API.

We also used the <Area /> component from the @ant-design/plots package to render the chart. We passed the config object to the <Area /> component to configure the chart.

Now, let’s update the <Dashboard /> component to use the <MetricCard /> component we created.

src/pages/dashboard/index.tsx
import { Row, Col } from "antd";
import { MetricCard } from "../../components/metricCard";
export const Dashboard = () => {
    return (
        <Row gutter={[32, 32]}>
            <Col xs={24} sm={24} xl={8}>
                <MetricCard variant="companies" />
            </Col>
            <Col xs={24} sm={24} xl={8}>
                <MetricCard variant="contacts" />
            </Col>
            <Col xs={24} sm={24} xl={8}>
                <MetricCard variant="deals" />
            </Col>
        </Row>
    );
};

If you navigate to the "/" path, you should see the updated dashboard page.

Dashboard page with MetricCard components

Creating DealChart component

We’ll add charts to the dashboard to show the deals summary by creating a <DealChart /> component. We’ll use the Ant Design Charts to build the chart and Ant Design components to build the card.

First, install the dayjs package for managing the dates.

npm install dayjs

Create a src/components/dealChart/index.tsx file with the following code:

Show <DealChart /> component
src/components/dealChart/index.tsx
import React, { useMemo } from "react";
import { useList } from "@refinedev/core";
import { Card, Typography } from "antd";
import { Area, AreaConfig } from "@ant-design/plots";
import { DollarOutlined } from "@ant-design/icons";
import dayjs from "dayjs";

export const DealChart: React.FC<{}> = () => {
    const { data } = useList({
        resource: "dealStages",
        filters: [{ field: "title", operator: "in", value: ["WON", "LOST"] }],
        meta: {
            fields: [
                "title",
                {
                    dealsAggregate: [
                        { groupBy: ["closeDateMonth", "closeDateYear"] },
                        { sum: ["value"] },
                    ],
                },
            ],
        },
    });

    const dealData = useMemo(() => {
        const won = data?.data
            .find((node) => node.title === "WON")
            ?.dealsAggregate.map((item: any) => {
                const { closeDateMonth, closeDateYear } = item.groupBy!;
                const date = dayjs(`${closeDateYear}-${closeDateMonth}-01`);
                return {
                    timeUnix: date.unix(),
                    timeText: date.format("MMM YYYY"),
                    value: item.sum?.value,
                    state: "Won",
                };
            });

        const lost = data?.data
            .find((node) => node.title === "LOST")
            ?.dealsAggregate.map((item: any) => {
                const { closeDateMonth, closeDateYear } = item.groupBy!;
                const date = dayjs(`${closeDateYear}-${closeDateMonth}-01`);
                return {
                    timeUnix: date.unix(),
                    timeText: date.format("MMM YYYY"),
                    value: item.sum?.value,
                    state: "Lost",
                };
            });

        return [...(won || []), ...(lost || [])].sort(
            (a, b) => a.timeUnix - b.timeUnix,
        );
    }, [data]);

    const config: AreaConfig = {
        isStack: false,
        data: dealData,
        xField: "timeText",
        yField: "value",
        seriesField: "state",
        animation: true,
        startOnZero: false,
        smooth: true,
        legend: { offsetY: -6 },
        yAxis: {
            tickCount: 4,
            label: {
                formatter: (v) => `$${Number(v) / 1000}k`,
            },
        },
        tooltip: {
            formatter: (data) => ({
                name: data.state,
                value: `$${Number(data.value) / 1000}k`,
            }),
        },
        areaStyle: (datum) => {
            const won = "l(270) 0:#ffffff 0.5:#b7eb8f 1:#52c41a";
            const lost = "l(270) 0:#ffffff 0.5:#f3b7c2 1:#ff4d4f";
            return { fill: datum.state === "Won" ? won : lost };
        },
        color: (datum) => (datum.state === "Won" ? "#52C41A" : "#F5222D"),
    };

    return (
        <Card
            style={{ height: "432px" }}
            headStyle={{ padding: "8px 16px" }}
            bodyStyle={{ padding: "24px 24px 0px 24px" }}
            title={
                <div
                    style={{
                        display: "flex",
                        alignItems: "center",
                        gap: "8px",
                    }}
                >
                    <DollarOutlined />
                    <Typography.Text style={{ marginLeft: ".5rem" }}>
                        Deals
                    </Typography.Text>
                </div>
            }
        >
            <Area {...config} height={325} />
        </Card>
    );
};

In the above code, similar to the <MetricCard /> component, we used the useCustom hook to fetch the data from the GraphQL API and render the chart with the data.

After fetching the data, we grouped the data by the date fields and “LOST” and “WON” deal stages. Then, we passed the grouped data to the data property of the <Area /> component.

Now, let’s update the <Dashboard /> component to use the <DealChart /> component we created.

src/pages/dashboard/index.tsx
import { Row, Col } from "antd";
import { MetricCard } from "../../components/metricCard";
import { DealChart } from "../../components/dealChart";

export const Dashboard = () => {
    return (
        <Row gutter={[32, 32]}>
            <Col xs={24} sm={24} xl={8}>
                <MetricCard variant="companies" />
            </Col>
            <Col xs={24} sm={24} xl={8}>
                <MetricCard variant="contacts" />
            </Col>
            <Col xs={24} sm={24} xl={8}>
                <MetricCard variant="deals" />
            </Col>
            <Col span={24}>
                <DealChart />
            </Col>
        </Row>
    );
};

The charts will look like below.

Dashboard page with DealChart component

Step 4 — Building Company CRUD pages

In this phase, we’re going to develop the ‘list,’ ‘create,’ ‘edit,’ and ‘show’ pages for companies. However, before we start working on these pages, we should first update the <Refine /> component to include the ‘companies’ resource.

Show <App /> code
src/App.tsx
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, useNotificationProvider } from "@refinedev/antd";
import dataProvider, {
    GraphQLClient,
    liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
    UnsavedChangesNotifier,
    DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { DashboardOutlined, ShopOutlined} from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";

import "@refinedev/antd/dist/reset.css";

const API_URL = "https://api.crm.refine.dev/graphql";
const WS_URL = "wss://api.crm.refine.dev/graphql";
const ACCESS_TOKEN =    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw";

const gqlClient = new GraphQLClient(API_URL, {
    headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
    },
});

const wsClient = createClient({
    url: WS_URL,
    connectionParams: () => ({
        headers: {
            Authorization: `Bearer ${ACCESS_TOKEN}`,
        },
    }),
});

function App() {
    return (
        <BrowserRouter>
            <RefineKbarProvider>
                <ColorModeContextProvider>
                    <Refine
                        dataProvider={dataProvider(gqlClient)}
                        liveProvider={liveProvider(wsClient)}
                        notificationProvider={useNotificationProvider}
                        routerProvider={routerBindings}
                        resources={[
                            {
                                name: "dashboard",
                                list: "/",
                                meta: {
                                    icon: <DashboardOutlined />,
                                },
                            },
                            {
                                name: "companies",
                                list: "/companies",
                                create: "/companies/create",
                                edit: "/companies/edit/:id",
                                show: "/companies/show/:id",
                                meta: {
                                    canDelete: true,
                                    icon: <ShopOutlined />,
                                },
                            },
                        ]}
                        options={{
                            syncWithLocation: true,
                            warnWhenUnsavedChanges: true,
                            liveMode: "auto",
                        }}
                    >
                        <Routes>
                            <Route
                                element={
                                    <ThemedLayoutV2>
                                        <Outlet />
                                    </ThemedLayoutV2>
                                }
                            >
                                <Route path="/">
                                    <Route index element={<Dashboard />} />
                                </Route>
                            </Route>
                        </Routes>
                        <RefineKbar />
                        <UnsavedChangesNotifier />
                        <DocumentTitleHandler />
                    </Refine>
                </ColorModeContextProvider>
            </RefineKbarProvider>
        </BrowserRouter>
    );
}

export default App;

The resource definition mentioned doesn’t actually create any CRUD pages itself. Instead, it establishes the routes that these CRUD pages will follow. These routes are essential for ensuring the proper functionality of various Refine hooks and components.

For instance, the useNavigation hook relies on these routes (list, create, edit, and show) to help users navigate between different pages in your application. Additionally, certain data hooks, like useTable, will automatically use the resource name if you don’t explicitly provide the resource prop.

List Page

The List page will display company data in a table. To fetch the data, we’ll use the useTable hook from @refinedev/antd package, and to render the table, we’ll use the <Table /> component from the antd.

Let’s create a src/pages/companies/list.tsx file with the following code:

Show <CompanyList /> component
src/pages/companies/list.tsx
import React from "react";
import { IResourceComponentsProps, BaseRecord } from "@refinedev/core";
import {
    useTable,
    List,
    EditButton,
    ShowButton,
    DeleteButton,
    UrlField,
    TextField,
} from "@refinedev/antd";
import { Table, Space, Avatar, Input, Form } from "antd";

export const CompanyList: React.FC<IResourceComponentsProps> = () => {
    const { tableProps, searchFormProps } = useTable({
        meta: {
            fields: [
                "id",
                "avatarUrl",
                "name",
                "businessType",
                "companySize",
                "country",
                "website",
                { salesOwner: ["id", "name"] },
            ],
        },
        onSearch: (params: { name: string }) => [
            {
                field: "name",
                operator: "contains",
                value: params.name,
            },
        ],
    });

    return (
        <List
            headerButtons={({ defaultButtons }) => (
                <>
                    <Form
                        {...searchFormProps}
                        onValuesChange={() => {
                            searchFormProps.form?.submit();
                        }}
                    >
                        <Form.Item noStyle name="name">
                            <Input.Search placeholder="Search by name" />
                        </Form.Item>
                    </Form>
                    {defaultButtons}
                </>
            )}
        >
            <Table {...tableProps} rowKey="id">
                <Table.Column
                    title="Name"
                    render={(
                        _,
                        record: { name: string; avatarUrl: string },
                    ) => (
                        <Space>
                            <Avatar
                                src={record.avatarUrl}
                                size="large"
                                shape="square"
                                alt={record.name}
                            />
                            <TextField value={record.name} />
                        </Space>
                    )}
                />
                <Table.Column dataIndex="businessType" title="Type" />
                <Table.Column dataIndex="companySize" title="Size" />
                <Table.Column dataIndex="country" title="Country" />
                <Table.Column
                    dataIndex={["website"]}
                    title="Website"
                    render={(value: string) => <UrlField value={value} />}
                />
                <Table.Column
                    dataIndex={["salesOwner", "name"]}
                    title="Sales Owner"
                />
                <Table.Column
                    title="Actions"
                    dataIndex="actions"
                    render={(_, record: BaseRecord) => (
                        <Space>
                            <EditButton
                                hideText
                                size="small"
                                recordItemId={record.id}
                            />
                            <ShowButton
                                hideText
                                size="small"
                                recordItemId={record.id}
                            />
                            <DeleteButton
                                hideText
                                size="small"
                                recordItemId={record.id}
                            />
                        </Space>
                    )}
                />
            </Table>
        </List>
    );
};

We fetched data using the useTable hook and specified the fields to retrieve by setting them in the meta.fields property. This data was then displayed in a table format using the <Table /> component.

For a better understanding of how GraphQL queries are formulated, you can refer to the GraphQL guide in the documentation.

Our table includes various columns like company name, business type, size, country, website, and sales owner. We followed the guidelines from the Ant Design Table for setting up these columns and used specific components like <TextField /> and <UrlField /> from @refinedev/antd and <Avatar /> from antd for customization.

We added functionality with <EditButton />, <ShowButton />, and <DeleteButton /> components for different actions on the records.

For filtering, we utilized the useTable features, implementing a search form with the <Form /> component. User searches trigger through the onSearch prop of useTable.

To compile the company CRUD pages, we need to create an index.ts file in the src/pages/companies directory, following the provided code.

export * from "./list";

Next, import the <CompanyList /> component in src/App.tsx and add a route for rendering it.

Show <App /> code
src/App.tsx
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, useNotificationProvider } from "@refinedev/antd";
import dataProvider, {
    GraphQLClient,
    liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
    UnsavedChangesNotifier,
    DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { DashboardOutlined, ShopOutlined } from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import { CompanyList } from "./pages/companies";

import "@refinedev/antd/dist/reset.css";

const API_URL = 'https://api.crm.refine.dev/graphql';
const WS_URL = 'wss://api.crm.refine.dev/graphql';
const ACCESS_TOKEN =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw';

const gqlClient = new GraphQLClient(API_URL, {
    headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
    },
});

const wsClient = createClient({
    url: WS_URL,
    connectionParams: () => ({
        headers: {
            Authorization: `Bearer ${ACCESS_TOKEN}`,
        },
    }),
});

function App() {
    return (
        <BrowserRouter>
            <RefineKbarProvider>
                <ColorModeContextProvider>
                    <Refine
                        dataProvider={dataProvider(gqlClient)}
                        liveProvider={liveProvider(wsClient)}
                        notificationProvider={useNotificationProvider}
                        routerProvider={routerBindings}
                        resources={[
                            {
                                name: "dashboard",
                                list: "/",
                                meta: {
                                    icon: <DashboardOutlined />,
                                },
                            },
                            {
                                name: "companies",
                                list: "/companies",
                                create: "/companies/create",
                                edit: "/companies/edit/:id",
                                show: "/companies/show/:id",
                                meta: {
                                    canDelete: true,
                                    icon: <ShopOutlined />,
                                },
                            },
                        ]}
                        options={{
                            syncWithLocation: true,
                            warnWhenUnsavedChanges: true,
                            liveMode: "auto",
                        }}
                    >
                        <Routes>
                            <Route
                                element={
                                    <ThemedLayoutV2>
                                        <Outlet />
                                    </ThemedLayoutV2>
                                }
                            >
                                <Route path="/">
                                    <Route index element={<Dashboard />} />
                                </Route>
                                <Route path="/companies">
                                    <Route index element={<CompanyList />} />
                                </Route>
                            </Route>
                        </Routes>
                        <RefineKbar />
                        <UnsavedChangesNotifier />
                        <DocumentTitleHandler />
                    </Refine>
                </ColorModeContextProvider>
            </RefineKbarProvider>
        </BrowserRouter>
    );
}

export default App;

Now, if you navigate to the “/companies” path, you should see the list page.

Companies list page

Create Page

The Create page will show a form to create a new company record.

We will use the <Form /> component, and for managing form submissions, the useForm hook will be utilized.

Let’s create a src/pages/companies/create.tsx file with the following code:

Show <CompanyCreate /> component
src/pages/companies/create.tsx
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const CompanyCreate: React.FC<IResourceComponentsProps> = () => {
    const { formProps, saveButtonProps } = useForm();

    const { selectProps } = useSelect({
        resource: "users",
        meta: {
            fields: ["name", "id"],
        },
        optionLabel: "name",
    });

    return (
        <Create saveButtonProps={saveButtonProps}>
            <Form {...formProps} layout="vertical">
                <Form.Item
                    label="Name"
                    name={["name"]}
                    rules={[{ required: true }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item
                    label="Sales Owner"
                    name="salesOwnerId"
                    rules={[{ required: true }]}
                >
                    <Select {...selectProps} />
                </Form.Item>
                <Form.Item label="Business Type" name={["businessType"]}>
                    <Select
                        options={[
                            { label: "B2B", value: "B2B" },
                            { label: "B2C", value: "B2C" },
                            { label: "B2G", value: "B2G" },
                        ]}
                    />
                </Form.Item>
                <Form.Item label="Company Size" name={["companySize"]}>
                    <Select
                        options={[
                            { label: "Enterprise", value: "ENTERPRISE" },
                            { label: "Large", value: "LARGE" },
                            { label: "Medium", value: "MEDIUM" },
                            { label: "Small", value: "SMALL" },
                        ]}
                    />
                </Form.Item>
                <Form.Item label="Country" name={["country"]}>
                    <Input />
                </Form.Item>
                <Form.Item label="Website" name={["website"]}>
                    <Input />
                </Form.Item>
            </Form>
        </Create>
    );
};

We used the useSelect hook to fetch relationship data. We passed the selectProps to the <Select /> component to render the options.

You might have noticed that we didn’t use the meta.fields prop in the useForm hook. This is because we don’t need to retrieve data of the record that was just created after submitting the form. However, if you require this data, you can include the meta.fields prop.

To export the component, let’s update the src/pages/company/index.ts file with the following code:

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

Next, import the <CompanyCreate /> component in src/App.tsx and add a route for rendering it.

Show <App /> code

The highlighted code shows the new code. You can copy and paste the highlighted code to your src/App.tsx file.

src/App.tsx
//...
import { CompanyList, CompanyCreate } from "./pages/companies";
function App() {
  return (
    //...
    <Refine
      //...
    >
      <Routes>
        <Route
          element={
            <ThemedLayoutV2>
              <Outlet />
            </ThemedLayoutV2>
          }
        >
            //...
            <Route path="/companies">
                <Route index element={<CompanyList />} />
                <Route path="create" element={<CompanyCreate />} />
            </Route>
        </Route>
      </Routes>
      // ...
    </Refine>
    //...
  );
}
export default App;

Now, if you navigate to the “/companies/create” path, you should see the create page.

Company Create Page

Edit Page

The edit page will show a form to edit an existing company record. The form will be the same as the create page. However, it will be filled with the existing record data.

Let’s create a src/pages/companies/edit.tsx file with the following code:

Show <CompanyEdit /> component
src/pages/companies/edit.tsx
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const CompanyEdit: React.FC<IResourceComponentsProps> = () => {
    const { formProps, saveButtonProps } = useForm({
        meta: {
            fields: [
                "id",
                "name",
                "businessType",
                "companySize",
                "country",
                "website",
            ],
        },
    });

    return (
        <Edit saveButtonProps={saveButtonProps}>
            <Form {...formProps} layout="vertical">
                <Form.Item
                    label="Name"
                    name={["name"]}
                    rules={[{ required: true }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item label="Business Type" name={["businessType"]}>
                    <Select
                        options={[
                            { label: "B2B", value: "B2B" },
                            { label: "B2C", value: "B2C" },
                            { label: "B2G", value: "B2G" },
                        ]}
                    />
                </Form.Item>

                <Form.Item label="Company Size" name={["companySize"]}>
                    <Select
                        options={[
                            { label: "Enterprise", value: "ENTERPRISE" },
                            { label: "Large", value: "LARGE" },
                            { label: "Medium", value: "MEDIUM" },
                            { label: "Small", value: "SMALL" },
                        ]}
                    />
                </Form.Item>
                <Form.Item label="Country" name={["country"]}>
                    <Input />
                </Form.Item>
                <Form.Item label="Website" name={["website"]}>
                    <Input />
                </Form.Item>
            </Form>
        </Edit>
    );
};

We used the useForm hook to handle the form submission. We passed the formProps and saveButtonProps to the <Form /> component and <Edit /> component respectively.

We specified the fields we wanted to fill the form with by passing them to the meta.fields property.

To render the form fields, we used input components from the antd library.

To export the component, let’s update the src/pages/company/index.ts file with the following code:

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

Next, import the <CompanyEdit /> component in src/App.tsx and add a route for rendering it.

Show <App /> code

The highlighted code shows the new code. You can copy and paste the highlighted code to your src/App.tsx file.

src/App.tsx
//...
import { CompanyList, CompanyCreate, CompanyEdit } from "./pages/companies";

function App() {
  return (
    //...
    <Refine
    //...
    >
      <Routes>
        <Route
          element={
            <ThemedLayoutV2>
              <Outlet />
            </ThemedLayoutV2>
          }
        >
          //...

          <Route path="/companies">
            <Route index element={<CompanyList />} />
            <Route path="create" element={<CompanyCreate />} />
            <Route path="edit/:id" element={<CompanyEdit />} />
          </Route>
        </Route>
      </Routes>
      // ...
    </Refine>
  );
}

export default App;

Now, if you navigate to the “/companies/:id/edit” path, you should see the edit page.

Company edit page

Show Page

The show page will show the details of an existing company record.

Let’s create a src/pages/companies/show.tsx file with the following code:

Show <CompanyShow /> component
src/pages/companies/show.tsx
import React from "react";
import { IResourceComponentsProps, useShow } from "@refinedev/core";
import { Show, NumberField, TextField } from "@refinedev/antd";
import { Typography } from "antd";

const { Title } = Typography;

export const CompanyShow: React.FC<IResourceComponentsProps> = () => {
    const { queryResult } = useShow({
        meta: {
            fields: [
                "id",
                "name",
                "businessType",
                "companySize",
                "country",
                "website",
            ],
        },
    });
    const { data, isLoading } = queryResult;

    const record = data?.data;

    return (
        <Show isLoading={isLoading}>
            <Title level={5}>Id</Title>
            <NumberField value={record?.id ?? ""} />
            <Title level={5}>Name</Title>
            <TextField value={record?.name} />
            <Title level={5}>Business Type</Title>
            <TextField value={record?.businessType} />
            <Title level={5}>Company Size</Title>
            <TextField value={record?.companySize} />
            <Title level={5}>Country</Title>
            <TextField value={record?.country} />
            <Title level={5}>Website</Title>
            <TextField value={record?.website} />
        </Show>
    );
};

To fetch the data, we’ll use the useShow hook. Again, we specified the fields we wanted to fetch by passing them to the meta.fields property. We then passed the resulting queryResult.isLoading to the <Show /> component to show the loading indicator while fetching the data.

To render the record data, we used the <NumberField /> and <TextField /> components from the @refinedev/antd package.

To export the component, let’s update the src/pages/company/index.ts file with the following code:

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

Next, import the <CompanyShow /> component in src/App.tsx and add a route for rendering it.

Show <App /> code

The highlighted code shows the new code. You can copy and paste the highlighted code to your src/App.tsx file.

src/App.tsx
//...
import {
    CompanyList,
    CompanyCreate,
    CompanyEdit,
    CompanyShow,
} from "./pages/companies";

function App() {
  return (
    //...
    <Refine
    //...
    >
      <Routes>
        <Route
          element={
            <ThemedLayoutV2>
              <Outlet />
            </ThemedLayoutV2>
          }
        >
          //...

          <Route path="/companies">
            <Route index element={<CompanyList />} />
            <Route path="create" element={<CompanyCreate />} />
            <Route path="edit/:id" element={<CompanyEdit />} />
            <Route path="show/:id" element={<CompanyShow />} />
          </Route>
        </Route>
      </Routes>
      // ...
    </Refine>
  );
}

export default App;

Now, if you navigate to the “/companies/show/:id” path, you should see the show page.

Company show page

Step 5 — Building Contacts CRUD pages

In the previous step, we built the company CRUD pages. In this step, we’ll build the contact CRUD pages similarly. So, we won’t repeat the explanations we made in the previous step. If you need more information about the steps, you can refer to the previous step.

Let’s start by defining the contact resource in src/App.tsx file as follows:

Show <App /> code
src/App.tsx
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, useNotificationProvider } from "@refinedev/antd";
import dataProvider, {
    GraphQLClient,
    liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
    UnsavedChangesNotifier,
    DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import {
    DashboardOutlined,
    ShopOutlined,
    TeamOutlined,
} from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import {
    CompanyList,
    CompanyCreate,
    CompanyEdit,
    CompanyShow,
} from "./pages/companies";

import "@refinedev/antd/dist/reset.css";

const API_URL = 'https://api.crm.refine.dev/graphql';
const WS_URL = 'wss://api.crm.refine.dev/graphql';
const ACCESS_TOKEN =  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw';

const gqlClient = new GraphQLClient(API_URL, {
    headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
    },
});

const wsClient = createClient({
    url: WS_URL,
    connectionParams: () => ({
        headers: {
            Authorization: `Bearer ${ACCESS_TOKEN}`,
        },
    }),
});


function App() {
    return (
        <BrowserRouter>
            <RefineKbarProvider>
                <ColorModeContextProvider>
                    <Refine
                        dataProvider={dataProvider(gqlClient)}
                        liveProvider={liveProvider(wsClient)}
                        notificationProvider={useNotificationProvider}
                        routerProvider={routerBindings}
                        resources={[
                            {
                                name: "dashboard",
                                list: "/",
                                meta: {
                                    icon: <DashboardOutlined />,
                                },
                            },
                            {
                                name: "companies",
                                list: "/companies",
                                create: "/companies/create",
                                edit: "/companies/edit/:id",
                                show: "/companies/show/:id",
                                meta: {
                                    canDelete: true,
                                    icon: <ShopOutlined />,
                                },
                            },
                            {
                                name: "contacts",
                                list: "/contacts",
                                create: "/contacts/create",
                                edit: "/contacts/edit/:id",
                                show: "/contacts/show/:id",
                                meta: {
                                    canDelete: true,
                                    icon: <TeamOutlined />,
                                },
                            },
                        ]}
                        options={{
                            syncWithLocation: true,
                            warnWhenUnsavedChanges: true,
                            liveMode: "auto",
                        }}
                    >
                        <Routes>
                            <Route
                                element={
                                    <ThemedLayoutV2>
                                        <Outlet />
                                    </ThemedLayoutV2>
                                }
                            >
                                <Route path="/">
                                    <Route index element={<Dashboard />} />
                                </Route>

                                <Route path="/companies">
                                    <Route index element={<CompanyList />} />
                                    <Route
                                        path="create"
                                        element={<CompanyCreate />}
                                    />
                                    <Route
                                        path="edit/:id"
                                        element={<CompanyEdit />}
                                    />
                                    <Route
                                        path="show/:id"
                                        element={<CompanyShow />}
                                    />
                                </Route>
                            </Route>
                        </Routes>
                        <RefineKbar />
                        <UnsavedChangesNotifier />
                        <DocumentTitleHandler />
                    </Refine>
                </ColorModeContextProvider>
            </RefineKbarProvider>
        </BrowserRouter>
    );
}

export default App;

Let’s create CRUD pages for the contact resource as follows:

Create src/pages/contacts/list.tsx file with the following code:

Show <ContactList /> component
src/pages/contacts/list.tsx
import React from "react";
import { IResourceComponentsProps, BaseRecord } from "@refinedev/core";
import {
    useTable,
    List,
    EditButton,
    ShowButton,
    DeleteButton,
    EmailField,
    TextField,
} from "@refinedev/antd";
import { Table, Space, Avatar, Form, Input } from "antd";

export const ContactList: React.FC<IResourceComponentsProps> = () => {
    const { tableProps, searchFormProps } = useTable({
        meta: {
            fields: [
                "avatarUrl",
                "id",
                "name",
                "email",
                { company: ["id", "name"] },
                "jobTitle",
                "phone",
                "status",
            ],
        },
        onSearch: (params: { name: string }) => [
            {
                field: "name",
                operator: "contains",
                value: params.name,
            },
        ],
    });

    return (
        <List
            headerButtons={({ defaultButtons }) => (
                <>
                    <Form
                        {...searchFormProps}
                        onValuesChange={() => {
                            searchFormProps.form?.submit();
                        }}
                    >
                        <Form.Item noStyle name="name">
                            <Input.Search placeholder="Search by name" />
                        </Form.Item>
                    </Form>
                    {defaultButtons}
                </>
            )}
        >
            <Table {...tableProps} rowKey="id">
                <Table.Column
                    title="Name"
                    width={200}
                    render={(
                        _,
                        record: { name: string; avatarUrl: string },
                    ) => (
                        <Space>
                            <Avatar src={record.avatarUrl} alt={record.name} />
                            <TextField value={record.name} />
                        </Space>
                    )}
                />
                <Table.Column dataIndex={["company", "name"]} title="Company" />
                <Table.Column dataIndex="jobTitle" title="Job Title" />
                <Table.Column
                    dataIndex={["email"]}
                    title="Email"
                    render={(value) => <EmailField value={value} />}
                />
                <Table.Column dataIndex="phone" title="Phone" />
                <Table.Column dataIndex="status" title="Status" />
                <Table.Column
                    title="Actions"
                    dataIndex="actions"
                    render={(_, record: BaseRecord) => (
                        <Space>
                            <EditButton
                                hideText
                                size="small"
                                recordItemId={record.id}
                            />
                            <ShowButton
                                hideText
                                size="small"
                                recordItemId={record.id}
                            />
                            <DeleteButton
                                hideText
                                size="small"
                                recordItemId={record.id}
                            />
                        </Space>
                    )}
                />
            </Table>
        </List>
    );
};

Create src/pages/contacts/create.tsx file with the following code:

Show <ContactCreate /> component
src/pages/contacts/create.tsx
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const ContactCreate: React.FC<IResourceComponentsProps> = () => {
    const { formProps, saveButtonProps } = useForm();

    const { selectProps: companySelectProps } = useSelect({
        resource: "companies",
        optionLabel: "name",
        meta: {
            fields: ["id", "name"],
        },
    });

    const { selectProps: salesOwnerSelectProps } = useSelect({
        resource: "users",
        meta: {
            fields: ["name", "id"],
        },
        optionLabel: "name",
    });

    return (
        <Create saveButtonProps={saveButtonProps}>
            <Form {...formProps} layout="vertical">
                <Form.Item
                    label="Name"
                    name={["name"]}
                    rules={[{ required: true }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item
                    label="Email"
                    name={["email"]}
                    rules={[{ required: true }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item
                    label="Company"
                    name={["companyId"]}
                    rules={[{ required: true }]}
                >
                    <Select {...companySelectProps} />
                </Form.Item>
                <Form.Item
                    label="Sales Owner"
                    name="salesOwnerId"
                    rules={[{ required: true }]}
                >
                    <Select {...salesOwnerSelectProps} />
                </Form.Item>
                <Form.Item label="Job Title" name={["jobTitle"]}>
                    <Input />
                </Form.Item>
                <Form.Item label="Phone" name={["phone"]}>
                    <Input />
                </Form.Item>
                <Form.Item label="Status" name={["status"]}>
                    <Select
                        options={[
                            { label: "NEW", value: "NEW" },
                            { label: "CONTACTED", value: "CONTACTED" },
                            { label: "INTERESTED", value: "INTERESTED" },
                            { label: "UNQUALIFIED", value: "UNQUALIFIED" },
                            { label: "QUALIFIED", value: "QUALIFIED" },
                            { label: "NEGOTIATION", value: "NEGOTIATION" },
                            { label: "LOST", value: "LOST" },
                            { label: "WON", value: "WON" },
                            { label: "CHURNED", value: "CHURNED" },
                        ]}
                    />
                </Form.Item>
            </Form>
        </Create>
    );
};

Create src/pages/contacts/edit.tsx file with the following code:

Show <ContactEdit /> component
src/pages/contacts/edit.tsx
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const ContactEdit: React.FC<IResourceComponentsProps> = () => {
    const { formProps, saveButtonProps } = useForm({
        meta: {
            fields: [
                "avatarUrl",
                "id",
                "name",
                "email",
                "jobTitle",
                "phone",
                "status",
            ],
        },
    });

    return (
        <Edit saveButtonProps={saveButtonProps}>
            <Form {...formProps} layout="vertical">
                <Form.Item
                    label="Name"
                    name={["name"]}
                    rules={[{ required: true }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item
                    label="Email"
                    name={["email"]}
                    rules={[{ required: true }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item label="Job Title" name={["jobTitle"]}>
                    <Input />
                </Form.Item>
                <Form.Item label="Phone" name={["phone"]}>
                    <Input />
                </Form.Item>
                <Form.Item label="Status" name={["status"]}>
                    <Select
                        options={[
                            { label: "NEW", value: "NEW" },
                            { label: "CONTACTED", value: "CONTACTED" },
                            { label: "INTERESTED", value: "INTERESTED" },
                            { label: "UNQUALIFIED", value: "UNQUALIFIED" },
                            { label: "QUALIFIED", value: "QUALIFIED" },
                            { label: "NEGOTIATION", value: "NEGOTIATION" },
                            { label: "LOST", value: "LOST" },
                            { label: "WON", value: "WON" },
                            { label: "CHURNED", value: "CHURNED" },
                        ]}
                    />
                </Form.Item>
            </Form>
        </Edit>
    );
};

Create src/pages/contacts/show.tsx file with the following code:

Show <ContactShow /> component
src/pages/contacts/show.tsx
import React from "react";
import { IResourceComponentsProps, useShow } from "@refinedev/core";
import { Show, NumberField, TextField, EmailField } from "@refinedev/antd";
import { Typography } from "antd";

const { Title } = Typography;

export const ContactShow: React.FC<IResourceComponentsProps> = () => {
    const { queryResult } = useShow({
        meta: {
            fields: [
                "id",
                "name",
                "email",
                { company: ["id", "name"] },
                "jobTitle",
                "phone",
                "status",
            ],
        },
    });
    const { data, isLoading } = queryResult;
    const record = data?.data;

    return (
        <Show isLoading={isLoading}>
            <Title level={5}>Id</Title>
            <NumberField value={record?.id ?? ""} />
            <Title level={5}>Name</Title>
            <TextField value={record?.name} />
            <Title level={5}>Email</Title>
            <EmailField value={record?.email} />
            <Title level={5}>Company</Title>
            <TextField value={record?.company?.name} />
            <Title level={5}>Job Title</Title>
            <TextField value={record?.jobTitle} />
            <Title level={5}>Phone</Title>
            <TextField value={record?.phone} />
            <Title level={5}>Status</Title>
            <TextField value={record?.status} />
        </Show>
    );
};

After creating the CRUD pages, let’s create a src/pages/contacts/index.ts file to export the pages as follows:

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

To render the contact CRUD pages, let’s update the src/App.tsx file with the following code:

Show <App /> code
src/App.tsx
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, notificationProvider } from "@refinedev/antd";
import dataProvider, {
    GraphQLClient,
    liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
    UnsavedChangesNotifier,
    DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import {
    DashboardOutlined,
    ShopOutlined,
    TeamOutlined,
} from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import {
    CompanyList,
    CompanyCreate,
    CompanyEdit,
    CompanyShow,
} from "./pages/companies";
import {
    ContactList,
    ContactCreate,
    ContactEdit,
    ContactShow,
} from "./pages/contacts";

import "@refinedev/antd/dist/reset.css";

const API_URL = "https://api.crm.refine.dev/graphql";
const WS_URL = "wss://api.crm.refine.dev/graphql";

const gqlClient = new GraphQLClient(API_URL, {
    headers: {
        Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw`,
    },
});
const wsClient = createClient({ url: WS_URL });

function App() {
    return (
        <BrowserRouter>
            <RefineKbarProvider>
                <ColorModeContextProvider>
                    <Refine
                        dataProvider={dataProvider(gqlClient)}
                        liveProvider={liveProvider(wsClient)}
                        notificationProvider={notificationProvider}
                        routerProvider={routerBindings}
                        resources={[
                            {
                                name: "dashboard",
                                list: "/",
                                meta: {
                                    icon: <DashboardOutlined />,
                                },
                            },
                            {
                                name: "companies",
                                list: "/companies",
                                create: "/companies/create",
                                edit: "/companies/edit/:id",
                                show: "/companies/show/:id",
                                meta: {
                                    canDelete: true,
                                    icon: <ShopOutlined />,
                                },
                            },
                            {
                                name: "contacts",
                                list: "/contacts",
                                create: "/contacts/create",
                                edit: "/contacts/edit/:id",
                                show: "/contacts/show/:id",
                                meta: {
                                    canDelete: true,
                                    icon: <TeamOutlined />,
                                },
                            },
                        ]}
                        options={{
                            syncWithLocation: true,
                            warnWhenUnsavedChanges: true,
                            liveMode: "auto",
                        }}
                    >
                        <Routes>
                            <Route
                                element={
                                    <ThemedLayoutV2>
                                        <Outlet />
                                    </ThemedLayoutV2>
                                }
                            >
                                <Route path="/">
                                    <Route index element={<Dashboard />} />
                                </Route>
                                <Route path="/companies">
                                    <Route index element={<CompanyList />} />
                                    <Route
                                        path="create"
                                        element={<CompanyCreate />}
                                    />
                                    <Route
                                        path="edit/:id"
                                        element={<CompanyEdit />}
                                    />
                                    <Route
                                        path="show/:id"
                                        element={<CompanyShow />}
                                    />
                                </Route>
                                <Route path="/contacts">
                                    <Route index element={<ContactList />} />
                                    <Route
                                        path="create"
                                        element={<ContactCreate />}
                                    />
                                    <Route
                                        path="edit/:id"
                                        element={<ContactEdit />}
                                    />
                                    <Route
                                        path="show/:id"
                                        element={<ContactShow />}
                                    />
                                </Route>
                            </Route>
                        </Routes>
                        <RefineKbar />
                        <UnsavedChangesNotifier />
                        <DocumentTitleHandler />
                    </Refine>
                </ColorModeContextProvider>
            </RefineKbarProvider>
        </BrowserRouter>
    );
}
export default App;

After these changes, you should be able to navigate to the contact CRUD pages as below:

Slide #1Slide #2Slide #3Slide #4

Step 6 — Deploying to the DigitalOcean App platform

In this step, we’ll deploy the application to the DigitalOcean App Platform. To do that, we’ll host the source code on GitHub and connect the GitHub repository to the App Platform.

Pushing the Code to GitHub

Log in to your GitHub account and create a new repository named crm-app. You can make the repository public or private:

Create an new repository

After creating the repository, navigate to the project directory and run the following command to initialize a new Git repository:

git init

Next, add all the files to the Git repository with this command:

git add .

Then, commit the files with this command:

git commit -m "Initial commit"

Next, add the GitHub repository as a remote repository with this command:

git remote add origin <your-github-repository-url>

Next, specify that you want to push your code to the main branch with this command:

git branch -M main

Finally, push the code to the GitHub repository with this command:

git push -u origin main

When prompted, enter your GitHub credentials to push your code.

You’ll receive a success message after the code is pushed to the GitHub repository.

In this section, you pushed your project to GitHub so that you can access it using DigitalOcean Apps. The next step is to create a new DigitalOcean App using your project and set up automatic deployment.

Deploying to the DigitalOcean App Platform

In this step, you’ll take your React application and set it up on the DigitalOcean App Platform. You’ll link your GitHub repository to DigitalOcean, set up the building process, and create your initial project deployment. Once your project is live, any future changes you make will automatically trigger a new build and update.

By the end of this step, you’ll have successfully deployed your application on DigitalOcean with continuous delivery capabilities.

Log in to your DigitalOcean account and navigate to the Apps page. Click the Create App button:

Digital Ocean create a new app

If you haven’t connected your GitHub account to DigitalOcean, you’ll be prompted to do so. Click the Connect to GitHub button. A new window will open, asking you to authorize DigitalOcean to access your GitHub account.

After you authorize DigitalOcean, you’ll be redirected back to the DigitalOcean Apps page. The next step is to select your GitHub repository. After you select your repository, you’ll be prompted to select a branch to deploy. Select the main branch and click the Next button.

DigitalOcean select repository

After that, you’ll see the configuration steps for your application. In this tutorial, you can click the Next button to skip the configuration steps. However, you can also configure your application as you wish.

Wait for the build to complete. After the build is complete, press Live App to access your project in the browser. It will be the same as the project you tested locally, but this will be live on the web with a secure URL. Also, you can follow this tutorial available on the DigitalOcean community site to learn how to deploy react-based applications to App Platform.

DigitalOcean live preview

Conclusion

In this tutorial, we built a React CRM application using Refine from scratch and got familiar with how to build a fully functional CRUD app.

Also, we’ll demonstrate how to deploy your application to the DigitalOcean App Platform.

If you want to learn more about Refine, you can check out the documentation, and if you have any questions or feedback, you can join the Refine Discord Server.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Salih Özdemir, Software Engineer.

author


Default avatar

Sr Technical Writer

Sr. Technical Writer@ DigitalOcean | Medium Top Writers(AI & ChatGPT) | 2M+ monthly views & 34K Subscribers | Ex Cloud Consultant @ AMEX | Ex SRE(DevOps) @ NUTANIX


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.