Creating a Todo app with Refine and Supabase
This article will cover the technical aspects of how I made a Todo app in a few hours using Refine and Supabase. I tried to take a few detours from the official way of doing things to showcase a few possible ways one can achieve the same result.
All the code is available at https://github.com/TheEmi/TodoRefine
You can see an instance of the app running at https://todo-app-rosy-pi-29.vercel.app/
Getting started
To begin, we will head to refine.new which is a quick tool for creating the Refine project with the standard integrations already written. Selecting Vite as the react platform, Ant Design for the UI framework and Supabase for both backend and authentication. This will create a template that we can download and open to get a head-start on our project.
Next, we will go to supabase.com, create an account and a project.
To connect our Supabase instance we will store our SUPABASE_URL and SUPABASE_ANON_KEY in a .env file to avoid publishing this on GitHub.
Here is what the .env in the root of the directory looks like:
VITE_SUPABASE_URL=https://vlkudqvakedjdcnakadcnejj.supabase.co
VITE_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZsa3VkcXZxdWd6b189dnNjKJsdn6ImFub24iLCJpYXQiOjE3MTg0NTk1OTQsImV4cCI6MjAzNDAzNTU5NH0.7IRAWj13jrkiMCB_sUBEWJCd44XrC0SvZX-aHCO7ogw
Next, we can use the environment variables in the ./utility/supabaseClient.ts:
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.VITE_SUPABASE_KEY;
What do you want the project to do?
Our configuration is done and now we need to have a clear vision of what our project will accomplish so we can model our database and build the views we need.
I needed a Todo app to track daily activities and allow me to check tasks I've done. There are hundreds of other apps but the advantage is that I can customize it however I see fit. That being said I want 3 main sections:
- Daily Todo
- Calendar with all days
- Default items
Here is a bit more detail:
- When we first log in I want to see today's checklist of activities I need to do. This list should be customizable and should contain a checkbox for when an activity is finished.
- A calendar view to navigate between days and plan activities ahead. Clicking a day will go to the same view we have at 1.
- Default items that will be allocated to each day created. This should be an editable list containing all recurring habits.
Database modeling
Now that we have a clear view of what we need we can create tables and assign fields to them in Supabase.
I will create 2 tables:
- todo: To store the activities of all days and all users.
create table
public.todo (
id bigint generated by default as identity,
created_at timestamp with time zone not null default now(),
user_id uuid null default auth.uid (),
todo json null default '{}'::json,
day date null,
constraint todo_pkey primary key (id)
) tablespace pg_default;
- default_todo: To store default habits (one per user).
create table
public.default_todo (
id bigint generated by default as identity,
created_at timestamp with time zone not null default now(),
user_id uuid null default auth.uid (),
defaults json null default '{}'::json,
constraint default_todo_pkey primary key (id)
) tablespace pg_default;
Now we can create a default_todo row when a user registers by creating a database function and trigger on auth.users:
CREATE
OR REPLACE FUNCTION insert_default_todo () RETURNS TRIGGER AS $$
begin insert into public.default_todo (user_id, defaults)
values (NEW.id, '{"items":[]}');
return NEW;
end;
$$ LANGUAGE plpgsql security definer;
CREATE TRIGGER insert_default_todo_trigger
after insert on auth.users
for each row execute procedure public.insert_default_todo();
Don't forget about policies! This is where user data filtering will happen. We can set RLS (Row level security) to only allow insert for authenticated users:
alter policy "Enable insert for authenticated users only"
on "public"."todo"
to authenticated
with check (
true
);
And data access for select and update only where user_id
is the same as authenticated user id (auth.id()
)
alter policy "Enable select for users based on user_id"
on "public"."todo"
to public
using (
(( SELECT auth.uid() AS uid) = user_id)
);
The last thing we will do on Supabase is create a database function to showcase them. In the SQL Editor we can write a function like this:
CREATE
OR REPLACE FUNCTION get_todo_by_day (current_day DATE ) RETURNS TABLE (
id BIGINT,
todo JSON,
DAY DATE
) AS $$
BEGIN
RETURN QUERY
SELECT t.id, t.todo, t.day
FROM todo as t
WHERE t.day = current_day;
END;
$$ LANGUAGE plpgsql;
This function takes a DATE as a parameter to return the row for that day. This is replaceable by a select where(day = parameter_day) but we want to experiment with RPC functions.
Refine
We can now start on the frontend by first defining our Refine component in the App.tsx file. This is what tells Refine where to access its data. For us, we will simply use it to define the pages we want to see in the layout and implement a custom data query. Note the data, auth, router, and notification provider are also defined here.
<Refine
dataProvider={dataProvider(supabaseClient)}
authProvider={authProvider}
routerProvider={routerBindings}
notificationProvider={useNotificationProvider}
resources={[
{
name: "today",
list: "/today",
meta: {
label: "Today"
}
},
{
name: "todo",
list: "/todo",
meta: {
label: "Calendar"
}
},
{
name: "default_todo",
list: "/default_todo",
meta: {
label: "Default Todo"
}
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
projectId: "8BijMs-4mgC2j-TUz0Rv",
title: { text: "Todo App", icon: <CheckSquareOutlined/> },
}}
>
Another change we need in the App.tsx file is to define the components for our routes.
<Route path="/today">
<Route index element={<TodayTodo />} />
</Route>
<Route path="/todo">
<Route index element={<TodoCalendar />} />
</Route>
<Route path="/default_todo">
<Route index element={<Default_todo />} />
</Route>
We can see the pages in the Sider of our Layout and accessing them will load our page components. Now let's move on to actually writing the code for each page!
Default Todo items
To fetch the data we can call useList({ resource: "default_todo" })
and access the first element knowing each user will only have one default_todo row.
This page needs to display current items, with the possibility of adding and removing items. Luckily, Ant Design has the Form.List component that implements this. We just need to combine it with whatever style we want to achieve. I chose to use a Table instead of mapping over all fields of the Form. Here is a basic hierarchy to better understand this:
<Form>
<Form.List>
{(fields, operator) =>{
return (<>
<Table dataSource={fields}>
............ Column configuration ..............
</Table>
<Button onClick=( ()=> operator.add();)/>
</>
);
}
}
</Form.List>
<Form>
Today's todo
We will first begin by returning the current day from the todo table by using the database function we created.
supabaseClient
.rpc("get_todo_by_day", { current_day: dayjs().format("YYYY-MM-DD") })
.then((data) => {});
If this returns an empty array we will attempt to get the default_todo items for the current user and create an entry in the todo list and set the form to display these items as well.
supabaseClient.from("default_todo").select("*").then((data) => {
if(data.data){
mutateCreate({
resource: "todo",
values: {
todo: {items: data.data[0].defaults.items},
day: dayjs().format("YYYY-MM-DD") },
});
form.setFieldsValue({ defaults: data.data[0].defaults.items });
}
});
Calendar view
We will begin by adding a Calendar component from Ant Design and define the onSelect and cellRender properties.
<Calendar onSelect={handleSelect} cellRender={cellRender} />
cellRender will filter the data for any given calendar cell and return a list with the items on that day.
handleSelect will just set the selected date to pass it to a child component:
<SpecificTodo day={selectedDay} returnToCalendar={callbackReturn} />
This component is a copy of the today's todo component with the option to return to calender via a callback function.
With this we have a functional Todo app with authentication, user-specific data filtering, and infinite opportunities for the future, in just 3 hours of configuring and coding.
Hope this helps anyone who wanted to take Refine or Supabase for a spin!
Top comments (2)
Highly recommend the color coding your blocks of code.
You can achieve this by:
Outside of this, it looks great. Thanks for posting. Nice read!
Thank you for the pointer!