Recreating Steam - Medusa & Next.js

Introduction

GitHub Repository

All the code presented in this article is accessible here

What is Medusa?

Medusa stands as the leading Open Source headless commerce platform on GitHub, using Node.js. Its architecture allows for seamless integration with any tech stack, enabling the creation of cross-platform eCommerce stores with ease.

What is Next.js?

Next.js is one of, if not the most used React-based framework for building scalable and fast web applications. It offers features such as server-side rendering and optimized performance for quick page loads. Additionally, it also supports statically exported websites, making it a versatile & widely used tool for modern web development.

Prerequisites

Before you start, make sure your Node.js version is up-to-date.

Create a backend using Medusa CLI

Install the Medusa CLI

Medusa CLI can be installed by using either npm or yarn. This tutorial uses npm.

npm install @medusajs/medusa-cli -g

Create a new Medusa server

medusa new my-medusa-store --seed

The --seed parameter fills the database with a default user and default products. my-medusa-store is your store's name; you may replace it with your preferred name.

If the setup was successful, the following should output in your console:

info: Your new Medusa project is ready for you! To start developing run:

cd my-medusa-store
medusa develop

Connect the Admin panel to the Medusa Server

Photo of the Admin panel

The Medusa Admin Panel simplifies the management of your store by offering a centralized platform to oversee your products, discounts, customers, and adminis.

Clone the Medusa Admin repository:

git clone https://github.com/medusajs/admin medusa-admin
cd medusa-admin

Run the command below to install all dependencies:

npm install

Start it:

npm start

To access the Medusa Admin, go to localhost:7000 on your browser. The admin panel operates on port 7000. An administrator account has already been generated for you, as you have used the --seed parameter, with the email address admin@medusa-test.com and the password supersecret.

Visit this guide to learn more about Medusa's Admin panel.

Medusa Admin docs

Create the storefront

Create the pages

In a different directory, create a new Next.js project:

npx create-next-app steam-remake-storefront

You may replace steam-remake-storefront with your store's name. After installing, open the project in your favorite code editor.

In the styles directory, delete the Home.module.css file. Replace the globals.css file with this:

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

html {
    color-scheme: dark;
}
body {
    color: white;
    background: #1B2838;
}

Head over to the pages directory and replace index.js with this:

import Head from 'next/head'
import {useEffect, useState} from "react";

import Navbar from "../components/Navbar";
import Navbar_store from "../components/Navbar_store"
import BigShowcase from "../components/Landing/BigShowcase";
import BrowseIncentive from "../components/Landing/BrowseIncentive";
import Spotlight from "../components/Landing/Spotlight";
import ProductList from "../components/Landing/ProductList";

export default function Home() {
    const [getCart, setCart] = useState(null)
    useEffect(() => {
        var id = localStorage.getItem("cart_id");

        if (id) {
            fetch(`http://localhost:9000/store/carts/${id}`, {
                credentials: "include",
            })
                .then((response) => response.json())
                .then(({cart}) => setCart(cart.items.length))
        }
        if (!id) {
            fetch(`http://localhost:9000/store/carts`, {
                method: "POST",
                credentials: "include",
            })
                .then((response) => response.json())
                .then(({cart}) => {
                    localStorage.setItem("cart_id", cart.id);
                    setCart(cart.items.length);
                })
        }
    })
    return (
        <div>
            <Head>
                <title>Steam Remake</title>
                <meta name="description" content="Made using Next & Medusa!"/>
            </Head>
            <Navbar/>
            <Navbar_store items={getCart}/>
            <BigShowcase/>
            <BrowseIncentive/>
            <Spotlight/>
            <div style={{background: 'linear-gradient( to bottom, rgba(42,71,94,1.0) 5%, rgba(42,71,94,0.0) 70%)', borderTopLeftRadius: '6px', borderTopRightRadius: '6px'}}>
                <ProductList/>
            </div>
            <div style={{marginBottom: '1%'}}/>
        </div>
    )
}

You may replace the description (Made using Next & Medusa) and the title (Steam Remake) with your own store's name and promo. You might see that the code above gives you errors; it is using components that do not exist yet. Likewise, you'll create them later.

Create a new file in the pages directory called product.js with these contents:

import Head from 'next/head'
import {useRouter} from "next/router";
import {useEffect, useState} from "react";

import Navbar from "../components/Navbar";
import Navbar_store from "../components/Navbar_store"
import Title from "../components/Product/Title"
import Thumbnail from "../components/Product/Thumbnail"
import AddToCart from "../components/Product/AddToCart";

export default function Product() {
    const router = useRouter()
    const [getProdTitle, setProdTitle] = useState('Placeholder')
    const [getProdDescription, setProdDescription] = useState('Placeholder')
    const [getProdVariant, setProdVariant] = useState('Placeholder')
    const [getProdPrice, setProdPrice] = useState('Placeholder')
    const [getProdPriceCurrency, setProdPriceCurrency] = useState('Placeholder')
    const [getProdThumbnail, setProdThumbnail] = useState('')
    const [getCartId, setCartId] = useState(null)
    const [getCart, setCart] = useState(null)

    useEffect(() => {
        if (!router.query.id) {
            router.push('/404')
        }
        if (router.query.id) {
            fetch(`http://localhost:9000/store/products/${router.query.id}`, {
                credentials: "include",
            })
                .then((response) => response.json())
                .then((obj) => {
                    if (!obj.product) return router.push('/404')

                    setProdTitle(obj.product.title);
                    setProdDescription(obj.product.description);
                    setProdVariant(obj.product.variants[0].id);
                    setProdPrice(obj.product.variants[0].prices[0].amount);
                    setProdPriceCurrency(obj.product.variants[0].prices[0].currency_code);
                    setProdThumbnail(obj.product.thumbnail);
                })

            var id = localStorage.getItem("cart_id");

            if (id) {
                fetch(`http://localhost:9000/store/carts/${id}`, {
                    credentials: "include",
                })
                    .then((response) => response.json())
                    .then(({cart}) => {
                        setCart(cart['items']['length'])
                        setCartId(cart.id)
                    })
            }
            if (!id) {
                fetch(`http://localhost:9000/store/carts`, {
                    method: "POST",
                    credentials: "include",
                })
                    .then((response) => response.json())
                    .then(({cart}) => {
                        localStorage.setItem("cart_id", cart['id']);
                        setCart(cart['items']['length']);
                        setCartId(cart.id)
                    })
            }
        }
    }, [router.query.id])

    return (
        <div>
            <Head>
                <title>Steam Remake - {getProdTitle}</title>
                <meta name="description" content="Made using Next & Medusa!"/>
            </Head>
            <Navbar/>
            <Navbar_store items={getCart}/>
            <Title title={getProdTitle} description={getProdDescription}/>
            <Thumbnail url={getProdThumbnail}/>
            <AddToCart cart={getCartId} id={getProdVariant} name={getProdTitle} price={getProdPrice} currency={getProdPriceCurrency}/>
            <div style={{marginBottom: '2%'}}/>
        </div>
    )
}

If you are hosting the Medusa server on a different machine, or if the Medusa server is using a different port, replace http://localhost:9000/ with the server's address or 9000 with the server's port, respectively.

Create the components

Build the navigation bars

A navigation bar is an element in a website that provides links to access the most important sections of a website. They are an important part of a website's UI (user interface), as they help users quickly find the information they are looking for.

Create a new folder in the root directory called components with these two files: Navbar.js

import Link from 'next/link'

export default function Navbar() {
    return (
        <div style={{
            display: 'flex',
            width: '100%',
            height: '100px',
            marginBottom: '1.3%',
            background: '#171A21',
            justifyContent: 'center',
            alignItems: 'center'
        }}>
            <h1 style={{marginRight: '5%'}}>ACME</h1>
            <Link style={{textAlign: 'center', marginRight: '1%'}} href="/">STORE</Link>
            <Link style={{textAlign: 'center', marginRight: '1%'}} href="/community">COMMUNITY</Link>
            <Link style={{textAlign: 'center', marginRight: '1%'}} href="/about">ABOUT</Link>
            <Link style={{textAlign: 'center', marginRight: '1%'}} href="/support">SUPPORT</Link>
        </div>
    )
}

Navbar_store.js

import Link from 'next/link'

export default function Navbar_store({items}) {
    return (
        <div>
            <div style={{
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center'
            }}>
                <div style={{
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    width: '940px',
                    gap: '2vw',
                    height: '35px',
                    background: 'linear-gradient(90deg, rgba(62, 103, 150, 0.919) 11.38%, rgba(58, 120, 177, 0.8) 25.23%, rgb(15, 33, 110) 100%)',
                    boxShadow: '0 0 3px rgba( 0, 0, 0, 0.4)',
                    fontSize: '13px'
                }}>
                    <Link href="/" style={{marginLeft: '2%'}}>Your Store</Link>
                    <Link href="/" style={{marginLeft: '2%'}}>New & Noteworthy</Link>
                    <Link href="/" style={{marginLeft: '2%'}}>Points Shop</Link>
                    <Link href="/" style={{marginLeft: '2%'}}>News</Link>
                    <Link href="/" style={{marginLeft: '2%'}}>Labs</Link>
                    <Link href="/cart" style={{
                        marginLeft: '2%',
                        backgroundColor: '#718E02',
                        width: '100px',
                        height: '20px',
                        textAlign: 'center'
                    }}>CART ({items})</Link>
                </div>
            </div>
        </div>
    )
}

Screenshot of the Navigation Bars

Build the landing page's components

Now, create a new directory inside the components folder called Landing. It should contain 4 files: BigShowcase.js

import Image from 'next/image'
import Link from 'next/link'

export default function BigShowcase() {

    return (
        <div style={{display: 'flex', justifyContent: 'center', marginTop: '1.5vh'}}>
            <div style={{width: '940px', height: '392px'}}>
                <h2 style={{
                    fontSize: '14px',
                    fontWeight: 'bold',
                    fontFamily: '"Motiva Sans", Sans-serif',
                    letterSpacing: '0.04em',
                    textTransform: 'uppercase'
                }}>Featured & Recommended</h2>

                <Link href="/product/">
                    <div style={{
                        width: '940px',
                        height: '353px',
                        display: 'flex',
                        justifyContent: 'space-around',
                        alignItems: 'center',
                        boxShadow: '0 0 7px 0px #000'
                    }}>
                        <Image style={{float: 'left', boxShadow: '0px 0px 30px 1px #171A21', zIndex: '1'}}
                               src="/previewSs/previewBanner.jpg"
                               alt="Big Showcase Image" width={628} height={353}/>
                        <div
                            style={{width: '312px', height: '353px', backgroundImage: 'url(/previewSs/previewBg.jpg)'}}>
                            <div style={{margin: '0 0 0 2%'}}>
                                <p style={{fontSize: '24px', marginTop: '5%'}}>medusa-stonkers</p>
                                <div style={{display: 'flex', flexWrap: 'wrap', gap: '3px'}}>
                                    <Image alt='Preview Image' src="/PreviewSs/1.png" width={150} height={84}/>
                                    <Image alt='Preview Image' src="/PreviewSs/2.png" width={150} height={84}/>
                                    <Image alt='Preview Image' src="/PreviewSs/3.png" width={150} height={84}/>
                                    <Image alt='Preview Image' src="/PreviewSs/4.png" width={150} height={84}/>
                                    <p style={{marginTop: '10%', fontSize: '18px'}}>Now available</p>
                                </div>
                                <p style={{marginTop: '10px', fontSize: '11px'}}>$0.00</p>
                            </div>
                        </div>
                    </div>
                </Link>
            </div>
        </div>
    )
}

Screenshot of BigShowcase

BrowseIncentive.js

import Link from 'next/link'

export default function BrowseIncentive() {
    return (
        <div style={{display: 'flex', justifyContent: 'center', marginTop: '10px'}}>
            <div style={{width: '940px', height: '95px'}}>
                <h2 style={{
                    fontSize: '14px',
                    fontWeight: 'bold',
                    fontFamily: '"Motiva Sans", Sans-serif',
                    letterSpacing: '0.04em',
                    textTransform: 'uppercase'
                }}>Browse ACME</h2>
                <div style={{display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px'}}>
                    <Link href='/' style={{
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        height: '58px',
                        fontFamily: '"Motiva Sans", Sans-serif',
                        fontSize: '16px',
                        textTransform: 'uppercase',
                        fontWeight: '500',
                        letterSpacing: '0.03em',
                        textAlign: 'center',
                        background: 'linear-gradient(90deg, #06BFFF 0%, #2D73FF 100%)',
                        borderRadius: '3px',
                        boxShadow: '0 0 4px #000'
                    }}>New Releases</Link>
                    <Link href='/' style={{
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        height: '58px',
                        fontFamily: '"Motiva Sans", Sans-serif',
                        fontSize: '16px',
                        textTransform: 'uppercase',
                        fontWeight: '500',
                        letterSpacing: '0.03em',
                        textAlign: 'center',
                        background: 'linear-gradient(90deg, #06BFFF 0%, #2D73FF 100%)',
                        borderRadius: '3px',
                        boxShadow: '0 0 4px #000'
                    }}>Specials</Link>
                    <Link href='/' style={{
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        height: '58px',
                        fontFamily: '"Motiva Sans", Sans-serif',
                        fontSize: '16px',
                        textTransform: 'uppercase',
                        fontWeight: '500',
                        letterSpacing: '0.03em',
                        textAlign: 'center',
                        background: 'linear-gradient(90deg, #06BFFF 0%, #2D73FF 100%)',
                        borderRadius: '3px',
                        boxShadow: '0 0 4px #000'
                    }}>Free Games</Link>
                    <Link href='/' style={{
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        height: '58px',
                        fontFamily: '"Motiva Sans", Sans-serif',
                        fontSize: '16px',
                        textTransform: 'uppercase',
                        fontWeight: '500',
                        letterSpacing: '0.03em',
                        textAlign: 'center',
                        background: 'linear-gradient(90deg, #06BFFF 0%, #2D73FF 100%)',
                        borderRadius: '3px',
                        boxShadow: '0 0 4px #000'
                    }}>By User Tags</Link>
                </div>
            </div>
        </div>
    )
}

Photo of BrowseIncentive

Spotlight.js

import Link from "next/link";
import {useEffect, useState} from "react";

export default function Spotlight() {
    const [products, setProducts] = useState([]);

    useEffect(() => {
        fetch("http://localhost:9000/store/products?limit=3")
            .then((response) => response.json())
            .then((data) => {
                setProducts(data.products);
            });
    }, []);

    return (
        <div style={{display: "flex", justifyContent: "center", marginTop: "10px"}}>
            <div style={{width: "940px"}}>
                <h2 style={{
                    fontSize: '14px',
                    fontWeight: 'bold',
                    fontFamily: '"Motiva Sans", Sans-serif',
                    letterSpacing: '0.04em',
                    textTransform: 'uppercase'
                }}>New 🌟</h2>
                <div style={{display: "grid", gridTemplateColumns: "repeat(3, 1fr)", justifyContent: 'center', gap: "8px"}}>
                    {products.map((product) => (
                        <Link key={product.id} href={`/product?id=${product.id}`}>
                            <div key={product.id} style={{
                            height: "350px",
                            backgroundImage: `url(${product.thumbnail}`,
                            backgroundSize: 'contain',
                            backgroundPosition: 'center center'
                        }}/>
                        </Link>
                    ))}
                </div>
            </div>
        </div>
    );
}

Screenshot of Spotlight

ProductList.js

import Image from 'next/image';
import Link from "next/link";
import {useEffect, useState} from "react";

export default function ProductList() {
    const [products, setProducts] = useState([]);

    useEffect(() => {
        fetch("http://localhost:9000/store/products")
            .then((response) => response.json())
            .then((data) => {
                setProducts(data.products);
            });
    }, []);

    return (
        <div style={{display: "flex", justifyContent: "center", marginTop: "10px"}}>
            <div style={{width: "940px"}}>
                <h2 style={{
                    fontSize: '14px',
                    fontWeight: 'bold',
                    fontFamily: '"Motiva Sans", Sans-serif',
                    letterSpacing: '0.04em',
                    textTransform: 'uppercase'
                }}>Product List</h2>
                <div style={{
                    display: "flex",
                    flexDirection: 'column',
                    gap: "8px"
                }}>
                    {products.map((product) => (
                        <Link key={product.id} href={`/product?id=${product.id}`}>
                            <div style={{
                                display: 'flex',
                                flexDirection: 'row',
                                width: '940px',
                                height: '69px',
                                position: 'relative',
                                background: 'rgba(0, 0, 0, 0.2)'
                            }}>
                                <Image src={product.thumbnail} width={1080} height={1080}
                                       style={{width: 'auto', height: '69px', paddingRight: '12px'}}/>
                                <div style={{
                                    display: 'flex',
                                    flexDirection: 'row',
                                    justifyContent: 'space-between',
                                    position: 'relative',
                                    width: '860px'
                                }}>
                                    <div>
                                        <p style={{
                                            color: '#C7D5E0',
                                            marginTop: '10px',
                                            marginBottom: '0px',
                                            fontSize: '1.25em',
                                            zIndex: '1',
                                            textOverflow: 'ellipsis',
                                            whiteSpace: 'nowrap',
                                            display: 'block',
                                            overflow: 'hidden',
                                            transition: 'color 0.25s',
                                        }}>{product.title}</p>
                                        <p style={{marginTop: '0px', fontSize: '12px', color: 'gray'}}>{product.description}</p>
                                    </div>
                                    <div style={{position: 'absolute', right: '0'}}>
                                        <p style={{color: '#BEEE11'}}>{product.variants[0].prices[0].amount / 100}{product.variants[0].prices[0].currency_code.toUpperCase()}</p>
                                    </div>
                                </div>
                            </div>
                        </Link>
                    ))}
                </div>
            </div>
        </div>
    );
}

Photo of ProductList

Build the products page's components

Create a new folder inside the components directory called Product, which will contain 3 files: AddToCart.js

export default function AddToCart({cart, id, name, price, currency}) {
    return (
        <div style={{display: 'flex', justifyContent: 'center', marginTop: '2%'}}>
            <div style={{
                display: 'flex',
                width: '940px',
                height: '73px',
                position: 'relative',
                background: 'linear-gradient( -60deg, rgba(226,244,255,0.3) 5%,rgba(84, 107, 115, 0.3) 95%)',
                alignItems: 'center'
            }}>
                <p style={{
                    fontFamily: '"Motiva Sans", Sans-serif',
                    fontSize: '21px',
                    marginTop: '0',
                    marginBottom: '10px',
                    marginLeft: '1rem'
                }}>Buy {name}</p>
                <div style={{position: 'absolute', right: '16px', bottom: '-17px', left: '16px', textAlign: 'right'}}>
                    <div style={{
                        height: '32px',
                        verticalAlign: 'bottom',
                        display: 'inline-flex',
                        background: 'black',
                        padding: '2px 2px 2px 0',
                        borderRadius: '2px'
                    }}>
                        <div style={{
                            background: 'black',
                            fontSize: '13px',
                            paddingTop: '8px',
                            paddingLeft: '12px',
                            paddingRight: '12px',
                            textTransform: 'uppercase',
                            height: '24px'
                        }}>{price / 100} {currency}</div>
                        <div style={{
                            position: 'relative',
                            fontSize: '12px',
                            display: 'inline-block',
                            marginLeft: '2px',
                            verticalAlign: 'middle'
                        }}>
                            <div onClick={() => {
                                fetch(`http://localhost:9000/store/carts/${cart}/line-items`, {
                                    method: "POST",
                                    credentials: "include",
                                    headers: {
                                        "Content-Type": "application/json",
                                    },
                                    body: JSON.stringify({
                                        variant_id: id,
                                        quantity: 1,
                                    }),
                                })
                                    .then((response) => console.log(response.json()))
                            }} style={{
                                width: '110px',
                                height: '30px',
                                borderRadius: '2px',
                                border: 'none',
                                padding: '1px',
                                display: 'inline-flex',
                                justifyContent: 'center',
                                alignItems: 'center',
                                cursor: 'pointer',
                                color: '#D2EFA9',
                                background: 'linear-gradient( to right, #8ed629 5%, #6aa621 95%)',
                                textShadow: '1px 1px 0px rgba( 0, 0, 0, 0.3 )',
                                fontFamily: '"Motiva Sans", Sans-serif',
                                fontSize: '16px'
                            }}>
                                Add to Cart
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}

Photo of AddToCart

Thumbnail.js

import Image from "next/image";

export default function Thumbnail({url}) {
    return (
        <div style={{display: 'flex', justifyContent: 'center'}}>
            <Image alt="Thumbnail" src={url} width={940} height={0} style={{height: 'auto'}}/>
        </div>
    )
}

Title.js

export default function Title({title, description}) {
    return (
        <div style={{display: 'flex', justifyContent: 'center'}}>
            <div style={{width: '940px'}}>
                <p style={{
                    marginBottom: '0',
                    fontFamily: '"Motiva Sans", Sans-serif',
                    fontSize: '12px',
                    color: '#8f98A0'
                }}>{description}</p>
                <p style={{marginTop: '0', fontFamily: '"Motiva Sans", Sans-serif', fontSize: '26px'}}>{title}</p>
            </div>
        </div>
    )
}

Images

At last, for the BigShowcase component, you will need 6 images. In the public directory, create a previewSs folder and enter it. All images should have the .jpg format. The mandatory banner image is named previewBanner.jpg; The showcase images are named 1.jpg, 2.jpg, 3.jpg, 4.jpg, but you may remove them as you'd like by deleting lines 34-37 from components/BigShowcase.js.

The last image, which is also mandatory, is called previewBg.jpg, and you can get it from the GitHub repository

Testing the storefront

Run the Medusa server

Open the directory in which you installed the Medusa server in your terminal and run medusa develop. In the case of 'medusa' is not recognized as an internal or external command, run npx medusa develop.

Run the Next.js storefront

In your terminal, open the directory in which you have created the Next.js storefront and execute npm run dev.

CORS header Access-Control-Allow-Origin missing

In the case of this error, open the file medusa-config.js (it is in the directory in which you have installed the Medusa backend/server) in your preferred code editor. On line 29, you may replace the string parameter with the URL of your storefront (in this case, http://localhost:3000).

// CORS to avoid issues when consuming Medusa from a client
const STORE_CORS = process.env.STORE_CORS || "http://localhost:3000";

Conclusion

In this tutorial, you created a Next.js eCommerce storefront based on Steam and using Medusa. This storefront only includes the front and product pages; you will need to make a checkout page and recreate the other web pages yourself, such as the COMMUNITY or NEWS pages.

Here is a tutorial on how to add a checkout page.