Skip to main content
Refine/Blog
All posts
24 min read

Create Your Easy Customizable Internal Issue Tracker With Refine and Supabase

Create Your Easy Customizable Internal Issue Tracker With Refine and Supabase
Last updated at

In this article, we will make a customizable internal issue tracker web application with Supabase and Refine.

This web application will allow us to create issues and tasks for your team members. You will also be able to choose the priority of these tasks, their tags, and which person to assign them to.

We will use Supabase in the backend. Let's start by creating our Supabase account and tables.

Refine

Create Supabase Database

We have to go to Supabase and create an organization and database. Then open the SQL Editor and create our tables there.

The tables we need for this project are label, priority, status, users, and tasks. In the tasks table, label_id, priority_id, and status_id are bigint, and user_id is uuid because these fields are related to the other tables.

You can create these tables and their relations from the Supabase SQL Editor with a single query.

Supabase SQL Editor viewSupabase SQL Editor viewRunning the SQL query in Supabase SQL EditorRunning the SQL query in Supabase SQL Editor
Show SQL to copy into the Supabase SQL Editor
create extension if not exists "pgcrypto";

-- USERS
create table public.users (
id uuid primary key default gen_random_uuid(),
email text not null unique,
name text
);

-- LABELS
create table public.label (
id bigint generated by default as identity primary key,
title text not null unique,
color text not null
);

-- PRIORITIES
create table public.priority (
id bigint generated by default as identity primary key,
title text not null unique
);

-- STATUS
create table public.status (
id bigint generated by default as identity primary key,
title text not null unique
);

-- TASKS
create table public.tasks (
id bigint generated by default as identity primary key,
title text not null,
description text,
start_time date,
end_time date,
label_id bigint references public.label(id) on delete set null,
priority_id bigint references public.priority(id) on delete set null,
status_id bigint references public.status(id) on delete set null,
user_id uuid not null references public.users(id) on delete cascade
);

-- permissions
grant usage on schema public to anon, authenticated;
grant select, insert, update, delete on all tables in schema public to anon, authenticated;

-- RSL is disabled for simplicity, you can enable it and add the necessary policies for a production-ready application
alter table public.users disable row level security;
alter table public.label disable row level security;
alter table public.priority disable row level security;
alter table public.status disable row level security;
alter table public.tasks disable row level security;

-- seed users
insert into public.users (id, email, name) values
('11111111-1111-1111-1111-111111111111', 'admin@example.com', 'Admin User'),
('22222222-2222-2222-2222-222222222222', 'editor@example.com', 'Editor User');

-- seed labels
insert into public.label (title, color) values
('Work', '#3B82F6'),
('Personal', '#10B981'),
('Bug', '#EF4444'),
('Feature', '#8B5CF6');

-- seed priorities
insert into public.priority (title) values
('Low'),
('Medium'),
('High');

-- seed status
insert into public.status (title) values
('To Do'),
('In Progress'),
('Done');

-- seed tasks
insert into public.tasks
(title, description, start_time, end_time, label_id, priority_id, status_id, user_id)
values
(
'Update landing page',
'Revise the hero section and navigation bar',
'2026-03-18',
'2026-03-20',
1,
2,
1,
'11111111-1111-1111-1111-111111111111'
),
(
'Fix login bug',
'Investigate the session refresh issue',
'2026-03-18',
'2026-03-19',
3,
3,
2,
'22222222-2222-2222-2222-222222222222'
),
(
'Add dashboard chart',
'Create a weekly activity chart for the admin panel',
'2026-03-19',
'2026-03-22',
4,
2,
1,
'11111111-1111-1111-1111-111111111111'
);
Show schema reference: table columns, relationships, seed data, and the sample task

label

ColumnTypeConstraintsDescription
idbigintPK, identityUnique label identifier
titletextNOT NULL, UNIQUELabel title
colortextNOT NULLLabel color

priority

ColumnTypeConstraintsDescription
idbigintPK, identityUnique priority identifier
titletextNOT NULL, UNIQUEPriority title

status

ColumnTypeConstraintsDescription
idbigintPK, identityUnique status identifier
titletextNOT NULL, UNIQUEStatus title

users

ColumnTypeConstraintsDescription
iduuidPK, default gen_random_uuid()Unique user identifier
emailtextNOT NULL, UNIQUEUser email address

tasks

ColumnTypeConstraintsDescription
idbigintPK, identityUnique task identifier
titletextNOT NULLTask title
descriptiontextnullableTask description
start_timedatenullableStart date
end_timedatenullableEnd date
label_idbigintFK → label.id, ON DELETE SET NULLRelated label
priority_idbigintFK → priority.id, ON DELETE SET NULLRelated priority
status_idbigintFK → status.id, ON DELETE SET NULLRelated status
user_iduuidFK → users.id, ON DELETE CASCADEAssigned user

Relationships

Source TableSource ColumnTarget TableTarget ColumnRule
taskslabel_idlabelidON DELETE SET NULL
taskspriority_idpriorityidON DELETE SET NULL
tasksstatus_idstatusidON DELETE SET NULL
tasksuser_idusersidON DELETE CASCADE

This query creates all five tables and adds the foreign key relationships. The users table here is for task assignment data in the app. You can create the authentication user separately from the register page.

Refine Project Setup

Now let's create the task manager panel with create refine-app.

npm create refine-app@latest refine-task-manager

Choose the following options when prompted:

✔ Choose a project template · Refine(Vite)
✔ What would you like to name your project?: · refine-task-manager
✔ Choose your backend service to connect: · Supabase
✔ Do you want to use a UI Framework?: · Ant Design
✔ Do you want to add example pages?: · No
✔ Choose a package manager: · npm
✔ Would you mind sending us your choices so that we can improve create refine-app? · yes

After the project setup is loaded, let's start by entering our project and making the necessary changes.

Supabase Client Setup

create refine-app already prepared the Supabase data provider and auth provider for us. At this point, we only need to add the required Supabase project credentials to our Refine app.

src/providers/constants.ts
src/providers/constants.ts
// Supabase Dashboard -> Integrations -> Data API
export const SUPABASE_URL = "YOUR_SUPABASE_URL";
// Supabase Dashboard -> Project Settings -> API Keys -> Publishable key
export const SUPABASE_KEY = "YOUR_SUPABASE_API_KEY";

If you want to inspect the generated provider files, you can check src/providers/supabase-client.ts and src/providers/data.ts. We use these providers in the <Refine /> component inside App.tsx.

You can also check the official Supabase data provider documentation here: Supabase data provider docs.

Now we can access and list the tables we created via Supabase.

Add Login and Register Pages

Our purpose here is to log in with a registered Supabase user. If you do not have a registered user yet, you can create one from the register page in Refine.

Custom Login Page

src/pages/login/index.tsx
src/pages/login/index.tsx
import React from "react";
import { AuthPage } from "@refinedev/antd";

export const Login: React.FC = () => {
return (
<AuthPage
type="login"
forgotPasswordLink={false}
formProps={{
initialValues: {
email: "info@refine.dev",
password: "refine-supabase",
},
}}
/>
);
};

Custom Register Page

src/pages/register/index.tsx
src/pages/register/index.tsx
import React from "react";
import { AuthPage } from "@refinedev/antd";

export const Register: React.FC = () => {
return <AuthPage type="register" />;
};

Let's also add these pages to our router.

App.tsx
App.tsx
import { Authenticated, Refine } from "@refinedev/core";
import { ThemedLayout, useNotificationProvider } from "@refinedev/antd";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import routerProvider, {
CatchAllNavigate,
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
import { App as AntdApp } from "antd";

import "@ant-design/v5-patch-for-react-19";
import "@refinedev/antd/dist/reset.css";

import { Header } from "./components";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { Login } from "./pages/login";
import { Register } from "./pages/register";
import authProvider from "./providers/auth";
import { dataProvider } from "./providers/data";

function App() {
return (
<BrowserRouter>
<RefineKbarProvider>
<ColorModeContextProvider>
<AntdApp>
<Refine
dataProvider={dataProvider}
authProvider={authProvider}
routerProvider={routerProvider}
notificationProvider={useNotificationProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
<Route
element={
<Authenticated
key="authenticated-routes"
fallback={<CatchAllNavigate to="/login" />}
>
<ThemedLayout Header={Header}>
<Outlet />
</ThemedLayout>
</Authenticated>
}
>
<Route index element={<div>Blog Tracker App</div>} />
</Route>

<Route
element={
<Authenticated key="auth-pages" fallback={<Outlet />}>
<CatchAllNavigate to="/" />
</Authenticated>
}
>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</AntdApp>
</ColorModeContextProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}

export default App;

Here we define our login and register pages with AuthPage and add a temporary protected route so the app can work before we add our resources.

Login pageLogin page

We can now create Supabase users and log in from our Refine interface.

Add Resource

Adding resources according to the table name we created in Supabase

At this step, let's first create dummy pages for the resources that App.tsx uses. This keeps the project working while we replace these files with real implementations in the next sections.

src/pages
├── dashboard
│ └── index.tsx
├── login
│ └── index.tsx
├── register
│ └── index.tsx
├── task
│ ├── create.tsx
│ ├── edit.tsx
│ ├── index.tsx
│ ├── list.tsx
│ └── show.tsx
└── user
├── index.tsx
└── list.tsx

Here are the dummy pages we need for the resources used in App.tsx.

src/pages/dashboard/index.tsx
src/pages/dashboard/index.tsx
export const Dashboard = () => {
return <div>Dashboard page will be added in the next steps.</div>;
};
src/pages/task/list.tsx
src/pages/task/list.tsx
export const TaskList = () => {
return <div>Task list page will be added in the next steps.</div>;
};
src/pages/task/create.tsx
src/pages/task/create.tsx
export const TaskCreate = () => {
return <div>Task create page will be added in the next steps.</div>;
};
src/pages/task/edit.tsx
src/pages/task/edit.tsx
export const TaskEdit = () => {
return <div>Task edit page will be added in the next steps.</div>;
};
src/pages/task/show.tsx
src/pages/task/show.tsx
export const TaskShow = () => {
return <div>Task detail page will be added in the next steps.</div>;
};
src/pages/task/index.tsx
src/pages/task/index.tsx
export { TaskCreate } from "./create";
export { TaskEdit } from "./edit";
export { TaskList } from "./list";
export { TaskShow } from "./show";
src/pages/user/list.tsx
src/pages/user/list.tsx
export const UserList = () => {
return <div>User list page will be added in the next steps.</div>;
};
src/pages/user/index.tsx
src/pages/user/index.tsx
export { UserList } from "./list";

Before we replace these dummy pages with real implementations, let's also define the shared types we will use across the task, user, label, priority, and status records.

src/types.ts
src/types.ts
import type { Dayjs } from "dayjs";

export interface IAuthUser {
id: string;
email: string;
}

export interface ILabel {
id: number;
title: string;
color: string;
}

export interface IPriority {
id: number;
title: string;
}

export interface IStatus {
id: number;
title: string;
}

export interface ITask {
id: number;
title: string;
description: string;
start_time: string;
end_time: string;
label_id: number;
priority_id: number;
status_id: number;
user_id: string;
}

export interface ITaskFilterVariables {
title?: string;
label_id?: number;
priority_id?: number;
user_id?: string;
status_id?: number;
start_time?: [Dayjs, Dayjs];
end_time?: [Dayjs, Dayjs];
}

After creating these dummy pages, we can add our resources and routes in App.tsx.

App.tsx
App.tsx
import { Authenticated, GitHubBanner, Refine } from "@refinedev/core";
import {
ErrorComponent,
ThemedLayout,
useNotificationProvider,
} from "@refinedev/antd";
import { DashboardOutlined } from "@ant-design/icons";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";

import { App as AntdApp } from "antd";
import "@ant-design/v5-patch-for-react-19";
import "@refinedev/antd/dist/reset.css";

import routerProvider, {
CatchAllNavigate,
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { BrowserRouter, Outlet, Route, Routes } from "react-router";

import { Header } from "./components";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import { Login } from "./pages/login";
import { Register } from "./pages/register";
import { TaskCreate, TaskEdit, TaskList, TaskShow } from "./pages/task";
import { UserList } from "./pages/user";
import authProvider from "./providers/auth";
import { dataProvider } from "./providers/data";

function App() {
return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<ColorModeContextProvider>
<AntdApp>
<DevtoolsProvider>
<Refine
dataProvider={dataProvider}
authProvider={authProvider}
routerProvider={routerProvider}
resources={[
{
name: "dashboard",
list: "/",
meta: {
label: "Dashboard",
icon: <DashboardOutlined />,
},
},
{
name: "users",
list: "/users",
},
{
name: "tasks",
list: "/tasks",
show: "/tasks/show/:id",
create: "/tasks/create",
edit: "/tasks/edit/:id",
},
]}
notificationProvider={useNotificationProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
<Route
element={
<Authenticated
key="authenticated-routes"
fallback={<CatchAllNavigate to="/login" />}
>
<ThemedLayout Header={Header}>
<Outlet />
</ThemedLayout>
</Authenticated>
}
>
<Route index element={<Dashboard />} />
<Route path="users" element={<UserList />} />
<Route path="tasks">
<Route index element={<TaskList />} />
<Route path="edit/:id" element={<TaskEdit />} />
<Route path="create" element={<TaskCreate />} />
<Route path="show/:id" element={<TaskShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>

<Route
element={
<Authenticated key="auth-pages" fallback={<Outlet />}>
<NavigateToResource resource="dashboard" />
</Authenticated>
}
>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
<DevtoolsPanel />
</DevtoolsProvider>
</AntdApp>
</ColorModeContextProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}

export default App;
Dummy pages and initial resource setupDummy pages and initial resource setup

We can now create lists of tasks and make changes to them.

Add List and Filter

Now let's replace the dummy task pages with actual implementations.

src/pages/task/list.tsx
src/pages/task/list.tsx
import React from "react";
import { useMany, type HttpError, type CrudFilters } from "@refinedev/core";

import {
useSelect,
useTable,
List,
TextField,
TagField,
DateField,
ShowButton,
EditButton,
DeleteButton,
} from "@refinedev/antd";

import { SearchOutlined } from "@ant-design/icons";
import { Table, Space, Row, Col, Card } from "antd";
import { Form, type FormProps, Input, Select, DatePicker, Button } from "antd";

import type {
ILabel,
IPriority,
ITask,
ITaskFilterVariables,
IStatus,
IAuthUser,
} from "../../types";

export const TaskList = () => {
const { tableProps, result, searchFormProps } = useTable<
ITask,
HttpError,
ITaskFilterVariables
>({
onSearch: (params) => {
const filters: CrudFilters = [];
const {
title,
label_id,
priority_id,
user_id,
status_id,
start_time,
end_time,
} = params;

filters.push(
{
field: "title",
operator: "eq",
value: title,
},

{
field: "label_id",
operator: "eq",
value: label_id,
},

{
field: "priority_id",
operator: "eq",
value: priority_id,
},

{
field: "user_id",
operator: "eq",
value: user_id,
},

{
field: "status_id",
operator: "eq",
value: status_id,
},

{
field: "start_time",
operator: "gte",
value: start_time ? start_time[0].toISOString() : undefined,
},

{
field: "start_time",
operator: "lte",
value: start_time ? start_time[1].toISOString() : undefined,
},

{
field: "end_time",
operator: "gte",
value: end_time ? end_time[0].toISOString() : undefined,
},

{
field: "end_time",
operator: "lte",
value: end_time ? end_time[1].toISOString() : undefined,
},
);
return filters;
},
});

const labelIds =
result?.data?.map((item) => item.label_id).filter(Boolean) ?? [];
const priorityIds =
result?.data?.map((item) => item.priority_id).filter(Boolean) ?? [];
const assignedIds =
result?.data?.map((item) => item.user_id).filter(Boolean) ?? [];
const statusIds =
result?.data?.map((item) => item.status_id).filter(Boolean) ?? [];

const { result: labels } = useMany<ILabel>({
resource: "label",
ids: labelIds,
queryOptions: { enabled: !!labelIds.length },
});

const { result: priority } = useMany<IPriority>({
resource: "priority",
ids: priorityIds,
queryOptions: { enabled: !!priorityIds.length },
});

const { result: assigned } = useMany<IAuthUser>({
resource: "users",
ids: assignedIds,
queryOptions: { enabled: !!assignedIds.length },
});

const { result: status } = useMany<IStatus>({
resource: "status",
ids: statusIds,
queryOptions: { enabled: !!statusIds.length },
});

return (
<Row gutter={[16, 16]}>
<Col lg={6} xs={24}>
<Card title="Task Filter">
<Filter formProps={searchFormProps} />
</Card>
</Col>
<Col lg={18} xs={24}>
<List>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="title" title="Title" />
<Table.Column
dataIndex="label_id"
title="Label"
render={(value) => {
return (
<TagField
color={
labels?.data.find((item) => item.id === value)?.color
}
value={
labels?.data.find((item) => item.id === value)?.title
}
/>
);
}}
/>
<Table.Column
dataIndex="priority_id"
title="Priority"
render={(value) => {
return (
<TextField
value={
priority?.data.find((item) => item.id === value)?.title
}
/>
);
}}
/>
<Table.Column
dataIndex="user_id"
title="Assigned"
render={(value) => {
return (
<TagField
value={
assigned?.data.find((item) => item.id === value)?.email
}
/>
);
}}
/>
<Table.Column
dataIndex="status_id"
title="Status"
render={(value) => {
return (
<TextField
value={
status?.data.find((item) => item.id === value)?.title
}
/>
);
}}
/>
<Table.Column
dataIndex="start_time"
title="Start Date"
render={(value) => (
<DateField format="DD/MM/YYYY" value={value} />
)}
/>
<Table.Column
dataIndex="end_time"
title="Due Date"
render={(value) => (
<DateField format="DD/MM/YYYY" value={value} />
)}
/>
<Table.Column<ITask>
title="Actions"
dataIndex="actions"
render={(_, record): React.ReactNode => {
return (
<Space>
<ShowButton
size="small"
recordItemId={record.id}
hideText
/>
<EditButton
size="small"
recordItemId={record.id}
hideText
/>
<DeleteButton
size="small"
recordItemId={record.id}
hideText
/>
</Space>
);
}}
/>
</Table>
</List>
</Col>
</Row>
);
};

const { RangePicker } = DatePicker;

const Filter: React.FC<{ formProps: FormProps }> = ({ formProps }) => {
const { selectProps: labelSelectProps } = useSelect<ILabel>({
resource: "label",
});

const { selectProps: priorityProps } = useSelect<IPriority>({
resource: "priority",
});

const { selectProps: statusProps } = useSelect<IStatus>({
resource: "status",
});

const { selectProps: assigneeProps } = useSelect<IAuthUser>({
resource: "users",
optionValue: () => "id",
optionLabel: () => "email",
});

return (
<Form layout="vertical" {...formProps}>
<Form.Item label="Search" name="title">
<Input placeholder="Title" prefix={<SearchOutlined />} />
</Form.Item>
<Form.Item label="Label" name="label_id">
<Select {...labelSelectProps} allowClear placeholder="Search Label" />
</Form.Item>
<Form.Item label="Priority" name="priority_id">
<Select {...priorityProps} allowClear placeholder="Search Priority" />
</Form.Item>
<Form.Item label="Status" name="status_id">
<Select {...statusProps} allowClear placeholder="Search Status" />
</Form.Item>
<Form.Item label="Assigned" name="user_id">
<Select {...assigneeProps} allowClear placeholder="Search Assignee" />
</Form.Item>
<Form.Item label="Start Date" name="start_time">
<RangePicker />
</Form.Item>
<Form.Item label="Due Date" name="end_time">
<RangePicker />
</Form.Item>
<Form.Item>
<Button htmlType="submit" type="primary">
Filter
</Button>
</Form.Item>
</Form>
);
};
Task list viewTask list view

Using Refine's tableSearch and list, we can create our list and perform filtering.

As seen in the example, we listed and showed the task table we created in supabase with refine. Now you can make changes as you want with refine.

Now how do we create task? Let's examine how we can edit them and see their details.

Create Task

src/pages/task/create.tsx
src/pages/task/create.tsx
import { useForm, Create, useSelect } from "@refinedev/antd";

import { Form, Input, Select, DatePicker } from "antd";

import type { ITask, ILabel, IPriority, IStatus, IAuthUser } from "../../types";

export const TaskCreate = () => {
const { formProps, saveButtonProps } = useForm<ITask>();

const { selectProps: labelSelectProps } = useSelect<ILabel>({
resource: "label",
});

const { selectProps: prioritySelectPorps } = useSelect<IPriority>({
resource: "priority",
});

const { selectProps: assigneeSelectProps } = useSelect<IAuthUser>({
resource: "users",
optionValue: "id",
optionLabel: "email",
});

const { selectProps: statusSelectProps } = useSelect<IStatus>({
resource: "status",
});

return (
<Create saveButtonProps={saveButtonProps}>
<Form {...formProps} wrapperCol={{ span: 12 }} layout="vertical">
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input />
</Form.Item>
<Form.Item label="Label" name="label_id">
<Select {...labelSelectProps} />
</Form.Item>
<Form.Item label="Priority" name="priority_id">
<Select {...prioritySelectPorps} />
</Form.Item>
<Form.Item label="Assign To" name="user_id">
<Select {...assigneeSelectProps} />
</Form.Item>
<Form.Item label="Select Status" name="status_id">
<Select {...statusSelectProps} />
</Form.Item>
<Form.Item label="Start Date" name="start_time">
<DatePicker style={{ width: "50%" }} />
</Form.Item>
<Form.Item label="Due Date" name="end_time">
<DatePicker style={{ width: "50%" }} />
</Form.Item>
</Form>
</Create>
);
};
Create task formCreate task form

Edit Task

src/pages/task/edit.tsx
src/pages/task/edit.tsx
import { useForm, Edit, useSelect } from "@refinedev/antd";

import { Form, Input, Select } from "antd";

import type { ITask, ILabel, IPriority, IStatus, IAuthUser } from "../../types";

export const TaskEdit = () => {
const { formProps, saveButtonProps } = useForm<ITask>();

const { selectProps: labelSelectProps } = useSelect<ILabel>({
resource: "label",
});

const { selectProps: priorityProps } = useSelect<IPriority>({
resource: "priority",
});

const { selectProps: assigneeProps } = useSelect<IAuthUser>({
resource: "users",
optionValue: "id",
optionLabel: "email",
});

const { selectProps: statusProps } = useSelect<IStatus>({
resource: "status",
});

return (
<Edit saveButtonProps={saveButtonProps}>
<Form {...formProps} wrapperCol={{ span: 12 }} layout="vertical">
<Form.Item label="Title" name="title">
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input />
</Form.Item>
<Form.Item label="Label" name="label_id">
<Select {...labelSelectProps} />
</Form.Item>
<Form.Item label="Priority" name="priority_id">
<Select {...priorityProps} />
</Form.Item>
<Form.Item label="Status" name="status_id">
<Select {...statusProps} />
</Form.Item>
<Form.Item label="Assignee" name="user_id">
<Select {...assigneeProps} />
</Form.Item>
</Form>
</Edit>
);
};
Edit task formEdit task form

Show Task

src/pages/task/show.tsx
src/pages/task/show.tsx
import { useShow, useOne } from "@refinedev/core";
import { Show, DateField } from "@refinedev/antd";
import { Typography, Tag } from "antd";
import type { ITask, ILabel, IPriority, IStatus, IAuthUser } from "../../types";

const { Title, Text } = Typography;

export const TaskShow: React.FC = () => {
const { query: queryResult } = useShow<ITask>();
const { data, isLoading } = queryResult;
const record = data?.data;

const { result: assigned } = useOne<IAuthUser>({
resource: "users",
id: record?.user_id ?? "",
queryOptions: { enabled: !!record?.user_id },
});

const { result: label } = useOne<ILabel>({
resource: "label",
id: record?.label_id ?? "",
queryOptions: { enabled: !!record?.label_id },
});

const { result: priority } = useOne<IPriority>({
resource: "priority",
id: record?.priority_id ?? "",
queryOptions: { enabled: !!record?.priority_id },
});

const { result: status } = useOne<IStatus>({
resource: "status",
id: record?.status_id ?? "",
queryOptions: { enabled: !!record?.status_id },
});

return (
<Show isLoading={isLoading}>
<Title level={5}>Task:</Title>
<Text>{record?.title || "-"}</Text>

<Title level={5}>Task Description:</Title>
<Text>{record?.description}</Text>

<Title level={5}>Assigned To:</Title>
<Text>
<Tag>{assigned?.email ?? "-"}</Tag>
</Text>

<Title level={5}>Label:</Title>
<Text>
<Tag>{label?.title ?? "-"}</Tag>
</Text>

<Title level={5}>Priority:</Title>
<Text>{priority?.title ?? "-"}</Text>

<Title level={5}>Status:</Title>
<Text>{status?.title ?? "-"}</Text>

<Title level={5}>Start Date:</Title>
<DateField format="DD/MM/YYYY" value={record?.start_time ?? "-"} />

<Title level={5}>Due Date:</Title>
<DateField format="DD/MM/YYYY" value={record?.end_time ?? "-"} />
</Show>
);
};
Task detail viewTask detail view

By using Refine's basic views such as create, edit and show, we can now create tasks, edit these tasks and view their details.

Add User List

Now let's replace the dummy user list page with a simple list page.

src/pages/user/list.tsx
src/pages/user/list.tsx
import React from "react";

import { useTable, List } from "@refinedev/antd";

import { Table } from "antd";

import type { IAuthUser } from "../../types";

export const UserList = () => {
const { tableProps } = useTable<IAuthUser>();

return (
<List>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="email" title="Email" />
<Table.Column dataIndex="id" title="ID" />
</Table>
</List>
);
};

Add Custom Chart

src/components/task/pie.tsx
src/components/task/pie.tsx
import React, { useContext } from "react";
import { Pie, type PieConfig } from "@ant-design/charts";
import { Empty, Space, Typography, theme } from "antd";
import { ColorModeContext } from "../../contexts/color-mode";

interface ChartProps {
data: {
type: string;
value: number;
}[];
}

interface ChartDatum {
type: string;
value: number;
percent?: number;
}

export const TaskChart: React.FC<ChartProps> = ({ data }) => {
const { token } = theme.useToken();
const { mode } = useContext(ColorModeContext);
const totalValue = data.reduce((sum, item) => sum + item.value, 0);

const colorMap = React.useMemo(() => {
const chartColors = [
"#1677ff",
"#13c2c2",
"#fa8c16",
"#52c41a",
"#722ed1",
"#eb2f96",
"#faad14",
"#2f54eb",
];

return data.reduce<Record<string, string>>((acc, item, index) => {
acc[item.type] = chartColors[index % chartColors.length];
return acc;
}, {});
}, [data]);

if (!data.length) {
return (
<div
style={{
minHeight: 320,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Empty description="No task data yet" />
</div>
);
}

const config: PieConfig = {
data,
height: 280,
angleField: "value",
colorField: "type",
radius: 0.88,
innerRadius: 0.55,
legend: false,
theme: mode,
color: ({ type }: ChartDatum) => colorMap[type] ?? token.colorPrimary,
tooltip: {
title: (datum: ChartDatum) => datum.type,
items: [
(datum: ChartDatum) => {
const percentage = totalValue
? Math.round((datum.value / totalValue) * 100)
: 0;

return {
name: "Tasks",
value: `${datum.value} (${percentage}%)`,
};
},
],
},
label: {
type: "inner",
offset: "-50%",
content: ({ percent }: ChartDatum) =>
`${Math.round((percent ?? 0) * 100)}%`,
style: {
fontSize: 12,
fontWeight: 700,
fill: token.colorTextLightSolid,
textAlign: "center",
},
autoRotate: false,
},
pieStyle: {
lineWidth: 2,
stroke: token.colorBgContainer,
},
interactions: [{ type: "element-active" }],
};

return (
<>
<Space wrap size={[16, 8]} style={{ marginBottom: 16 }}>
{data.map((item) => (
<Space key={item.type} size={8}>
<span
style={{
width: 10,
height: 10,
borderRadius: 999,
display: "inline-block",
background: colorMap[item.type],
}}
/>
<Typography.Text style={{ color: token.colorTextSecondary }}>
{item.type}
</Typography.Text>
</Space>
))}
</Space>
<Pie {...config} />
</>
);
};
src/pages/dashboard/index.tsx
src/pages/dashboard/index.tsx
import React from "react";
import { useList, useMany } from "@refinedev/core";
import { Row, Col, Card } from "antd";
import type { ITask, ILabel, IPriority, IStatus, IAuthUser } from "../../types";
import { TaskChart } from "../../components";
import { groupBy } from "../../helper";

export const Dashboard = () => {
const { result: taskList } = useList<ITask>({
resource: "tasks",
});

const labelIds =
taskList?.data?.map((item) => item.label_id).filter(Boolean) ?? [];
const priorityIds =
taskList?.data?.map((item) => item.priority_id).filter(Boolean) ?? [];
const assignedIds =
taskList?.data?.map((item) => item.user_id).filter(Boolean) ?? [];
const statusIds =
taskList?.data?.map((item) => item.status_id).filter(Boolean) ?? [];

const { result: labels } = useMany<ILabel>({
resource: "label",
ids: labelIds,
queryOptions: { enabled: !!labelIds.length },
});

const { result: priority } = useMany<IPriority>({
resource: "priority",
ids: priorityIds,
queryOptions: { enabled: !!priorityIds.length },
});

const { result: assigned } = useMany<IAuthUser>({
resource: "users",
ids: assignedIds,
queryOptions: { enabled: !!assignedIds.length },
});

const { result: status } = useMany<IStatus>({
resource: "status",
ids: statusIds,
queryOptions: { enabled: !!statusIds.length },
});

const chartSections = [
{
key: "labels",
title: "Tasks by Label",
data:
labels?.data.map((item) => ({
type: item.title,
value: groupBy(labelIds)[item.id] ?? 0,
})) ?? [],
},
{
key: "priority",
title: "Tasks by Priority",
data:
priority?.data.map((item) => ({
type: item.title,
value: groupBy(priorityIds)[item.id] ?? 0,
})) ?? [],
},
{
key: "status",
title: "Tasks by Status",
data:
status?.data.map((item) => ({
type: item.title,
value: groupBy(statusIds)[item.id] ?? 0,
})) ?? [],
},
{
key: "assignee",
title: "Tasks by Assignee",
data:
assigned?.data.map((item) => ({
type: item.email,
value: groupBy(assignedIds)[item.id] ?? 0,
})) ?? [],
},
];

return (
<Row gutter={[16, 16]}>
{chartSections.map((section) => (
<Col key={section.key} xxl={6} xl={12} md={12} sm={24} xs={24}>
<Card
title={section.title}
headStyle={{ padding: "12px 16px" }}
bodyStyle={{ padding: "16px 16px 8px" }}
>
<TaskChart data={section.data} />
</Card>
</Col>
))}
</Row>
);
};
Dashboard overviewDashboard overview

Final version of our <App.tsx/>.

App.tsx
App.tsx
import { Authenticated, GitHubBanner, Refine } from "@refinedev/core";
import {
useNotificationProvider,
ThemedLayout,
ErrorComponent,
} from "@refinedev/antd";
import routerProvider, {
NavigateToResource,
CatchAllNavigate,
UnsavedChangesNotifier,
DocumentTitleHandler,
} from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import { DashboardOutlined } from "@ant-design/icons";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";

import { App as AntdApp } from "antd";
import "@ant-design/v5-patch-for-react-19";
import "@refinedev/antd/dist/reset.css";

import { Header } from "./components";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import { Login } from "./pages/login";
import { Register } from "./pages/register";
import { TaskCreate, TaskEdit, TaskList, TaskShow } from "./pages/task";
import { UserList } from "./pages/user";
import authProvider from "./providers/auth";
import { dataProvider } from "./providers/data";

function App() {
return (
<DevtoolsProvider>
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<ColorModeContextProvider>
<AntdApp>
<Refine
dataProvider={dataProvider}
authProvider={authProvider}
routerProvider={routerProvider}
resources={[
{
name: "dashboard",
list: "/",
meta: {
label: "Dashboard",
icon: <DashboardOutlined />,
},
},
{
name: "users",
list: "/users",
},
{
name: "tasks",
list: "/tasks",
show: "/tasks/show/:id",
create: "/tasks/create",
edit: "/tasks/edit/:id",
},
]}
notificationProvider={useNotificationProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
<Route
element={
<Authenticated
key="authenticated-routes"
fallback={<CatchAllNavigate to="/login" />}
>
<ThemedLayout Header={Header}>
<Outlet />
</ThemedLayout>
</Authenticated>
}
>
<Route index element={<Dashboard />} />

<Route path="users" element={<UserList />} />

<Route path="tasks">
<Route index element={<TaskList />} />
<Route path="edit/:id" element={<TaskEdit />} />
<Route path="create" element={<TaskCreate />} />
<Route path="show/:id" element={<TaskShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>

<Route
element={
<Authenticated key="auth-pages" fallback={<Outlet />}>
<NavigateToResource resource="dashboard" />
</Authenticated>
}
>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</AntdApp>
</ColorModeContextProvider>
</RefineKbarProvider>
<DevtoolsPanel />
</BrowserRouter>
</DevtoolsProvider>
);
}

export default App;

Overview Project

Our project is done.

As you can see, we made a simple and short task manager internal tool using Refine on our front end and Supabase as its data provider. The same approach works for building a Supabase admin panel or any other data-driven dashboard.

Here is repo

For more information about Refine: Refine GitHub Page

For other examples and articles that will interest you with Refine: https://refine.dev/blog/

If you want to add OTP-based authentication to your Supabase app, check out our guide on OTP authentication with Supabase and Twilio in React.

Example

Run on your local
npm create refine-app@latest -- --example blog-issue-tracker