DEV Community

Stefan Hodoroaba
Stefan Hodoroaba

Posted on

Todo App in 3 hours

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

  1. Daily Todo
  2. Calendar with all days
  3. Default items

Here is a bit more detail:

  1. 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.
  2. A calendar view to navigate between days and plan activities ahead. Clicking a day will go to the same view we have at 1.
  3. 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:

  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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/> },
    }}
>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Default Todo items

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) => {});
Enter fullscreen mode Exit fullscreen mode

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 });
        }
});
Enter fullscreen mode Exit fullscreen mode

Today's 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}  />
Enter fullscreen mode Exit fullscreen mode

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}  />
Enter fullscreen mode Exit fullscreen mode

This component is a copy of the today's todo component with the option to return to calender via a callback function.

Calendar view

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)

Collapse
 
jgdevelops profile image
Julian Gaston

Highly recommend the color coding your blocks of code.
You can achieve this by:

Image description

Outside of this, it looks great. Thanks for posting. Nice read!

Collapse
 
stefan_hodoroaba profile image
Stefan Hodoroaba

Thank you for the pointer!