Recreating Nike - Medusa & Next.js
Tags: nextjs, opensource, programming, javascript
Published: 2023-01-31 09:04:20

## Introduction
### GitHub Repository

All the code presented in this article is accessible [here](https://github.com/stefa-n/nike-remake-storefront)

### What is Medusa?

[Medusa](https://www.medusajs.com/) is the #1 Open Source headless commerce platform on GitHub. It uses Node.js, and its architecture makes it easy to incorporate Medusa with any tech stack to build cross-platform eCommerce stores.

### What is Next.js?

Next.js is an Open Source React framework. It adds several extra features, including server-side rendering.
Next.js is used by many large websites, including Netflix, Starbucks, Twitch, and Nike.

### 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](https://i.imgur.com/BxdZxnm.png)

The Medusa Admin panel makes it easy to manage your store by providing a way to manage all of your products, discounts, customers, and administrator users in one place.

### 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
```

Medusa admin runs on port `7000`. Navigate to `localhost:7000` using your browser to access your admin panel.
Because you used the `--seed` parameter, an admin account has been created. The e-mail is `admin@medusa-test.com`, and the password is `supersecret`.

Visit [this guide](https://docs.medusajs.com/user-guide/) to learn more about Medusa's Admin panel.
![Medusa Admin docs](https://i.imgur.com/ju2DjWb.png)



## Create the pages
In a different directory, create a new Next.js project:
```
npx create-next-app nike-remake-storefront
```

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

### Functionality
When the user first visits the site, a cart is created and saved into their browser's local storage which will be used to handle any products being added.

Next, the user loads the three newest products, and they are loaded in the `Newest Arrivals` showcase.

When the user clicks on one of the products, they are taken to the `product` page. The page is being passed an `id` parameter, which is hidden from the user.

The `product` page then takes that parameter and gives it to the `Gallery` and `AddToBag` components.

Finally, the `cart` page fetches the user's cart using an API request and gives them to the `Bag` and `Subtotal components`:
* The `Bag` component takes all the products the user has in their cart and shows them in a list containing the product's name, description, and price.
* The `Subtotal` component shows the subtotal of the user's order and prompts them with a button to check out.

### Styling
In the `styles` directory, delete the Home.module.css file.
Replace the `globals.css` file with this:
```css
html,
body {
  padding: 0;
  margin: 0;
  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}

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

* {
  box-sizing: border-box;
}

@media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
  body {
    color: black;
    background: white;
  }
}
```
All styles will be handled in the component files themselves.

### Create the `index` page

In the root directory, create a `components` directory. Next, create a `Navbar.js` file with the following contents:
```js
import Image from 'next/image'
import Link from "next/link";
import {useEffect, useState} from 'react';

export default function Navbar() {
    const [getCartLength, setCartLength] = useState(0)

    useEffect(() => {
        let id = localStorage.getItem("cart_id");

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

    useEffect(() => {
        let prevScrollpos = window.scrollY;
        window.onscroll = function () {
            let currentScrollPos = window.scrollY;
            if (prevScrollpos > currentScrollPos || 170 > currentScrollPos) {
                window.document.getElementById('navbar').style.top = "0";
            } else {
                window.document.getElementById('navbar').style.top = "-60px";
            }
            prevScrollpos = currentScrollPos;
        };
    }, []);
    return (
        <div id='navbar' style={{
            display: 'flex',
            width: '100%',
            height: '60px',
            background: 'white',
            position: 'fixed',
            top: '0px',
            transition: 'top 0.3s'
        }}>
            <Link style={{position: 'absolute', left: '2.5rem'}} href='/'>
                <Image src='/logo.png' width={60} height={60} alt='Store logo'/>
            </Link>
            <Link href='/cart' style={{
                display: 'flex',
                alignItems: 'center',
                position: 'absolute',
                right: '2.5rem',
                height: '60px'
            }}>
                <p>{getCartLength}</p>
                <Image src='/navbar/cart.svg' width={36} height={36} alt='Cart'/>
            </Link>
            <div style={{
                display: 'flex',
                flexWrap: 'wrap',
                gap: '12px',
                justifyContent: 'center',
                alignItems: 'center',
                width: '100%',
                height: '60px'
            }}>
                <Link href='/' style={{fontSize: '16px', fontWeight: '500', lineHeight: '1.5'}}>Products</Link>
                <Link href='/' style={{fontSize: '16px', fontWeight: '500', lineHeight: '1.5'}}>Shoes</Link>
                <Link href='/' style={{fontSize: '16px', fontWeight: '500', lineHeight: '1.5'}}>Sale</Link>
            </div>
        </div>
    )
}
```
This component uses `useEffect`; the script inside it (lines 7-16) will only run client-side, and it checks to see if the user scrolls. If the user scrolls upward, it will show the navigation bar; else, it will hide it.
We also implemented all the cart functionality inside it, since it will be a global component.
![Photo of the Navbar](https://i.imgur.com/3qNpKdY.png)

Next, you'll need the `Headline` element.
Inside the `components/Landing` folder, create a `Headline.js` file with the following contents:
```js
import Image from 'next/image'

export default function Headline() {
    return (
        <div>
            <div style={{
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center',
                flexDirection: 'column',
                width: '100%',
                height: '208px'
            }}>
                <p style={{
                    marginBottom: '0'
                }}>Inspired By A Popular Shoe Store</p>
                <h1 style={{
                    fontWeight: '800',
                    fontSize: '72px',
                    margin: '0',
                    letterSpacing: '-6px',
                    textAlign: 'center',
                    textTransform: 'uppercase'
                }}>Do you like it?</h1>
                <p style={{
                    fontSize: '24px',
                    textAlign: 'center'
                }}>This is a store that you&apos;ll like. This is the store that sells the best shoes. This
                    is <b>THE</b> store.</p>
            </div>
            <Image src='/headline/banner.png' width={1920} height={1080} alt='Banner image'
                   style={{width: '100%', height: 'auto'}}/>
        </div>
    )
}
```
![Photo of the Headline](https://i.imgur.com/ZSXLERq.png)

After that, the user needs somewhere where he can see the newest products. Create a `Products.js` file in the `components/Landing` folder with these contents:
```js
import Link from "next/link";

import {useEffect, useState} from "react";

export default function Products() {
    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={{marginLeft: '3rem', marginRight: '3rem'}}>
            <div>
                <p style={{fontSize: '24px'}}>Newest Arrivals</p>
            </div>
            <div style={{
                display: 'flex',
                flexWrap: 'wrap',
                gap: '0.8rem',
                justifyContent: 'center'
            }}>
                {products.map((product) => (
                    <Link key={product.id} href={`/product?prod=${product.id}`} as={'/'} style={{width: '598px'}}>
                        <div key={product.id} style={{
                            width: '100%',
                            height: 'calc(0.389 * 100vw)',
                            backgroundImage: `url(${product.thumbnail}`,
                            backgroundSize: 'contain',
                            backgroundPosition: 'center center',
                            backgroundRepeat: 'no-repeat'
                        }}/>
                        <p style={{display: 'inline-block', marginBottom: '0'}}>{product.title}</p>
                        <p style={{
                            display: 'inline-block',
                            float: 'right'
                        }}>{product.variants[0].prices[0].currency_code.toUpperCase()} {product.variants[0].prices[0].amount / 100}</p>
                        <p style={{marginTop: '0', width: '80%', color: '#757575'}}>{product.description}</p>
                    </Link>
                ))}
            </div>
        </div>
    )
}
```
This component uses both `useEffect` and `useState`; `useState` is a hook that allows you to store state variables.
This component also uses `.map`, which calls a function for every element in an array, in this example the element being one of the three products received from the Medusa server using `fetch`.
![Photo of the Products](https://i.imgur.com/fj0Efgi.png)
Finally, open the `pages/index.js` file and assemble the front page:
```js
import Head from 'next/head';

import Navbar from '../components/Navbar';
import Products from '../components/Landing/Products';
import Headline from '../components/Landing/Headline';

export default function Home() {
    return (
        <div>
            <Head>
                <title>Nike Remake</title>
                <meta name="description" content="Made using Next & Medusa!"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>
            <Navbar/>
            <div style={{height: '60px'}}/>
            <Headline/>
            <Products/>
        </div>
    )
}
```
You now have a complete front page.
![Photo of the Front Page](https://i.imgur.com/znZpeA2.jpg)

### Create the `product` page
Start by creating the components.
Inside the `components/Product` directory, create two files:
* AddToBag.js
* Gallery.js

`AddToBag` is self-explanatory: it is the button that adds the selected product to the cart.
`Gallery` is the image gallery of the product.

Inside the `AddToBag.js` file, paste this in:
```js
import Router from 'next/router';

export default function AddToBag({cart, id}) {
    return (
        <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(() => Router.push('/'))
        }}
             style={{
                 position: 'absolute',
                 bottom: '0',
                 display: 'flex',
                 justifyContent: 'center',
                 width: '100%',
                 minHeight: '60px',
                 background: 'black',
                 color: 'white',
                 borderRadius: '30px'
             }}>
            <p style={{margin: '1.3276rem'}}>Add to Bag</p>
        </div>
    )
}
```
This component checks when the button is clicked by using the `onClick` property and sends a post-request to the server with the `cart` and `variant ID`, so the product gets added to the user's cart.

The content of the `Gallery.js` file is:
```js
import Image from 'next/image'

export default function Gallery({gallery}) {
    return (
        <div style={{
            display: 'grid',
            gridTemplateColumns: 'repeat(auto-fit, minmax(444px, 1fr))',
            justifyContent: 'center',
            width: '50vw'
        }}>
            {
                gallery.map((product) => {
                    return <Image src={product.url} key={product.url} alt='Preview Image' width={4000} height={4000}
                                  style={{width: 'auto', height: '542px'}}/>
                })
            }
        </div>
    )
}
```
And finally, create the product page by creating the `pages/product.js` file with the following contents:
```js
import Head from 'next/head';
import {useRouter} from 'next/router';
import {useEffect, useState} from "react";

import Navbar from '../components/Navbar';
import Gallery from '../components/Product/Gallery';
import AddToBag from '../components/Product/AddToBag';
import Products from '../components/Products';

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

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

                    setProdTitle(obj.product.title.toString());
                    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);
                    setProdGallery(obj.product.images)
                })
        }
    }, [])
    return (
        <div>
            <Head>
                <title>Nike Remake - {getProdTitle}</title>
                <meta name="description" content="Made using Next & Medusa!"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>
            <Navbar/>
            <div style={{height: '100px'}}/>
            <div style={{display: 'flex', justifyContent: 'center'}}>
                <Gallery gallery={getProdGallery}/>
                <div style={{marginLeft: '48px', position: 'relative'}}>
                    <p style={{fontSize: '28px', marginBottom: '0px'}}>{getProdTitle}</p>
                    <p style={{fontSize: '16px', marginTop: '0px', width: '456px'}}>{getProdDescription}</p>
                    <p>{getProdPriceCurrency.toUpperCase()} {getProdPrice / 100}</p>
                    <AddToBag cart={getCartId} id={getProdVariant}/>
                </div>
            </div>
        </div>
    )
}
```
![Photo of the Product page](https://i.imgur.com/zVhtNH0.png)

### Create the `cart` page
Start by creating the components.
The first component will be the `Bag` component, which will show you what items you have in your bag.

Create a new directory called `Cart` in the `components` folder, and create a new file called `Bag.js`.

```js
import Image from 'next/image'

export default function Bag({items, currency}) {
    return (
        <div style={{width: '45%'}}>
            <p style={{fontSize: '22px'}}>Bag</p>
            {
                items.map((item) => {
                    return (
                        <div key={item.id} style={{display: 'flex', paddingBottom: '24px', width: '95%'}}>
                            <Image alt={`Image of ${item.title}`} src={item.thumbnail} width={256} height={256}
                                   style={{width: 'auto', height: '150px'}}/>
                            <div style={{paddingLeft: '16px', width: '100%'}}>
                                <p style={{fontSize: '16px', display: 'inline-block'}}>{item.title}</p>
                                <p style={{
                                    display: 'inline-block',
                                    float: 'right'
                                }}>{currency.toUpperCase()} {item.unit_price / 100}</p>
                                <p style={{color: 'rgb(117, 117, 117)'}}>{item.variant.product.description}</p>
                                <p style={{color: 'rgb(117, 117, 117)'}}>{item.description}</p>
                                <div style={{width: '100%', height: '0.1rem', backgroundColor: '#E5E5E5'}}/>
                            </div>
                        </div>
                    )
                })
            }
        </div>
    )
}
```
![Photo of the Bag component](https://i.imgur.com/mxTv3oW.png)

Next, create another file called `Subtotal.js`, which will host your `Subtotal` component:
```js
import Router from 'next/router';
export default function Subtotal({subtotal}) {
    return (
        <div style={{width: '17vw', height: '295px', minWidth: '250px'}}>
            <p style={{fontSize: '22px'}}>Summary</p>
            <div>
                <p style={{display: 'inline-block'}}>Subtotal</p>
                <p style={{display: 'inline-block', float: 'right'}}>{subtotal / 100}</p>
            </div>
            <div style={{width: '100%', height: '0.1rem', backgroundColor: '#E5E5E5'}}/>
            <div onClick={() => {
                Router.push('/checkout')
            }}
                 style={{
                     display: 'flex',
                     justifyContent: 'center',
                     width: '100%',
                     minHeight: '60px',
                     marginTop: '1rem',
                     background: 'black',
                     color: 'white',
                     borderRadius: '30px'
                 }}>
                <p style={{margin: '1.3276rem'}}>Checkout</p>
            </div>
        </div>
    )
}
```
![Photo of the Subtotal component](https://i.imgur.com/qA678lK.png)

Finally, assemble the page by creating a file named `cart.js` in the `pages` directory:
```js
import Head from 'next/head';
import Router from 'next/router';
import {useEffect, useState} from 'react'

import Navbar from '../components/Navbar';
import Bag from '../components/Cart/Bag';
import Subtotal from '../components/Cart/Subtotal';

export default function Cart() {
    const [getCartCurrency, setCartCurrency] = useState('EUR')
    const [getCartSubtotal, setCartSubtotal] = useState('EUR')
    const [getCartItems, setCartItems] = useState([])

    useEffect(() => {
        const cart = localStorage.getItem('cart_id');
        if (cart) {
            fetch(`http://localhost:9000/store/carts/${cart}`, {
                credentials: "include",
            })
                .then((response) => response.json())
                .then(({cart}) => {
                    setCartSubtotal(cart.subtotal)
                    setCartCurrency(cart.region.currency_code)
                    setCartItems(cart.items);
                })
                .catch(() => {
                    Router.push('/')
                })
        }
    })
    return (
        <div>
            <Head>
                <title>Nike Remake - Cart</title>
                <meta name="description" content="Made using Next & Medusa!"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>
            <Navbar/>
            <div style={{height: '100px'}}/>
            <div style={{display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center'}}>
                <Bag items={getCartItems} currency={getCartCurrency}/>
                <Subtotal subtotal={getCartSubtotal}/>
            </div>
        </div>
    )
}
```
![Photo of the cart page](https://i.imgur.com/Ts7PVk8.png)

## Conclusion
In this tutorial, you have successfully created a storefront similar to Nike's.
The storefront only implements the add-to-cart functionality and product listing.
You may need to implement a page that lists products.
[Here is a tutorial](https://docs.medusajs.com/advanced/storefront/how-to-implement-checkout-flow/) on implementing the checkout flow.
[Here is a blog post](https://medusajs.com/blog/implementing-5-features-in-nike-ecommerce-store-with-medusa/) about implementing 5 features from Nike's store into Medusa.

If you have any issues or questions related to Medusa, reach out to the Medusa team & community on [Discord](https://discord.com/invite/medusajs)