Build a Meal Planner using Supabase, NextJS 14 & ShadcnUI (v0)

Nour JandaliNour Jandali
5 min read

Introduction

In this article, we'll explore how to integrate Supabase's features, such as Auth, Database, and Storage, with a Next.js 14 application. Our project includes user authentication, data management, and file storage, showcasing the power of Supabase and the capabilities of Next.js 14.

1. Supabase Auth: Setting Up Authentication

  • Sign-up function

  const signUp = async (formData: FormData) => {
    "use server";

    const origin = headers().get("origin");
    const email = formData.get("email") as string;
    const password = formData.get("password") as string;
    const cookieStore = cookies();
    const supabase = createClient(cookieStore);

    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${origin}/auth/callback`,
      },
    });

    if (error) {
      return redirect("/login?message=Could not authenticate user");
    }

    return redirect("/login?message=Check email to continue sign in process");
  };

The signUp function is a server-side implementation in a Next.js application for registering new users with Supabase. It extracts user credentials (email and password) from the provided form data, initializes the Supabase client using cookie-based session management, and then calls Supabase's auth.signUp method to register the user. The function includes error handling to manage signup failures and redirects users to the login page with appropriate messages—either prompting them to check their email for verification (on successful signup) or indicating an authentication error (if signup fails). This approach ensures a secure and streamlined user registration process.

  • Sign-in function

  const signIn = async (formData: FormData) => {
    "use server";

    const email = formData.get("email") as string;
    const password = formData.get("password") as string;
    const cookieStore = cookies();
    const supabase = createClient(cookieStore);

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });

    if (error) {
      return redirect("/login?message=Could not authenticate user");
    }

    return redirect("/");
  };

The signIn function is a server-side implementation in a Next.js application for authenticating users with Supabase. It extracts user credentials (email and password) from the provided form data, initializes the Supabase client using cookie-based session management, and then calls Supabase's auth.signInWithPassword method to authenticate the user. The function includes error handling to manage sign-in failures, redirecting users to the login page with an error message if authentication fails. On successful authentication, it redirects users to the homepage. This approach ensures a secure and efficient user sign-in process.

  • Protected pages

To protect pages from being accessed by an unauthenticated user, checking the session shall be done.

export default async function Index() {
  const cookieStore = cookies();
  const supabase = createClient(cookieStore);

  const {
    data: { session },
  } = await supabase.auth.getSession();

  if (!session) {
    return <Error />;
  }

  return (
    <div>
      <Meals />  {/* <AddMeals />  */}
    </div>
  );
}
  • The code retrieves the user's session from Supabase. If no session is found (indicating the user is not authenticated), it renders the Error component.

  • If a user session exists, indicating the user is authenticated, the component renders Meals or AddMeals components (this can be done for all pages you want to prevent being accessed by an unauthenticated user), displaying content presumably meant for logged-in users only.

2. Supabase Database: Setting Up Database

In our Next.js application, we utilize Supabase for database interactions. The provided snippets show how to add new meal records and retrieve them from the 'meals' table in the Supabase database.

Create a new table from Supabase Database Table Editor, called "meals".

  • Adding Meals to the Database

"use server";

import { createClient } from "@/utils/supabase/server";
import { cookies } from "next/headers";

export async function handleAddMeals(mealDetails: any) {
  const cookieStore = cookies();
  const supabase = createClient(cookieStore);
  const { data, error } = await supabase.from("meals").insert([mealDetails]);

  if (error) {
    console.error("Error inserting data: ", error);
    return { success: false, error };
  } else {
    console.log("Data inserted successfully: ", data);
    return { success: true, data };
  }
}

The handleAddMeals function is designed to insert a new meal record into the Supabase Database.

  • Retrieving Meals from the Database

"use server";
import { createClient } from "@/utils/supabase/server";
import { cookies } from "next/headers";

export async function handleShowMeals() {
  const cookieStore = cookies();
  const supabase = createClient(cookieStore);
  const { data: meals, error } = await supabase.from("meals").select();

  if (error) {
    console.error("Error inserting data: ", error);
    return { success: false, error };
  } else {
    console.log("Data inserted successfully: ", meals);
    return { success: true, meals };
  }
}

The handleShowMeals function retrieves all meal records from the database.

3. Supabase Storage: Setting Up Supabase Storage Bucket

  • File Upload Handling (handleFileChange): This function manages the file upload process. When a user selects a file, the function uploads it to Supabase Storage using a unique path generated by randomNameId. Upon successful upload, the image URL is constructed using the fullPath provided by Supabase and is then used to update the mealDetails state with the new image URL.
const randomNameId = `name-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;  

  const handleFileChange = async (event: any) => {
    const file = event.target.files[0];
    const { data, error } = await supabase.storage
      .from("images")
      .upload(`/public/${randomNameId}`, file, {
        cacheControl: "3600",
        upsert: false,
      });
    setMealDetails((prev) => ({
      ...prev,
      image: `${supabaseUrl}/storage/v1/object/public/${
        (data as any).fullPath
      }`,
    }));
  };
  • Supabase Storage Interaction: The code demonstrates interaction with Supabase Storage for managing file uploads. It utilizes Supabase's storage.from("images").upload method, specifying the 'images' bucket for storage. This method handles the uploading of files with unique paths, ensures caching through the cacheControl parameter, and avoids overwriting existing files by setting upsert to false.

4. UI (ShadcnUI - v0)

With the help of v0.dev, it was so easy to create a simple UI in few minutes. All pages were created using v0: landing page, meals and add meals pages.

Prompts:

  • Landing page: A landing page for a Meal Planner containing "add meals page (add meal's name, calories, carbs, protein and fats)" and also a page containing "meals created showing a card with the information"

  • Error page: Create a not logged-in error page.

  • All Meals page: A meal planner page featuring (meal's image, meal's name, calories, carbohydrates, protein and fats, each consisting of number of grams)

  • Show Meals page: A meal creating page featuring meal's image, meal's name, calories, carbohydrates, protein and fats, each consisting of number of grams where you can add all of its information.

GitHub Repository

https://github.com/nourjandali/meal-planner

0
Subscribe to my newsletter

Read articles from Nour Jandali directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nour Jandali
Nour Jandali