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:
Prerequisites
To follow along with this tutorial, you need the following:
Basic knowledge of Next.js and PostgreSQL
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.
Step 2: Get your database connection string
- Navigate to Project Settings > Connect.
- You will find the connection string under Connection String > URL. Save it — you’ll need it for the Medusa setup.
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
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:
Medusa backend: http://localhost:9000
Medusa admin dashboard: http://localhost:7001
To access the admin dashboard, create an admin account or use the default credentials ( Email: admin@medusa-test.com Password: supersecret
)
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:
Navigate to
src/admin/routes
in your project directory.-
Create a folder structure:
sales/page.tsx
.
└── my-store/ └── src/ └── admin/ └── routes/ └── sales/ └── page.tsx
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
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.
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
The Sales route will now appear on the admin dashboard sidebar.
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
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;
💡 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;
}
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;
}
}
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",
];
};
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,
};
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
- 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;
`);
}
}
- Run the migration:
npm run build
npx medusa migrations run
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.
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;
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;
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;
- 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;
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 likefirst_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)