Development

Store development

Developing the high-fidelity prototype, an e-commerce platform.

Categories
Development
Research methods

There are no research methods for this deliverable.

Research question

There is no research question for this deliverable.

Context

To maintain a proper project structure, I documented the development process and the technologies used during this process.

The store was built using the previously set requirements from my requirements prioritisation, using the MoSCoW method. Throughout the development process, I used agile methodology to develop and document features.

Results

The development of this project was based on my final designs as seen in prototype usability testing.

Tech stack

To figure out which tech stack to use, I researched multiple aspects of building a modern e-commerce platform:

Commit structure

For all my commits, I used the conventional commit guidelines to maintain well-structured commit messages.

CI/CD

All of the code for this project was hosted on Moonly's internal private GitHub repository. Because this project's deliverable was a prototype demo, I developed the store locally and kept it as a local demo for now due to time constraints.

Code reviews

Once the development process began, me and my company supervisor held weekly code reviews where we looked at code quality, possible issues I was stuck on, and general progress of the development. These weekly meetings helped me reassure myself of certain decisions made and also made me a better developer by seeing how I could improve certain code.

Process

Setup

To start, I used Shopify's recommended way of setting up a new Hydrogen app with their CLI:

npm create @shopify/hydrogen@latest

Afterwards, I set up and installed Tailwind, relevant fonts, and Shadcn/ui for styling.

Hydrogen template fixes

I quickly realized that the default Hydrogen boilerplate code was quite messy (and somewhat inefficient?), meaning I had to remove and move around tons of code like default styling, unused CSS classes, unused API queries, and move code into separate component files.

Since the base styling that came with the Hydrogen setup was nothing like my designs, as well as being standard CSS, I removed all of it and rebuilt the styles according to my own designs using Tailwind. As I have plenty of experience using CSS and Tailwind, this was not an issue.

To make sure my code is well-organized and structured, I created separate files for components, as well as folders to group similar components.

To use sample data, I was able to extract some demo products from the old Masita store and import them.

After fixing some of the boilerplate code issues, I started working on the homepage and being able to show a list of products using GraphQL queries (which the Shopify API uses), which returned Shopify product objects to me that I could then map over in my Hydrogen app.

CollectionProductsByHandleQuery.ts
export const COLLECTION_WITH_PRODUCTS_BY_HANDLE_QUERY = `#graphql 
query getCollectionWithProductsByHandle($handle: String, $first: Int, $last: Int, $startCursor: String, $endCursor: String) {
  collection(handle: $handle) {
    id
    title
    handle
    image {
      id
      url
      altText
      width
      height
    }
    products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
      nodes {
        id
        title
        handle
        description
        productType
        availableForSale
        tags
        priceRange {
          minVariantPrice {
            amount
            currencyCode
          }
          maxVariantPrice {
            amount
            currencyCode
          }
        }
        featuredImage {
          url
          altText
        }
        variants(first: 1) {
          edges {
            node {
              title
              quantityAvailable
              id
              selectedOptions {
                name
                value
              }
              compareAtPrice {
                amount
                currencyCode
              }
              price {
                amount
                currencyCode
              }
              image {
                url
                altText
              }
            }
          }
        }
      }
      pageInfo {
        hasPreviousPage
        hasNextPage
        endCursor
        startCursor
      }
    }
  }
}` as const;
index.tsx
{
  data.collections.map((collection) => {
    return (
      (collection?.collection?.products?.nodes?.length ?? 0) > 0 &&
      collection?.collection?.products?.nodes.some(
        (product) => product.availableForSale,
      ) && (
        <ProductPreviewList
          key={collection.collection?.title}
          title={collection.collection?.title}
          products={collection.collection?.products.nodes}
        />
      )
    );
  });
}

I did the same for the product page, but instead of querying an entire collection, I used the url to query a single product. Since I had never used GraphQL before, It took some time to figure out which data I needed to query but after some troubleshooting I was able to find the right queries.

The Hydrogen boilerplate code came with some cart functions, however there were a couple bugs with this code which meant that the cart often got stuck on loading the cart. To fix this, I made sure that the query didn't return a promise.

Cart API fixes

Cart API fixes

Feature development

After fixing the cart, I wanted to add something to the front-end to see discounts. so I created discount codes in the back-end, and added badges for them in the front-end that would be visible on items on which the discount code was applicable. It took me a bit of time to make a dynamic pricing component as I had to do tons of math and converting integers to strings as part of the Shopify API only returns strings.

Cart overview discounts

Cart overview discounts

Custom kit builder

With my basic flow of buying a product done (product overview / collection of products -> product page -> add to cart -> cart overview -> checkout), I started working on adding the product customizer. The idea for this was to add a customize button to the product page, but as dynamically as possible. That means the button should only be visible if the product has "custom" fields (such as custom name or number) attached to it in the back-end, indicating that it is customizable. The customizer should also only show the "custom" fields that are relevant to that specific product.

I ran into some issues with sending the custom data to the cart once the user was done customizing, as Shopify had multiple API's to do this, but they were all under different naming conventions which had me a little confused. After some time, I was able to find out that I could send my custom data to the product as attributes, which worked seamlessly with the Shopify checkout flow, as a new customized item is automatically seen as a 1-of-1 (unless it has the exact same customization) meaning it will get added to the cart as a separate item.

CustomizerForm.tsx
import {useState} from 'react';
import {
  DialogClose,
  DialogContent,
  DialogFooter,
  DialogTitle,
} from '~/components/ui/dialog';
import {Input} from '~/components/ui/input';
import {Label} from '~/components/ui/label';
import {AddToCartButton} from '../AddToCartButton';
import {CustomizerImage} from './CustomizerImage';

import type {ProductFragment} from 'storefrontapi.generated';

export function CustomizerForm({
  selectedVariant,
}: {
  selectedVariant: ProductFragment['selectedVariant'];
}) {
const metafieldValue = selectedVariant?.metafield?.value;
let lowerCaseValues: string[] = [];
if (metafieldValue && selectedVariant?.metafield?.key === 'customization') {
const values: string[] = JSON.parse(metafieldValue) as string[];
lowerCaseValues = values.map((value: string) => value.toLowerCase());
}
const [customValues, setCustomValues] = useState( lowerCaseValues.reduce((acc, value) => ({...acc, [value]: ''}), {}), ); const handleInputChange = (name: string, value: string) => { setCustomValues((prevValues) => ({...prevValues, [name]: value})); }; return ( <DialogContent className="max-w-max"> <div className="flex gap-8 flex-col lg:flex-row"> <CustomizerImage selectedVariant={selectedVariant} /> <div className="flex flex-col gap-8"> <DialogTitle>Customize</DialogTitle> <div className="space-y-4"> {lowerCaseValues.map((value) => ( <div key={value} className="space-y-1"> <Label htmlFor={`custom-${value}`} className="font-semibold"> Custom {value} </Label> <Input id={`custom-${value}`} placeholder={value.charAt(0).toUpperCase() + value.slice(1)} value={customValues[value as keyof typeof customValues]} onChange={(e) => handleInputChange(value, e.target.value)} /> </div> ))} </div> <DialogFooter className="flex items-end h-full"> <DialogClose asChild> <AddToCartButton disabled={ !selectedVariant || selectedVariant.currentlyNotInStock } onClick={() => { if (window.location.href.includes('#cart-aside')) { window.location.href = window.location.href.replace( '#cart-aside', '', ); } else { window.location.href = window.location.href + '#cart-aside'; } }} lines={ selectedVariant ? [ { merchandiseId: selectedVariant.id, quantity: 1,
attributes: lowerCaseValues
.map((value) => ({
key: `Custom ${value}`,
value:
customValues[
value as keyof typeof customValues
] ?? '',
}))
.filter((attribute) => attribute.value !== ''),
}, ] : [] } > {selectedVariant?.currentlyNotInStock ? 'Sold out' : 'Add to cart'} </AddToCartButton> </DialogClose> </DialogFooter> </div> </div> </DialogContent> ); }

Masita hydrogen - Customizer demo

Custom item cart overview

Custom item cart overview

Missing features

While a requirement for the project was to have integrations with external sales channels like bol.com, the solution I have chosen for this (Channable) only offers a paid solution. Since this project is still technically a prototype, I have chosen to not include it in the scope, however it is possible and quite simple to integrate by simply installing the Channable Shopify plugin, which doesn't require any front-end changes.

The must-haves are all working in my prototype. Due to being ill near the end of my internship, I wasn't able to finish all of my could-have features. Luckily, these features were all "extra's" and are not needed to present my prototype demo.

A complete list of available features and missing features can be found in the advisory report.

Unfortunately, the source code is not publicly available due to it being hosted on an internal GitHub repo.

Conclusion

I was able to build a working live prototype that had all the necessary could and should haves implemented. I also kept clean, scalable, dynamic code in mind to make sure that when this code is possibly used as a template for future projects, developers would be left with a positive development experience.

Final prototype - live demo

Learning outcomes

1: Professional Duties

By developing a live prototype based on validated designs, taking into account code quality and modern, I show that I can produce professional products.

2: Situation-Orientation

By combining my previous development experience with new technologies and other internal tools to build a valuable product, I show that I can adapt to the way of working of the company.

© 2025 Luc Swinkels. All rights reserved.