DEV Community

Cover image for How to Build a Custom MedusaJS Admin Dashboard with NextJS, Supabase, and Tailwind CSS
Victor Yakubu
Victor Yakubu

Posted on

How to Build a Custom MedusaJS Admin Dashboard with NextJS, Supabase, and Tailwind CSS

When it comes to managing an e-commerce business, no two business needs are the same. For example, one e-commerce store might need a dashboard focused on tracking real-time inventory, while another prioritizes visualizing sales trends and customer engagement. That’s why off-the-shelf admin dashboards or pre-built templates frequently fall short: they’re not designed to accommodate the specific needs of individual businesses.

However, a customizable solution like Medusa solves this by providing the building blocks and REST API endpoints you need to build custom e-commerce solutions that can adapt to the business’s current needs. This ensures you retain control over every aspect of your e-commerce ecosystem.

In this article, you’ll learn how to:

  • Set up an e-commerce backend using MedusaJS and Supabase.

  • Build a custom sales dashboard using Next.js and Tailwind CSS.

  • Extend MedusaJS’s admin functionality to meet specific business requirements.

By the end of this guide, you’ll have an admin dashboard that looks like this:

Custom Admin Dashboard with MedusaJS

Prerequisites

To follow along with this tutorial, you need the following:

  1. Basic knowledge of Next.js and PostgreSQL

  2. Node.js (v16 or later) — Install it from nodejs.org.

Setting up your Medusa store locally

Follow these steps to set up your Medusa store:

Step 1: Create a PostgreSQL database using Supabase

  • Visit Supabase and sign up.

  • Click “Start your project” and create a new account.

  • From your dashboard, click “New Project”

  • Fill in the details for your database, i.e., Project Name and Database password. You can either use the default region or select one closer to you.

  • After that, click “ Create new project”. It will create a new Supabase project with an entire Postgres database.

Supabase

Step 2: Get your database connection string

  • Navigate to Project Settings > Connect.

Supabase Sign up Dashboard

  • You will find the connection string under Connection String > URL. Save it — you’ll need it for the Medusa setup.

Supabase Dashboard

Step 3: Install and set up Medusa locally

Run the following command to install and set up your Medusa project, connecting it to your Supabase database:

npx create-medusa-app@latest - seed - db-url postgresql://postgres:<password>@<host>.supabase.co:5432/postgres
Enter fullscreen mode Exit fullscreen mode

Replace <password> in the command with the database password you created in Step 1.

Command flags explained:

  • — seed: Seeds the database with demo data.

  • — db-url: Specifies your database connection URL.

You can see the other CLI options that create-medusa-app accepts in the documentation.

Once the installation has been completed, your project will be served in the following ports:

  1. Medusa backend: http://localhost:9000

  2. Medusa admin dashboard: http://localhost:7001

supabase

To access the admin dashboard, create an admin account or use the default credentials ( Email: admin@medusa-test.com Password: supersecret)

Supabase

Add a custom Sales Overview page.

Aside from the default features, MedusaJS allows you to add custom routes to your admin dashboard, enabling you to track specific business metrics like sales performance. Here’s how you can add a custom Sales Overview page:

1. Creating a custom Admin UI route

MedusaJS admin routes are React components in the src/admin/routes directory. To create a custom route for sales:

  1. Navigate to src/admin/routes in your project directory.

  2. Create a folder structure: sales/page.tsx.

    └── my-store/
    └── src/
        └── admin/
            └── routes/
                └── sales/
                    └── page.tsx
    
  3. Inside page.tsx, export a React component for the sales page:

const Sales = () => {
  return (
    <div>
      See the number of products sold and the number remaining in stock
    </div>
  )
}

export default Sales

Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:7001/a/sales to see your custom page. However, it is not accessible from the sidebar; you will fix that in the next section.

MedusaJS

2. Adding the route to the Sidebar

To make the route accessible from the sidebar:

  • Import RouteConfig from @medusajs/admin and define the route configuration:
import { RouteConfig } from "@medusajs/admin"
import { CircleStack } from '@medusajs/icons';

const Sales = () => {
  return (
    <div>
      See the number of product sold and the number left in stock
    </div>
  )
}

export const config: RouteConfig = {
  link: {
    label: "Sales",
    icon: CircleStack,
  },
}

export default Sales
Enter fullscreen mode Exit fullscreen mode

The Sales route will now appear on the admin dashboard sidebar.

Sales route

Extending your Product Entity to add custom fields

To make the Sales Overview page functional, you need to ensure your database supports all the required data. Specifically, you’ll want to display each product’s current stock, price, and units sold. While the first two are available in the default schema, the units_sold data isn’t. So, how do you add custom fields?

Step 1: Reviewing the current schema

Start by exploring the existing data structure. You can make a GET request to the /admin/products endpoint to see the available fields for each product. This endpoint is accessible at:

http://localhost:9000/admin/products
Enter fullscreen mode Exit fullscreen mode

You’ll notice there isn’t a column for units_sold. This is where customization becomes necessary.

Step 2: Modifying your database schema to the units_sold Column

Since Medusa uses a PostgreSQL database with TypeORM as the ORM, you’ll need to update both the database schema and the MedusaJS product entity model to add new columns.

To start, modify your database schema to include a units_sold column. For better granularity, you can add quarterly columns (units_sold_q1, units_sold_q2, etc.). Run the following SQL commands directly on your database:

ALTER TABLE product ADD COLUMN units_sold_q1 INT DEFAULT 0;
ALTER TABLE product ADD COLUMN units_sold_q2 INT DEFAULT 0;
ALTER TABLE product ADD COLUMN units_sold_q3 INT DEFAULT 0;
ALTER TABLE product ADD COLUMN units_sold_q4 INT DEFAULT 0;
Enter fullscreen mode Exit fullscreen mode

💡 After running these commands, the new columns will exist in your database but won’t yet appear in API responses or the admin dashboard. To fix this, you’ll need to update the MedusaJS entity model.

Step 3: Extending the Product Entity Model

Medusa uses TypeORM to define its models. To add the new columns to the product entity, create a file named product.ts in ./src/models/:

import { Column, Entity } from "typeorm";
import { Product as MedusaProduct } from "@medusajs/medusa";
@Entity()
export class Product extends MedusaProduct {
@Column({ default: 0 })
units_sold_q1: number;
@Column({ default: 0 })
units_sold_q2: number;
@Column({ default: 0 })
units_sold_q3: number;
@Column({ default: 0 })
units_sold_q4: number;
}
Enter fullscreen mode Exit fullscreen mode

The @Column decorator ensures that TypeORM maps the fields to your database.

Step 4: Updating type definitions

If you’re using TypeScript, add the new columns to your IDE’s autocomplete by extending Medusa’s core Product interface. Create a file at src/index.d.ts:

export declare module "@medusajs/medusa/dist/models/product" {
declare interface Product {
units_sold_q1: number;
units_sold_q2: number;
units_sold_q3: number;
units_sold_q4: number;
 }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Exposing new fields in the API

Medusa’s API won’t return your custom columns by default. To include these fields, you need to modify the products endpoint configuration. Create a file at src/loaders/extend-product-fields.ts:

export default async function () {
const imports = await import(
"@medusajs/medusa/dist/api/routes/admin/products/index"
) as any;
imports.defaultAdminProductFields = [
…imports.defaultAdminProductFields,
"units_sold_q1",
"units_sold_q2",
"units_sold_q3",
"units_sold_q4",
 ];
};
Enter fullscreen mode Exit fullscreen mode

Then, register this loader in your Medusa project by adding it to the loaders array in medusa-config.js.

Step 6: Creating a TypeORM data source

To synchronize your new fields with the database, create a TypeORM data source file at the root of your Medusa project. Save it as datasource.js:

const { DataSource } = require("typeorm");
const AppDataSource = new DataSource({
type: "postgres",
port: 5432,
username: "<YOUR_DB_USERNAME>",
password: "<YOUR_DB_PASSWORD>",
database: "<YOUR_DB_NAME>",
entities: ["dist/models/*.js"],
migrations: ["dist/migrations/*.js"],
});
module.exports = {
datasource: AppDataSource,
};
Enter fullscreen mode Exit fullscreen mode

Step 7: Writing and running migrations

To update the database schema programmatically, create and execute a migration:

  • Generate a migration file:
npm run build
npx typeorm migration:create src/migrations/AddUnitsSoldColumnToProduct
Enter fullscreen mode Exit fullscreen mode
  • Edit the generated migration file:
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddUnitsSoldColumnToProduct implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE product ADD COLUMN units_sold_q1 INT DEFAULT 0;
      ALTER TABLE product ADD COLUMN units_sold_q2 INT DEFAULT 0;
      ALTER TABLE product ADD COLUMN units_sold_q3 INT DEFAULT 0;
      ALTER TABLE product ADD COLUMN units_sold_q4 INT DEFAULT 0;
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE product DROP COLUMN units_sold_q1;
      ALTER TABLE product DROP COLUMN units_sold_q2;
      ALTER TABLE product DROP COLUMN units_sold_q3;
      ALTER TABLE product DROP COLUMN units_sold_q4;
    `);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Run the migration:
npm run build
npx medusa migrations run
Enter fullscreen mode Exit fullscreen mode

Step 8: Verifying your changes

After running the migration, your database schema will include the new columns, and they’ll be visible in your /admin/products API responses. Test these changes by sending a GET request to the endpoint.

Now, your MedusaJS project is equipped to track and display quarterly sales data, enabling you to create a fully functional Sales Overview page tailored to your needs.

Building the Sales Page UI

Now that the data flows seamlessly between your admin dashboard and the Medusa backend, it’s time to implement the code for your Admin UI. This section will guide you through building the user interface for a Sales Overview Page.

MedusaJS comes bundled with Medusa UI, a React-based design system that includes components, hooks, utility functions, icons, and pre-made Tailwind CSS classes. These tools ensure a consistent and professional design across your admin dashboard.

Your Sales Overview Page will feature three main components:

  • Header: Displays key metrics like revenue and customer counts.

  • Bar Chart: Visualizes sales data by quarter.

  • Recent Orders Section: Lists recent customer orders.

Admin Dashboard

Creating the Sales Page layout

In the page.tsx file, structure the layout of your Sales Page:

import { RouteConfig } from '@medusajs/admin';
import { CircleStack } from '@medusajs/icons';
import TopCards from './components/TopCards';
import BarChart from './components/BarChart';
import RecentOrders from './components/RecentOrders';
const Sales = () => {
return (
<div>
<TopCards />
<div className="p-4 grid grid-cols-2 gap-4">
<BarChart />
<RecentOrders />
</div>
</div>
);
};
// Adding route into the admin dashboard sidebar
export const config: RouteConfig = {
link: {
label: 'Sales',
icon: CircleStack,
},
};
<style></style>;
export default Sales;
Enter fullscreen mode Exit fullscreen mode

Next, create a component folder under src/admin/routes/sales; inside the component folder, create the following files: TopCards.tsx, BarChart.tsx and RecentOrders.tsx.

Header section

The Header Section displays sales statistics such as quarterly revenue and total customers. Create this component in TopCards.tsx:

src/admin/routes/sales/components/TopCards.tsx

import { Text } from '@medusajs/ui';
const TopCards = () => {
  return (
    <div className="gap-y-large flex flex-col">
      <div className=" 'p-4 gap-y-2xsmall flex flex-col">
        <h2 className="inter-xlarge-semibold">Sales</h2>
        <Text className="inter-base-regular text-grey-50">
          See the number of product sold and the number remaining in stock
        </Text>
      </div>

      <div className=" flex sm:flex-none gap-4 p-4">
        <div className="lg:col-span-2 col-span-1 bg-white flex justify-between w-full border p-4 rounded-lg cursor-pointer hover:shadow-lg transform hover:scale-[103%] transition duration-300 ease-out border-l-[4px] border-[#1F2937]">
          <div className="flex flex-col w-full pb-4">
            <p className="text-2xl font-bold">$7,845</p>
            <p className="text-gray-600">Quarterly Revenue</p>
          </div>
          <p className="bg-green-200 flex justify-center items-center p-2 rounded-lg">
            <span className="text-green-700 text-lg">+18%</span>
          </p>
        </div>
        <div className="lg:col-span-2 col-span-1 bg-white flex justify-between w-full border p-4 rounded-lg cursor-pointer hover:shadow-lg transform hover:scale-[103%] transition duration-300 ease-out border-l-[4px] border-[#1F2937]">
          <div className="flex flex-col w-full pb-4">
            <p className="text-2xl font-bold">$1,14,783</p>
            <p className="text-gray-600">YTD Revenue</p>
          </div>
          <p className="bg-green-200 flex justify-center items-center p-2 rounded-lg">
            <span className="text-green-700 text-lg">+11%</span>
          </p>
        </div>
        <div className="bg-white flex justify-between w-full border p-4 rounded-lg cursor-pointer hover:shadow-lg transform hover:scale-[103%] transition duration-300 ease-out border-l-[4px] border-[#1F2937]">
          <div className="flex flex-col w-full pb-4">
            <p className="text-2xl font-bold">10,845</p>
            <p className="text-gray-600">Customers</p>
          </div>
          <p className="bg-green-200 flex justify-center items-center p-2 rounded-lg">
            <span className="text-green-700 text-lg">+17%</span>
          </p>
        </div>
      </div>
    </div>
  );
};

export default TopCards;
Enter fullscreen mode Exit fullscreen mode

Bar chart section

The Bar chart visualizes sales revenue by quarter. Create this component in BarChart.tsx:

src/admin/routes/sales/components/BarChart.tsx

import React, { useState, useEffect } from 'react';
import { Bar } from 'react-chartjs-2';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';
ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend
);

import {useAdminProducts} from 'medusa-react'

const BarChart = () => {

    const { products} = useAdminProducts();
    const [totalRevenueQ1, setTotalRevenueQ1] = useState(null);
    const [totalRevenueQ2, setTotalRevenueQ2] = useState(null);
    const [totalRevenueQ3, setTotalRevenueQ3] = useState(null);
    const [totalRevenueQ4, setTotalRevenueQ4] = useState(null);

    useEffect(() => {
      async function calculateTotalRevenue() {
        // Check if products is available
        if (!products) {
          console.error('Products data is not available.');
          return;
        }

        // Step 1: Map through Products and calculate revenue for each product, each quarter
        const revenuePerProductQ1 = products.map((product) => {
          const amount = product.variants[0].prices[0].amount;
          const sold_q1 = product.units_sold_q1;
          console.log('amount:', amount, 'sold_q1:', sold_q1);
          return amount * sold_q1;
        });
        const revenuePerProductQ2 = products.map((product) => {
          const amount = product.variants[0].prices[0].amount;
          const sold_q2 = product.units_sold_q2;
          return amount * sold_q2;
        });
        const revenuePerProductQ3 = products.map((product) => {
          const amount = product.variants[0].prices[0].amount;
          const sold_q3 = product.units_sold_q3;
          return amount * sold_q3;
        });
        const revenuePerProductQ4 = products.map((product) => {
          const amount = product.variants[0].prices[0].amount;
          const sold_q4 = product.units_sold_q4;
          return amount * sold_q4;
        });

        // Step 2: Sum the individual multiplication results to get the total revenue for each quarter
        const totalRevenueQ1 = revenuePerProductQ1.reduce(
          (total, revenue) => total + revenue,
          0
        );
        const totalRevenueQ2 = revenuePerProductQ2.reduce(
          (total, revenue) => total + revenue,
          0
        );
        const totalRevenueQ3 = revenuePerProductQ3.reduce(
          (total, revenue) => total + revenue,
          0
        );
        const totalRevenueQ4 = revenuePerProductQ4.reduce(
          (total, revenue) => total + revenue,
          0
        );

        // Set quarterly totalRevenue states with the calculated value
        setTotalRevenueQ1(totalRevenueQ1);
        setTotalRevenueQ2(totalRevenueQ2);
        setTotalRevenueQ3(totalRevenueQ3);
        setTotalRevenueQ4(totalRevenueQ4);
      }
      // calculate total revenue when products data is available
      if (products) {
        calculateTotalRevenue();
      }

    }, [products]);

   const data = {
     labels: ['Q1', 'Q2', 'Q3', 'Q4'],
     datasets: [
       {
         label: 'Total Revenue',
         data: [totalRevenueQ1, totalRevenueQ2, totalRevenueQ3, totalRevenueQ4],
         backgroundColor: '#32de84',
         borderColor: 'rgb(0,128,0)',
       },
     ],
   };

  return (
    <>
      <div className="w-full md:col-span-2 relative lg:h-[70vh] h-[50vh] m-auto p-4 border rounded-lg bg-white">
        <div className="w-100% h-[70vh]">
          <Bar data={data} />
        </div>
      </div>
    </>
  );
};

export default BarChart;
Enter fullscreen mode Exit fullscreen mode
  • The useAdminProducts hook gives you access to the list of available products in the database and also automatically handles auth, passing along credentials in its requests to the Medusa backend as long as you’re signed in to the Admin dashboard. You can learn more about the Admin APIs for managing products here.

Recent Orders section

The Recent Orders Section lists customer orders. Create this component in RecentOrders.tsx:

src/admin/routes/sales/components/RecentOrders.tsx

import { ShoppingBag } from '@medusajs/icons';
import { useAdminOrders } from 'medusa-react';

const RecentOrders = () => {
  const { orders, isLoading } = useAdminOrders();

  return (
    <div className="w-full col-span-1 relative lg:h-[70vh] h-[50vh] m-auto p-4 rounded-lg bg-white overflow-scroll">
      <h1 className="text-center inter-large-semibold text-[#1F2937]">
        Recent Orders
      </h1>
      {isLoading && <span>Loading...</span>}
      {orders && !orders.length && <span>No Orders</span>}
      {orders && orders.length > 0 && (
        <ul>
          {orders.map((order, id) => (
            <li
              key={order.id}
              className="bg-gray-50 hover:bg-gray-100 rounded-lg my-3 p-2 flex items-center cursor-pointer"
            >
              <div className="bg-green-200 rounded-lg p-3">
                <ShoppingBag />
              </div>
              <div className="pl-4">
                <p className="text-gray-800 font-bold">
                  €{order.payments[0].amount}
                </p>
                <p className="text-gray-400 text-sm">
                  {order.customer.first_name}
                </p>
              </div>
              <p className="lg:flex md:hidden absolute right-6 text-sm">
                {order.items[0].title}
              </p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default RecentOrders;
Enter fullscreen mode Exit fullscreen mode
  • The useAdminOrders hook gives you access to the list of orders made by clients, and only authorised credentials have access to this.

  • The useAdminOrders hook also gives you access to all of its properties like first_name, payments_amount, title, etc.

Your Sales Overview Page is now functional and visually appealing with these components. You can modify and add as many functionalities as you want to suit your needs.

Conclusion

This tutorial demonstrated how to build a custom MedusaJS admin dashboard using Next.js, Supabase, and Tailwind CSS. By following these steps, you learned how to set up a Medusa backend, create a dynamic user interface, and extend the Medusa functionalities to build tailored components like the Sales Overview Page.

Check out the Medusa documentation to learn more about advanced features and other customization options you can add to your e-commerce experience.

Top comments (0)