Entrepreneur Loses $6 Million, Bounces Back

I’m always amazed at the resilience of many successful business people, those that overcome big-time failures. Warren Buffett summed it up when he said, to paraphrase, “Success is the process of overcoming mistakes.”

Shakil Prasla recently made a big mistake. An astute ecommerce operator and owner, Prasla purchased in late 2020 Covid-related protective gloves at roughly twice the ultimate selling price. His company, Gloves.com, lost $6 million.

He told me, “It took three months for our products to arrive. By then vaccines were out, and demand fell. We panicked. We did a fire sale at a 50% discount from our purchase price. We lost about $6 million. It sucks.”

I first interviewed Prasla for this podcast back in 2019. He was as transparent then as now, sharing his experiences to help others. He did that again recently when he and I discussed his gloves mistake and moving forward.

What follows is our entire audio conversation and a transcript, which has been edited for length and clarity.

Eric Bandholz: You and I first spoke on this podcast two years ago. You were building an impressive portfolio of ecommerce businesses with borrowed money. But you experienced a setback earlier this year.

Shakil Prasla: Yes. The path that I chose has pros and cons. It’s risky. Taking on debt requires cash flow to repay it. When the cash flow is not there, you have to pull from somewhere or borrow even more money. Most of my decisions have worked out. But some have not. I’m currently in a crisis where one hasn’t worked out. But I’ve learned a lot about myself. I would do it again.

Bandholz: What happened?

Prasla: Last year during the pandemic, we started getting inquiries about masks, gowns, and gloves. We didn’t carry those products, but our manufacturers in China told us they could make them. So we started importing masks and gowns and learning about personal protective equipment.

We weren’t doing it on a large scale, but we built some good relationships. And during the summer, prices tanked for masks and gowns. So we focused on gloves. I saw long-term potential for that business. Dentists, physicians, nurses, tattoo artists, barbers — all will use gloves, even after Covid.

So I bought Gloves.com, the domain name. I believe in those exact domain-match keywords for search engine optimization and branding credibility. So I bought Gloves.com for seven figures. I had never spent that much on a domain. And, separately, we bought a bunch of inventory.

In Q4 2020, when we bought Gloves.com, all was going well. We were making 30% profit margins with high volume. I decided to focus on scale to gain even more customers. I went all in. We bought over $10 million in inventory. Coordinating all of that — manufacturing, containers — created a production delay. Manufacturers like to say, “It’s going to ship in one to two weeks.” That’s never the case. You have to double it. For us, the delay was extensive. And by the time the container shipped, the Long Beach, California port was already congested.

It took three months for our products to arrive. By then vaccines were out, and demand fell. We panicked. We did a fire sale at a 50% discount from our purchase price. We lost about $6 million. It sucks. It wasn’t investor money. It was two partners and me.

Bandholz: Why didn’t you hold on to the inventory?

Prasla: Covid inflated the price of PPE products. Before Covid, a box of gloves cost $3 to $4. During Covid, it increased to $15. We paid $10 per box. Once the demand fell, the prices returned to pre-Covid levels. We knew that would occur, but we didn’t think it would come so quickly.

So having paid $10 per box, our gloves were now selling in the U.S. for $5. We ended up selling a good portion of the inventory to non-U.S. customers in regions that lacked vaccines.

I violated the number one rule of investing: Don’t put all your eggs in one basket.

Bandholz: Are you bankrupt now?

Prasla: No, but I am cash flow poor.

My partners and I have had a lot of low points, a lot of emotional breakdowns. It carries to our other businesses, our relationships. It affects our health, our sleep, and other aspects of our lifestyle. I’m the heaviest I’ve ever been. But I’ve accepted it. I’m much happier now than a few months ago.

We live in a culture where people share the good stories but not the bad. But we learn from those bad ones. I’ve learned that going for quick money can result in quick losses. I need to stick to what I know best, which was predictable cash flow.

Bandholz: How long will it take to dig yourself out of this?

Prasla: Two years. I’m going to focus methodically on my companies that have the best long-term prospects. And Gloves.com is going to be a sustainable business. We’re starting to see an uptick. Our goal is to grow that company and sell it in two years. We’ve sold a lot of the expensive inventory and replenished it with cheaper alternatives.

Bandholz: Did you have to sell your house or your cars?

Prasla: No, I kept the house and the cars. I still have the same level of personal debt. I changed one mindset: I’m no longer a brand whore. I still need nice clothing and things. I want to spoil my wife. But I spoke with my family and said, “We’re going to pause on lavish expenses, such as travel. We’re cutting expenses to get through this.”

Bandholz: So this experience hasn’t convinced you to live debt-free?

Prasla: I get a thrill from carrying debt. It creates a surge of energy, knowing that I need to keep making these payments. Beyond that, it’s the return-on-investment aspect. Your dollar goes way further by borrowing, even when buying businesses.

Here’s an example. Say I buy an ecommerce brand for $1 million at roughly three times its annual profit of $333,000. Without debt, my ROI on the $1 million is 33%. But if I borrowed money — typically the U.S. Small Business Administration will finance with 10% to 20% down — the return on my money is much higher.

So with that $1 million I can buy multiple businesses. It’s much riskier, but I’m okay with that.

Bandholz: But you’re not buying anything for two years. Did I hear that right?

Prasla: That’s right. I’m not going to buy anything. I need to focus on growing my existing brands. Plus buying brands usually involves enlarging our team. All of that takes funding, which I don’t have now.

Bandholz: All of us are grateful to you for coming on the show and opening up about what happened with Gloves.com. I know you. You’re an amazing person, a pillar of the community. And now you have another link on the chain of the knowledge, so to speak. How can people reach out?

Prasla: My website is ShakilPrasla.com. My email address is on that site. I’m also on Twitter and LinkedIn.

How To Build An E-Commerce Site With Angular 11, Commerce Layer And Paypal

Nowadays it’s essential to have an online presence when running a business. A lot more shopping is done online than in previous years. Having an e-commerce store allows shop owners to open up other streams of revenue they couldn’t take advantage of with just a brick and mortar store. Other shop owners however, run their businesses online entirely without a physical presence. This makes having an online store crucial.

Sites such as Etsy, Shopify and Amazon make it easy to set up a store pretty quickly without having to worry about developing a site. However, there may be instances where shop owners may want a personalized experience or maybe save on the cost of owning a store on some of these platforms.

Headless e-commerce API platforms provide backends that store sites can interface with. They manage all processes and data related to the store like customer, orders, shipments, payments, and so on. All that’s needed is a frontend to interact with this information. This gives owners a lot of flexibility when it comes to deciding how their customers will experience their online store and how they choose to run it.

In this article, we will cover how to build an e-commerce store using Angular 11. We shall use Commerce Layer as our headless e-commerce API. Although there may be tonnes of ways to process payments, we’ll demonstrate how to use just one, Paypal.

Prerequisites

Before building the app, you need to have Angular CLI installed. We shall use it to initialize and scaffold the app. If you don’t have it installed yet, you can get it through npm.

npm install -g @angular/cli

You’ll also need a Commerce Layer developer account. Using the developer account, you will need to create a test organization and seed it with test data. Seeding makes it easier to develop the app first without worrying about what data you’ll have to use. You can create an account at this link and an organization here.

Lastly, you will need a Paypal Sandbox account. Having this type of account will allow us to test transactions between businesses and users without risking actual money. You can create one here. A sandbox account has a test business and test personal account already created for it.

Commerce Layer And Paypal Config

To make Paypal Sandbox payments possible on Commerce Layer, you’ll need to set up API keys. Head on over to the accounts overview of your Paypal developer account. Select a business account and under the API credentials tab of the account details, you will find the Default Application under REST Apps.

To associate your Paypal business account with your Commerce Layer organization, go to your organization’s dashboard. Here you will add a Paypal payment gateway and a Paypal payment method for your various markets. Under Settings > Payments, select Payment Gateways > Paypal and add your Paypal client Id and secret.

After creating the gateway, you will need to create a Paypal payment method for each market you are targeting to make Paypal available as an option. You’ll do this under Settings > Payments > Payment Methods > New Payment Method.

A Note About Routes Used

Commerce Layer provides a route for authentication and another different set of routes for their API. Their /oauth/token authentication route exchanges credentials for a token. This token is required to access their API. The rest of the API routes take the pattern /api/:resource.

The scope of this article only covers the frontend portion of this app. I opted to store the tokens server side, use sessions to track ownership, and provide http-only cookies with a session id to the client. This will not be covered here as it is outside the scope of this article. However, the routes remain the same and exactly correspond to the Commerce Layer API. Although, there are a couple of custom routes not available from the Commerce Layer API that we’ll use. These mainly deal with session management. I’ll point these out as we get to them and describe how you can achieve a similar result.

Another inconsistency you may notice is that the request bodies differ from what the Commerce Layer API requires. Since the requests are passed on to another server to get populated with a token, I structured the bodies differently. This was to make it easier to send requests. Whenever there are any inconsistencies in the request bodies, these will be pointed out in the services.

Since this is out of scope, you will have to decide how to store tokens securely. You’ll also need to slightly modify request bodies to match exactly what the Commerce Layer API requires. When there is an inconsistency, I will link to the API reference and guides detailing how to correctly structure the body.

App Structure

To organize the app, we will break it down into four main parts. A better description of what each of the modules does is given under their corresponding sections:

  1. the core module,
  2. the data module,
  3. the shared module,
  4. the feature modules.

The feature modules will group related pages and components together. There will be four feature modules:

  1. the auth module,
  2. the product module,
  3. the cart module,
  4. the checkout module.

As we get to each module, I’ll explain what its purpose is and break down its contents.

Below is a tree of the src/app folder and where each module resides.

src
├── app
│ ├── core
│ ├── data
│ ├── features
│ │ ├── auth
│ │ ├── cart
│ │ ├── checkout
│ │ └── products
└── shared

Generating The App And Adding Dependencies

We’ll begin by generating the app. Our organization will be called The LIme Brand and will have test data already seeded by Commerce Layer.

ng new lime-app

We’ll need a couple of dependencies. Mainly Angular Material and Until Destroy. Angular Material will provide components and styling. Until Destroy automatically unsubscribes from observables when components are destroyed. To install them run:

npm install @ngneat/until-destroy
ng add @angular/material

Assets

When adding addresses to Commerce Layer, an alpha-2 country code needs to be used. We’ll add a json file containing these codes to the assets folder at assets/json/country-codes.json. You can find this file linked here.

Styles

The components we’ll create share some global styling. We shall place them in styles.css which can be found at this link.

Environment

Our configuration will consist of two fields. The apiUrl which should point to the Commerce Layer API. apiUrl is used by the services we will create to fetch data. The clientUrl should be the domain the app is running on. We use this when setting redirect URLs for Paypal. You can find this file at this link.

Shared Module

The shared module will contain services, pipes, and components shared across the other modules.

ng g m shared

It consists of three components, one pipe, and two services. Here’s what that will look like.

src/app/shared
├── components
│ ├── item-quantity
│ │ ├── item-quantity.component.css
│ │ ├── item-quantity.component.html
│ │ └── item-quantity.component.ts
│ ├── simple-page
│ │ ├── simple-page.component.css
│ │ ├── simple-page.component.html
│ │ └── simple-page.component.ts
│ └── title
│ ├── title.component.css
│ ├── title.component.html
│ └── title.component.ts
├── pipes
│ └── word-wrap.pipe.ts
├── services
│ ├── http-error-handler.service.ts
│ └── local-storage.service.ts
└── shared.module.ts

We shall also use the shared module to export some commonly used Angular Material components. This makes it easier to use them out of the box instead of importing each component across various modules. Here’s what shared.module.ts will contain.

@NgModule({ declarations: [SimplePageComponent, TitleComponent, WordWrapPipe, ItemQuantityComponent], imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, RouterModule], exports: [ CommonModule, ItemQuantityComponent, MatButtonModule, MatIconModule, MatSnackBarModule, MatTooltipModule, SimplePageComponent, TitleComponent, WordWrapPipe ]
})
export class SharedModule { }

Components

Item Quantity Component

This component sets the quantity of items when adding them to the cart. It will be used in the cart and products modules. A material selector would have been an easy choice for this purpose. However, the style of the material select didn’t match the material inputs used in all the other forms. A material menu looked very similar to the material inputs used. So I decided to create a select component with it instead.

ng g c shared/components/item-quantity

The component will have three input properties and one output property. quantity sets the initial quantity of items, maxValue indicates the maximum number of items that can be selected in one go, and disabled indicates whether the component should be disabled or not. The setQuantityEvent is triggered when a quantity is selected.

When the component is initialized, we’ll set the values that appear on the material menu. There also exists a method called setQuantity that will emit setQuantityEvent events.

This is the component file.

@Component({ selector: 'app-item-quantity', templateUrl: './item-quantity.component.html', styleUrls: ['./item-quantity.component.css']
})
export class ItemQuantityComponent implements OnInit { @Input() quantity: number = 0; @Input() maxValue?: number = 0; @Input() disabled?: boolean = false; @Output() setQuantityEvent = new EventEmitter<number>(); values: number[] = []; constructor() { } ngOnInit() { if (this.maxValue) { for (let i = 1; i <= this.maxValue; i++) { this.values.push(i); } } } setQuantity(value: number) { this.setQuantityEvent.emit(value); }
}

This is its template.

<button mat-stroked-button [matMenuTriggerFor]="menu" [disabled]="disabled"> {{quantity}} <mat-icon ngIf="!disabled">expand_more</mat-icon>
</button>
<mat-menu #menu="matMenu"> <button ngFor="let no of values" (click)="setQuantity(no)" mat-menu-item>{{no}}</button>
</mat-menu>

Here is its styling.

button { margin: 3px;
}

Title Component

This component doubles as a stepper title as well as a plain title on some simpler pages. Although Angular Material provides a stepper component, it wasn’t the best fit for a rather long checkout process, wasn’t as responsive on smaller displays, and required a lot more time to implement. A simpler title however could be repurposed as a stepper indicator and be useful across multiple pages.

ng g c shared/components/title

The component has four input properties: a title, a subtitle, a number (no), and centerText, to indicate whether to center the text of the component.

@Component({ selector: 'app-title', templateUrl: './title.component.html', styleUrls: ['./title.component.css']
})
export class TitleComponent { @Input() title: string = ''; @Input() subtitle: string = ''; @Input() no?: string; @Input() centerText?: boolean = false;
}

Below is its template. You can find its styling linked here.

<div id="header"> <h1 *ngIf="no" class="mat-display-1" id="no">{{no}}</h1> <div [ngClass]="{ 'centered-section': centerText}"> <h1 class="mat-display-2">{{title}}</h1> <p id="subheading">{{subtitle}}</p> </div>
</div>

Simple Page Component

There are multiple instances where a title, an icon, and a button were all that were needed for a page. These include a 404 page, an empty cart page, an error page, a payment page, and an order placement page. That’s the purpose the simple page component will serve. When the button on the page is clicked, it will either redirect to a route or perform some action in response to a buttonEvent.

To make it:

ng g c shared/components/simple-page

This is its component file.

@Component({ selector: 'app-simple-page', templateUrl: './simple-page.component.html', styleUrls: ['./simple-page.component.css']
})
export class SimplePageComponent { @Input() title: string = ''; @Input() subtitle?: string; @Input() number?: string; @Input() icon?: string; @Input() buttonText: string = ''; @Input() centerText?: boolean = false; @Input() buttonDisabled?: boolean = false; @Input() route?: string | undefined; @Output() buttonEvent = new EventEmitter(); constructor(private router: Router) { } buttonClicked() { if (this.route) { this.router.navigateByUrl(this.route); } else { this.buttonEvent.emit(); } }
}

And its template contains:

<div id="container"> <app-title no="{{number}}" title="{{title}}" subtitle="{{subtitle}}" [centerText]="centerText"></app-title> <div *ngIf="icon" id="icon-container"> <mat-icon color="primary" class="icon">{{icon}}</mat-icon> </div> <button mat-flat-button color="primary" (click)="buttonClicked()" [disabled]="buttonDisabled"> {{buttonText}} </button>
</div>

It’s styling can be found here.

Pipes

Word Wrap Pipe

Some products’ names and other types of information displayed on the site are really long. In some instances, getting these long sentences to wrap in material components is challenging. So we’ll use this pipe to cut the sentences down to a specified length and add ellipses to the end of the result.

To create it run:

ng g pipe shared/pipes/word-wrap

It will contain:

import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'wordWrap'
})
export class WordWrapPipe implements PipeTransform { transform(value: string, length: number): string { return `${value.substring(0, length)}...`; }
}

Services

HTTP Error Handler Service

There are quite a number of http services in this project. Creating an error handler for each method is repetitive. So creating one single handler that can be used by all methods makes sense. The error handler can be used to format an error and also pass on the errors to other external logging platforms.

Generate it by running:

ng g s shared/services/http-error-handler

This service will contain only one method. The method will format the error message to be displayed depending on whether it’s a client or server error. However, there is room to improve it further.

@Injectable({ providedIn: 'root'
})
export class HttpErrorHandler { constructor() { } handleError(err: HttpErrorResponse): Observable { let displayMessage = ''; if (err.error instanceof ErrorEvent) { displayMessage = Client-side error: ${err.error.message}; } else { displayMessage = Server-side error: ${err.message}; } return throwError(displayMessage); }
}

Local Storage Service

We shall use local storage to keep track of the number of items in a cart. It’s also useful to store the Id of an order here. An order corresponds to a cart on Commerce Layer.

To generate the local storage service run:

ng g s shared/services/local-storage

The service will contain four methods to add, delete, and get items from local storage and another to clear it.

import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root'
})
export class LocalStorageService { constructor() { } addItem(key: string, value: string) { localStorage.setItem(key, value); } deleteItem(key: string) { localStorage.removeItem(key); } getItem(key: string): string | null { return localStorage.getItem(key); } clear() { localStorage.clear(); }
}

Data Module

This module is responsible for data retrieval and management. It’s what we’ll use to get the data our app consumes. Below is its structure:

src/app/data
├── data.module.ts
├── models
└── services

To generate the module run:

ng g m data

Models

The models define how the data we consume from the API is structured. We’ll have 16 interface declarations. To create them run:

for model in \
address cart country customer-address \
customer delivery-lead-time line-item order \
payment-method payment-source paypal-payment \
price shipment shipping-method sku stock-location; \
do ng g interface "data/models/${model}"; done

The following table links to each file and gives a description of what each interface is.

InterfaceDescription
AddressRepresents a general address.
CartClient side version of an order tracking the number of products a customer intends to purchase.
CountryAlpha-2 country code.
Customer AddressAn address associated with a customer.
CustomerA registered user.
Delivery Lead TimeRepresents the amount of time it will take to delivery a shipment.
Line ItemAn itemized product added to the cart.
OrderA shopping cart or collection of line items.
Payment MethodA payment type made available to an order.
Payment SourceA payment associated with an order.
Paypal PaymentA payment made through Paypal
PricePrice associated with an SKU.
ShipmentCollection of items shipped together.
Shipping MethodMethod through which a package is shipped.
SKUA unique stock-keeping unit.
Stock LocationLocation that contains SKU inventory.

Services

This folder contains the services that create, retrieve, and manipulate app data. We’ll create 11 services here.

for service in \
address cart country customer-address \
customer delivery-lead-time line-item \
order paypal-payment shipment sku; \
do ng g s "data/services/${service}"; done

Address Service

This service creates and retrieves addresses. It’s important when creating and assigning shipping and billing addresses to orders. It has two methods. One to create an address and another to retrieve one.

The route used here is /api/addresses. If you’re going to use the Commerce Layer API directly, make sure to structure the data as demonstrated in this example.

@Injectable({ providedIn: 'root'
})
export class AddressService { private url: string = ${environment.apiUrl}/api/addresses; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createAddress(address: Address): Observable<Address> { return this.http.post<Address>(this.url, address) .pipe(catchError(this.eh.handleError)); } getAddress(id: string): Observable<Address> { return this.http.get<Address>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); }
}

Cart Service

The cart is responsible for maintaining the quantity of items added and the order Id. Making API calls to get the number of items in an order everytime a new line item is created can be expensive. Instead, we could just use local storage to maintain the count on the client. This eliminates the need to make unnecessary order fetches every time an item is added to the cart.

We also use this service to store the order Id. A cart corresponds to an order on Commerce Layer. Once the first item is added to the cart, an order is created. We need to preserve this order Id so we can fetch it during the checkout process.

Additionally, we need a way to communicate to the header that an item has been added to the cart. The header contains the cart button and displays the amount of items in it. We’ll use an observable of a BehaviorSubject with the current value of the cart. The header can subscribe to this and track changes in the cart value.

Lastly, once an order has been completed the cart value needs to be cleared. This ensures that there’s no confusion when creating subsequent newer orders. The values that were stored are cleared once the current order is marked as placed.

We’ll accomplish all this using the local storage service created earlier.

@Injectable({ providedIn: 'root'
})
export class CartService { private cart = new BehaviorSubject({ orderId: this.orderId, itemCount: this.itemCount }); cartValue$ = this.cart.asObservable(); constructor(private storage: LocalStorageService) { } get orderId(): string { const id = this.storage.getItem('order-id'); return id ? id : ''; } set orderId(id: string) { this.storage.addItem('order-id', id); this.cart.next({ orderId: id, itemCount: this.itemCount }); } get itemCount(): number { const itemCount = this.storage.getItem('item-count'); return itemCount ? parseInt(itemCount) : 0; } set itemCount(amount: number) { this.storage.addItem('item-count', amount.toString()); this.cart.next({ orderId: this.orderId, itemCount: amount }); } incrementItemCount(amount: number) { this.itemCount = this.itemCount + amount; } decrementItemCount(amount: number) { this.itemCount = this.itemCount - amount; } clearCart() { this.storage.deleteItem('item-count'); this.cart.next({ orderId: '', itemCount: 0 }); }
}

Country Service

When adding addresses on Commerce Layer, the country code has to be an alpha 2 code. This service reads a json file containing these codes for every country and returns it in its getCountries method.

@Injectable({ providedIn: 'root'
})
export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable<Country[]> { return this.http.get<Country[]>('./../../../assets/json/country-codes.json'); }
}

Customer Address Service

This service is used to associate addresses with customers. It also fetches a specific or all addresses related to a customer. It is used when the customer adds their shipping and billing addresses to their order. The createCustomer method creates a customer, getCustomerAddresses gets all of a customer’s addresses, and getCustomerAddress gets a specific one.

When creating a customer address, be sure to structure the post body according to this example.

@Injectable({ providedIn: 'root'
})
export class CustomerAddressService { private url: string = ${environment.apiUrl}/api/customer_addresses; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomerAddress(addressId: string, customerId: string): Observable<CustomerAddress> { return this.http.post<CustomerAddress>(this.url, { addressId: addressId, customerId: customerId }) .pipe(catchError(this.eh.handleError)); } getCustomerAddresses(): Observable<CustomerAddress[]> { return this.http.get<CustomerAddress[]>(${this.url}) .pipe(catchError(this.eh.handleError)); } getCustomerAddress(id: string): Observable<CustomerAddress> { return this.http.get<CustomerAddress>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); }
}

Customer Service

Customers are created and their information retrieved using this service. When a user signs up, they become a customer and are created using the createCustomerMethod. getCustomer returns the customer associated with a specific Id. getCurrentCustomer returns the customer currently logged in.

When creating a customer, structure the data like this. You can add their first and last names to the metadata, as shown in its attributes.

The route /api/customers/current is not available on Commerce Layer. So you’ll need to figure out how to get the currently logged in customer.

@Injectable({ providedIn: 'root'
})
export class CustomerService { private url: string = ${environment.apiUrl}/api/customers; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomer(email: string, password: string, firstName: string, lastName: string): Observable<Customer> { return this.http.post<Customer>(this.url, { email: email, password: password, firstName: firstName, lastName: lastName }) .pipe(catchError(this.eh.handleError)); } getCurrentCustomer(): Observable<Customer> { return this.http.get<Customer>(${this.url}/current) .pipe(catchError(this.eh.handleError)); } getCustomer(id: string): Observable<Customer> { return this.http.get<Customer>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); }
}

Delivery Lead Time Service

This service returns information about shipping timelines from various stock locations.

@Injectable({ providedIn: 'root'
})
export class DeliveryLeadTimeService { private url: string = ${environment.apiUrl}/api/delivery_lead_times; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getDeliveryLeadTimes(): Observable<DeliveryLeadTime[]> { return this.http.get<DeliveryLeadTime[]>(this.url) .pipe(catchError(this.eh.handleError)); }
}

Line Item Service

Items added to the cart are managed by this service. With it, you can create an item the moment it is added to the cart. An item’s information can also be fetched. The item may also be updated when its quantity changes or deleted when removed from the cart.

When creating items or updating them, structure the request body as shown in this example.

@Injectable({ providedIn: 'root'
})
export class LineItemService { private url: string = ${environment.apiUrl}/api/line_items; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createLineItem(lineItem: LineItem): Observable<LineItem> { return this.http.post<LineItem>(this.url, lineItem) .pipe(catchError(this.eh.handleError)); } getLineItem(id: string): Observable<LineItem> { return this.http.get<LineItem>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); } updateLineItem(id: string, quantity: number): Observable<LineItem> { return this.http.patch<LineItem>(${this.url}/${id}, { quantity: quantity }) .pipe(catchError(this.eh.handleError)); } deleteLineItem(id: string): Observable<LineItem> { return this.http.delete<LineItem>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); }
}

Order Service

Similar to the line item service, the order service allows you to create, update, delete, or get an order. Additionally, you may choose to get the shipments associated with an order separately using the getOrderShipments method. This service is used heavily throughout the checkout process.

There are different kinds of information about an order that are required throughout checkout. Since it may be expensive to fetch a whole order and its relations, we specify what we want to get from an order using GetOrderParams. The equivalent of this on the CL API is the include query parameter where you list the order relationships to be included. You can check what fields need to be included for the cart summary here and for the various checkout stages here.

In the same manner, when updating an order, we use UpdateOrderParams to specify update fields. This is because in the server that populates the token, some extra operations are performed depending on what field is being updated. However, if you’re making direct requests to the CL API, you do not need to specify this. You can do away with it since the CL API doesn’t require you to specify them. Although, the request body should resemble this example.

@Injectable({ providedIn: 'root'
})
export class OrderService { private url: string = ${environment.apiUrl}/api/orders; constructor( private http: HttpClient, private eh: HttpErrorHandler) { } createOrder(): Observable<Order> { return this.http.post<Order>(this.url, {}) .pipe(catchError(this.eh.handleError)); } getOrder(id: string, orderParam: GetOrderParams): Observable<Order> { let params = {}; if (orderParam != GetOrderParams.none) { params = { [orderParam]: 'true' }; } return this.http.get<Order>(${this.url}/${id}, { params: params }) .pipe(catchError(this.eh.handleError)); } updateOrder(order: Order, params: UpdateOrderParams[]): Observable<Order> { let updateParams = []; for (const param of params) { updateParams.push(param.toString()); } return this.http.patch<Order>( ${this.url}/${order.id}, order, { params: { 'field': updateParams } } ) .pipe(catchError(this.eh.handleError)); } getOrderShipments(id: string): Observable<Shipment[]> { return this.http.get<Shipment[]>(${this.url}/${id}/shipments) .pipe(catchError(this.eh.handleError)); }
}

Paypal Payment Service

This service is responsible for creating and updating Paypal payments for orders. Additionally, we can get a Paypal payment given its id. The post body should have a structure similar to this example when creating a Paypal payment.

@Injectable({ providedIn: 'root'
})
export class PaypalPaymentService { private url: string = ${environment.apiUrl}/api/paypal_payments; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createPaypalPayment(payment: PaypalPayment): Observable<PaypalPayment> { return this.http.post<PaypalPayment>(this.url, payment) .pipe(catchError(this.eh.handleError)); } getPaypalPayment(id: string): Observable<PaypalPayment> { return this.http.get<PaypalPayment>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); } updatePaypalPayment(id: string, paypalPayerId: string): Observable<PaypalPayment> { return this.http.patch<PaypalPayment>( ${this.url}/${id}, { paypalPayerId: paypalPayerId } ) .pipe(catchError(this.eh.handleError)); }
}

Shipment Service

This service gets a shipment or updates it given its id. The request body of a shipment update should look similar to this example.

@Injectable({ providedIn: 'root'
})
export class ShipmentService { private url: string = ${environment.apiUrl}/api/shipments; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getShipment(id: string): Observable<Shipment> { return this.http.get<Shipment>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); } updateShipment(id: string, shippingMethodId: string): Observable<Shipment> { return this.http.patch<Shipment>( ${this.url}/${id}, { shippingMethodId: shippingMethodId } ) .pipe(catchError(this.eh.handleError)); }
}

SKU Service

The SKU service gets products from the store. If multiple products are being retrieved, they can be paginated and have a page size set. Page size and page number should be set as query params like in this example if you’re making direct requests to the API. A single product can also be retrieved given its id.

@Injectable({ providedIn: 'root'
})
export class SkuService { private url: string = ${environment.apiUrl}/api/skus; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getSku(id: string): Observable<Sku> { return this.http.get<Sku>(${this.url}/${id}) .pipe(catchError(this.eh.handleError)); } getSkus(page: number, pageSize: number): Observable<Sku[]> { return this.http.get<Sku[]>( this.url, { params: { 'page': page.toString(), 'pageSize': pageSize.toString() } }) .pipe(catchError(this.eh.handleError)); }
}

Core Module

The core module contains everything central to and common across the application. These include components like the header and pages like the 404 page. Services responsible for authentication and session management also fall here, as well as app-wide interceptors and guards.

The core module tree will look like this.

src/app/core
├── components
│ ├── error
│ │ ├── error.component.css
│ │ ├── error.component.html
│ │ └── error.component.ts
│ ├── header
│ │ ├── header.component.css
│ │ ├── header.component.html
│ │ └── header.component.ts
│ └── not-found
│ ├── not-found.component.css
│ ├── not-found.component.html
│ └── not-found.component.ts
├── core.module.ts
├── guards
│ └── empty-cart.guard.ts
├── interceptors
│ └── options.interceptor.ts
└── services ├── authentication.service.ts ├── header.service.ts └── session.service.ts

To generate the module and its contents run:

ng g m core
ng g g core/guards/empty-cart
ng g s core/header/header
ng g interceptor core/interceptors/options
for comp in header error not-found; do ng g c "core/${comp}"; done
for serv in authentication session; do ng g s "core/authentication/${serv}"; done

The core module file should like this. Note that routes have been registered for the NotFoundComponent and ErrorComponent.

@NgModule({ declarations: [HeaderComponent, NotFoundComponent, ErrorComponent], imports: [ RouterModule.forChild([ { path: '404', component: NotFoundComponent }, { path: 'error', component: ErrorComponent }, { path: '**', redirectTo: '/404' } ]), MatBadgeModule, SharedModule ], exports: [HeaderComponent]
})
export class CoreModule { }

Services

The services folder holds the authentication, session, and header services.

Authentication Service

The AuthenticationService allows you to acquire client and customer tokens. These tokens are used to access the rest of the API’s routes. Customer tokens are returned when a user exchanges an email and password for it and have a wider range of permissions. Client tokens are issued without needing credentials and have narrower permissions.

getClientSession gets a client token. login gets a customer token. Both methods also create a session. The body of a client token request should look like this and that of a customer token like this.

@Injectable({ providedIn: 'root'
})
export class AuthenticationService { private url: string = ${environment.apiUrl}/oauth/token; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getClientSession(): Observable<object> { return this.http.post<object>( this.url, { grantType: 'client_credentials' }) .pipe(catchError(this.eh.handleError)); } login(email: string, password: string): Observable<object> { return this.http.post<object>( this.url, { username: email, password: password, grantType: 'password' }) .pipe(catchError(this.eh.handleError)); }
}

Session Service

The SessionService is responsible for session management. The service will contain an observable from a BehaviorSubject called loggedInStatus to communicate whether a user is logged in. setLoggedInStatus sets the value of this subject, true for logged in, and false for not logged in. isCustomerLoggedIn makes a request to the server to check if the user has an existing session. logout destroys the session on the server. The last two methods access routes that are unique to the server that populates the request with a token. They are not available from Commerce Layer. You’ll have to figure out how to implement them.

@Injectable({ providedIn: 'root'
})
export class SessionService { private url: string = ${environment.apiUrl}/session; private isLoggedIn = new BehaviorSubject(false); loggedInStatus = this.isLoggedIn.asObservable(); constructor(private http: HttpClient, private eh: HttpErrorHandler) { } setLoggedInStatus(status: boolean) { this.isLoggedIn.next(status); } isCustomerLoggedIn(): Observable<{ message: string }> { return this.http.get<{ message: string }>(${this.url}/customer/status) .pipe(catchError(this.eh.handleError)); } logout(): Observable<{ message: string }> { return this.http.get<{ message: string }>(${this.url}/destroy) .pipe(catchError(this.eh.handleError)); }
}

Header Service

The HeaderService is used to communicate whether the cart, login, and logout buttons should be shown in the header. These buttons are hidden on the login and signup pages but present on all other pages to prevent confusion. We’ll use an observable from a BehaviourSubject called showHeaderButtons that shares this. We’ll also have a setHeaderButtonsVisibility method to set this value.

@Injectable({ providedIn: 'root'
})
export class HeaderService { private headerButtonsVisibility = new BehaviorSubject(true); showHeaderButtons = this.headerButtonsVisibility.asObservable(); constructor() { } setHeaderButtonsVisibility(visible: boolean) { this.headerButtonsVisibility.next(visible); }
}

Components

Error Component

This component is used as an error page. It is useful in instances when server requests fail and absolutely no data is displayed on a page. Instead of showing a blank page, we let the user know that a problem occurred. Below is it’s template.

<app-simple-page title="An error occurred" subtitle="There was a problem fetching your page" buttonText="GO TO HOME" icon="report" [centerText]="true" route="/">
</app-simple-page>

This is what the component will look like.

Not Found Component

This is a 404 page that the user gets redirected to when they request a route not available on the router. Only its template is modified.

<app-simple-page title="404: Page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested page could not be found" [centerText]="true" route="/"></app-simple-page>

Header Component

The HeaderComponent is basically the header displayed at the top of a page. It will contain the app title, the cart, login, and logout buttons.

When this component is initialized, a request is made to check whether the user has a current session. This happens when subscribing to this.session.isCustomerLoggedIn(). We subscribe to this.session.loggedInStatus to check if the user logs out throughout the life of the app. The this.header.showHeaderButtons subscription decides whether to show all the buttons on the header or hide them. this.cart.cartValue$ gets the count of items in the cart.

There exists a logout method that destroys a user’s session and assigns them a client token. A client token is assigned because the session maintaining their customer token is destroyed and a token is still required for each API request. A material snackbar communicates to the user whether their session was successfully destroyed or not.

We use the @UntilDestroy({ checkProperties: true }) decorator to indicate that all subscriptions should be automatically unsubscribed from when the component is destroyed.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit { cartAmount: number = 0; isLoggedIn: boolean = false; showButtons: boolean = true; constructor( private session: SessionService, private snackBar: MatSnackBar, private cart: CartService, private header: HeaderService, private auth: AuthenticationService ) { } ngOnInit() { this.session.isCustomerLoggedIn() .subscribe( () => { this.isLoggedIn = true; this.session.setLoggedInStatus(true); } ); this.session.loggedInStatus.subscribe(status => this.isLoggedIn = status); this.header.showHeaderButtons.subscribe(visible => this.showButtons = visible); this.cart.cartValue$.subscribe(cart => this.cartAmount = cart.itemCount); } logout() { concat( this.session.logout(), this.auth.getClientSession() ).subscribe( () => { this.snackBar.open('You have been logged out.', 'Close', { duration: 4000 }); this.session.setLoggedInStatus(false); }, err => this.snackBar.open('There was a problem logging you out.', 'Close', { duration: 4000 }) ); }
}

Below is the header template and linked here is its styling.

<div id="header-container"> <div id="left-half" routerLink="/"> <h1><span id="lime-text">Lime</span><span id="store-text">Store</span></h1> </div> <div id="right-half"> <div id="button-container" ngIf="showButtons"> <button mat-icon-button color="primary" aria-label="shopping cart"> <mat-icon [matBadge]="cartAmount" matBadgeColor="accent" aria-label="shopping cart" routerLink="/cart">shopping_cart</mat-icon> </button> <button mat-icon-button color="primary" aria-label="login" ngIf="!isLoggedIn"> <mat-icon aria-label="login" matTooltip="login" routerLink="/login">login</mat-icon> </button> <button mat-icon-button color="primary" aria-label="logout" *ngIf="isLoggedIn" (click)="logout()"> <mat-icon aria-label="logout" matTooltip="logout">logout</mat-icon> </button> </div> </div>
</div>

Guards

Empty Cart Guard

This guard prevents users from accessing routes relating to checkout and billing if their cart is empty. This is because to proceed with checkout, there needs to be a valid order. An order corresponds to a cart with items in it. If there are items in the cart, the user can proceed to a guarded page. However, if the cart is empty, the user is redirected to an empty-cart page.

@Injectable({ providedIn: 'root'
})
export class EmptyCartGuard implements CanActivate { constructor(private cart: CartService, private router: Router) { } canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { if (this.cart.orderId) { if (this.cart.itemCount > 0) { return true; } } return this.router.parseUrl('/empty'); }
}

Interceptors

Options Interceptor

This interceptor intercepts all outgoing HTTP requests and adds two options to the request. These are a Content-Type header and a withCredentials property. withCredentials specifies whether a request should be sent with outgoing credentials like the http-only cookies that we use. We use Content-Type to indicate that we are sending json resources to the server.

@Injectable()
export class OptionsInterceptor implements HttpInterceptor { constructor() { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { request = request.clone({ headers: request.headers.set('Content-Type', 'application/json'), withCredentials: true }); return next.handle(request); }
}

Feature Modules

This section contains the main features of the app. As mentioned earlier, the features are grouped in four modules: auth, product, cart, and checkout modules.

Products Module

The products module contains pages that display products on sale. These include the product page and the product list page. It’s structured as shown below.

src/app/features/products
├── pages
│ ├── product
│ │ ├── product.component.css
│ │ ├── product.component.html
│ │ └── product.component.ts
│ └── product-list
│ ├── product-list.component.css
│ ├── product-list.component.html
│ └── product-list.component.ts
└── products.module.ts

To generate it and its components:

ng g m features/products
ng g c features/products/pages/product
ng g c features/products/pages/product-list

This is the module file:

@NgModule({ declarations: [ProductListComponent, ProductComponent], imports: [ RouterModule.forChild([ { path: 'product/:id', component: ProductComponent }, { path: '', component: ProductListComponent } ]), LayoutModule, MatCardModule, MatGridListModule, MatPaginatorModule, SharedModule ]
})
export class ProductsModule { }

Product List Component

This component displays a paginated list of available products for sale. It is the first page that is loaded when the app starts.

The products are displayed in a grid. Material grid list is the best component for this. To make the grid responsive, the number of grid columns will change depending on the screen size. The BreakpointObserver service allows us to determine the size of the screen and assign the columns during initialization.

To get the products, we call the getProducts method of the SkuService. It returns the products if successful and assigns them to the grid. If not, we route the user to the error page.

Since the products displayed are paginated, we will have a getNextPage method to get the additional products.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit { cols = 4; length = 0; pageIndex = 0; pageSize = 20; pageSizeOptions: number[] = [5, 10, 20]; pageEvent!: PageEvent | void; products: Sku[] = []; constructor( private breakpointObserver: BreakpointObserver, private skus: SkuService, private router: Router, private header: HeaderService) { } ngOnInit() { this.getProducts(1, 20); this.header.setHeaderButtonsVisibility(true); this.breakpointObserver.observe([ Breakpoints.Handset, Breakpoints.Tablet, Breakpoints.Web ]).subscribe(result => { if (result.matches) { if (result.breakpoints['(max-width: 599.98px) and (orientation: portrait)'] || result.breakpoints['(max-width: 599.98px) and (orientation: landscape)']) { this.cols = 1; } else if (result.breakpoints['(min-width: 1280px) and (orientation: portrait)'] || result.breakpoints['(min-width: 1280px) and (orientation: landscape)']) { this.cols = 4; } else { this.cols = 3; } } }); } private getProducts(page: number, pageSize: number) { this.skus.getSkus(page, pageSize) .subscribe( skus => { this.products = skus; this.length = skus[0].__collectionMeta.recordCount; }, err => this.router.navigateByUrl('/error') ); } getNextPage(event: PageEvent) { this.getProducts(event.pageIndex + 1, event.pageSize); } trackSkus(index: number, item: Sku) { return ${item.id}-${index}; }
}

The template is shown below and its styling can be found here.

<mat-grid-list cols="{{cols}}" rowHeight="400px" gutterSize="20px" class="grid-layout"> <mat-grid-tile *ngFor="let product of products; trackBy: trackSkus"> <mat-card> <img id="card-image" mat-card-image src="{{product.imageUrl}}" alt="product photo"> <mat-card-content> <mat-card-title matTooltip="{{product.name}}">{{product.name |wordWrap:35}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> </mat-card-content> <mat-card-actions> <button mat-flat-button color="primary" [routerLink]="['/product', product.id]"> View </button> </mat-card-actions> </mat-card> </mat-grid-tile>
</mat-grid-list>
<mat-paginator [length]="length" [pageIndex]="pageIndex" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" (page)="pageEvent = getNextPage($event)">
</mat-paginator>

The page will look like this.

Product Component

Once a product is selected from the product list page, this component displays its details. These include the product’s full name, price, and description. There’s also a button to add the item to the product cart.

On initialization, we get the id of the product from the route parameters. Using the id, we fetch the product from the SkuService.

When the user adds an item to the cart, the addItemToCart method is called. In it, we check if an order has already been created for the cart. If not, a new one is made using the OrderService. Afterwhich, a line item is created in the order that corresponds to the product. If an order already exists for the cart, just the line item is created. Depending on the status of the requests, a snackbar message is displayed to the user.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-product', templateUrl: './product.component.html', styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit { id: string = ''; product!: Sku; quantity: number = 0; constructor( private route: ActivatedRoute, private skus: SkuService, private location: Location, private router: Router, private header: HeaderService, private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.paramMap .pipe( mergeMap(params => { const id = params.get('id') this.id = id ? id : ''; return this.skus.getSku(this.id); }), tap((sku) => { this.product = sku; }) ).subscribe({ error: (err) => this.router.navigateByUrl('/error') }); this.header.setHeaderButtonsVisibility(true); } addItemToCart() { if (this.quantity > 0) { if (this.cart.orderId == '') { this.orders.createOrder() .pipe( mergeMap((order: Order) => { this.cart.orderId = order.id || ''; return this.lineItems.createLineItem({ orderId: order.id, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }); }) ) .subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } else { this.lineItems.createLineItem({ orderId: this.cart.orderId, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }).subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } } else { this.snackBar.open('Select a quantity greater than 0.', 'Close', { duration: 8000 }); } } setQuantity(no: number) { this.quantity = no; } goBack() { this.location.back(); } private showSuccessSnackBar() { this.snackBar.open('Item successfully added to cart.', 'Close', { duration: 8000 }); } private showErrorSnackBar() { this.snackBar.open('Failed to add your item to the cart.', 'Close', { duration: 8000 }); }
}

The ProductComponent template is as follows and its styling is linked here.

<div id="container"> <mat-card *ngIf="product" class="product-card"> <img mat-card-image src="{{product.imageUrl}}" alt="Photo of a product"> <mat-card-content> <mat-card-title>{{product.name}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> <p> {{product.description}} </p> </mat-card-content> <mat-card-actions> <app-item-quantity [quantity]="quantity" [maxValue]="10" (setQuantityEvent)="setQuantity($event)"></app-item-quantity> <button mat-raised-button color="accent" (click)="addItemToCart()"> <mat-icon>add_shopping_cart</mat-icon> Add to cart </button> <button mat-raised-button color="primary" (click)="goBack()"> <mat-icon>storefront</mat-icon> Continue shopping </button> </mat-card-actions> </mat-card>
</div>

The page will look like this.

Auth Module

The Auth module contains pages responsible for authentication. These include the login and signup pages. It‘s structured as follows.

src/app/features/auth/
├── auth.module.ts
└── pages ├── login │ ├── login.component.css │ ├── login.component.html │ └── login.component.ts └── signup ├── signup.component.css ├── signup.component.html └── signup.component.ts

To generate it and its components:

ng g m features/auth
ng g c features/auth/pages/signup
ng g c features/auth/pages/login

This is its module file.

@NgModule({ declarations: [LoginComponent, SignupComponent], imports: [ RouterModule.forChild([ { path: 'login', component: LoginComponent }, { path: 'signup', component: SignupComponent } ]), MatFormFieldModule, MatInputModule, ReactiveFormsModule, SharedModule ]
})
export class AuthModule { }

Signup Component

A user signs up for an account using this component. A first name, last name, email, and password are required for the process. The user also needs to confirm their password. The input fields will be created with the FormBuilder service. Validation is added to require that all the inputs have values. Additional validation is added to the password field to ensure a minimum length of eight characters. A custom matchPasswords validator ensures that the confirmed password matches the initial password.

When the component is initialized, the cart, login, and logout buttons in the header are hidden.This is communicated to the header using the HeaderService.

After all the fields are marked as valid, the user can then sign up. In the signup method, the createCustomer method of the CustomerService receives this input. If the signup is successful, the user is informed that their account was successfully created using a snackbar. They are then rerouted to the home page.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-signup', templateUrl: './signup.component.html', styleUrls: ['./signup.component.css']
})
export class SignupComponent implements OnInit { signupForm = this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], confirmedPassword: ['', [Validators.required]] }, { validators: this.matchPasswords }); @ViewChild(FormGroupDirective) sufDirective: FormGroupDirective | undefined; constructor( private customer: CustomerService, private fb: FormBuilder, private snackBar: MatSnackBar, private router: Router, private header: HeaderService ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } matchPasswords(signupGroup: AbstractControl): ValidationErrors | null { const password = signupGroup.get('password')?.value; const confirmedPassword = signupGroup.get('confirmedPassword')?.value; return password == confirmedPassword ? null : { differentPasswords: true }; } get password() { return this.signupForm.get('password'); } get confirmedPassword() { return this.signupForm.get('confirmedPassword'); } signup() { const customer = this.signupForm.value; this.customer.createCustomer( customer.email, customer.password, customer.firstName, customer.lastName ).subscribe( () => { this.signupForm.reset(); this.sufDirective?.resetForm(); this.snackBar.open('Account successfully created. You will be redirected in 5 seconds.', 'Close', { duration: 5000 }); setTimeout(() => this.router.navigateByUrl('/'), 6000); }, err => this.snackBar.open('There was a problem creating your account.', 'Close', { duration: 5000 }) ); }
}

Below is the template for the SignupComponent.

<form id="container" [formGroup]="signupForm" (ngSubmit)="signup()"> <h1 class="mat-display-3">Create Account</h1> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput formControlName="email" type="email"> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput formControlName="password" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Confirm Password</mat-label> <input matInput formControlName="confirmedPassword" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <div ngIf="confirmedPassword?.invalid && (confirmedPassword?.dirty || confirmedPassword?.touched)"> <mat-error ngIf="signupForm.hasError('differentPasswords')"> Your passwords do not match. </mat-error> </div> <div ngIf="password?.invalid && (password?.dirty || password?.touched)"> <mat-error ngIf="password?.hasError('minlength')"> Your password should be at least 8 characters. </mat-error> </div> <button mat-flat-button color="primary" [disabled]="!signupForm.valid">Sign Up</button>
</form>

The component will turn out as follows.

Login Component

A registered user logs into their account with this component. An email and password need to be entered. Their corresponding input fields would have validation that makes them required.

Similar to the SignupComponent, the cart, login, and logout buttons in the header are hidden. Their visibility is set using the HeaderService during component initialization.

To login, the credentials are passed to the AuthenticationService. If successful, the login status of the user is set using the SessionService. The user is then routed back to the page they were on. If unsuccessful, a snackbar is displayed with an error and the password field is reset.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit { loginForm = this.fb.group({ email: ['', Validators.required], password: ['', Validators.required] }); constructor( private authService: AuthenticationService, private session: SessionService, private snackBar: MatSnackBar, private fb: FormBuilder, private header: HeaderService, private location: Location ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } login() { const credentials = this.loginForm.value; this.authService.login( credentials.email, credentials.password ).subscribe( () => { this.session.setLoggedInStatus(true); this.location.back(); }, err => { this.snackBar.open( 'Login failed. Check your login credentials.', 'Close', { duration: 6000 }); this.loginForm.patchValue({ password: '' }); } ); }
}

Below is the LoginComponent template.

<form id="container" [formGroup]="loginForm" (ngSubmit)="login()"> <h1 class="mat-display-3">Login</h1> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput type="email" formControlName="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput type="password" formControlName="password" required> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="!loginForm.valid">Login</button> <p id="newAccount" class="mat-h3">Not registered yet? <a id="newAccountLink" routerLink="/signup">Create an account.</a></p>
</form>

Here is a screenshot of the page.

Cart Module

The cart module contains all pages related to the cart. These include the order summary page, a coupon and gift card code page, and an empty cart page. It’s structured as follows.

src/app/features/cart/
├── cart.module.ts
└── pages ├── codes │ ├── codes.component.css │ ├── codes.component.html │ └── codes.component.ts ├── empty │ ├── empty.component.css │ ├── empty.component.html │ └── empty.component.ts └── summary ├── summary.component.css ├── summary.component.html └── summary.component.ts

To generate it, run:

ng g m features/cart
ng g c features/cart/codes
ng g c features/cart/empty
ng g c features/cart/summary

This is the module file.

@NgModule({ declarations: [SummaryComponent, CodesComponent, EmptyComponent], imports: [ RouterModule.forChild([ { path: '', canActivate: [EmptyCartGuard], children: [ { path: 'cart', component: SummaryComponent }, { path: 'codes', component: CodesComponent } ] }, { path: 'empty', component: EmptyComponent } ]), MatDividerModule, MatFormFieldModule, MatInputModule, MatMenuModule, ReactiveFormsModule, SharedModule ]
})
export class CartModule { }

Codes Component

As mentioned earlier, this component is used to add any coupon or gift card codes to an order. This allows the user to apply discounts to the total of their order before proceeding to checkout.

There will be two input fields. One for coupons and another for gift card codes.

The codes are added by updating the order. The updateOrder method of the OrderService updates the order with the codes. Afterwhich, both fields are reset and the user is informed of the success of the operation with a snackbar. A snackbar is also shown when an error occurs. Both the addCoupon and addGiftCard methods call the updateOrder method.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-codes', templateUrl: './codes.component.html', styleUrls: ['./codes.component.css']
})
export class CodesComponent { couponCode = new FormControl(''); giftCardCode = new FormControl(''); @ViewChild(FormControlDirective) codesDirective: FormControlDirective | undefined; constructor( private cart: CartService, private order: OrderService, private snackBar: MatSnackBar ) { } private updateOrder(order: Order, params: UpdateOrderParams[], codeType: string) { this.order.updateOrder(order, params) .subscribe( () => { this.snackBar.open(Successfully added ${codeType} code., 'Close', { duration: 8000 }); this.couponCode.reset(); this.giftCardCode.reset(); this.codesDirective?.reset(); }, err => this.snackBar.open(There was a problem adding your ${codeType} code., 'Close', { duration: 8000 }) ); } addCoupon() { this.updateOrder({ id: this.cart.orderId, couponCode: this.couponCode.value }, [UpdateOrderParams.couponCode], 'coupon'); } addGiftCard() { this.updateOrder({ id: this.cart.orderId, giftCardCode: this.giftCardCode.value }, [UpdateOrderParams.giftCardCode], 'gift card'); } }

The template is shown below and its styling can be found at this link.

<div id="container"> <app-title title="Redeem a code" subtitle="Enter a coupon code or gift card" [centerText]="true"></app-title> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Coupon Code</mat-label> <input matInput [formControl]="couponCode" required> <mat-icon matPrefix>card_giftcard</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="couponCode.invalid" (click)="addCoupon()">Redeem</button> </div> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Gift Card Code</mat-label> <input matInput [formControl]="giftCardCode" required> <mat-icon matPrefix>redeem</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="giftCardCode.invalid" (click)="addGiftCard()">Redeem</button> </div> <button color="primary" mat-flat-button routerLink="/cart"> <mat-icon>shopping_cart</mat-icon> CONTINUE TO CART </button>
</div>

Here is a screenshot of the page.

Empty Component

It should not be possible to check out with an empty cart. There needs to be a guard that prevents users from accessing checkout module pages with empty carts. This has already been covered as part of the CoreModule. The guard redirects requests to checkout pages with an empty cart to the EmptyCartComponent.

It’s a very simple component that has some text indicating to the user that their cart is empty. It also has a button that the user can click to go to the homepage to add things to their cart. So we’ll use the SimplePageComponent to display it. Here is the template.

<app-simple-page title="Your cart is empty" subtitle="There is currently nothing in your cart. Head to the home page to add items." buttonText="GO TO HOME PAGE" icon="shopping_basket" [centerText]="true" route="/">
</app-simple-page>

Here is a screenshot of the page.

Summary Component

This component summarizes the cart/order. It lists all the items in the cart, their names, quantities, and pictures. It additionally breaks down the cost of the order including taxes, shipping, and discounts. The user should be able to view this and decide whether they are satisfied with the items and cost before proceeding to checkout.

On initialization, the order and its line items are fetched using the OrderService. A user should be able to modify the line items or even remove them from the order. Items are removed when the deleteLineItem method is called. In it the deleteLineItem method of the LineItemService receives the id of the line item to be deleted. If a deletion is successful, we update the item count in the cart using the CartService.

The user is then routed to the customer page where they begin the process of checking out. The checkout method does the routing.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-summary', templateUrl: './summary.component.html', styleUrls: ['./summary.component.css']
})
export class SummaryComponent implements OnInit { order: Order = {}; summary: { name: string, amount: string | undefined, id: string }[] = []; constructor( private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar, private router: Router ) { } ngOnInit() { this.orders.getOrder(this.cart.orderId, GetOrderParams.cart) .subscribe( order => this.processOrder(order), err => this.showOrderError('retrieving your cart') ); } private processOrder(order: Order) { this.order = order; this.summary = [ { name: 'Subtotal', amount: order.formattedSubtotalAmount, id: 'subtotal' }, { name: 'Discount', amount: order.formattedDiscountAmount, id: 'discount' }, { name: 'Taxes (included)', amount: order.formattedTotalTaxAmount, id: 'taxes' }, { name: 'Shipping', amount: order.formattedShippingAmount, id: 'shipping' }, { name: 'Gift Card', amount: order.formattedGiftCardAmount, id: 'gift-card' } ]; } private showOrderError(msg: string) { this.snackBar.open(There was a problem ${msg}., 'Close', { duration: 8000 }); } checkout() { this.router.navigateByUrl('/customer'); } deleteLineItem(id: string) { this.lineItems.deleteLineItem(id) .pipe( mergeMap(() => this.orders.getOrder(this.cart.orderId, GetOrderParams.cart)) ).subscribe( order => { this.processOrder(order); this.cart.itemCount = order.skusCount || this.cart.itemCount; this.snackBar.open(Item successfully removed from cart., 'Close', { duration: 8000 }) }, err => this.showOrderError('deleting your order') ); }
}

Below is the template and its styling is linked here.

<div class="container" ngIf="order"> <h3 id="order-id">Order #{{order.number}} ({{order.skusCount}} items)</h3> <div class="line-item" ngFor="let item of order.lineItems"> <div id="product-details"> <img ngIf="item.imageUrl" class="image-xs" src="{{item.imageUrl}}" alt="product photo"> <div ngIf="!item.imageUrl" class="image-xs no-image"></div> <div id="line-details"> <div>{{item.name}}</div> <div> {{item.formattedUnitAmount }} </div> </div> </div> <div id="product-config"> <app-item-quantity [quantity]="item.quantity || 0" [disabled]="true"></app-item-quantity> <div class="itemTotal"> {{item.formattedTotalAmount }} </div> <button mat-icon-button color="warn" (click)="deleteLineItem(item.id || '')"> <mat-icon>clear</mat-icon> </button> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem" *ngFor="let item of summary" [id]="item.id"> <h3 class="costLabel">{{item.name}}</h3> <p> {{item.amount }} </p> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem" id="total"> <h2 id="totalLabel">Total</h2> <h2> {{order.formattedTotalAmountWithTaxes}} </h2> </div> </div> <div id="checkout-button"> <button color="accent" mat-flat-button routerLink="/codes"> <mat-icon>redeem</mat-icon> ADD GIFT CARD/COUPON </button> <button color="primary" mat-flat-button (click)="checkout()"> <mat-icon>point_of_sale</mat-icon> CHECKOUT </button> </div>
</div>

Here is a screenshot of the page.

Checkout Module

This module is responsible for the checkout process. Checkout involves providing a billing and shipping address, a customer email, and selecting a shipping and payment method. The last step of this process is placement and confirmation of the order. The structure of the module is as follows.

src/app/features/checkout/
├── components
│ ├── address
│ ├── address-list
│ └── country-select
└── pages ├── billing-address ├── cancel-payment ├── customer ├── payment ├── place-order ├── shipping-address └── shipping-methods

This module is the biggest by far and contains 3 components and 7 pages. To generate it and its components run:

ng g m features/checkout
for comp in \
address address-list country-select; do \
ng g c "features/checkout/components/${comp}" \
; done
for page in \
billing-address cancel-payment customer \
payment place-order shipping-address \
shipping-methods; do \
ng g c "features/checkout/pages/${page}"; done

This is the module file.

@NgModule({ declarations: [ CustomerComponent, AddressComponent, BillingAddressComponent, ShippingAddressComponent, ShippingMethodsComponent, PaymentComponent, PlaceOrderComponent, AddressListComponent, CountrySelectComponent, CancelPaymentComponent ], imports: [ RouterModule.forChild([ { path: '', canActivate: [EmptyCartGuard], children: [ { path: 'billing-address', component: BillingAddressComponent }, { path: 'cancel-payment', component: CancelPaymentComponent }, { path: 'customer', component: CustomerComponent }, { path: 'payment', component: PaymentComponent }, { path: 'place-order', component: PlaceOrderComponent }, { path: 'shipping-address', component: ShippingAddressComponent }, { path: 'shipping-methods', component: ShippingMethodsComponent } ] } ]), MatCardModule, MatCheckboxModule, MatDividerModule, MatInputModule, MatMenuModule, MatRadioModule, ReactiveFormsModule, SharedModule ]
})
export class CheckoutModule { }

Components

Country Select Component

This component lets a user select a country as part of an address. The material select component has a pretty different appearance when compared to the input fields in the address form. So for the sake of uniformity, a material menu component is used instead.

When the component is initialized, the country code data is fetched using the CountryService. The countries property holds the values returned by the service. These values will be added to the menu in the template.

The component has one output property, setCountryEvent. When a country is selected, this event emits the alpha-2 code of the country.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-country-select', templateUrl: './country-select.component.html', styleUrls: ['./country-select.component.css']
})
export class CountrySelectComponent implements OnInit { country: string = 'Country'; countries: Country[] = []; @Output() setCountryEvent = new EventEmitter<string>(); constructor(private countries: CountryService) { } ngOnInit() { this.countries.getCountries() .subscribe( countries => { this.countries = countries; } ); } setCountry(value: Country) { this.country = value.name || ''; this.setCountryEvent.emit(value.code); }}

Below is its template and linked here is its styling.

<button id="country-select" mat-stroked-button [matMenuTriggerFor]="countryMenu"> {{country}} <mat-icon>expand_more</mat-icon>
</button>
<mat-menu #countryMenu="matMenu"> <button *ngFor="let cnt of countries" (click)="setCountry(cnt)" mat-menu-item>{{cnt.name}}</button>
</mat-menu>

Address Component

This is a form for capturing addresses. It is used by both the shipping and billing address pages. A valid Commerce Layer address should contain a first and last name, an address line, a city, zip code, state code, country code, and phone number.

The FormBuilder service will create the form group. Since this component is used by multiple pages, it has a number of input and output properties. The input properties include the button text, title displayed, and text for a checkbox. The output properties will be event emitters for when the button is clicked to create the address and another for when the checkbox value changes.

When the button is clicked, the addAddress method is called and the createAddress event emits the complete address. Similarly, when the checkbox is checked, the isCheckboxChecked event emits the checkbox value.

@Component({ selector: 'app-address', templateUrl: './address.component.html', styleUrls: ['./address.component.css']
})
export class AddressComponent { @Input() buttonText: string = ''; @Input() showTitle?: boolean = false; @Output() createAddress = new EventEmitter<Address>(); @Input() checkboxText: string = ''; @Output() isCheckboxChecked = new EventEmitter<boolean>(); countryCode: string = ''; addressForm = this.fb.group({ firstName: [''], lastName: [''], line1: [''], city: [''], zipCode: [''], stateCode: [''], phone: [''] }); @ViewChild(FormGroupDirective) afDirective: FormGroupDirective | undefined; constructor(private fb: FormBuilder) { } setCountryCode(code: string) { this.countryCode = code; } addAddress() { this.createAddress.emit({ firstName: this.addressForm.get('firstName')?.value, lastName: this.addressForm.get('lastName')?.value, line1: this.addressForm.get('line1')?.value, city: this.addressForm.get('city')?.value, zipCode: this.addressForm.get('zipCode')?.value, stateCode: this.addressForm.get('stateCode')?.value || 'N/A', countryCode: this.countryCode, phone: this.addressForm.get('phone')?.value }); } setCheckboxValue(change: MatCheckboxChange) { if (this.isCheckboxChecked) { this.isCheckboxChecked.emit(change.checked); } }
}

This is its template and its styling is linked here.

<form id="container" [formGroup]="addressForm"> <p class="mat-headline" *ngIf="showTitle">Or add a new address</p> <div class="row"> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Address</mat-label> <input matInput formControlName="line1"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>City</mat-label> <input matInput formControlName="city"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>State Code</mat-label> <input matInput formControlName="stateCode"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Zip Code</mat-label> <input matInput formControlName="zipCode"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Phone</mat-label> <input matInput formControlName="phone"> </mat-form-field> <app-country-select (setCountryEvent)="setCountryCode($event)"></app-country-select> </div> <mat-checkbox color="accent" (change)="setCheckboxValue($event)"> {{checkboxText}} </mat-checkbox> <button id="submit-button" mat-flat-button color="primary" (click)="addAddress()"> {{buttonText}} </button>
</form>

Address List Component

When a customer logs in, they can access their existing addresses. Instead of having them re-enter an address, they can pick from an address list. This is the purpose of this component. On initialization, all the customer’s addresses are fetched using the CustomerAddressService if they are logged in. We will check their login status using the SessionService.

This component has a setAddressEvent output property. When an address is selected, setAddressEvent emits its id to the parent component.

@Component({ selector: 'app-address-list', templateUrl: './address-list.component.html', styleUrls: ['./address-list.component.css']
})
export class AddressListComponent implements OnInit { addresses: CustomerAddress[] = []; @Output() setAddressEvent = new EventEmitter<string>(); constructor( private session: SessionService, private customerAddresses: CustomerAddressService, private snackBar: MatSnackBar ) { } ngOnInit() { this.session.loggedInStatus .pipe( mergeMap( status => iif(() => status, this.customerAddresses.getCustomerAddresses()) )) .subscribe( addresses => { if (addresses.length) { this.addresses = addresses } }, err => this.snackBar.open('There was a problem getting your existing addresses.', 'Close', { duration: 8000 }) ); } setAddress(change: MatRadioChange) { this.setAddressEvent.emit(change.value); }
}

Here is its template. You can find its styling here.

<div id="container"> <p class="mat-headline">Pick an existing address</p> <mat-error ngIf="!addresses.length">You have no existing addresses</mat-error> <mat-radio-group ngIf="addresses.length" class="addresses" (change)="setAddress($event)"> <mat-card class="address" *ngFor="let address of addresses"> <mat-radio-button [value]="address.address?.id" color="primary"> <p>{{address.address?.firstName}} {{address.address?.lastName}},</p> <p>{{address.address?.line1}},</p> <p>{{address.address?.city}},</p> <p>{{address.address?.zipCode}},</p> <p>{{address.address?.stateCode}}, {{address.address?.countryCode}}</p> <p>{{address.address?.phone}}</p> </mat-radio-button> </mat-card> </mat-radio-group>
</div>

Pages

Customer Component

An order needs to be associated with an email address. This component is a form that captures the customer email address. When the component is initialized, the current customer’s email address is fetched if they are logged in. We get the customer from the CustomerService. If they do not wish to change their email address, this email will be the default value.

If the email is changed or a customer is not logged in, the order is updated with the inputted email. We use the OrderService to update the order with the new email address. If successful, we route the customer to the billing address page.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-customer', templateUrl: './customer.component.html', styleUrls: ['./customer.component.css']
})
export class CustomerComponent implements OnInit { email = new FormControl('', [Validators.required, Validators.email]); constructor( private orders: OrderService, private customers: CustomerService, private cart: CartService, private router: Router, private snackBar: MatSnackBar ) { } ngOnInit() { this.customers.getCurrentCustomer() .subscribe( customer => this.email.setValue(customer.email) ); } addCustomerEmail() { this.orders.updateOrder( { id: this.cart.orderId, customerEmail: this.email.value }, [UpdateOrderParams.customerEmail]) .subscribe( () => this.router.navigateByUrl('/billing-address'), err => this.snackBar.open('There was a problem adding your email to the order.', 'Close', { duration: 8000 }) ); }
}

Here is the component template and linked here is its styling.

<div id="container"> <app-title no="1" title="Customer" subtitle="Billing information and shipping address"></app-title> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput [formControl]="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="email.invalid" (click)="addCustomerEmail()"> PROCEED TO BILLING ADDRESS </button>
</div>

Here is a screenshot of the customer page.

Billing Address Component

The billing address component lets a customer either add a new billing address or pick from their existing addresses. Users who are not logged in have to input a new address. Those who have logged in get an option to pick between new or existing addresses.

The showAddress property indicates whether existing addresses should be shown on the component. sameShippingAddressAsBilling indicates whether the shipping address should be the same as what the billing address is set. When a customer selects an existing address, then its id is assigned to selectedCustomerAddressId.

When the component is initialized, we use the SessionService to check if the current user is logged in. If they are logged in, we will display their existing addresses if they have any.

As mentioned earlier, if a user is logged in, they can pick an existing address as their billing address. In the updateBillingAddress method, if they are logged in, the address they select is cloned and set as the order’s billing address. We do this by updating the order using the updateOrder method of the OrderService and supplying the address Id.

If they are not logged in, the user has to provide an address. Once provided, the address is created using the createAddress method. In it, the AddressService takes the input and makes the new address. After which, the order is updated using the id of the newly created address. If there is an error or either operation is successful, we show a snackbar.

If the same address is selected as a shipping address, the user is routed to the shipping methods page. If they’d like to provide an alternate shipping address, they are directed to the shipping address page.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-billing-address', templateUrl: './billing-address.component.html', styleUrls: ['./billing-address.component.css']
})
export class BillingAddressComponent implements OnInit { showAddresses: boolean = false; sameShippingAddressAsBilling: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateBillingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameShippingAddressAsBilling(change: boolean) { this.sameShippingAddressAsBilling = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, billingAddressId: address.id }, [UpdateOrderParams.billingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, billingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.billingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameShippingAddressAsBilling, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.shippingAddressSameAsBilling]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private navigateTo(path: string) { setTimeout(() => this.router.navigateByUrl(path), 4000); } private showSuccessSnackBar() { this.snackBar.open('Billing address successfully added. Redirecting...', 'Close', { duration: 3000 }); if (this.sameShippingAddressAsBilling) { this.navigateTo('/shipping-methods'); } else { this.navigateTo('/shipping-address'); } }
}

Here is the template. This link points to its styling.

<app-title no="2" title="Billing Address" subtitle="Address to bill charges to"></app-title>
<app-address-list ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list>
<mat-divider ngIf="showAddresses"></mat-divider>
<app-address [showTitle]="showAddresses" buttonText="PROCEED TO NEXT STEP" checkboxText="Ship to the same address" (isCheckboxChecked)="setSameShippingAddressAsBilling($event)" (createAddress)="updateBillingAddress($event)"></app-address>

This is what the billing address page will look like.

Shipping Address Component

The shipping address component behaves a lot like the billing address component. However, there are a couple of differences. For one, the text displayed on the template is different. The other key differences are in how the order is updated using the OrderService once an address is created or selected. The fields that the order updates are shippingAddressCloneId for selected addresses and shippingAddress for new addresses. If a user chooses to change the billing address, to be the same as the shipping address, the billingAddressSameAsShipping field is updated.

After a shipping address is selected and the order is updated, the user is routed to the shipping methods page.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-shipping-address', templateUrl: './shipping-address.component.html', styleUrls: ['./shipping-address.component.css']
})
export class ShippingAddressComponent implements OnInit { showAddresses: boolean = false; sameBillingAddressAsShipping: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateShippingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameBillingAddressAsShipping(change: boolean) { this.sameBillingAddressAsShipping = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, shippingAddressId: address.id }, [UpdateOrderParams.shippingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, shippingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.shippingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameBillingAddressAsShipping, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.billingAddressSameAsShipping]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private showSuccessSnackBar() { this.snackBar.open('Shipping address successfully added. Redirecting...', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/shipping-methods'), 4000); }
}

Here is the template and its styling can be found here.

<app-title no="3" title="Shipping Address" subtitle="Address to ship package to"></app-title>
<app-address-list ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list>
<mat-divider ngIf="showAddresses"></mat-divider>
<app-address [showTitle]="showAddresses" buttonText="PROCEED TO SHIPPING METHODS" checkboxText="Bill to the same address" (isCheckboxChecked)="setSameBillingAddressAsShipping($event)" (createAddress)="updateShippingAddress($event)"></app-address>

The shipping address page will look like this.

Shipping Methods Component

This component displays the number of shipments required for an order to be fulfilled, the available shipping methods, and their associated costs. The customer can then select a shipping method they prefer for each shipment.

The shipments property contains all the shipments of the order. The shipmentsForm is the form within which the shipping method selections will be made.

When the component is initialized, the order is fetched and will contain both its line items and shipments. At the same time, we get the delivery lead times for the various shipping methods. We use the OrderService to get the order and the DeliveryLeadTimeService for the lead times. Once both sets of information are returned, they are combined into an array of shipments and assigned to the shipments property. Each shipment will contain its items, the shipping methods available, and the corresponding cost.

After the user has selected a shipping method for each shipment, the selected shipping method is updated for each in setShipmentMethods. If successful, the user is routed to the payments page.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-shipping-methods', templateUrl: './shipping-methods.component.html', styleUrls: ['./shipping-methods.component.css']
})
export class ShippingMethodsComponent implements OnInit { shipments: Shipment[] | undefined = []; shipmentsForm: FormGroup = this.fb.group({}); constructor( private orders: OrderService, private dlts: DeliveryLeadTimeService, private cart: CartService, private router: Router, private fb: FormBuilder, private shipments: ShipmentService, private snackBar: MatSnackBar ) { } ngOnInit() { combineLatest([ this.orders.getOrder(this.cart.orderId, GetOrderParams.shipments), this.dlts.getDeliveryLeadTimes() ]).subscribe( ([lineItems, deliveryLeadTimes]) => { let li: LineItem; let lt: DeliveryLeadTime[]; this.shipments = lineItems.shipments?.map((shipment) => { if (shipment.id) { this.shipmentsForm.addControl(shipment.id, new FormControl('', Validators.required)); } if (shipment.lineItems) { shipment.lineItems = shipment.lineItems.map(item => { li = this.findItem(lineItems, item.skuCode || ''); item.imageUrl = li.imageUrl; item.name = li.name; return item; }); } if (shipment.availableShippingMethods) { lt = this.findLocationLeadTime(deliveryLeadTimes, shipment); shipment.availableShippingMethods = shipment.availableShippingMethods?.map( method => { method.deliveryLeadTime = this.findMethodLeadTime(lt, method); return method; }); } return shipment; }); }, err => this.router.navigateByUrl('/error') ); } setShipmentMethods() { const shipmentsFormValue = this.shipmentsForm.value; combineLatest(Object.keys(shipmentsFormValue).map( key => this.shipments.updateShipment(key, shipmentsFormValue[key]) )).subscribe( () => { this.snackBar.open('Your shipments have been updated with a shipping method.', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/payment'), 4000); }, err => this.snackBar.open('There was a problem adding shipping methods to your shipments.', 'Close', { duration: 5000 }) ); } private findItem(lineItems: LineItem[], skuCode: string): LineItem { return lineItems.filter((item) => item.skuCode == skuCode)[0]; } private findLocationLeadTime(times: DeliveryLeadTime[], shipment: Shipment): DeliveryLeadTime[] { return times.filter((dlTime) => dlTime?.stockLocation?.id == shipment?.stockLocation?.id); } private findMethodLeadTime(times: DeliveryLeadTime[], method: ShippingMethod): DeliveryLeadTime { return times.filter((dlTime) => dlTime?.shippingMethod?.id == method?.id)[0]; }
}

Here is the template and you can find the styling at this link.

<form id="container" [formGroup]="shipmentsForm"> <app-title no="4" title="Shipping Methods" subtitle="How to ship your packages"></app-title> <div class="shipment-container" ngFor="let shipment of shipments; let j = index; let isLast = last"> <h1>Shipment {{j+1}} of {{shipments?.length}}</h1> <div class="row" ngFor="let item of shipment.lineItems"> <img class="image-xs" [src]="item.imageUrl" alt="product photo"> <div id="shipment-details"> <h4 id="item-name">{{item.name}}</h4> <p>{{item.skuCode}}</p> </div> <div id="quantity-section"> <p id="quantity-label">Quantity: </p>{{item.quantity}} </div> </div> <mat-radio-group [formControlName]="shipment?.id || j"> <mat-radio-button ngFor="let method of shipment.availableShippingMethods" [value]="method.id"> <div class="radio-button"> <p>{{method.name}}</p> <div> <p class="radio-label">Cost:</p> <p> {{method.formattedPriceAmount}}</p> </div> <div> <p class="radio-label">Timeline:</p> <p> Available in {{method.deliveryLeadTime?.minDays}}-{{method.deliveryLeadTime?.maxDays}} days</p> </div> </div> </mat-radio-button> </mat-radio-group> <mat-divider ngIf="!isLast"></mat-divider> </div> <button mat-flat-button color="primary" [disabled]="shipmentsForm.invalid" (click)="setShipmentMethods()">PROCEED TO PAYMENT</button>
</form>

This is a screenshot of the shipping methods page.

Payments Component

In this component, the user clicks the payment button if they wish to proceed to pay for their order with Paypal. The approvalUrl is the Paypal link that the user is directed to when they click the button.

During initialization, we get the order with the payment source included using the OrderService. If a payment source is set, we get its id and retrieve the corresponding Paypal payment from the PaypalPaymentService. The Paypal payment will contain the approval url. If no payment source has been set, we update the order with Paypal as the preferred payment method. We then proceed to create a new Paypal payment for the order using the PaypalPaymentService. From here, we can get the approval url from the newly created order.

Lastly, when the user clicks the button, they are redirected to Paypal where they can approve the purchase.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-payment', templateUrl: './payment.component.html', styleUrls: ['./payment.component.css']
})
export class PaymentComponent implements OnInit { approvalUrl: string = ''; constructor( private orders: OrderService, private cart: CartService, private router: Router, private payments: PaypalPaymentService ) { } ngOnInit() { const orderId = this.cart.orderId; this.orders.getOrder(orderId, GetOrderParams.paymentSource) .pipe( concatMap((order: Order) => { const paymentSourceId = order.paymentSource?.id; const paymentMethod = order.availablePaymentMethods?.filter( (method) => method.paymentSourceType == 'paypal_payments' )[0]; return iif( () => paymentSourceId ? true : false, this.payments.getPaypalPayment(paymentSourceId || ''), this.orders.updateOrder({ id: orderId, paymentMethodId: paymentMethod?.id }, [UpdateOrderParams.paymentMethod]) .pipe(concatMap( order => this.payments.createPaypalPayment({ orderId: orderId, cancelUrl: ${environment.clientUrl}/cancel-payment, returnUrl: ${environment.clientUrl}/place-order }) )) ); })) .subscribe( paypalPayment => this.approvalUrl = paypalPayment?.approvalUrl || '', err => this.router.navigateByUrl('/error') ); } navigateToPaypal() { window.location.href = this.approvalUrl; }
}

Here is its template.

<app-simple-page number="5" title="Payment" subtitle="Pay for your order" buttonText="PROCEED TO PAY WITH PAYPAL" icon="point_of_sale" (buttonEvent)="navigateToPaypal()" [buttonDisabled]="approvalUrl.length ? false : true"></app-simple-page>

Here’s what the payments page will look like.

Cancel Payment Component

Paypal requires a cancel payment page. This component serves this purpose. This is its template.

<app-simple-page title="Payment cancelled" subtitle="Your Paypal payment has been cancelled" icon="money_off" buttonText="GO TO HOME" [centerText]="true" route="/"></app-simple-page>

Here’s a screenshot of the page.

Place Order Component

This is the last step in the checkout process. Here the user confirms that they indeed want to place the order and begin its processing. When the user approves the Paypal payment, this is the page they are redirected to. Paypal adds a payer id query parameter to the url. This is the user’s Paypal Id.

When the component is initialized, we get the payerId query parameter from the url. The order is then retrieved using the OrderService with the payment source included. The id of the included payment source is used to update the Paypal payment with the payer id, using the PaypalPayment service. If any of these fail, the user is redirected to the error page. We use the disableButton property to prevent the user from placing the order until the payer Id is set.

When they click the place-order button, the order is updated with a placed status. Afterwhich the cart is cleared, a successful snack bar is displayed, and the user is redirected to the home page.

@UntilDestroy({ checkProperties: true })
@Component({ selector: 'app-place-order', templateUrl: './place-order.component.html', styleUrls: ['./place-order.component.css']
})
export class PlaceOrderComponent implements OnInit { disableButton = true; constructor( private route: ActivatedRoute, private router: Router, private payments: PaypalPaymentService, private orders: OrderService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.queryParams .pipe( concatMap(params => { const payerId = params['PayerID']; const orderId = this.cart.orderId; return iif( () => payerId.length > 0, this.orders.getOrder(orderId, GetOrderParams.paymentSource) .pipe( concatMap(order => { const paymentSourceId = order.paymentSource?.id || ''; return iif( () => paymentSourceId ? paymentSourceId.length > 0 : false, this.payments.updatePaypalPayment(paymentSourceId, payerId) ); }) ) ); })) .subscribe( () => this.disableButton = false, () => this.router.navigateByUrl('/error') ); } placeOrder() { this.disableButton = true; this.orders.updateOrder({ id: this.cart.orderId, place: true }, [UpdateOrderParams.place]) .subscribe( () => { this.snackBar.open('Your order has been successfully placed.', 'Close', { duration: 3000 }); this.cart.clearCart(); setTimeout(() => this.router.navigateByUrl('/'), 4000); }, () => { this.snackBar.open('There was a problem placing your order.', 'Close', { duration: 8000 }); this.disableButton = false; } ); }
}

Here is the template and its associated styling.

<app-simple-page title="Finalize Order" subtitle="Complete your order" [number]="'6'" icon="shopping_bag" buttonText="PLACE YOUR ORDER" (buttonEvent)="placeOrder()" [buttonDisabled]="disableButton"></app-simple-page>

Here is a screenshot of the page.

App Module

All requests made to Commerce Layer, other than for authentication, need to contain a token. So the moment the app is initialized, a token is fetched from the /oauth/token route on the server and a session is initialized. We’ll use the APP_INITIALIZER token to provide an initialization function in which the token is retrieved. Additionally, we’ll use the HTTP_INTERCEPTORS token to provide the OptionsInterceptor we created earlier. Once all the modules are added the app module file should look something like this.

@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, BrowserAnimationsModule, AuthModule, ProductsModule, CartModule, CheckoutModule, CoreModule ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: OptionsInterceptor, multi: true }, { provide: APP_INITIALIZER, useFactory: (http: HttpClient) => () => http.post<object>( ${environment.apiUrl}/oauth/token, { 'grantType': 'client_credentials' }, { withCredentials: true }), multi: true, deps: [HttpClient] } ], bootstrap: [AppComponent]
})
export class AppModule { }

App Component

We’ll modify the app component template and its styling which you can find here.

<div id="page"> <app-header></app-header> <div id="content"> <router-outlet></router-outlet> </div>
</div>

Conclusion

In this article, we’ve covered how you could create an e-commerce Angular 11 app with Commerce Layer and Paypal. We’ve also touched on how to structure the app and how you could interface with an e-commerce API.

Although this app allows a customer to make a complete order, it is not by any means finished. There is so much you could add to improve it. For one, you may choose to enable item quantity changes in the cart, link cart items to their product pages, optimize the address components, add additional guards for checkout pages like the place-order page, and so on. This is just the starting point.

If you’d like to understand more about the process of making an order from start to finish, you could check out the Commerce Layer guides and API. You can view the code for this project at this repository.

Investment Robust in Retail, Ecommerce Technology

The pandemic has posed obstacles for both brick-and-mortar and online merchants. Among the challenges are labor shortages, inventory problems, delivery conundrums, and the need for more personalization. New providers are addressing those needs, and venture capitalists and other investors are eager to fund them.

Record Investment

According to CB Insights, roughly $31 billion flowed from investors to retail (brick-and-mortar and ecommerce) technology firms in Q2 of 2021, an increase of about 4% over Q1 2021 and more than triple the amount from Q2 2020. A funding frenzy has materialized, culminating in larger funding rounds and higher valuations for the technology start-ups.

Ecommerce-specific enablers were the recipients of the top five deals worth more than $100 million in Q2. Food and grocery delivery companies are major investment targets as they anticipate that people will continue to shop online. Technology that enables mobile commerce also experienced a boost. Tools that optimize warehouse operations, delivery routes, and most anything along the supply chain will continue to be an avenue for investment because of continuing stress in the system.

Top Recipients

CB Insights reports that the following companies valued at $1 billion or higher received the most funding in the second quarter:

  • J&T Express is an Indonesian-based delivery service valued at $7.8 billion. Total funding to date is $1.93 billion.
  • Mollie, a Netherlands-based online payment company for ecommerce businesses, has a valuation of $6.5 billion. Total funding thus far is $934 million.
  • Back Market is a France-based online marketplace for refurbished consumer electronics and appliances, valued at $3.2 billion. Total funding to date is $511 million.
  • ContentSquare, also headquartered in France, provides analytics for ecommerce retailers and brands to track consumers’ behavior. It is valued at $2.8 billion.
  • Meesho, an Indian online marketplace, allows merchants to sell via WhatsApp, Facebook, and Instagram. Its valuation is $2.1 billion. Funding thus far is $491 million.

Platform Technology

Shogun, based in California, provides a platform to help ecommerce merchants design their own responsive storefronts. In June it raised $67.5 million in Series C funding. The money will be used to continue building out its two main products: Page Builder (a drag-and-drop tool for Shopify merchants) and Shogun Frontend (for headless commerce).

Talkshoplive is a live-stream social shopping platform out of California. Earlier this month, it raised $6 million in follow-on funding from VCs and angel investors. Celebrities such as Oprah Winfrey, Paul McCartney, Garth Brooks, and Dolly Parton use the platform, as do small businesses that host their own shopping channels.

Brick-and-mortar

In the brick-and-mortar arena, contactless checkout technology is receiving much funding as stores see this as a way to ease customer concerns about social distancing and to streamline checkouts.

Retailers are deploying cameras and artificial intelligence to track foot traffic, demographics, shopper behavior, and in-store marketing.

Zippin, a California-based provider of cashier-less checkout technology, turned to equity crowdfunding site OurCrowd to raise a second round in May. Zippin’s customers include stores in sports stadiums and airports in the U.S. and Japan.

Mobile

Yalo, headquartered in California, received $50 million in May to fund its conversational commerce products. Yalo already enables commerce on WhatsApp and other messaging apps.

Tapcart, also based in California, offers a platform for developing mobile shopping apps. Shopify merchants, for example, can develop their own mobile apps using Tapcart. The company raised a Series C $50 million in May with Shopify as one of the investors.

Via, in California, received $15 million in Series A funding in June for its suite of mobile ecommerce tools.

Logistics

Brinng, based in Israel, raised $100 million in June to continue developing last-mile delivery tools for dispatch and routing, click and collect, and fleet management.

Locus, headquartered in India, raised $50 million in June to further develop its last mile
logistics platform that provides real-time tracking, warehouse management, and vehicle
allocation solutions.

There Is No Such Thing As A CSS Absolute Unit

When we start learning CSS, we find that CSS units of measurement are categorized as relative or absolute. Absolute units are rooted in physical units, such as pixels, centimeters, and inches. But over the years, all absolute units in CSS have lost their connection to the physical world and have become different kinds of relative units, at least from the perspective of the web.

It’s important to note that there are still significant differences between relative and absolute units. CSS relative units are sized according to other style definitions defined by parent elements or are affected by the size of a parent container. As for absolute units, we will dive in and see how they are affected by other things, such as the screen and the device’s operating system.

Relative units include units such as %, em, rem, viewport units (vw and vh), and more. The most common absolute unit is the pixel (px). Besides that, we have the centimeter unit (cm) and the inches unit (in).

Now, let’s explore why CSS absolute units aren’t so absolute.

CSS Pixels

Pixels have been the most common unit of CSS, dating to the beginning of the web. In the old world of desktop screens, before we had any smartphones, the screen’s pixels were always equivalent to CSS pixels.

In 2007, for example, the most common desktop resolution was 1024×768 pixels. Back then, we would normally give our web pages a fixed width of 1000 pixels to fit the entire page, and the leftover pixels would be saved for the browser’s scrollbar.

Smartphone Screens

Smartphones brought another quiet evolution, starting the era of high-density screens. If we consider an iPhone 12 Pro, whose screen is 1170 pixels wide, we would count every 3 pixels on the device as 1 pixel in the CSS.

When we size in mobile, we measure according to CSS pixels, not according to device pixels. To sum up:

  • CSS pixel are logical pixels.
  • Device pixels are real physical pixels.

Okay, but what about desktop devices? Do they still work with the same old pixel calculation? Let’s talk about that.

Desktop Screens In 2021

High-density screens came to laptops several years later. The 2014 MacBooks got the first “retina” screens (retina being synonymous with high density).

These days, most laptops have a high-density screen.

Let’s consider MacBooks:

  • The 13.3-inch MacBook Pro has a screen that is 2560 pixels wide but that behaves like 1440 pixels. This means that every 1.778 physical pixels act like 1 logical pixel.
  • The 16-inch MacBook Pro has a screen that is 3072 pixels wide but that behaves like 1792 pixels. This means that every 1.714 physical pixels act like 1 logical pixel.

Among PC laptops, I tested two 15.6-inch screens, one with full HD resolution and the other with 4K resolution. The results were interesting:

  • The 15.6-inch full-HD screen is 1920 pixels wide but behaves like 1536 pixels. This means that every 1.25 physical pixels act like 1 logical pixel.
  • The 15.6-inch 4K screen is 3840 pixels wide but behaves, again, like 1536 pixels. This means that every 2.5 physical pixels act like 1 logical pixel.

As you can see, the connection between the real physical (i.e. device) pixels and the CSS (i.e. logical) pixels has almost vanished.

Screens Have Become Denser Over The Years

In the past, if you looked closely at a screen, you could actually see its pixels. When the technology of screens improved, manufacturers started to create higher-density screens.

Recommended reading: What Does A Foldable Web Actually Mean?

Why Do We Calculate Logical Pixels Differently?

Over the years, as screens became denser, we couldn’t fit more content in the same screen size merely because the screen has more pixels.

Think about it for a moment. Consider the Samsung Galaxy S21 Ultra. Its narrower dimension is 1440 physical pixels. We could easily fit it in a regular desktop screen. But if we did, the text would be small to the point of being unreadable. Because of this, we separate physical pixels from logical pixels.

Sizes in CSS (i.e. width and height), then, are calculated according to CSS logical pixels. Of course, we can use physical pixels to load high-density content, such as images and videos, like so:

<img src="image-size-1200px.jpg" width="300" >

OK, CSS pixels aren’t equal to a device’s physical pixels. But we have centimeters and inches. Those are physical units connected to the physical world, right? Let’s check them out.

CSS Inches And CSS Centimeters

Wherever we use physical units like inches and centimeters, we know these are absolute units.

I had a thought that if CSS pixels aren’t equal to device pixels, then maybe it would be a good idea to use physical units such as inches and centimeters on the web. They are absolute units, right?

To be sure, I tested it. I created a box with a width and height of 1 centimeter and gave it a background color of red. I grabbed a real tape measure and got a surprise:

A CSS centimeter isn’t equal to a physical centimeter.

Here I am testing a CSS centimeter unit with a tape measure on a mid-2019 13-inch MacBook:

The result is the same for CSS inches:

A CSS inch isn’t equal to a physical inch.

The same holds true for pica (pc) and millimeter (mm) units. These correspond to a part of either a CSS inch or a CSS centimeter, neither of which is connected to a real inch or a real centimeter.

Why CSS Inches And Centimeters Aren’t Real Inches And Centimeters

Since the 1980s, the PC market has determined a CSS inch to be equivalent to 96 pixels. This calculation of pixels was directly tied to the DPI/PPI (pixels per inch) standard of Microsoft’s Windows operating system for monitors at the time, the most common of which had a standard of 96 DPI.

This meant that 1 CSS inch would always be equivalent to 96 CSS pixels.

As for CSS centimeters, every centimeter is directly calculated from inches, which means that 1 inch is equivalent to 2.54 centimeters. This means that every 1 CSS centimeter will always be equal to 37.7952756 CSS pixels.

In other words: 1cm = 37.7952756px (96px / 2.54).

Because a CSS inch and a CSS centimeter are directly converted from CSS pixels, and because screens have gotten more DPIs over the years, we’ve gotten to the point where these units don’t represent what they’re supposed to represent on screens.

CSS Point Unit

The point (pt) unit is one of the less-recognized units of CSS. As Wikipedia states:

“In typography, the point is the smallest unit of measure. It is used for measuring font size, leading, and other items on a printed page.”

The Wikipedia page shows a ruler with the point scale on the bottom and the inch scale on the top:

Before we get into why this unit isn’t really an absolute unit for the web, let’s go over the basic units of screens and printers.

PPI And DPI

We’ve already mentioned DPI, and you might have heard those terms in the past, but if you’ve never understood what exactly they’re all about, here is quick primer:

  • PPI
    Screens are built from a lot of small light dots, called pixels. To measure the density of pixels, we count the number of pixels that fit 1 inch, called pixels per inch (PPI).
  • DPI
    Printers print color dots. To represent the density of printer dots, we count the number of dots that fit 1 inch of paper, called dots per inch (DPI).

In short, these are two ways to measure the density of visual information that we can fit in 1 inch.

  • PPI: pixels per inch (for screens)
  • DPI: dots per inch (for printers)

It is important to mention that the count of CSS pixels and dots in 1 inch are for both the width and the height. This means that on a screen of 96 PPI, a box with a height and width of 1 inch will have a total size of 9216 pixels (96×96 px = 9216 px).

Here is a visual demonstration of 1 inch with a screen of 10 PPI:

Here are some examples of real calculations of CSS PPI:

CSS Resolution
(Pixels)
CSS PPICSS Inches
(width and height)
96×96961×1
141×1411411×1

“DPI” For Screens

Manufacturers of mobile and desktop devices prefer to express their screen measurements in DPI, not PPI. But don’t let that confuse you: It is always PPI for screens and DPI for printers.

DPI/PPI Standards

To represent all those dots and pixels, we have the point (pt) unit.

But the point unit of CSS derives from the default printer DPI, which, again, was decided in the 1980s and is equal to 72 DPI. This means that 1 inch of CSS always equals 72 points.

  • 1 inch = 72 points
  • 1 point = 1/72nd of 1 inch

Pixels For Web, Dots For Printers

For the web, the DPI unit has no meaning. The web DPI is defined according to a different standard (96 DPI), which we already talked about when we calculated a CSS inch and CSS centimeter. Because of this, there is no reason to use the point unit on the web.

Note: 1 point isn’t equal to (CSS) pixels.

  • 1 point = 1.333 pixels
  • 72 points = 1 inch
  • 72 points = 96 pixels

Printers

In this article, I mainly wanted to demonstrate why there aren’t any absolute units for the web. But what about using them for printers? Is there a reason to use CSS inches or centimeters or point units for printers?

My Printing Test

I ran a small test to check whether the 1980s standard of DPI works correctly on printers. I created two boxes: one with a width and height of 72 points, and the second one with a width and height of 1 inch.

I printed these two boxes on a laser printer that I have in my office. Here is my Codepen for testing points and inches for printers:

Printers are able to print more DPIs, but if we are working at 100% zoom on the printer, then 72 points (or 1 inch) of CSS will equal a real physical inch.

Reminder: This article is more about the connection of absolute units to the web rather than to printers. Of course, the results might change on different types of printers.

Recommended reading: Using HSL Colors In CSS

Trying To Create Accurate Sizes On The Web

If we look at the 16-inch MacBook Pro, which has a ratio of 1.714 physical pixels to every 1 CSS pixel, we can’t accurately predict sizes on the web.

If we try to guess the real device pixel ratio on the 16-inch MacBook Pro using JavaScript’s window.devicePixelRatio, it will return an incorrect ratio of 2, instead of 1.714. (And this is without taking into account the zoom state of the web browser and operating system.)

Why We Need Real Absolute CSS Units

When we want to define a fixed size for a sidebar element, we would use CSS pixels. But if you think about it, CSS pixels have no meaning these days. As we saw above, on most smartphones and desktops, CSS pixels don’t describe device pixels anymore.

Based on this, I believe we need actual physical units for CSS (like a real centimeter or inch unit) because CSS pixels no longer have any true meaning on the web.

It’s worth mentioning that Firefox had implemented an actual physical millimeter unit (mozmm), but removed it in version 59. I don’t know why they removed it. Perhaps it’s because so many things already depend on CSS pixels, such as responsive images and the em and rem units. If we tried to add a new physical measurement, maybe it would cause more problems than it solves.

It seems that web folk have gotten so used to thinking in pixels that, even though the CSS pixel unit has lost its connection to device pixels, we’ll keep using the unit.

And in case you still think that CSS pixels are an excellent unit of measurement, try to explain to a new web developer what this unit is actually measuring.

For now, we don’t have any real way to describe physical sizes in CSS.

So, the CSS pixel is the worst kind of absolute unit — except for all the others.

To Summarize

At the beginning of this article, I said that the absolute CSS units have become like new kinds of relative units. We started with CSS pixels, and we saw the difference between CSS pixels and device pixels.

Then, we found that CSS inches and CSS centimeters are directly converted from CSS pixels and aren’t connected to real inches and centimeters. In the end, we talked about the point unit and, again, about how this unit has no absolute meaning for the web.

Final Words

That’s all. I hope you’ve enjoyed this article and learned from my experience. If you like this post, I would appreciate hearing about it and sharing it.

References

Getting Investors to Buy Your Inventory

When it comes to funding inventory, retail businesses can be creative, using loans, credit cards, supplier terms, and even advances from family. Whatever the method, raising the money can be a challenge.

“Every business needs to fund inventory before it gets the opportunity to earn revenue by selling it, and there is no single [funding] solution for all companies,” said Sean De Clercq, CEO of Kickfurther, a platform that connects investors with emerging brands.

Kickfurther describes these connections as consignment opportunities (co-ops) and separates what it does from other funding or crowdfunding options. Before describing these co-ops, it is worth mentioning some of the ways retailers, direct-to-consumer brands, and even consumer packaged goods companies acquire inventory.

Funding Inventory

Outside of savings, bank loans, credit cards, and individual investors, businesses have alternatives for funding inventory.

For example, very small companies based in the United States can apply to Kiva for interest-free loans of up to $15,000 and take as much as three years to pay.

Indosole, a brand that sells shoes made from recycled tires, is one of many companies that has used Kiva for funding. Kiva partners with institutions to provide interest-free, small business loans. Kiva and its partners also provide non-financial support and resources to help businesses succeed.

Screenshot from Kiva's home page showing IndosoleScreenshot from Kiva's home page showing Indosole

Indosole is a featured business on the Kiva website. Kiva partners with institutions to provide interest-free, small business loans.

Although not-for-profit lenders such as Kiva are rare, they do exist. The Accion Opportunity Fund is another example of affordable financing.

As a retailer grows and its inventory levels increase, other forms of financing become available from services such as Kabbage, BlueVine, Clearco, OnDeck, or similar. These companies offer various forms of term loans and lines of credit that established businesses can access. For the most part, lenders in this category will evaluate a business based on credit history and historical and projected revenue and cash flow.

Crowdfunding Inventory

Another alternative for some businesses is crowdfunding. Crowdfunding platforms are, in a way, the Airbnb or Uber of retail or direct-to-consumer inventory.

Airbnb, for example, doesn’t own rental properties; it simply connects folks who do with others who want a short-term rental. And Uber doesn’t own cars necessarily. Rather it connects folks who do with others who want a ride.

Similarly, crowdfunding platforms connect businesses with backers.

For example, electric bike maker Reevo raised millions on Indiegogo, effectively selling pre-orders to customers who, in some cases, will wait a year or more to receive the bike.

Screenshot of Indiegogo home page showing ReevoScreenshot of Indiegogo home page showing Reevo

Reevo raised funds on the crowdfunding platform Indiegogo.

Consigning Inventory

Perhaps the least discussed option for funding inventory is consignment or even crowdfunding consignment.

“We’re specific to physical-product companies, which gives us the ability to look at things like production and distribution risk. Because we are very specific to that niche, we are also able to get our businesses funded for less,” said Kickfurther’s De Clercq during a live interview with the CommerceCo by Practical Ecommerce community on July 15, 2021.

Kickfurther reviews businesses before allowing them to present their co-ops on the platform. Once approved, funding usually comes quickly. Kickfurther investors can support entrepreneurs and earn a return with consignment opportunities. Fractional investment starts at just $20.

At the time of writing, recently funded co-ops included drink maker Greater Than and apparel brand House of Fluff.

Inventory consignment is not new. Consider the Winmark Corporation. The company owns several nationwide second-hand retail brands, including Play It Again Sports, Plato’s Closet, and Once Upon a Child. Each of these obtains inventory from retail consignment: Individuals drop off second-hand goods, and the stores pay those individuals when the product sells.

Kickfurther applies this idea to ecommerce companies.

Benefit to Investors

In a technical sense, “As investors, we take ownership in [inventory] and consign it to the company to sell on our behalf, and when they sell it, then the underlying cash gets distributed,” said Michael Fox-Rabinovitz, managing partner of Chartwell Capital and the author of “Own a Fraction, Earn a Fortune.” Fox-Rabinovitz invests through Kickfurther and also joined the CommerceCo community during the live interview.

“Realistically, we look at the deal. If we like it, we’ll allocate cash to it…it is really fractional ownership in a sense,” Fox-Rabinovitz said.

But at least some investors don’t necessarily think about the Kickfurther co-ops in this way.

The investors inject money into a company that is both interesting to them and a sound investment. They profit from supporting a growing business that develops exciting new products.

Creating An Accessible Dialog From Scratch

First of all, don’t do this at home. Do not write your own dialogs or a library to do so. There are plenty of them out there already that have been tested, audited, used and reused and you should prefer these ones over your own. a11y-dialog is one of them, but there are more (listed at the end of this article).

Let me take this post as an opportunity to remind you all to be cautious when using dialogs. It is tentalizing to address all design problems with them, especially on mobile, but there often are other ways to overcome design issues. We tend to quickly fall into using dialogs not because they are necessarily the right choice but because they are easy. They set aside screen estate problems by trading them for context switching, which is not always the right trade-off. The point is: consider whether a dialog is the right design pattern before using it.

In this post, we’re going to write a small JavaScript library for authoring accessible dialogs from the very beginning (essentially recreating a11y-dialog). The goal is to understand what goes into it. We’re not going to deal with styling too much, just the JavaScript part. We will use modern JavaScript for sake of simplicity (such as classes and arrow functions), but keep in mind that this code might not work in legacy browsers.

  1. Defining the API
  2. Instantiating the dialog
  3. Showing and hiding
  4. Closing with overlay
  5. Closing with escape
  6. Trapping focus
  7. Maintaining focus
  8. Restoring focus
  9. Giving an accessible name
  10. Handling custom events
  11. Cleaning up
  12. Bring it all together
  13. Wrapping up

Defining The API

First, we want to define how we’re going to use our dialog script. We are going to keep it as simple as possible to begin with. We give it the root HTML element for our dialog, and the instance we get has a .show(..) and a .hide(..) method.

class Dialog { constructor(element) {} show() {} hide() {}
}

Instantiating The Dialog

Let’s say we have the following HTML:

<div id="my-dialog">This will be a dialog.</div>

And we instantiate our dialog like this:

const element = document.querySelector('#my-dialog')
const dialog = new Dialog(element)

There are a few things we need to do under the hood when instantiating it:

  • Hide it so it’s hidden by default (hidden).
  • Mark it as a dialog for assistive technologies (role="dialog").
  • Make the rest of the page inert when open (aria-modal="true").
constructor (element) { // Store a reference to the HTML element on the instance so it can be used // across methods. this.element = element this.element.setAttribute('hidden', true) this.element.setAttribute('role', 'dialog') this.element.setAttribute('aria-modal', true)
}

Note that we could have added these 3 attributes in our initial HTML not to have to add them with JavaScript, but this way it’s out of sight, out of mind. Our script can make sure things will work as they should, regardless of whether we’ve thought about adding all our attributes or not.

Showing And Hiding

We have two methods: one to show the dialog and one to hide it. These methods won’t do much (for now) besides toggling the hidden attribute on the root element. We’re also going to maintain a boolean on the instance to quickly be able to assess if the dialog is shown or not. This will come in handy later.

show() { this.isShown = true this.element.removeAttribute('hidden')
} hide() { this.isShown = false this.element.setAttribute('hidden', true)
}

To avoid the dialog being visible before JavaScript kicks in and hides it by adding the attribute, it might be interesting to add hidden to the dialog directly in the HTML from the get go.

<div id="my-dialog" hidden>This will be a dialog.</div>

Closing With Overlay

Clicking outside of the dialog should close it. There are several ways to do so. One way could be to listen to all click events on the page and filter out those happening within the dialog, but that’s relatively complex to do.

Another approach would be to listen to click events on the overlay (sometimes called “backdrop”). The overlay itself can be as simple as a <div> with some styles.

So when opening the dialog, we need to bind click events on the overlay. We could give it an ID or a certain class to be able to query it, or we could give it a data attribute. I tend to favor these for behavior hooks. Let’s modify our HTML accordingly:

<div id="my-dialog" hidden> <div data-dialog-hide></div> <div>This will be a dialog.</div>
</div>

Now, we can query the elements with the data-dialog-hide attribute within the dialog and give them a click listener that hides the dialog.

constructor (element) { // … rest of the code // Bind our methods so they can be used in event listeners without losing the // reference to the dialog instance this._show = this.show.bind(this) this._hide = this.hide.bind(this) const closers = [...this.element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.addEventListener('click', this._hide))
}

The nice thing about having something quite generic like this is that we can use the same thing for the close button of the dialog as well.

<div id="my-dialog" hidden> <div data-dialog-hide></div> <div> This will be a dialog. <button type="button" data-dialog-hide>Close</button> </div>
</div>

Closing With Escape

Not only should the dialog be hidden when clicking outside of it, but it also should be hidden when pressing Esc. When opening the dialog, we can bind a keyboard listener to the document, and remove it when closing it. This way, it only listens to key presses while the dialog is open instead of all the time.

show() { // … rest of the code // Note: `_handleKeyDown` is the bound method, like we did for `_show`/`_hide` document.addEventListener('keydown', this._handleKeyDown)
} hide() { // … rest of the code // Note: `_handleKeyDown` is the bound method, like we did for `_show`/`_hide` document.removeEventListener('keydown', this._handleKeyDown)
} handleKeyDown(event) { if (event.key === 'Escape') this.hide()
}

Trapping Focus

Now that’s the good stuff. Trapping the focus within the dialog is kind of at the essence of the whole thing, and has to be the most complicated part (although probably not as complicated as you might think).

The idea is pretty simple: when the dialog is open, we listen for Tab presses. If pressing Tab on the last focusable element of the dialog, we programmatically move the focus to the first. If pressing Shift + Tab on the first focusable element of the dialog, we move it to the last one.

The function might look like this:

function trapTabKey(node, event) { const focusableChildren = getFocusableChildren(node) const focusedItemIndex = focusableChildren.indexOf(document.activeElement) const lastIndex = focusableChildren.length - 1 const withShift = event.shiftKey if (withShift && focusedItemIndex === 0) { focusableChildren[lastIndex].focus() event.preventDefault() } else if (!withShift && focusedItemIndex === lastIndex) { focusableChildren[0].focus() event.preventDefault() }
}

The next thing we need to figure out is how to get all the focusable elements of the dialog (getFocusableChildren). We need to query all the elements that can theoretically be focusable, and then we need to make sure they effectively are.

The first part can be done with focusable-selectors. It’s a teeny tiny package I wrote which provides this array of selectors:

module.exports = [ 'a[href]:not([tabindex^="-"])', 'area[href]:not([tabindex^="-"])', 'input:not([type="hidden"]):not([type="radio"]):not([disabled]):not([tabindex^="-"])', 'input[type="radio"]:not([disabled]):not([tabindex^="-"]):checked', 'select:not([disabled]):not([tabindex^="-"])', 'textarea:not([disabled]):not([tabindex^="-"])', 'button:not([disabled]):not([tabindex^="-"])', 'iframe:not([tabindex^="-"])', 'audio[controls]:not([tabindex^="-"])', 'video[controls]:not([tabindex^="-"])', '[contenteditable]:not([tabindex^="-"])', '[tabindex]:not([tabindex^="-"])',
]

And this is enough to get you 99% there. We can use these selectors to find all focusable elements, and then we can check every one of them to make sure it is actually visible on screen (and not hidden or something).

import focusableSelectors from 'focusable-selectors' function isVisible(element) { return element => element.offsetWidth || element.offsetHeight || element.getClientRects().length
} function getFocusableChildren(root) { const elements = [...root.querySelectorAll(focusableSelectors.join(','))] return elements.filter(isVisible)
}

We can now update our handleKeyDown method:

handleKeyDown(event) { if (event.key === 'Escape') this.hide() else if (event.key === 'Tab') trapTabKey(this.element, event)
}

Maintaining Focus

One thing that’s often overlooked when creating accessible dialogs is making sure the focus remains within the dialog even after the page has lost focus. Think of it this way: what happens if once the dialog is open? We focus the URL bar of the browser, and then start tabbing again. Our focus trap is not going to work, since it only preserves the focus within the dialog when it’s inside the dialog to begin with.

To fix that problem, we can bind a focus listener to the <body> element when the dialog is shown, and move the focus to the first focusable element within the dialog.

show () { // … rest of the code // Note: `_maintainFocus` is the bound method, like we did for `_show`/`_hide` document.body.addEventListener('focus', this._maintainFocus, true)
} hide () { // … rest of the code // Note: `_maintainFocus` is the bound method, like we did for `_show`/`_hide` document.body.removeEventListener('focus', this._maintainFocus, true)
} maintainFocus(event) { const isInDialog = event.target.closest('[aria-modal="true"]') if (!isInDialog) this.moveFocusIn()
} moveFocusIn () { const target = this.element.querySelector('[autofocus]') || getFocusableChildren(this.element)[0] if (target) target.focus()
}

Which element to focus when opening the dialog is not enforced, and it could depend on which type of content the dialog displays. Generally speaking, there are a couple of options:

  • Focus the first element.
    This is what we do here, since it is made easier by the fact that we already have a getFocusableChildren function.
  • Focus the close button.
    This is also a good solution, especially if the button is absolutely positioned relatively to the dialog. We can conveniently make this happen by placing our close button as the first element of our dialog. If the close button lives in the flow of the dialog content, at the very end, it could be a problem if the dialog has a lot of content (and therefore is scrollable), as it would scroll the content to the end on open.
  • Focus the dialog itself.
    This is not very common among dialog libraries, but it should also work (although it would require adding tabindex="-1" to it so that’s possible since a <div> element is not focusable by default).

Note that we check whether there is an element with the autofocus HTML attribute within the dialog, in which case we would move the focus to it instead of the first item.

Restoring Focus

We’ve managed to successfully trap the focus within the dialog, but we forgot to move the focus inside the dialog once it opens. Similarly, we need to restore the focus back to the element that had it before the dialog was open.

When showing the dialog, we can start by keeping a reference to the element that has the focus (document.activeElement). Most of the time, this will be the button that was interacted with to open the dialog, but in rare cases where a dialog is opened programmatically, it could be something else.

show() { this.previouslyFocused = document.activeElement // … rest of the code this.moveFocusIn()
}

When hiding the dialog, we can move the focus back to that element. We guard it with a condition to avoid a JavaScript error if the element somehow no longer exists (or if it was a SVG):

hide() { // … rest of the code if (this.previouslyFocused && this.previouslyFocused.focus) { this.previouslyFocused.focus() }
}

Giving An Accessible Name

It is important our dialog has an accessible name, which is how it will be listed in the accessibility tree. There are a couple of ways to address it, one of which is to define a name in the aria-label attribute, but aria-label has issues.

Another way is to have a title within our dialog (whether hidden or not), and to associate our dialog to it with the aria-labelledby attribute. It might look like this:

<div id="my-dialog" hidden aria-labelledby="my-dialog-title"> <div data-dialog-hide></div> <div> <h1 id="my-dialog-title">My dialog title</h1> This will be a dialog. <button type="button" data-dialog-hide>Close</button> </div>
</div>

I guess we could make our script apply this attribute dynamically based on the presence of the title and whatnot, but I’d say this is just as easily solved by authoring proper HTML, to begin with. No need to add JavaScript for that.

Handling Custom Events

What if we want to react to the dialog being open? Or closed? There is currently no way to do it, but adding a small event system should not be too difficult. We need a function to register events (let’s call it .on(..)), and a function to unregister them (.off(..)).

class Dialog { constructor(element) { this.events = { show: [], hide: [] } } on(type, fn) { this.events[type].push(fn) } off(type, fn) { const index = this.events[type].indexOf(fn) if (index > -1) this.events[type].splice(index, 1) }
}

Then when showing and hiding the method, we’ll call all functions that have been registered for that particular event.

class Dialog { show() { // … rest of the code this.events.show.forEach(event => event()) } hide() { // … rest of the code this.events.hide.forEach(event => event()) }
}

Cleaning Up

We might want to provide a method to clean up a dialog in case we’re done using it. It would be responsible for unregistering event listeners so they don’t last more than they should.

class Dialog { destroy() { const closers = [...this.element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.removeEventListener('click', this._hide)) this.events.show.forEach(event => this.off('show', event)) this.events.hide.forEach(event => this.off('hide', event)) }
}

Bringing It All Together

import focusableSelectors from 'focusable-selectors' class Dialog { constructor(element) { this.element = element this.events = { show: [], hide: [] } this._show = this.show.bind(this) this._hide = this.hide.bind(this) this._maintainFocus = this.maintainFocus.bind(this) this._handleKeyDown = this.handleKeyDown.bind(this) element.setAttribute('hidden', true) element.setAttribute('role', 'dialog') element.setAttribute('aria-modal', true) const closers = [...element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.addEventListener('click', this._hide)) } show() { this.isShown = true this.previouslyFocused = document.activeElement this.element.removeAttribute('hidden') this.moveFocusIn() document.addEventListener('keydown', this._handleKeyDown) document.body.addEventListener('focus', this._maintainFocus, true) this.events.show.forEach(event => event()) } hide() { if (this.previouslyFocused && this.previouslyFocused.focus) { this.previouslyFocused.focus() } this.isShown = false this.element.setAttribute('hidden', true) document.removeEventListener('keydown', this._handleKeyDown) document.body.removeEventListener('focus', this._maintainFocus, true) this.events.hide.forEach(event => event()) } destroy() { const closers = [...this.element.querySelectorAll('[data-dialog-hide]')] closers.forEach(closer => closer.removeEventListener('click', this._hide)) this.events.show.forEach(event => this.off('show', event)) this.events.hide.forEach(event => this.off('hide', event)) } on(type, fn) { this.events[type].push(fn) } off(type, fn) { const index = this.events[type].indexOf(fn) if (index > -1) this.events[type].splice(index, 1) } handleKeyDown(event) { if (event.key === 'Escape') this.hide() else if (event.key === 'Tab') trapTabKey(this.element, event) } moveFocusIn() { const target = this.element.querySelector('[autofocus]') || getFocusableChildren(this.element)[0] if (target) target.focus() } maintainFocus(event) { const isInDialog = event.target.closest('[aria-modal="true"]') if (!isInDialog) this.moveFocusIn() }
} function trapTabKey(node, event) { const focusableChildren = getFocusableChildren(node) const focusedItemIndex = focusableChildren.indexOf(document.activeElement) const lastIndex = focusableChildren.length - 1 const withShift = event.shiftKey if (withShift && focusedItemIndex === 0) { focusableChildren[lastIndex].focus() event.preventDefault() } else if (!withShift && focusedItemIndex === lastIndex) { focusableChildren[0].focus() event.preventDefault() }
} function isVisible(element) { return element => element.offsetWidth || element.offsetHeight || element.getClientRects().length
} function getFocusableChildren(root) { const elements = [...root.querySelectorAll(focusableSelectors.join(','))] return elements.filter(isVisible)
}

Wrapping Up

That was quite something, but we eventually got there! Once again, I would advise against rolling out your own dialog library since it’s not the most straightforward and errors could be highly problematic for assistive technology users. But at least now you know how it works under the hood!

If you need to use dialogs in your project, consider using one of the following solutions (kind reminder that we have our comprehensive list of accessible components as well):

Here are more things that could be added but were not for sake of simplicity:

4 Favorite Blogs from Ecommerce Companies

Publishing a blog on an ecommerce site builds community, creates trust, and gives consumers a reason to return. Sales would likely follow.

But online merchants with long-term, quality blogs are rare. What follows are four of my favorites.

Mr Porter

Mr Porter is an online retailer of men’s fashion items, founded in 2011. The company sells roughly 300 designer brands. A companion site, Net-A-Porter, sells female fashion goods.

In 2014 Mr Porter launched “The Journal,” a weekly web magazine with a six-times-per-year print edition. In March 2020, the web version began posting daily, presumably due to its global popularity — The Journal claims 2.5 million monthly visitors.

Home page of The JournalHome page of The Journal

Mr Porter’s “The Journal” launched in 2014. It now claims 2.5 million monthly visitors worldwide.

The Journal’s posts are short and easy to absorb. The categories — Latest, Fashion, Grooming, Watches, Travel, Lifestyle — cover mostly fashion and accessories with additional topics to keep readers’ interest. All are related to Mr Porter’s products. For example, the products in the post “Every Polo Shirt You’ll Need This Summer” are on Mr Porter’s digital shelves. The items are linked in the body content.

Goop

Actress Gwyneth Paltrow founded Goop in 2008 as a weekly email newsletter providing health and wellness advice. She soon added a website followed by ecommerce. Article content remains Goop’s focus. The company now hosts a wellness summit, a celebrity cruise, a podcast, and a Netflix series.

Goop’s articles are accessible from the home page. Categories are Beauty, Food & Home, Style, Travel, and Wellness. Good PhD, a sixth category, is produced by the company’s science and research team to “compile the most significant studies and information on an array of health topics, conditions, and diseases.”

Home page of Goop.Home page of Goop.

Goop’s articles are accessible from the home page. Categories are Beauty, Food & Home, Style, Travel, Wellness, and Good PhD.

Similar to The Journal from Mr Porter, Goop’s articles link to its products. For example, “6 Reasons to Make the Shift to Mineral Sunscreen” links to six products.

Goop’s newsletter summarizes the best blog posts as well as items for purchase.

Dermstore

Dermstore was founded in 1999 by a California-based dermatologist. It’s now a popular ecommerce site for skin care products, with over 350 brands. “The Dermstore Blog” addresses mostly skin care topics from experts. Categories are Skin Care, Anti-aging, Top Product Picks, Doctors Office, Hair Care Tips, and Dermatologist Reviewed. Dermstore’s content team consists of in-house and freelance writers, often including physicians and licensed aestheticians.

Home page of The Dermstore BlogHome page of The Dermstore Blog

“The Dermstore Blog” addresses skin care topics from experts. Categories are Skin Care, Anti-aging, Top Product Picks, Doctors Office, Hair Care Tips, and Dermatologist Reviewed.

Posts on The Dermstore Blog include “Buy Now” buttons and other links to the company’s products.

If your company can’t afford expert contributors, invite them to leave their opinion on a topic and then include it in a post as a quote. I once wrote for Dermstore and landed free expert interviews and quotes through the website Help a Reporter.

Motherly

Motherly started in 2015 as a blog focused on motherhood. The company has since added two podcasts, “The Motherly Podcast” and “Becoming Mama: A Pregnancy and Birth Podcast.” Motherly says its combined audience reaches 30 million users. The company raised $5.4 million in Series A funding in 2020 and then launched a store.

Motherly remains content-centric via in-house writers and submissions from mothers and practitioners such as pediatricians, nurses, and midwives. Categories are Pregnancy, Parenting, and Life. Posts include links to (paid) classes and store products, such as cribs, jogging strollers, and car seats.

Home page of MotherlyHome page of Motherly

Motherly started in 2015 as a blog focused on motherhood. In 2020 it launched a store. The site remains content-centric — categories are Pregnancy, Parenting, and Life.

CEO Jill Koziol said that Motherly’s brand is “woman-centered — not baby-centered — and expert-driven.” It then used that brand to sell products.

Building Trust

Trust is essential for building a sustainable brand. Content, done well, is an excellent way to build trust with customers and prospects. Each company above features expert authors on important topics multiple times per week. Consumers will enjoy reading the posts even if they don’t purchase a product.

But there’s nothing wrong with including product mentions, as the companies have demonstrated. The trick is to give away worthwhile information and include product mentions that support the topic.

Global vs. Local Styling In Next.js

I have had a great experience using Next.js to manage complex front-end projects. Next.js is opinionated about how to organize JavaScript code, but it doesn’t have built-in opinions about how to organize CSS.

After working within the framework, I have found a series of organizational patterns that I believe both conform to the guiding philosophies of Next.js and exercise best CSS practices. In this article, we’ll build a website (a tea shop!) together to demonstrate these patterns.

Note: You probably will not need prior Next.js experience, although it would be good to have a basic understanding of React and to be open to learning some new CSS techniques.

Writing “Old-Fashioned” CSS

When first looking into Next.js, we may be tempted to consider using some kind of CSS-in-JS library. Though there may be benefits depending on the project, CSS-in-JS introduces many technical considerations. It requires using a new external library, which adds to the bundle size. CSS-in-JS can also have a performance impact by causing additional renders and dependencies on the global state.

Recommended reading: “The Unseen Performance Costs Of Modern CSS-in-JS Libraries In React Apps)” by Aggelos Arvanitakis

Furthermore, the whole point of using a library like Next.js is to statically render assets whenever possible, so it doesn’t make so much sense to write JS that needs to be run in the browser to generate CSS.

There are a couple of questions we have to consider when organizing style within Next.js:

How can we fit within the conventions/best practices of the framework?

How can we balance “global” styling concerns (fonts, colors, main layouts, and so on) with “local” ones (styles regarding individual components)?

The answer I have come up with for the first question is to simply write good ol’ fashioned CSS. Not only does Next.js support doing so with no additional setup; it also yields results that are performant and static.

To solve the second problem, I take an approach that can be summarized in four pieces:

  1. Design tokens
  2. Global styles
  3. Utility classes
  4. Component styles

I’m indebted to Andy Bell’s idea of CUBE CSS (“Composition, Utility, Block, Exception”) here. If you haven’t heard of this organizational principle before, I recommended checking out its official site or feature on the Smashing Podcast. One of the principles we will take from CUBE CSS is the idea that we should embrace rather than fear the CSS cascade. Let’s learn these techniques by applying them to a website project.

Getting Started

We’ll be building a tea store because, well, tea is tasty. We’ll start by running yarn create next-app to make a new Next.js project. Then, we’ll remove everything in the styles/ directory (it’s all sample code).

Note: If you want to follow along with the finished project, you can check it out here.

Design Tokens

In pretty much any CSS setup, there’s a clear benefit to storing all globally shared values in variables. If a client asks for a color to change, implementing the change is a one-liner rather than a massive find-and-replace mess. Consequently, a key part of our Next.js CSS setup will be storing all site-wide values as design tokens.

We’ll use inbuilt CSS Custom Properties to store these tokens. (If you’re not familiar with this syntax, you can check out “A Strategy Guide To CSS Custom Properties”.) I should mention that (in some projects) I’ve opted to use SASS/SCSS variables for this purpose. I haven’t found any real advantage, so I usually only include SASS in a project if I find I need other SASS features (mix-ins, iteration, importing files, and so on). CSS custom properties, by contrast, also work with the cascade and can be changed over time rather than statically compiling. So, for today, let’s stick with plain CSS.

In our styles/ directory, let’s make a new design_tokens.css file:

:root { --green: #3FE79E; --dark: #0F0235; --off-white: #F5F5F3; --space-sm: 0.5rem; --space-md: 1rem; --space-lg: 1.5rem; --font-size-sm: 0.5rem; --font-size-md: 1rem; --font-size-lg: 2rem;
}

Of course, this list can and will grow over time. Once we add this file, we need to hop over to our pages/_app.jsx file, which is the main layout for all our pages, and add:

import '../styles/design_tokens.css'

I like to think of design tokens as the glue that maintains consistency across the project. We will reference these variables on a global scale, as well as within individual components, ensuring a unified design language.

Global Styles

Next up, let’s add a page to our website! Let’s hop into the pages/index.jsx file (this is our homepage). We’ll delete all the boilerplate and add something like:

export default function Home() { return <main> <h1>Soothing Teas</h1> <p>Welcome to our wonderful tea shop.</p> <p>We have been open since 1987 and serve customers with hand-picked oolong teas.</p> </main>
}

Unfortunately, it will look quite plain, so let’s set some global styles for basic elements, e.g. <h1> tags. (I like to think of these styles as “reasonable global defaults”.) We may override them in specific cases, but they’re a good guess as to what we will want if we don’t.

I’ll put this in the styles/globals.css file (which comes by default from Next.js):

*,
*::before,
*::after { box-sizing: border-box;
} body { color: var(--off-white); background-color: var(--dark);
} h1 { color: var(--green); font-size: var(--font-size-lg);
} p { font-size: var(--font-size-md);
} p, article, section { line-height: 1.5;
} :focus { outline: 0.15rem dashed var(--off-white); outline-offset: 0.25rem;
}
main:focus { outline: none;
} img { max-width: 100%;
}

Of course, this version is fairly basic, but my globals.css file doesn’t usually end up actually needing to get too large. Here, I style basic HTML elements (headings, body, links, and so on). There is no need to wrap these elements in React components or to constantly add classes just to provide basic style.

I also include any resets of default browser styles. Occasionally, I will have some site-wide layout style to provide a “sticky footer”, for example, but they only belong here if all pages share the same layout. Otherwise, it will need to be scoped inside individual components.

I always include some kind of :focus styling to clearly indicate interactive elements for keyboard users when focused. It’s best to make it an integral part of the site’s design DNA!

Now, our website is starting to shape up:

Utility Classes

One area where our homepage could certainly improve is that the text currently always extends to the sides of the screen, so let’s limit its width. We need this layout on this page, but I imagine that we might need it on other pages, too. This is a great use case for a utility class!

I try to use utility classes sparingly rather than as a replacement for just writing CSS. My personal criteria for when it makes sense to add one to a project are:

  1. I need it repeatedly;
  2. It does one thing well;
  3. It applies across a range of different components or pages.

I think this case meets all three criteria, so let’s make a new CSS file styles/utilities.css and add:

.lockup { max-width: 90ch; margin: 0 auto;
}

Then let’s add import '../styles/utilities.css' to our pages/_app.jsx. Finally, let’s change the <main> tag in our pages/index.jsx to <main className="lockup">.

Now, our page is coming together even more. Because we used the max-width property, we don’t need any media queries to make our layout mobile responsive. And, because we used the ch measurement unit — which equates to about the width of one character — our sizing is dynamic to the user’s browser font size.

As our website grows, we can continue adding more utility classes. I take a fairly utilitarian approach here: If I’m working and find I need another class for a color or something, I add it. I don’t add every possible class under the sun — it would bloat the CSS file size and make my code confusing. Sometimes, in larger projects, I like to break things up into a styles/utilities/ directory with a few different files; it’s up to the needs of the project.

We can think of utility classes as our toolkit of common, repeated styling commands that are shared globally. They help prevent us from constantly rewriting the same CSS between different components.

Component Styles

We’ve finished our homepage for the moment, but we still need to build a piece of our website: the online store. Our goal here will be to display a card grid of all the teas we want to sell, so we’ll need to add some components to our site.

Let’s start off by adding a new page at pages/shop.jsx:

export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main>
}

Then, we’ll need some teas to display. We’ll include a name, description, and image (in the public/ directory) for each tea:

const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ...
]

Note: This isn’t an article about data fetching, so we took the easy route and defined an array at the beginning of the file.

Next, we’ll need to define a component to display our teas. Let’s start by making a components/ directory (Next.js doesn’t make this by default). Then, let’s add a components/TeaList directory. For any component that ends up needing more than one file, I usually put all the related files inside a folder. Doing so prevents our components/ folder from getting unnavigable.

Now, let’s add our components/TeaList/TeaList.jsx file:

import TeaListItem from './TeaListItem' const TeaList = (props) => { const { teas } = props return <ul role="list"> {teas.map(tea => <TeaListItem tea={tea} key={tea.name} />)} </ul>
} export default TeaList

The purpose of this component is to iterate over our teas and to show a list item for each one, so now let’s define our components/TeaList/TeaListItem.jsx component:

import Image from 'next/image' const TeaListItem = (props) => { const { tea } = props return <li> <div> <Image src={tea.image} alt="" objectFit="cover" objectPosition="center" layout="fill" /> </div> <div> <h2>{tea.name}</h2> <p>{tea.description}</p> </div> </li>
} export default TeaListItem

Note that we’re using Next.js’s built-in image component. I set the alt attribute to an empty string because the images are purely decorative in this case; we want to avoid bogging screen reader users down with long image descriptions here.

Finally, let’s make a components/TeaList/index.js file, so that our components are easy to import externally:

import TeaList from './TeaList'
import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList

And then, let’s plug it all together by adding import TeaList from ../components/TeaList and a <TeaList teas={teas} /> element to our Shop page. Now, our teas will show up in a list, but it won’t be so pretty.

Colocating Style With Components Through CSS Modules

Let’s start off by styling our cards (the TeaListLitem component). Now, for the first time in our project, we’re going to want to add style that is specific to just one component. Let’s create a new file components/TeaList/TeaListItem.module.css.

You may be wondering about the module in the file extension. This is a CSS Module. Next.js supports CSS modules and includes some good documentation on them. When we write a class name from a CSS module such as .TeaListItem, it will automatically get transformed into something more like . TeaListItem_TeaListItem__TFOk_ with a bunch of extra characters tacked on. Consequently, we can use any class name we want without being concerned that it will conflict with other class names elsewhere in our site.

Another advantage to CSS modules is performance. Next.js includes a dynamic import feature. next/dynamic lets us lazy load components so that their code only gets loaded when needed, rather than adding to the whole bundle size. If we import the necessary local styles into individual components, then users can also lazy load the CSS for dynamically imported components. For large projects, we may choose to lazy load significant chunks of our code and only to load the most necessary JS/CSS upfront. As a result, I usually end up making a new CSS Module file for every new component that needs local styling.

Let’s start by adding some initial styles to our file:

.TeaListItem { display: flex; flex-direction: column; gap: var(--space-sm); background-color: var(--color, var(--off-white)); color: var(--dark); border-radius: 3px; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
}

Then, we can import style from ./TeaListItem.module.css in our TeaListitem component. The style variable comes in like a JavaScript object, so we can access this class-like style.TeaListItem.

Note: Our class name doesn’t need to be capitalized. I’ve found that a convention of capitalized class names inside of modules (and lowercase ones outside) differentiates local vs. global class names visually.

So, let’s take our new local class and assign it to the <li> in our TeaListItem component:

<li className={style.TeaListComponent}>

You may be wondering about the background color line (i.e. var(--color, var(--off-white));). What this snippet means is that by default the background will be our --off-white value. But, if we set a --color custom property on a card, it will override and choose that value instead.

At first, we’ll want all our cards to be --off-white, but we may want to change the value for individual cards later. This works very similarly to props in React. We can set a default value but create a slot where we can choose other values in specific circumstances. So, I encourage us to think of CSS custom properties like CSS’s version of props.

The style still won’t look great because we want to make sure that the images stay within their containers. Next.js’s Image component with the layout="fill" prop gets position: absolute; from the framework, so we can limit the size by putting in a container with position: relative;.

Let’s add a new class to our TeaListItem.module.css:

.ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden;
}

And then let’s add className={styles.ImageContainer} on the <div> that contains our <Image>. I use relatively “simple” names such as ImageContainer because we’re inside a CSS module, so we don’t have to worry about conflicting with the outside style.

Finally, we want to add a bit of padding on the sides of the text, so let’s add one last class and rely on the spacing variables we set up as design tokens:

.Title { padding-left: var(--space-sm); padding-right: var(--space-sm);
}

We can add this class to the <div> that contains our name and description. Now, our cards don’t look so bad:

Combining Global And Local Style

Next, we want to have our cards show in a grid layout. In this case, we’re just at the border between local and global styles. We could certainly code our layout directly on the TeaList component. But, I could also imagine that having a utility class that turns a list into a grid layout could be useful in several other places.

Let’s take the global approach here and add a new utility class in our styles/utilities.css:

.grid { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--min-item-width, 30ch), 1fr)); gap: var(--space-md);
}

Now, we can add the .grid class on any list, and we’ll get an automatically responsive grid layout. We can also change the --min-item-width custom property (by default 30ch) to change the minimum width of each element.

Note: Remember to think of custom properties like props! If this syntax looks unfamiliar, you can check out “Intrinsically Responsive CSS Grid With minmax() And min()” by Chris Coyier.

As we’ve written this style globally, it doesn’t require any fanciness to add className="grid" onto our TeaList component. But, let’s say we want to couple this global style with some additional local store. For example, we want to bring a bit more of the “tea aesthetic” in and to make every other card have a green background. All we’d need to do is make a new components/TeaList/TeaList.module.css file:

.TeaList > :nth-child(even) { --color: var(--green);
}

Remember how we made a --color custom property on our TeaListItem component? Well, now we can set it under specific circumstances. Note that we can still use child selectors within CSS modules, and it doesn’t matter that we’re selecting an element that is styled inside a different module. So, we can also use our local component styles to affect child components. This is a feature rather than a bug, as it allows us to take advantage of the CSS cascade! If we tried to replicate this effect some other way, we’d likely end up with some kind of JavaScript soup rather than three lines of CSS.

Then, how can we keep the global .grid class on our TeaList component while also adding the local .TeaList class? This is where the syntax can get a bit funky because we have to access our .TeaList class out of the CSS module by doing something like style.TeaList.

One option would be to use string interpolation to get something like:

<ul role="list" className={`${style.TeaList} grid`}>

In this small case, this might be good enough. If we’re mixing-and-matching more classes, I find that this syntax makes my brain explode a bit, so I will sometimes opt to use the classnames library. In this case, we end up with a more sensible-looking list:

<ul role="list" className={classnames(style.TeaList, "grid")}>

Now, we’ve finished up our Shop page, and we’ve made our TeaList component take advantage of both global and local styles.

A Balancing Act

We’ve now built our tea shop using only plain CSS to handle the styling. You may have noticed that we did not have to spend ages dealing with custom Webpack setups, installing external libraries, and so on. That’s because of the patterns that we’ve used work with Next.js out of the box. Furthermore, they encourage best CSS practices and fit naturally into the Next.js framework architecture.

Our CSS organization consisted of four key pieces:

  1. Design tokens,
  2. Global styles,
  3. Utility classes,
  4. Component styles.

As we continue building our site, our list of design tokens and utility classes will grow. Any styling that doesn’t make sense to add as a utility class, we can add into component styles using CSS modules. As a result, we can find a continuous balance between local and global styling concerns. We can also generate performant, intuitive CSS code that grows naturally alongside our Next.js site.

The Full-funnel Product Detail Page

Once found only at the bottom of a sales funnel, the ecommerce product detail page is evolving to address the entire buyer journey, from awareness to sale.

Consider a free, basic theme on Shopify called “Simple,” Its product pages are sales-focused, with eight sections:

  • Product name,
  • Price,
  • Description
  • Product options,
  • Images,
  • Two call-to-action buttons,
  • Social media buttons,
  • Product recommendations.

These sections are fundamental to conversions except, perhaps, the social media buttons and product recommendations. The rest — product name, price, description, images, and add-to-cart buttons — are more or less essential to completing the action.

Screenshot of Simple's product detail pageScreenshot of Simple's product detail page

The product detail page on Simple, a Shopify theme, provides bottom-of-funnel content to prompt a purchase.

Interestingly, this list of product content sections is similar to a typical Amazon product detail page years ago. A screen capture from the Internet Archive shows that the product page in 2007 for the “George Forman Grill with Bun Warmer” had the basics — name, price, description, picture, and add to cart button — plus ratings, tags, and a customer discussion section.

The page was aimed at the bottom of the funnel — the end of the buyer’s journey.

Screenshot from 2007 of George Forman Grill on Amazon.Screenshot from 2007 of George Forman Grill on Amazon.

Amazon’s product detail pages in 2007 contained content for end-of-journey actions.

Funnels and Journeys

Sales funnels and buyer’s journeys are meant to describe how prospects become customers. These models help us think about the required emotional, psychological, and physical steps.

Historically, such models have focused on the action steps. As an example, consider the popular AIDAR model, a hierarchical funnel with five stages:

  • Attention,
  • Interest,
  • Desire,
  • Action,
  • Retention.

A product detail page with just the basic info described above fits best near the bottom of the model in the “Action” or “Retention” step.

A diagram showing the five steps of the AIDAR model. A diagram showing the five steps of the AIDAR model.

The AIDAR model is an example of a marketing or sales funnel.

Full-funnel

Product detail pages in 2021 have evolved from closing the sale to serving the entire journey — a full-funnel page.

Take the product detail page on Amazon for Wow Apple Cider Vinegar Shampoo from July 22, 2021. On marketplaces such as Amazon, product pages have to attract attention in search, generate interest, and develop desire so that a shopper will make a purchase.

Screenshot of the Wow Apple Cider Vinegar Shampoo page on AmazonScreenshot of the Wow Apple Cider Vinegar Shampoo page on Amazon

The Wow Apple Cider Vinegar Shampoo page on Amazon has to attract attention in search, generate interest, and develop desire so that a shopper will make a purchase.

The product name is long.

“WOW Apple Cider Vinegar Shampoo – Reduce Dandruff, Frizz, Split Ends, For Hair Loss – Clean Scalp & Boost Gloss, Shine – Paraben, Sulfate Free – All Hair Types, Adults & Children – 500 mL”

This title is not likely aimed at the bottom of the funnel but rather near the top. WOW seems to be optimizing it for search on Amazon.

Moreover, the “About this item” section adds 200 more words explaining what the shampoo does. The description here may be aimed at the middle of the funnel to create a desire for the product.

Screenshot from Amazon of the extended description for Wow Apple Cider Vinegar ShampooScreenshot from Amazon of the extended description for Wow Apple Cider Vinegar Shampoo

On Amazon, the Wow Apple Cider Vinegar Shampoo page contains content aimed at consumers earlier in their shopping journey.

And the list goes on. The page includes three product videos and six product-related photos. There’s a 1,097-word product description (longer than most articles on Practical Ecommerce), two charts, two bulleted lists, and 20 more photos. And there are sections for specifications, ingredients, usage, reviews, and user videos.

The WOW shampoo product page had three videos, numerous photos, and more words than most articles on Practical Ecommerce.

Evolution

Several factors are likely responsible for the differences on the Amazon product detail pages for the George Forman grill in 2007 versus the WOW shampoo in 2021.

Competition. Worldwide retail ecommerce is booming, with sales nearly tripling in the six years from 2014 to 2020 when it hit $4.28 trillion, according to Statista. And B2B ecommerce is even bigger. Global B2B ecommerce revenue reached more than $6.6 trillion in 2020, according to Grand View Research.

Thus ecommerce is competitive. Product information can provide an edge.

In a discussion about search engine optimization and link building, James Wirth, senior director of strategy and growth marketing at Citation Labs, an agency, pointed out that his firm builds links to ecommerce product pages. That process requires link-worthy content.

Nowhere is ecommerce competition as intense as on Amazon, where a product page is not the culmination of a buyer’s journey but likely the first stop. That page is the only opportunity a brand or retailer has to grab attention, peak interest, and develop desire.

Covid-19. The pandemic intensified ecommerce competition, forced more businesses to compete online, and impacted buyer behavior.

In December 2020, Salsify, a maker of product information management software, surveyed 1,800 U.S. shoppers. About 40% of respondents had relied on ecommerce in some form since the outset of the pandemic. Roughly 24% only shopped online. And another 37% limited brick-and-mortar shopping to trusted stores.

Thus shoppers who might otherwise have gone to a local store were pursuing digital aisles. Those shoppers wanted product information, including top-of-funnel content.

Culture. Finally, shopping culture is changing.

Buyers are not just interested in price. They want to know if a product is good not just in quality, but also good for the world. Consumers increasing are interested in sustainability, social responsibility, and the well-being of workers.

This could be why shoppers might prefer a minority-owned company or why WOW’s shampoo description states that its products are vegan and cruelty-free. All of this needs to be communicated. Product pages might be the place to do it.

Gatsby Serverless Functions And The International Space Station

Gatsby recently announced the launch of Functions which opens up a new dimension of possibilities — and I for one couldn’t be more excited! With Gatsby now providing Serverless Functions on Gatsby Cloud (and Netlify also providing support via @netlify/plugin-gatsby), the framework that was once misunderstood to be “just for blogs” is now more than ever, (in my opinion) the most exciting technology provider in the Jamstack space.

The demo in this article is the result of a recent project I worked on where I needed to plot geographical locations around a 3D globe and I thought it might be fun to see if it were possible to use the same technique using off-planet locations. Spoiler alert: It’s possible! Here’s a sneak peek of what I’ll be talking about in this post, or if you prefer to jump ahead, the finished code can be found here.

Getting Started

With Gatsby Functions, you can create more dynamic applications using techniques typically associated with client-side applications by adding an api directory to your project and exporting a function, e.g.

|-- src |-- api -- some-function.js |-- pages
// src/api/some-function.js
export default function handler(req, res) { res.status(200).json({ hello: `world` })
}

If you already have a Gatsby project setup, great! but do make sure you’ve upgraded Gatsby to at least version v3.7

npm install gatsby@lastest --save

If not, then feel free to clone my absolute bare-bones Gatsby starter repo: mr-minimum.

Before I can start using Gatsby Functions to track the International Space Station, I first need to create a globe for it to orbit.

Step 1: Building The 3D Interactive Globe

I start by setting up a 3D interactive globe which can be used later to plot the current ISS location.

Install Dependencies

npm install @react-three/fiber @react-three/drei three three-geojson-geometry axios --save

Create The Scene

Create a new file in src/components called three-scene.js

// src/components/three-scene.js
import React from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei'; const ThreeScene = () => { return ( <Canvas gl={{ antialias: false, alpha: false }} camera={{ fov: 45, position: [0, 0, 300] }} onCreated={({ gl }) => { gl.setClearColor('#ffffff'); }} style={{ width: '100vw', height: '100vh', cursor: 'move' }} > <OrbitControls enableRotate={true} enableZoom={false} enablePan={false} /> </Canvas> );
}; export default ThreeScene;

The above sets up a new <Canvas /> element and can be configured using props exposed by React Three Fibre.

Elements that are returned as children of the canvas component will be displayed as part of the 3D scene. You’ll see above that I’ve included <OrbitControls /> which adds touch/mouse interactivity allowing users to rotate the scene in 3D space

Ensure ThreeScene is imported and rendered on a page somewhere in your site. In my example repo I’ve added ThreeScene to index.js:

// src/pages/index.js
import React from 'react'; import ThreeScene from '../components/three-scene'; const IndexPage = () => { return ( <main> <ThreeScene /> </main> );
}; export default IndexPage;

This won’t do much at the moment because there’s nothing to display in the scene. Let’s correct that!

Create The Sphere

Create a file in src/components called three-sphere.js:

// src/components/three-sphere.js
import React from 'react'; const ThreeSphere = () => { return ( <mesh> <sphereGeometry args={[100, 32, 32]} /> <meshBasicMaterial color="#f7f7f7" transparent={true} opacity={0.6} /> </mesh> );
}; export default ThreeSphere;

If the syntax above looks a little different to that of the Three.js docs it’s because React Three Fibre uses a declarative approach to using Three.js in React.

A good explanation of how constructor arguments work in React Three Fibre can be seen in the docs here: Constructor arguments

Now add ThreeSphere to ThreeScene:

// src/components/three-scene.js
import React from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei'; + import ThreeSphere from './three-sphere'; const ThreeScene = () => { return ( <Canvas gl={{ antialias: false, alpha: false }} camera={{ fov: 45, position: [0, 0, 300] }} onCreated={({ gl }) => { gl.setClearColor('#ffffff'); }} style={{ width: '100vw', height: '100vh', cursor: 'move' }} > <OrbitControls enableRotate={true} enableZoom={false} enablePan={false} />
+ <ThreeSphere /> </Canvas> );
}; export default ThreeScene;

You should now be looking at something similar to the image below.

Not very exciting, ay? Let’s do something about that!

Create The Geometry (To Visualize The Countries Of Planet Earth)

This next step requires the use of three-geojson-geometry and a CDN resource that contains Natural Earth Data. You can take your pick from a full list of suitable geometries here.

I’ll be using admin 0 countries. I chose this option because it provides enough geometry detail to see each country, but not so much that it will add unnecessary strain on your computer’s GPU.

Now, create a file in src/components called three-geo.js:

// src/components/three-geo.js
import React, { Fragment, useState, useEffect } from 'react';
import { GeoJsonGeometry } from 'three-geojson-geometry';
import axios from 'axios'; const ThreeGeo = () => {
const [isLoading, setIsLoading] = useState(true); const [geoJson, setGeoJson] = useState(null); useEffect(() => { axios .get( 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson' ) .then((response) => { setIsLoading(false); setGeoJson(response.data); }) .catch((error) => { console.log(error); throw new Error(); }); }, []); return ( <Fragment> {!isLoading ? ( <Fragment> {geoJson.features.map(({ geometry }, index) => { return ( <lineSegments key={index} geometry={new GeoJsonGeometry(geometry, 100)} > <lineBasicMaterial color="#e753e7" /> </lineSegments> ); })} </Fragment> ) : null} </Fragment> );
}; export default ThreeGeo;

There’s quite a lot going on in this file so I’ll walk you through it.

  1. Create an isLoading state instance using React hooks and set it to true. This prevents React from attempting to return data I don’t yet have.
  2. Using a useEffect I request the geojson from the CloudFront CDN.
  3. Upon successful retrieval I set the response in React state using setGeoJson(...) and set isLoading to false
  4. Using an Array.prototype.map I iterate over the “features” contained within the geojson response and return lineSegments with lineBasicMaterial for each geometry
  5. I set the lineSegments geometry to the return value provided by GeoJsonGeomtry which is passed the “features” geometry along with a radius of 100.

(You may have noticed I’ve used the same radius of 100 here as I’ve used in the sphereGeometry args in three-sphere.js. You don’t have to set the radius to the same value but it makes sense to use the same radii for ThreeSphere and ThreeGeo.

If you’re interested to know more about how GeoJsonGeometry works, here’s the open-source repository for reference: https://github.com/vasturiano/three-geojson-geometry. The repository has an example directory however, the syntax is slightly different from what you see here because the examples are written in vanilla JavaScript not React.

Combine The Sphere And Geometry

Now it’s time to overlay the geometry on top of the blank sphere: Add ThreeGeo to ThreeScene

// src/components/three-scene.js
import React from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei'; import ThreeSphere from './three-sphere';
+ import ThreeGeo from './three-geo'; const ThreeScene = () => { return ( <Canvas gl={{ antialias: false, alpha: false }} camera={{ fov: 45, position: [0, 0, 300] }} onCreated={({ gl }) => { gl.setClearColor('#ffffff'); }} style={{ width: '100vw', height: '100vh', cursor: 'move' }} > <OrbitControls enableRotate={true} enableZoom={false} enablePan={false} /> <ThreeSphere />
+ <ThreeGeo /> </Canvas> );
};

You should now be looking at something similar to the image below.

Now that’s slightly more exciting!

Step 2: Building A Serverless Function

Create A Function

This next step is where I use a Gatsby Function to request data from Where is ISS at, which returns the current location of the International Space Station.

Create a file in src/api called get-iss-location.js:

// src/api/get-iss-location.js
const axios = require('axios'); export default async function handler(req, res) { try { const { data } = await axios.get( 'https://api.wheretheiss.at/v1/satellites/25544' ); res.status(200).json({ iss_now: data }); } catch (error) { res.status(500).json({ error }); }
}

This function is responsible for fetching data from api.whereistheiss.at and upon success will return the data and a 200 status code back to the browser.

The Gatsby engineers have done such an amazing job at simplifying serverless functions that the above is all you really need to get going, but here’s a little more detail about what’s going on.

  • The function is a default export from a file named get-iss-location.js;
  • With Gatsby Functions the filename becomes the file path used in a client-side get request prefixed with api, e.g. /api/get-iss-location;
  • If the request to “Where is ISS at” is successful I return an iss_now object containing data from the Where is ISS at API and a status code of 200 back to the client;
  • If the request errors I send the error back to the client.

Step 3: Build The International Space Station

Creating The ISS Sphere

In this next step, I use Gatsby Functions to position a sphere that represents the International Space Station as it orbits the globe. I do this by repeatedly calling an axios.get request from a poll function and setting the response in React state.

Create a file in src/components called three-iss.js

// src/components/three-iss.js
import React, { Fragment, useEffect, useState } from 'react';
import * as THREE from 'three';
import axios from 'axios'; export const getVertex = (latitude, longitude, radius) => { const vector = new THREE.Vector3().setFromSpherical( new THREE.Spherical( radius, THREE.MathUtils.degToRad(90 - latitude), THREE.MathUtils.degToRad(longitude) ) ); return vector;
}; const ThreeIss = () => { const [issNow, setIssNow] = useState(null); const poll = () => { axios .get('/api/get-iss-location') .then((response) => { setIssNow(response.data.iss_now); }) .catch((error) => { console.log(error); throw new Error(); }); }; useEffect(() => { const pollInterval = setInterval(() => { poll(); }, 5000); poll(); return () => clearInterval(pollInterval); }, []); return ( <Fragment> {issNow ? ( <mesh position={getVertex( issNow.latitude, issNow.longitude, 120 )} > <sphereGeometry args={[2]} /> <meshBasicMaterial color="#000000" /> </mesh> ) : null} </Fragment> );
}; export default ThreeIss;

There’s quite a lot going on in this file so I’ll walk you through it.

  1. Create an issNow state instance using React hooks and set it to null. This prevents React from attempting to return data I don’t yet have;
  2. Using a useEffect I create a JavaScript interval that calls the poll function every 5 seconds;
  3. The poll function is where I request the ISS location from the Gatsby Function endpoint (/api/get-iss-location);
  4. Upon successful retrieval, I set the response in React state using setIssNow(...);
  5. I pass the latitude and longitude onto a custom function called getVertex, along with a radius.

You may have noticed that here I’m using a radius of 120. This does differ from the 100 radius value used in ThreeSphere and ThreeGeo. The effect of the larger radius is to position the ISS higher up in the 3D scene, rather than at ground level — because that’s logically where the ISS would be, right?
100 has the effect of the sphere and geometry overlapping to represent Earth, and 120 for the ISS has the effect of the space station “orbiting” the globe I’ve created.

One thing that took a bit of figuring out, at least for me, was how to use spherical two dimensional coordinates (latitude and longitude) in three dimensions, e.g. x,y,z. The concept has been explained rather well in this post by Mike Bostock.

The key to plotting lat / lng in 3D space lies within this formula… which makes absolutely no sense to me!

x=rcos(ϕ)cos(λ)
y=rsin(ϕ)
z=−rcos(ϕ)sin(λ)

Luckily, Three.js has a set of MathUtils which I’ve used like this:

  • Pass the latitude, longitude and radius into the getVertex(...) function
  • Create a new THREE.Spherical object from the above named parameters
  • Set the THREE.Vector3 object using the Spherical values returned by the setFromSpherical helper function.

These numbers can now be used to position elements in 3D space on their respective x, y, z axis — phew! Thanks, Three.js!

Now add ThreeIss to ThreeScene:

import React from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei'; import ThreeSphere from './three-sphere';
import ThreeGeo from './three-geo';
+ import ThreeIss from './three-iss'; const ThreeScene = () => { return ( <Canvas gl={{ antialias: false, alpha: false }} camera={{ fov: 45, position: [0, 0, 300] }} onCreated={({ gl }) => { gl.setClearColor('#ffffff'); }} style={{ width: '100vw', height: '100vh', cursor: 'move' }} > <OrbitControls enableRotate={true} enableZoom={false} enablePan={false} /> <ThreeSphere /> <ThreeGeo />
+ <ThreeIss /> </Canvas> );
}; export default ThreeScene;

Et voilà! You should now be looking at something similar to the image below.

The poll function will repeatedly call the Gatsby Function, which in turn requests the current location of the ISS and re-renders the React component each time a response is successful. You’ll have to watch carefully but the ISS will change position ever so slightly every 5 seconds.

The ISS is traveling at roughly 28,000 km/h and polling the Gatsby Function less often would reveal larger jumps in position. I’ve used 5 seconds here because that’s the most frequent request time as allowed by the Where is ISS at API

You might have also noticed that there’s no authentication required to request data from the Where is ISS at API. Meaning that yes, technically, I could have called the API straight from the browser, however, I’ve decided to make this API call server side using Gatsby Functions for two reasons:

  1. It wouldn’t have made a very good blog post about Gatsby Functions if i didn’t use them.
  2. Who knows what the future holds for Where is ISS at, it might at some point require authentication and adding API keys to server side API requests is pretty straightforward, moreover this change wouldn’t require any updates to the client side code.

Step 4: Make It Fancier! (Optional)

I’ve used the above approach to create this slightly more snazzy implementation: https://whereisiss.gatsbyjs.io,

In this site I’ve visualized the time delay from the poll function by implementing an Svg <circle /> countdown animation and added an extra <circle /> with a stroke-dashoffset to create the dashed lines surrounding it.

Step 5: Apply Your New Geo Rendering Skills In Other Fun Ways!

I recently used this approach for plotting geographical locations for the competition winners of 500 Bottles: https://500bottles.gatsbyjs.io. A limited edition FREE swag giveaway I worked on with Gatsby’s marketing team.

You can read all about how this site was made on the Gatsby blog: How We Made the Gatsby 500 Bottles Giveaway

In the 500 Bottles site I plot the geographical locations of each of the competition winners using the same method as described in ThreeIss, which allows anyone visiting the site to see where in the world the winners are.

Closing Thoughts

Gatsby Functions really open up a lot of possibilities for Jamstack developers and never having to worry about spinning up or scaling a server removes so many problems leaving us free to think about new ways they can be used.

I have a number of ideas I’d like to explore using the V4 Space X API’s so give me a follow if that’s your cup of tea: @PaulieScanlon

Further Reading

I hope you enjoyed this post. Ttfn 🕺!