Workshop Hands-on

Let’s Set Up the Accounts

  • Tienes Github?
  • Tienes Vercel?
  • Tienes Supabase?

Getting Started

  1. Go to Database.new and configure your instance.

  2. Go to the SQL editor and do the following:

    1. Create Tables
    create table todos (
      id bigint generated by default as identity primary key,
      user_id uuid references auth.users not null,
      task text check (char_length(task) > 3),
      is_complete boolean default false,
      inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
    );

    b. Create RLS Policies

    alter table todos enable row level security;
    create policy "Individuals can create todos." on todos for
        insert with check (auth.uid() = user_id);
    create policy "Individuals can view their own todos. " on todos for
        select using ((select auth.uid()) = user_id);
    create policy "Individuals can update their own todos." on todos for
        update using ((select auth.uid()) = user_id);
    create policy "Individuals can delete their own todos." on todos for
        delete using ((select auth.uid()) = user_id);
  3. Create the Next App

npx create-next-app -e with-supabase
  1. Configure env.local
NEXT_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<SUBSTITUTE_SUPABASE_ANON_KEY>
  1. Rename /protected/page.tsx to /protected/_page.tsx

  2. Create a new page.tsx in that location:

    export default function Home() {
      return <div>Hello World</div>;
    }
  3. Create the component. ToDoList.tsx

    "use client";
    import { useEffect, useState } from "react";
    import { createClient } from "@/utils/supabase/client";
     
    interface Todo {
      id: number;
      task: string;
      is_complete: boolean;
      inserted_at: string;
    }
     
    export default function ToDoList() {
      const supabase = createClient();
      const [todos, setTodos] = useState<Todo[]>([]);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<string | null>(null);
     
      // Fetch todos when component mounts
      useEffect(() => {
        fetchTodos();
      }, []);
     
      const fetchTodos = async () => {
        try {
          setLoading(true);
          const { data, error } = await supabase
            .from("todos")
            .select("*")
            .order("inserted_at", { ascending: false });
     
          if (error) {
            throw error;
          }
     
          setTodos(data || []);
        } catch (error) {
          setError(error instanceof Error ? error.message : "An error occurred");
        } finally {
          setLoading(false);
        }
      };
     
      const toggleTodoComplete = async (id: number, currentStatus: boolean) => {
        try {
          const { error } = await supabase
            .from("todos")
            .update({ is_complete: !currentStatus })
            .eq("id", id);
     
          if (error) {
            throw error;
          }
     
          // Update local state
          setTodos(
            todos.map((todo) =>
              todo.id === id ? { ...todo, is_complete: !currentStatus } : todo,
            ),
          );
        } catch (error) {
          setError(error instanceof Error ? error.message : "An error occurred");
        }
      };
     
      // Show loading state
      if (loading) {
        return (
          <div className="flex justify-center items-center p-4">
            <p>Loading...</p>
          </div>
        );
      }
     
      // Show error state
      if (error) {
        return (
          <div className="flex justify-center items-center p-4">
            <p className="text-red-500">Error: {error}</p>
          </div>
        );
      }
     
      return (
        <div className="max-w-2xl mx-auto p-4">
          <h2 className="text-2xl font-bold mb-4">Your ToDo List</h2>
     
          {todos.length === 0 ? (
            <p className="text-gray-500">No todos yet!</p>
          ) : (
            <ul className="space-y-2">
              {todos.map((todo) => (
                <li
                  key={todo.id}
                  className="flex items-center justify-between p-3 bg-white rounded shadow"
                >
                  <div className="flex items-center space-x-3">
                    <input
                      type="checkbox"
                      checked={todo.is_complete}
                      onChange={() => toggleTodoComplete(todo.id, todo.is_complete)}
                      className="h-5 w-5 rounded border-gray-300"
                    />
                    <span
                      className={`${todo.is_complete ? "line-through text-gray-500" : ""}`}
                    >
                      {todo.task}
                    </span>
                  </div>
                  <span className="text-sm text-gray-500">
                    {new Date(todo.inserted_at).toLocaleDateString()}
                  </span>
                </li>
              ))}
            </ul>
          )}
        </div>
      );
    }
     
  4. Create AddToDo.tsx

    "use client";
    import { useState, useEffect } from "react";
    import { createClient } from "@/utils/supabase/client";
     
    export default function AddToDo({ onAdd }: { onAdd?: () => void }) {
      const supabase = createClient();
      const [task, setTask] = useState("");
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);
      const [user, setUser] = useState<any>(null);
     
      useEffect(() => {
        const checkUser = async () => {
          const {
            data: { user },
          } = await supabase.auth.getUser();
          setUser(user);
        };
        checkUser();
      }, []);
     
      const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
     
        if (!user) {
          setError("Please login to add todos");
          return;
        }
     
        if (task.length < 4) {
          setError("Task must be at least 4 characters long");
          return;
        }
     
        try {
          setLoading(true);
          setError(null);
     
          const { error } = await supabase
            .from("todos")
            .insert([{ task, user_id: user.id }]);
     
          if (error) throw error;
     
          setTask("");
          if (onAdd) onAdd();
        } catch (err) {
          setError(err instanceof Error ? err.message : "Failed to add todo");
        } finally {
          setLoading(false);
        }
      };
     
      if (!user) {
        return (
          <div className="max-w-2xl mx-auto p-4">
            <p className="text-gray-600">Please login to add todos</p>
          </div>
        );
      }
     
      return (
        <form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4">
          <div className="flex gap-2">
            <input
              type="text"
              value={task}
              onChange={(e) => setTask(e.target.value)}
              placeholder="Add a new todo..."
              className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
              disabled={loading}
            />
            <button
              type="submit"
              disabled={loading}
              className={`px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 ${
                loading ? "opacity-50 cursor-not-allowed" : ""
              }`}
            >
              {loading ? "Adding..." : "Add Todo"}
            </button>
          </div>
          {error && <p className="mt-2 text-red-500 text-sm">{error}</p>}
        </form>
      );
    }
     
  5. Create a new Repo on GitHub

  6. Create project on Vercel

  7. Add environment variables on Vercel

  8. Add redirect URLs

Learn More

https://www.youtube.com/@odisealabs

https://youtu.be/T-qAtAKjqwc?si=1dHG5LjEIbh18Uby

https://supabase.com/docs/guides/getting-started/quickstarts/nextjs