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.
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.
Supabase Auth
- You need to install the
@supabase/supabase-js
package and the helper@supabase/ssr
package.
npm install @supabase/supabase-js @supabase/ssr
- 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>
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)getServerSideProps
client - To access Supabase fromgetServerSideProps
.// 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 }
getStaticProps
client - To access Supabase fromgetStaticProps
.// 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 }
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 }
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 }
Now that you have all types of client, let's create the
Login
andSignup
functions.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't have an account?{" "} <Link href="/signup" className="underline"> Sign up </Link> </div> </form> </CardContent> </Card> </div> ); }
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.
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 ๐!
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.
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> ); }
Store Products' images
You need to navigate to your Supabase dashboard, then go to
Storage
, from there you need to create anew bucket
. Let's call thatbucket
"images". Inside "images" create a directory called "public" and that's it! Let's write the upload function.You need to have your
supabaseUrl
in your file:const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
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)}`;
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.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>
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)!
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.