Build a Meal Planner using Supabase, NextJS 14 & ShadcnUI (v0)
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
orAddMeals
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 byrandomNameId
. Upon successful upload, the image URL is constructed using thefullPath
provided by Supabase and is then used to update themealDetails
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 thecacheControl
parameter, and avoids overwriting existing files by settingupsert
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
Subscribe to my newsletter
Read articles from Nour Jandali directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by