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.
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.
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
| Column | Type | Constraints | Description |
|---|---|---|---|
id | bigint | PK, identity | Unique label identifier |
title | text | NOT NULL, UNIQUE | Label title |
color | text | NOT NULL | Label color |
priority
| Column | Type | Constraints | Description |
|---|---|---|---|
id | bigint | PK, identity | Unique priority identifier |
title | text | NOT NULL, UNIQUE | Priority title |
status
| Column | Type | Constraints | Description |
|---|---|---|---|
id | bigint | PK, identity | Unique status identifier |
title | text | NOT NULL, UNIQUE | Status title |
users
| Column | Type | Constraints | Description |
|---|---|---|---|
id | uuid | PK, default gen_random_uuid() | Unique user identifier |
email | text | NOT NULL, UNIQUE | User email address |
tasks
| Column | Type | Constraints | Description |
|---|---|---|---|
id | bigint | PK, identity | Unique task identifier |
title | text | NOT NULL | Task title |
description | text | nullable | Task description |
start_time | date | nullable | Start date |
end_time | date | nullable | End date |
label_id | bigint | FK → label.id, ON DELETE SET NULL | Related label |
priority_id | bigint | FK → priority.id, ON DELETE SET NULL | Related priority |
status_id | bigint | FK → status.id, ON DELETE SET NULL | Related status |
user_id | uuid | FK → users.id, ON DELETE CASCADE | Assigned user |
Relationships
| Source Table | Source Column | Target Table | Target Column | Rule |
|---|---|---|---|---|
tasks | label_id | label | id | ON DELETE SET NULL |
tasks | priority_id | priority | id | ON DELETE SET NULL |
tasks | status_id | status | id | ON DELETE SET NULL |
tasks | user_id | users | id | ON 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
// 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
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
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
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.
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
export const Dashboard = () => {
return <div>Dashboard page will be added in the next steps.</div>;
};
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
export const TaskCreate = () => {
return <div>Task create page will be added in the next steps.</div>;
};
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
export const TaskShow = () => {
return <div>Task detail page will be added in the next steps.</div>;
};
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
export const UserList = () => {
return <div>User list page will be added in the next steps.</div>;
};
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
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
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;
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
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>
);
};
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
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>
);
};
Edit Task
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>
);
};
Show Task
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>
);
};
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
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
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
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>
);
};
Final version of our <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.
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
npm create refine-app@latest -- --example blog-issue-tracker


