In this article, we will make a project management web application with supabase and refine.
This web application will us to create issue 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 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 we can start creating our tables.
The tables we need to create for our project are as follows:
label
id
: biginttitle
: varcharcolor
: varchar
priority
id
biginttitle
varchar
status
id
biginttitle
varchar
users
id
uuid
tasks
id
biginttitle
varchardescription
varcharstart_time
dateend_time
datelabel
bigintpriority
bigintstatuts
bigintusers
uuid
We created our database tables. The important part here is that as you can see, in our tasks table, label, priority, status and users values are defined as bigint. To summarize the reason for this, we relation the label, priority, status and users tables that we created above with the corresponding values in our tasks table.
💡 TIP: Add Foreign key relationships steps:
1.Got to a table, right click the column you wish to add a foreign key for
2.Select 'edit column'
3.Select 'Add foreign key relation'
4.Select the table and column to make a foreign key for
5.Click save
So we can take reference their id and use their value in our tasks table.
Now let's go to the Supabase Table editor and create our constant values.
Label Table
Priority Table
Status Table
Let's create a test task to better understand key relation.
As you can see, since we have defined the ids of our label, status, priority and users tables as references to our tasks table, we can now use their values.
Let's look at how we can use this data we created on the Supabase side with refine .
Refine Project Setup
Now let's refine the task-manager panel. With superplate, we can quickly create a refine project
npx superplate-cli refine-task-manager
✔ Select your project type › refine
✔ What will be the name of your app · refine-task-manager
✔ Package manager: · npm
✔ Do you want to customize theme?: · css
✔ Data Provider: · supabase-data-provider
✔ Do you want to customize layout? · custom-layout
✔ i18n - Internationalization: · no
After the project setup is loaded, let's start by entering our project and making the necessary changes.
Let's add our supabase url and key to our refine project.
src/utility/supabaseClient.ts
import { createClient } from "@pankod/refine-supabase";
const SUPABASE_URL = "YOUR_SUPABASE_URL";
const SUPABASE_KEY = "YOUR_SUPABASE_API_KEY";
export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY);
Now we can access and list the tables we created via the supabase.
- Add custom login and signup page in App.tsx
Our purpose here is to log in if there is a registered user in the supabase. If you do not have a registered user, saving a user to the supabase with refine.
Custom Login Page
import React from "react";
import {
Row,
Col,
AntdLayout,
Card,
Typography,
Form,
Input,
Button,
useLogin,
useNavigation,
} from "@pankod/refine";
import "./styles.css";
const { Text, Title } = Typography;
export interface ILoginForm {
email: string;
password: string;
}
export const Login: React.FC = () => {
const [form] = Form.useForm<ILoginForm>();
const { mutate: login } = useLogin<ILoginForm>();
const { push } = useNavigation();
const CardTitle = (
<Title level={3} className="title">
Sign in your account
</Title>
);
return (
<AntdLayout className="layout">
<Row
justify="center"
align="middle"
style={{
height: "100vh",
}}
>
<Col xs={22}>
<div className="container">
<div className="imageContainer">
<img src="./refine.svg" alt="Refine Logo" />
</div>
<Card title={CardTitle} headStyle={{ borderBottom: 0 }}>
<Form<ILoginForm>
layout="vertical"
form={form}
onFinish={(values) => {
login(values);
}}
initialValues={{
email: "info@refine.dev",
password: "refine-supabase",
}}
>
<Form.Item
name="email"
label="Email"
rules={[{ required: true, type: "email" }]}
>
<Input size="large" placeholder="Email" />
</Form.Item>
<Form.Item
name="password"
label="Password"
rules={[{ required: true }]}
style={{ marginBottom: "12px" }}
>
<Input type="password" placeholder="●●●●●●●●" size="large" />
</Form.Item>
<Button type="primary" size="large" htmlType="submit" block>
Sign in
</Button>
</Form>
<div style={{ marginTop: 8 }}>
<Text style={{ fontSize: 12 }}>
Don’t have an account?
<a
href="#"
style={{ fontWeight: "bold", marginLeft: 12 }}
onClick={() => push("signup")}
>
Sign up
</a>
</Text>
</div>
</Card>
</div>
</Col>
</Row>
</AntdLayout>
);
};
Custom Sign Up Page
import React from "react";
import {
Row,
Col,
AntdLayout,
Card,
Typography,
Form,
Input,
Button,
useNavigation,
} from "@pankod/refine";
import "./styles.css";
import { supabaseClient } from "utility";
const { Text, Title } = Typography;
export interface ISignup {
email: string;
password: string;
}
export const Signup: React.FC = () => {
const [form] = Form.useForm<ISignup>();
const { push } = useNavigation();
const CardTitle = (
<Title level={3} className="title">
Sign Up
</Title>
);
const signup = async (email: string, password: string) => {
const { error } = await supabaseClient.auth.signUp({
email,
password,
});
if (error) {
return Promise.reject(error);
}
};
return (
<AntdLayout className="layout">
<Row
justify="center"
align="middle"
style={{
height: "100vh",
}}
>
<Col xs={22}>
<div className="container">
<div className="imageContainer">
<img src="./refine.svg" alt="Refine Logo" />
</div>
<Card title={CardTitle} headStyle={{ borderBottom: 0 }}>
<Form<ISignup>
layout="vertical"
form={form}
onFinish={(values) => {
signup(values.email, values.password);
push("login");
}}
>
<Form.Item
name="email"
label="Email"
rules={[{ required: true, type: "email" }]}
>
<Input size="large" placeholder="Email" />
</Form.Item>
<Form.Item
name="password"
label="Password"
rules={[{ required: true }]}
style={{ marginBottom: "12px" }}
>
<Input type="password" placeholder="●●●●●●●●" size="large" />
</Form.Item>
<Button
type="primary"
size="large"
htmlType="submit"
block
style={{ marginTop: 24 }}
>
Create Account
</Button>
</Form>
<div style={{ marginTop: 16 }}>
<Text style={{ fontSize: 12 }}>
Don’t have an account?
<a
href="#"
style={{ fontWeight: "bold" }}
onClick={() => push("login")}
>
Sign in
</a>
</Text>
</div>
</Card>
</div>
</Col>
</Row>
</AntdLayout>
);
};
-
App.tsx
import { Refine } from "@pankod/refine";
import routerProvider from "@pankod/refine-react-router";
import { dataProvider } from "@pankod/refine-supabase";
import authProvider from "./authProvider";
import { supabaseClient } from "utility";
import "@pankod/refine/dist/styles.min.css";
import { Login } from "./pages/login";
import { Signup } from "./pages/signup";
function App() {
return (
<Refine
dataProvider={dataProvider(supabaseClient)}
authProvider={authProvider}
LoginPage={Login}
routerProvider={{
...routerProvider,
routes: [
{
exact: true,
component: Signup,
path: "/signup",
},
] as typeof routerProvider.routes,
}}
/>
);
}
export default App;
Here we define our login and signup pages. We then use the refine's router-provider and useNavigaton hooks to switch between login and signup.
We can now create supabase users and log in from our refine interface.
Adding resources according to the table name we created in Supabase
import { Refine } from "@pankod/refine";
import routerProvider from "@pankod/refine-react-router";
import { dataProvider } from "@pankod/refine-supabase";
import authProvider from "./authProvider";
import { supabaseClient } from "utility";
import "@pankod/refine/dist/styles.min.css";
import { Login } from "./pages/login";
import { Signup } from "./pages/signup";
function App() {
return (
<Refine
dataProvider={dataProvider(supabaseClient)}
authProvider={authProvider}
LoginPage={Login}
routerProvider={{
...routerProvider,
routes: [
{
exact: true,
component: Signup,
path: "/signup",
},
] as typeof routerProvider.routes,
}}
resources={[
{
name: "users"
},
{
name: "tasks"
},
]}
/>
);
}
We can now create lists of tasks and make changes to them.
src/pages/task/list.tsx
import React from "react";
import {
useTable,
useMany,
List,
Table,
TextField,
TagField,
DateField,
Space,
ShowButton,
EditButton,
DeleteButton,
IResourceComponentsProps,
HttpError,
CrudFilters,
Row,
Col,
Card,
} from "@pankod/refine";
import {
ILabel,
IPriority,
ITask,
ITaskFilterVariables,
IStatus,
IAuthUser,
} from "interfaces";
import { Filter } from "../task";
export const TaskList: React.FC<IResourceComponentsProps> = () => {
const { tableProps, searchFormProps } = useTable<
ITask,
HttpError,
ITaskFilterVariables
>({
onSearch: (params) => {
const filters: CrudFilters = [];
const { title, label, priority, users, status, start_time, end_time } =
params;
filters.push(
{
field: "title",
operator: "eq",
value: title,
},
{
field: "label",
operator: "eq",
value: label,
},
{
field: "priority",
operator: "eq",
value: priority,
},
{
field: "users",
operator: "eq",
value: users,
},
{
field: "status",
operator: "eq",
value: status,
},
{
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 = tableProps?.dataSource?.map((item) => item.label) ?? [];
const priorityIds = tableProps?.dataSource?.map((item) => item.priority) ?? [];
const assignedIds = tableProps?.dataSource?.map((item) => item.users) ?? [];
const statusIds = tableProps?.dataSource?.map((item) => item.status) ?? [];
const { data: labels } = useMany<ILabel>({
resource: "label",
ids: labelIds,
});
const { data: priority } = useMany<IPriority>({
resource: "priority",
ids: priorityIds,
});
const { data: assigned } = useMany<IAuthUser>({
resource: "users",
ids: assignedIds,
});
const { data: status } = useMany<IStatus>({
resource: "status",
ids: statusIds,
});
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"
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"
title="Priority"
render={(value) => {
return (
<TextField
value={
priority?.data.find((item) => item.id === value)?.title
}
/>
);
}}
/>
<Table.Column
dataIndex="users"
title="Assigned"
render={(value) => {
return (
<TagField
value={
assigned?.data.find((item) => item.id === value)?.email
}
/>
);
}}
/>
<Table.Column
dataIndex="status"
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>
);
};
src/pages/task/filter.tsx
import React from "react";
import {
Form,
FormProps,
Input,
useSelect,
Select,
DatePicker,
Icons,
Button,
} from "@pankod/refine";
import { ITask, IPriority, IStatus, IAuthUser } from "interfaces";
const { RangePicker } = DatePicker
export const Filter: React.FC<{ formProps: FormProps }> = ({ formProps }) => {
const { selectProps: labelSelectProps } = useSelect<ITask>({
resource: "label",
});
const { selectProps: priorityProps } = useSelect<IPriority>({
resource: "priority",
});
const { selectProps: statusProps } = useSelect<IStatus>({
resource: "status"
});
const { selectProps: assigneProps } = useSelect<IAuthUser>({
resource: "users",
optionValue: "id",
optionLabel: "email",
});
return (
<Form layout="vertical" {...formProps}>
<Form.Item label="Search" name="title">
<Input
placeholder="Title"
prefix={<Icons.SearchOutlined />}
/>
</Form.Item>
<Form.Item label="Label" name="label">
<Select {...labelSelectProps} allowClear placeholder="Seach Label" />
</Form.Item>
<Form.Item label="Priority" name="priority">
<Select {...priorityProps} allowClear placeholder="Seach Priority" />
</Form.Item>
<Form.Item label="Status" name="status">
<Select {...statusProps} allowClear placeholder="Search Status" />
</Form.Item>
<Form.Item label="Assigned" name="users">
<Select {...assigneProps} 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>
);
};
src/interfaces
export interface IAuthUser {
id: string;
email: string;
}
export interface ILabel {
id: string;
title: string;
color: string;
}
export interface IPriority {
id: string;
title: string;
}
export interface IStatus {
id: string;
title: string
}
export interface ITask {
id: string;
title: string;
description: string;
start_time: string;
end_time: string;
label: string;
priority: string;
status: string;
users: string
}
export interface ITaskFilterVariables {
title: string;
label: string;
priority: string;
users: string;
status: string;
start_time: [Dayjs, Dayjs];
end_time: [Dayjs, Dayjs];
}
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.
src/pages/task/create.tsx
import {
useForm,
Create,
Form,
Input,
Select,
useSelect,
IResourceComponentsProps,
DatePicker,
} from "@pankod/refine";
import { ITask, ILabel, IPriority, IStatus, IAuthUser } from "interfaces";
export const TaskCreate: React.FC<IResourceComponentsProps> = () => {
const { formProps, saveButtonProps } = useForm<ITask>();
const { selectProps: labelSelectProps } = useSelect<ILabel>({
resource: "label",
});
const { selectProps: prioritySelectPorps } = useSelect<IPriority>({
resource: "priority",
});
const { selectProps: assigneSelectProps } = 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">
<Select {...labelSelectProps} />
</Form.Item>
<Form.Item label="Priority" name="priority">
<Select {...prioritySelectPorps} />
</Form.Item>
<Form.Item label="Assigne To" name="users">
<Select {...assigneSelectProps} />
</Form.Item>
<Form.Item label="Select Status" name="status">
<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>
);
};
src/pages/task/edit.tsx
import {
useForm,
Form,
Input,
Select,
Edit,
useSelect,
IResourceComponentsProps,
} from "@pankod/refine";
import { ITask, IPriority, IStatus, IAuthUser } from "interfaces";
export const EditTask: React.FC<IResourceComponentsProps> = () => {
const { formProps, saveButtonProps } = useForm<ITask>();
const { selectProps: labelSelectProps } = useSelect<ITask>({
resource: "label",
});
const { selectProps: priorityProps } = useSelect<IPriority>({
resource: "priority",
});
const { selectProps: assigneProps } = 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">
<Select {...labelSelectProps} />
</Form.Item>
<Form.Item label="Priority" name="priority">
<Select {...priorityProps} />
</Form.Item>
<Form.Item label="Status" name="status">
<Select {...statusProps} />
</Form.Item>
<Form.Item label="Assigne" name="users">
<Select {...assigneProps} />
</Form.Item>
</Form>
</Edit>
);
};
src/pages/task/show
import { useShow, Show, Typography, Tag, useOne, DateField } from "@pankod/refine";
import { ITask, ILabel, IPriority, IStatus, IAuthUser } from "interfaces";
const { Title, Text } = Typography;
export const TaskShow: React.FC = () => {
const { queryResult } = useShow<ITask>();
const { data, isLoading } = queryResult;
const record = data?.data;
const { data: assigned } = useOne<IAuthUser>({
resource: "users",
id: record?.users || "",
});
const { data: label } = useOne<ILabel>({
resource: "label",
id: record?.label || "",
});
const { data: priority } = useOne<IPriority>({
resource: "priority",
id: record?.priority || "",
});
const { data: status } = useOne<IStatus>({
resource: "status",
id: record?.status || "",
});
return (
<Show isLoading={isLoading}>
<Title level={5}>Task:</Title>
<Text>{record?.title || "-"}</Text>
<Title level={5}>Task Desciption:</Title>
<Text>{record?.description}</Text>
<Title level={5}>Assigned To:</Title>
<Text>
<Tag>{assigned?.data?.email ?? "-"}</Tag>
</Text>
<Title level={5}>Label:</Title>
<Text>
<Tag>{label?.data?.title ?? "-"}</Tag>
</Text>
<Title level={5}>Priority:</Title>
<Text>{priority?.data?.title ?? "-"}</Text>
<Title level={5}>Status:</Title>
<Text>{status?.data?.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.
Let's see how to add a dashboard page to our project together.
src/components/task/pie.tsx
import React from "react";
import { Pie } from "@ant-design/charts";
interface ChartProps {
data: {
type: any;
value: any;
}[];
}
export const TaskChart: React.FC<ChartProps> = ({ data }) => {
var config = {
data: data,
angleField: "value",
colorField: "type",
radius: 0.9,
label: {
type: "inner",
offset: "-30%",
content: function content(_ref: any) {
var percent = _ref.percent;
return "".concat((percent * 100).toFixed(0), "%");
},
style: {
fontSize: 14,
textAlign: "center",
},
},
interactions: [{ type: "element-active" }],
};
return <Pie {...config} />;
};
src/pages/dashboard/index.tsx
import React from "react";
import { useList, useMany, Row, Col, Card } from "@pankod/refine";
import { ITask, ILabel, IPriority, IStatus, IAuthUser } from "interfaces";
import { TaskChart } from "components/task/pie";
import { groupBy } from "helper";
export const Dashboard = () => {
const taskList = useList<ITask>({
resource: "tasks",
});
const labelIds = taskList.data?.data.map((item) => item.label) ?? [];
const priorityIds = taskList.data?.data.map((item) => item.priority) ?? [];
const assignedIds = taskList.data?.data.map((item) => item.users) ?? [];
const statusIds = taskList.data?.data.map((item) => item.status) ?? [];
const { data: labels } = useMany<ILabel>({
resource: "label",
ids: labelIds || [],
});
const { data: priority } = useMany<IPriority>({
resource: "priority",
ids: priorityIds || [],
});
const { data: assigned } = useMany<IAuthUser>({
resource: "users",
ids: assignedIds || [],
});
const { data: status } = useMany<IStatus>({
resource: "status",
ids: statusIds || [],
});
return (
<Row gutter={[16, 16]}>
<Col xl={7} lg={12} md={24} sm={24} xs={24}>
<Card>
<TaskChart
data={
labels?.data.map((i) => {
return { type: i.title, value: groupBy(labelIds)[i.id] };
}) ?? []
}
/>
</Card>
</Col>
<Col xl={7} lg={12} md={24} sm={24} xs={24}>
<Card>
<TaskChart
data={
priority?.data.map((i) => {
return { type: i.title, value: groupBy(priorityIds)[i.id] };
}) ?? []
}
/>
</Card>
</Col>
<Col xl={7} lg={12} md={24} sm={24} xs={24}>
<Card>
<TaskChart
data={
status?.data.map((i) => {
return { type: i.title, value: groupBy(statusIds)[i.id] };
}) ?? []
}
/>
</Card>
</Col>
<Col xl={7} lg={12} md={24} sm={24} xs={24}>
<Card>
<TaskChart
data={
assigned?.data.map((i) => {
return { type: i.email, value: groupBy(assignedIds)[i.id] };
}) ?? []
}
/>
</Card>
</Col>
</Row>
);
};
Final version of our <App.tsx/>
.
import { Refine } from "@pankod/refine";
import routerProvider from "@pankod/refine-react-router";
import { dataProvider } from "@pankod/refine-supabase";
import authProvider from "./authProvider";
import { supabaseClient } from "utility";
import "@pankod/refine/dist/styles.min.css";
import { UserList } from "./pages/user";
import { TaskList, TaskShow, TaskCreate, EditTask } from "./pages/task";
import { Dashboard } from "./pages/dashboard";
import { Login } from "./pages/login";
import { Signup } from "./pages/signup";
function App() {
return (
<Refine
dataProvider={dataProvider(supabaseClient)}
authProvider={authProvider}
DashboardPage={Dashboard}
LoginPage={Login}
routerProvider={{
...routerProvider,
routes: [
{
exact: true,
component: Signup,
path: "/signup",
},
] as typeof routerProvider.routes,
}}
resources={[
{
name: "users",
list: UserList,
},
{
name: "tasks",
list: TaskList,
edit: EditTask,
create: TaskCreate,
show: TaskShow,
},
]}
/>
);
}
Our project is done. Lets see how its look like.
Overview
Task Filter
DashboardPage
As you can see, we made a simple and short task manager application using refine on our front end and using its data-provider.
For more information about Refine: https://refine.dev/
For other examples and articles that will interest you with refine: https://refine.dev/blog/
Live Codesandbox Example: https://codesandbox.io/s/refine-supabase-issue-tracker-ifuh4