Workshop Hands-on
Let’s Set Up the Accounts
- Tienes Github?
- Tienes Vercel?
- Tienes Supabase?
Getting Started
-
Go to Database.new and configure your instance.
-
Go to the SQL editor and do the following:
- 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); -
Create the Next App
npx create-next-app -e with-supabase- Configure env.local
NEXT_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<SUBSTITUTE_SUPABASE_ANON_KEY>-
Rename /protected/page.tsx to /protected/_page.tsx
-
Create a new page.tsx in that location:
export default function Home() { return <div>Hello World</div>; } -
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> ); } -
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> ); } -
Create a new Repo on GitHub
-
Create project on Vercel
-
Add environment variables on Vercel
-
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