Build an E-Commerce Dashboard using Supabase and NextJS: Series 1

Build an E-Commerce Dashboard using Supabase and NextJS: Series 1

ยท

9 min read

Building an e-commerce dashboard from scratch can be challenging, but with Supabase and Next.js, it doesn't have to be. This blog post is your all-in-one guide to creating a dynamic, real-time e-commerce platform. I will start by showing you how to set up user authentication, so your customers can log in and sign up with ease. Then, you'll learn how to protect sensitive pages with server-side checks to ensure only authorized users can access them.

Next, we'll dive into managing your product inventory, including how to add products with images and keep everything in sync with real-time updates. Through easy-to-follow code snippets and step-by-step instructions, you'll be able to create a fully functional e-commerce dashboard that not only looks great but also works seamlessly.

  1. User Interface

Shadcn/ui offers a set of ready-to-use UI components that make creating a dashboard a breeze. I'll be using Shadcn/ui new blocks in this guide to simplify the setup and speed up the overall process. This collection of blocks provides everything you need to build a modern and functional e-commerce dashboard without spending extra time on design and styling.

  1. Supabase Auth

  1. You need to install the @supabase/supabase-js package and the helper @supabase/ssr package.
npm install @supabase/supabase-js @supabase/ssr
  1. Then, you need to create your supabase environment variables by creating a .env.local file in your project root directory.
NEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
  1. To access Supabase from your Next.js app, you need 4 types of Supabase clients. (Create a utils/supabase folder with a file for each type of client. Then copy the utility functions for each client type)

    1. getServerSideProps client - To access Supabase from getServerSideProps.

       // utils/supabase/server-props.ts
       import { createServerClient, type CookieOptions, serialize } from '@supabase/ssr'
       import { type GetServerSidePropsContext } from 'next'
      
       export function createClient(context: GetServerSidePropsContext) {
         const supabase = createServerClient(
           process.env.NEXT_PUBLIC_SUPABASE_URL!,
           process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
           {
             cookies: {
               get(name: string) {
                 return context.req.cookies[name]
               },
               set(name: string, value: string, options: CookieOptions) {
                 context.res.appendHeader('Set-Cookie', serialize(name, value, options))
               },
               remove(name: string, options: CookieOptions) {
                 context.res.appendHeader('Set-Cookie', serialize(name, '', options))
               },
             },
           }
         )
      
         return supabase
       }
      
    2. getStaticProps client - To access Supabase from getStaticProps.

       // utils/supabase/static-props.ts
       import { createClient as createClientPrimitive } from '@supabase/supabase-js'
      
       export function createClient() {
         const supabase = createClientPrimitive(
           process.env.NEXT_PUBLIC_SUPABASE_URL!,
           process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
         )
      
         return supabase
       }
      
    3. Component client - To access Supabase from within components.

       // utils/supabase/component.ts
       import { createBrowserClient } from '@supabase/ssr'
      
       export function createClient() {
         const supabase = createBrowserClient(
           process.env.NEXT_PUBLIC_SUPABASE_URL!,
           process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
         )
      
         return supabase
       }
      
    4. API route client - To access Supabase from API route handlers.

       //utils/supabase/api.ts
       import { createServerClient, type CookieOptions, serialize } from '@supabase/ssr'
       import { type NextApiRequest, type NextApiResponse } from 'next'
      
       export default function createClient(req: NextApiRequest, res: NextApiResponse) {
         const supabase = createServerClient(
           process.env.NEXT_PUBLIC_SUPABASE_URL!,
           process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
           {
             cookies: {
               get(name: string) {
                 return req.cookies[name]
               },
               set(name: string, value: string, options: CookieOptions) {
                 res.appendHeader('Set-Cookie', serialize(name, value, options))
               },
               remove(name: string, options: CookieOptions) {
                 res.appendHeader('Set-Cookie', serialize(name, '', options))
               },
             },
           }
         )
      
         return supabase
       }
      
  2. Now that you have all types of client, let's create the Login and Signup functions.

    1. Login:

       import { createClient } from "@/utils/supabase/component";
       import { useRouter } from "next/router";
      
       async function logIn() {
         const { error } = await supabase.auth.signInWithPassword({
           email,
           password,
         });
         if (error) {
           console.error(error);
         }
         router.push("/");
       }
      

      It is as simple as that! There are many SignIn options with Supabase, check this.

      Integrating this with shadcn/ui login block:

       import { createClient } from "@/utils/supabase/component";
       import { useRouter } from "next/router";
       import { useState } from "react";
       import Link from "next/link";
      
       import { Button } from "@/components/ui/button";
       import { Input } from "@/components/ui/input";
       import { Label } from "@/components/ui/label";
       import {
         Card,
         CardContent,
         CardDescription,
         CardHeader,
         CardTitle,
       } from "@/components/ui/card";
      
       export default function Login() {
         const router = useRouter();
         const supabase = createClient();
      
         const [email, setEmail] = useState("");
         const [password, setPassword] = useState("");
      
         async function logIn() {
           const { error } = await supabase.auth.signInWithPassword({
             email,
             password,
           });
           if (error) {
             console.error(error);
           }
           router.push("/");
         }
      
         return (
           <div className="w-full h-screen grid items-center justify-center">
             <Card>
               <CardHeader>
                 <CardTitle className="text-2xl">Login</CardTitle>
                 <CardDescription>
                   Enter your email below to login to your account
                 </CardDescription>
               </CardHeader>
               <CardContent>
                 <form>
                   <div className="grid gap-4">
                     <div className="grid gap-2">
                       <Label htmlFor="email">Email</Label>
                       <Input
                         id="email"
                         type="email"
                         placeholder="nour@example.com"
                         required
                         onChange={(e) => setEmail(e.target.value)}
                       />
                     </div>
                     <div className="grid gap-2">
                       <Label htmlFor="password">Password</Label>
                       <Input
                         id="password"
                         type="password"
                         required
                         onChange={(e) => setPassword(e.target.value)}
                       />
                     </div>
                     <Button type="button" className="w-full" onClick={logIn}>
                       Login
                     </Button>
                   </div>
                   <div className="mt-4 text-center text-sm">
                     Don&apos;t have an account?{" "}
                     <Link href="/signup" className="underline">
                       Sign up
                     </Link>
                   </div>
                 </form>
               </CardContent>
             </Card>
           </div>
         );
       }
      
    2. Signup:

       import { createClient } from "@/utils/supabase/component";
       import { useRouter } from "next/router";
      
       async function signUp() {
         const { error } = await supabase.auth.signUp({ email, password });
         if (error) {
           console.error(error);
         }
         router.push("/login");
       }
      

      Integrating this with shadcn/ui signup block:

       import { createClient } from "@/utils/supabase/component";
       import { useRouter } from "next/router";
       import { useState } from "react";
       import Link from "next/link";
      
       import { Button } from "@/components/ui/button";
       import { Input } from "@/components/ui/input";
       import { Label } from "@/components/ui/label";
       import {
         Card,
         CardContent,
         CardDescription,
         CardHeader,
         CardTitle,
       } from "@/components/ui/card";
      
       export default function Signup() {
         const router = useRouter();
         const supabase = createClient();
      
         const [email, setEmail] = useState("");
         const [password, setPassword] = useState("");
      
         async function signUp() {
           const { error } = await supabase.auth.signUp({ email, password });
           if (error) {
             console.error(error);
           }
           router.push("/login");
         }
         return (
           <div className="w-full h-screen grid items-center justify-center">
             <Card>
               <CardHeader>
                 <CardTitle className="text-xl">Sign Up</CardTitle>
                 <CardDescription>
                   Enter your information to create an account
                 </CardDescription>
               </CardHeader>
               <CardContent>
                 <div className="grid gap-4">
                   <div className="grid gap-2">
                     <Label htmlFor="email">Email</Label>
                     <Input
                       id="email"
                       type="email"
                       placeholder="m@example.com"
                       required
                       onChange={(e) => setEmail(e.target.value)}
                     />
                   </div>
                   <div className="grid gap-2">
                     <Label htmlFor="password">Password</Label>
                     <Input
                       id="password"
                       type="password"
                       onChange={(e) => setPassword(e.target.value)}
                     />
                   </div>
                   <Button type="button" className="w-full" onClick={signUp}>
                     Create an account
                   </Button>
                 </div>
                 <div className="mt-4 text-center text-sm">
                   Already have an account?{" "}
                   <Link href="/login" className="underline">
                     Log in
                   </Link>
                 </div>
               </CardContent>
             </Card>
           </div>
         );
       }
      

That's it! Now users can signup and login into your dashboard.

  1. Protect your pages from unauthenticated users.

    You can serve a page to authenticated users only by checking for the user data in getServerSideProps. Unauthenticated users will be redirected to the error page.

     // pages/index.tsx
     import type { User } from "@supabase/supabase-js";
     import type { GetServerSidePropsContext } from "next";
     import { createClient } from "@/utils/supabase/server-props";
    
     import Dashboard from "@/components/Dashboard";
    
     export default function Index({ user }: { user: User }) {
       return <Dashboard />;
     }
    
     export async function getServerSideProps(context: GetServerSidePropsContext) {
       const supabase = createClient(context);
    
       const { data, error } = await supabase.auth.getUser();
    
       if (error || !data) {
         return {
           redirect: {
             destination: "/error",
             permanent: false,
           },
         };
       }
    
       return {
         props: {
           user: data.user,
         },
       };
     }
    

With all the steps completed, your dashboard's authentication system is now fully functional ๐ŸŽ‰!

  1. Create the Dashboard

Here, I'll show how to: Add your products manually, store product images with Supabase Storage, fetch the products from Supabase database, and delete products.

  1. Add Products

       const handleAddProduct = async (productsInfo: Product) => {
         const { data, error } = await supabase
           .from("products")
           .insert([productsInfo]);
    
         if (error) {
           console.error("Insert operation failed:", error.message);
         }
         return data;
       };
    

    And that's it! You just need to populate your Database with your products' information. Let's see an example:

     import { useState } from "react";
     import { Product } from "@/types";
     import { createClient } from "@/utils/supabase/component";
    
     import { Label } from "@/components/ui/label";
     import { Input } from "@/components/ui/input";
     import { Button } from "@/components/ui/button";
    
     export function AddProduct() {
       const [productInfo, setProductInfo] = useState<Product>({
         image: null,
         name: "",
         status: "",
         price: "",
         quantity: "",
         timestamp: new Date().toISOString(),
       });
    
       const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
         setProductInfo({ ...productInfo, [e.target.id]: e.target.value });
       };
    
       const handleSubmit = async (e: React.FormEvent) => {
         e.preventDefault();
         await handleAddProduct(productInfo);
         setProductInfo({
           image: null,
           name: "",
           status: "",
           price: "",
           quantity: "",
           timestamp: "",
         });
       };
    
       const handleAddProduct = async (productsInfo: Product) => {
         const { data, error } = await supabase
           .from("products")
           .insert([productsInfo]);
    
         if (error) {
           console.error("Insert operation failed:", error.message);
         }
         return data;
       };
    
       return (
         <form
           onSubmit={handleSubmit}
           className="flex flex-col items-center justify-center"
         >
           <div className="space-y-4 w-full">
             <div className="grid w-full items-center gap-1.5">
               <Label htmlFor="name">Product name</Label>
               <Input
                 id="name"
                 placeholder="Product name"
                 type="text"
                 value={productInfo.name}
                 onChange={handleChange}
               />
             </div>
             <div className="grid w-full items-center gap-1.5">
               <Label htmlFor="status">Status</Label>
               <Input
                 id="status"
                 placeholder="Status"
                 type="text"
                 value={productInfo.status}
                 onChange={handleChange}
               />
             </div>
             <div className="grid w-full items-center gap-1.5">
               <Label htmlFor="price">Price</Label>
               <Input
                 id="price"
                 placeholder="Price"
                 type="number"
                 value={productInfo.price}
                 onChange={handleChange}
               />
             </div>
             <div className="grid w-full items-center gap-1.5">
               <Label htmlFor="quantity">Quantity</Label>
               <Input
                 id="quantity"
                 placeholder="Quantity"
                 type="number"
                 value={productInfo.quantity}
                 onChange={handleChange}
               />
             </div>
             <Button type="submit" className="w-full">
               Submit
             </Button>
           </div>
         </form>
       );
     }
    
  2. Store Products' images

    You need to navigate to your Supabase dashboard, then go to Storage, from there you need to create a new bucket. Let's call that bucket "images". Inside "images" create a directory called "public" and that's it! Let's write the upload function.

    1. You need to have your supabaseUrl in your file:

       const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
      
    2. Let's create a randomNameId so that not all images have the same name and get overwritten.

       const randomNameId = `name-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
      
    3. Let's write our handleFileChange function:

         const handleFileChange = async (
           event: React.ChangeEvent<HTMLInputElement>
         ) => {
           try {
             const file = event.target.files?.[0];
             if (file) {
               const { data, error } = await supabase.storage
                 .from("images")
                 .upload(`/public/${randomNameId}`, file, {
                   cacheControl: "3600",
                   upsert: false,
                 });
               if (error) {
                 throw error;
               }
               setProductInfo((prev) => ({
                 ...prev,
                 image: `${supabaseUrl}/storage/v1/object/public/images/${data?.path}`,
               }));
             }
           } catch (error) {
             console.error("An error occurred while uploading the file:", error);
           }
         };
      

      What it does is that, it uploads the image with a randomNameId to the "public" directory in "images" bucket. And then, we set the image to the correct url.

    4. We create an Input field for uploading the image

       <div className="grid items-center gap-1.5">
         <Label className="text-sm">Upload product image</Label>
         <Label htmlFor="file-upload" className="w-fit cursor-pointer">
           <img
             className="rounded-md size-16"
             src={productInfo.image || "/placeholder.svg"}
             alt="Product image"
           />
           <Input
             className="hidden"
             id="file-upload"
             type="file"
             onChange={handleFileChange}
           />
         </Label>
       </div>
      
  3. Fetch Products

     const [products, setProducts] = useState<Product[]>([]);
    
     const fetchProducts = async () => {
       setLoading(true);
       try {
         const { data, error } = await supabase.
             from("products").
             select("*");
    
         if (error) {
           throw new Error(error.message);
         }
         setProducts(data);
       } catch (err) {
         setError((err as Error).message);
       }
     };
    

    To use Supabase Realtime, you simply add such function:

     const subscribeToNewProducts = () => {
       supabase
         .channel("products")
         .on(
           "postgres_changes",
           { event: "INSERT", schema: "public", table: "products" },
           fetchProducts
         )
         .subscribe();
     };
    

    Now, whenever you add a new product, it adds the product in realtime (no need to refresh)!

  4. Delete Products

       const handleDelete = async (id: string) => {
         const { data, error } = await supabase
           .from("products")
           .delete()
           .eq("id", id);
    
         if (error) {
           console.log(error);
         }
         if (data) {
           console.log(data);
         }
    
         const newProducts = products.filter((product) => product.id !== id);
         setProducts(newProducts);
       };
    

And we are done! Now, you can have a functional Dashboard to start with ๐ŸŽ‰! In the next Series, I'll continue building the dashboard with all its features.

Demo video

Entire code is available at my GitHub.

ย