Shopify App Development using React based Hydrogen Framework: A Comprehensive Tutorial 101 (part 1)
Shopify has announced a React-based framework named Hydrogen to build a dynamic custom ecommerce front end for Shopify's Headless ecommerce business model. Shopify Hydrogen is also totally separate from OS2.0. Hydrogen-based Shopify Storefront Apps will work using GraphQL Storefront API. Shopify Hydrogen speeds up the development cycle of the ecommerce front-end.
Storefront channels using React stack are more efficient (speedier, more adaptable) and are SEO-friendly, opening new avenues for discovery and allowing for more immersive user experiences.
Shopify Hydrogen is the next-generation tech stack for Shopify's headless front-end ecommerce system that replaces the previous Shopify Liquid framework that has been in use to date. In this way, Shopify storefronts created on Liquid have to be rebuilt using Hydrogen.
Although these changes will require effort from developers, however, the benefits of this new approach to development justify the effort to provide future-ready dynamic ecommerce experiences.
React-based Hydrogen framework allows developers to build large ecommerce front-end apps that populate backend data without manually reloading the page. It's a quick and flexible modern framework that combines the work of developers into a distinct pattern, which helps reduce the difficulty of hiring a dev staff process. With React, the React framework front-end developers can build a presentation layer that integrates multiple backends without needing to master each backend technology.
Hands-On Videos:
Shopify App Development using React based Hydrogen Framework Hands-On Videos: https://youtube.com/playlist?list=PLucDsTg_UqT-fF-hxs21QtYcb7gI16iGa
I. Setting Things Up
For this post, we will create a first hydrogen web by modifying a hydrogen starter called SnowDevil that is available upon creating the app. The overall look is inspired by Shopify Supply Web, with blue, white, and green color palettes.
1. What is Hydrogen - in brief?
Hydrogen is a front-end web development framework used for building Shopify custom storefronts. It includes the structure, components, and tooling.
2. Difference between client.jsx and server.jsx
On Hydrogen, we would find a file that has a .client.jsx or .server.jsx extension. What is the difference between the two of them?
Server components (ended with .server.jsx)
Components that fetch data and render content on the server. Their dependencies aren't in the client bundle, and server components don't include client-side interactivity. Only server components can make calls to the Storefront API.
Client components (ended with .client.jsx)
A client component is a component that renders on the client. Client components include client-side stateful interactivity.
There's another component that is shared on both client and server. They are not ended with .server.jsx nor .client.jsx, but .normally end with jsx.
3. Create a new hydrogen app
What we need to do first is to create a new hydrogen project before creating anything. In your desired directory, type this command below:
npm init hydrogen-app@latest
The prompt will ask you to name your projects after initializing a new hydrogen app. After the app has already been created,
cd [project name]
npm install --legacy-peer-deps
npm run dev
Then, visit your local development environment on http://localhost:3000.
II. Modifying Snowdevil Web
1. Modifying Tailwind Configuration
Before modifying our SnowDevil component, let's adjust the Tailwind CSS configuration. To customize what color or size you would like to use, we can use this Tailwind configuration.
Go to tailwind.config.js, and replace the config with the code below. We add sizes, colors, font size, and font family choice to the config. These configurations are based on Shopify Supply's tailwind.config
const defaultTheme = require('tailwindcss/defaultTheme');
const secondary = '#1134B1';
const green = {
emerald: '#0F9C1D',
lime: '#D0F224',
DEFAULT:' #A5E3B9',
primary: '#95BF47',
};
const blue = {
sky: '#79DFFF',
};
const white = {
DEFAULT: '#FFFFFF',
'off-white': '#F4F4F4',
};
const ink = '#000000';
const gray = {
DEFAULT: '#3D3F40',
2: '#8D90091',
3: '#CCCCCC',
4: '#F4F4F4',
};
const reverse = '#FFFFFF';
const red = {
1: '#E96A4E',
2: '#E73E4D',
};
const shopPayPurple = '#5A31F4';
module.exports = {
content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
backgroundImage: {
'hero-pattern': "url('/src/assets/images/bg-1.png')",
},
boxShadow: {
DEFAULT: '6px 6px 30px rgba(0, 0, 0, 0.15)',
},
borderRadius: {
xl: '3.5rem',
},
spacing: {
xs: '2px',
sm: '4px',
med: '8px',
base: '12px',
lg: '16px',
xl: '24px',
1: '5px',
2: '10px',
3: '15px',
4: '20px',
5: '30px',
6: '40px',
98: '37rem',
},
strokeWidth: {
3: '3',
4: '4',
},
maxWidth: {
'1/3': '33.3%',
},
maxHeight: {
100: '40rem',
},
gridTemplateColumns: {
checkout: '100px auto;',
},
colors: {
primary: green.primary,
secondary,
white,
green,
ink,
gray,
red,
blue,
'shop-pay-purple': shopPayPurple,
},
fontFamily: {
title: ['Atyp Display'],
primary: ['Apfel Grotezk'],
},
fontSize: {
sm: ['14px', '16.8px'],
base: ['16px', '22.4px'],
lg: ['20px', '23px'],
xl: ['28px', '33.6px'],
'2xl': ['42px', '42px'],
'3xl': ['58px', '52.2px'],
'4xl': ['80px', '75.2px'],
'5xl': ['120px', '108px'],
mbase: ['16px', '20.8px'],
mlg: ['18px', '16.2px'],
mxl: ['22px', '26.4px'],
m2xl: ['24px', '24pxs'],
m3xl: ['28px, 33.6px'],
m4xl: ['34px', '34px'],
m5xl: ['42px', '37.8px'],
},
gradientColorStops: (theme) => ({
...theme('colors'),
'primary-start': blue.sky,
'primary-end': reverse,
'red-start': 'red-1',
'red-end': 'red-2',
}),
typography: (theme) => ({
DEFAULT: {
css: {
hr: {
borderColor: theme('colors.white.200'),
borderTopWidth: '1px',
marginTop: '2rem',
marginBottom: '2rem',
},
'ol > li::before': {
color: theme('colors.white.900'),
},
'ul > li::before': {
backgroundColor: theme('colors.white.900'),
},
},
},
}),
},
},
plugins: [require('@tailwindcss/typography')],
};
2. Modify Header
Next, after setting up the configuration, the first thing that we would be making is the first top component on the page. That is the header. We won't be making too much change to the header, only centering the items and adding top and bottom borders.
Go to src/components/Header.client.jsx to modify the header and change the code like below.
import {useState} from 'react';
import {Link} from '@shopify/hydrogen/client';
import CartToggle from './CartToggle.client';
import CurrencySelector from './CurrencySelector.client';
/**
* A client component that specifies the content of the header on the website
*/
export default function Header({collections, storeName}) {
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
return (
<div className="">
<header className="inline-block z-50 fixed duration-200 transform w-full h-16 sm:h-24 border-t-2 border-b-2 bg-white text-secondary border-secondary">
<div className='max-w-7xl mx-auto px-4 xm:px-6'>
<div className='flex justify-between items-center border-secondary py-3 md:justify-start md:space-x-10'>
<CurrencySelector />
<div className="flex justify-start lg:w-0 lg:flex-1">
<Link
className='uppercase font-title text-mlg text-secondary tracking-widest'
to="/">{storeName}</Link>
</div>
<nav>
<div className="md:flex items-center justify-center">
{collections.map((collection) => (
<div key={collection.id}>
<Link
to={`/collections/${collection.handle}`}
className="items-center justify-center block p-4 hover:opacity-80"
>
{collection.title}
</Link>
</div>
))}
</div>
</nav>
<div className='hidden md:flex items-center justify-end md:flex-1 lg:w-0'></div>
<CartToggle
handleClick={() => {
if (isMobileNavOpen) setIsMobileNavOpen(false);
}}
/>
</div>
</div>
</header>
</div>
3. Modify Hero
After finishing the header, we need to create the hero right below the header.
3.1. Modify Layout
Before creating the hero, we need to adjust the layout to match our needs. The layout component is a component that wraps up the children component between the header and the footer. The component also sets what color is the background of the web. Because we'll make the web with a blue background, let's edit the layout so that the className background is blue.
Go to src/components/Layout.server.jsx and scroll to line 43. You can see a div with a className like below, add min-w-full and bg-secondary in the className. That'll make our layout has a minimum width of 100 percent and a blue background.
From:
<div className="min-h-screen max-w-screen text-gray-700 font-sans">
To:
<div className="min-w-full min-h-screen max-w-screen text-gray-700 font-sans bg-secondary">
Go to line 55 and erase the bg-gray-50 from the className. We will clean up all the previous gray backgrounds that would clash with our new color.
From:
<main role="main" id="mainContent" className="relative bg-gray-50">
To:
<main role="main" id="mainContent" className="relative">
Now that we have finished adjusting the layout, let's make the Hero component.
3.2. Create Hero
For the hero, we would have a blue background, an image beneath the white texts, and a green button that doesn't redirect anywhere when clicked.
On the src/components folder, add a new file called Hero.client.jsx. Because we won't be needing to fetch any data from the Storefronts API, the hero will use a client-side rendering. Create an export default function Hero.
export default function Hero(){
}
3.2.1. Move Image
Because we need to use an image on the hero, we would need to import the image to our project. The best practice is to put all the images together in one directory to use an image or some images. Before moving our image to the project, create an assets folder in the src directory. Inside assets, make another folder called images. It's where we put all of our images there.
After our images folder is available, move your image that we'll be using for the hero to the images folder. For this project, I would use a picture of a basket of green vegetables to match the palette for this web and the green button below the hero text.
3.2.2. Simplify Image Import
The next thing we would do is import the image from the assets folder to our hero component. We could import the file from our images assets folder in our Hero component, but it could be bothersome if our image count increases that need to be placed in nested folders. There would be many dots and slashes on the path file when importing the image. The solution is to simplify our image import by giving each file name and giving the same path for every image that is /assets/images.
In the images folder, create a file called index.js. We will format the filenames to simplify the file import on our project in the index.js.
On index.js type,
export {default as Veggie} from './Veggie.png';
For the file formatting, the named export came from the file name. Because the image I am using is Veggie.png, I would export it as Veggie. If you have a different file name, you can rename the file export differently according to the file name or your preferences. Veggie is also the name when we try to import the image file, like when we import a module in our project.
That's it to simplify an image file import. Whenever a new file needs to be added to your code, you could add new export codes below what we made before.
3.2.3. Creating the Hero
We're all set up. Now let's try to create the hero. Go back to src/components/Hero.client.jsx, import the image file that we set up before at the top of the file for the hero. The format is like importing a module.
import { Veggie } from '../assets/images'
We'll also be needing a Link for the button that will redirect to nowhere, import Link from Shopify Hydrogen.
import { Veggie } from '../assets/images'
import { Link } from '@shopify/hydrogen/client'
We have all the needed tools to build the hero. On the export default function, add this code below. To see more documentation of this kind of look, check out shopify-supply from this link: https://github.com/Shopify/hydrogen-examples/tree/master/shopify_supply/src.
export default function Hero() {
return (
<div className="pt-16 text-white lg:pt-20">
<div className="grid grid-cols-12">
<img
src={Veggie}
alt="a hand pressing the button"
className="max-w-xs lg:col-span-4 col-span-full md:max-w-l lg:max-w-xl 2xl:max-w-2xl"
width="770"
height="846"
/>
<div className="flex flex-col row-auto px-4 pb-6 -mt-16 bg-transparent md:pt-6 lg:pt-36 2xl:pt-40 lg:pl-0 lg:col-span-7 xl:col-span-8 col-span-full lg:my-0">
<p className="pr-4 font-title text-m5xl sm:text-3xl xl:text-4xl 2xl:text-5xl 2xl:max-w-5xl">
Fresh swag from the lil' green bag
</p>
<p className="max-w-3xl px-8 pt-6 pb-5 lg:pl-48 font-paragraph text-mlg leading-relaxed lg:font-base font-mbase">
We bottled up the sweetest sound in the world: the sound of a sale,
'Cha-Ching'. Press it for an instant hit of serotonin.
</p>
<Link className="ml-8 lg:ml-48 btn btn-primary" to={'/'}>Learn more</Link>
</div>
</div>
</div>
)
}
3.2.4. Creating Custom Class
See line 26 for the styling of the <Link>. We can see that it has class btn and btn-primary. Those classes are not included in Tailwind CSS but are custom-made. Let's create btn and btn-primary classes ourselves.
Go to index.css. Like we can imply on the name, index.css is where we can make our style class to match our needs. On top of @layer utilities, create @layer components {}. The components layers are where we put any custom classes, like btn, card, and many more. Type the custom classes (btn and btn-primary) under @layer components {} like the code snippet below.
@layer components {
.btn {
@apply inline-block px-5 py-3 mx-auto my-0 border-2 border-solid font-title text-mlg text-secondary border-secondary whitespace-nowrap rounded-xl hover:shadow sm:text-lg sm:px-6;
}
.btn-primary {
@apply px-6 border-0 text-secondary bg-primary hover:text-secondary;
}
}
@layer utilities {
.scroll-snap-x {
scroll-snap-type: x mandatory;
}
4. Modify Welcome Component
There are many features that we won't need on our web on the Welcome component, like documentation links that appear the most. Let's remove all unneeded components and parts.
Go to src/components/Welcome.server.jsx and scroll down to the return () function. Remove <div className=" text-center mb-16"> with its containing items and remove the TemplateLinks and DocsButton component, since we won't be needing it anymore.
From:
return (
<div className="text-gray-900 pt-16 rounded-[40px] my-16 px-4 xl:px-12 bg-gradient-to-b from-white -mx-4 xl:-mx-12">
<div className="text-center mb-16">
<h1 className="font-extrabold mb-4 text-5xl md:text-7xl">
Hello, Hydrogen
</h1>
<p className="text-lg mb-8">
Welcome to your custom storefront. Let’s get building.
</p>
<div className="flex flex-col lg:flex-row justify-center items-center gap-8 text-gray-700">
<DocsButton
url="https://shopify.dev/custom-storefronts/hydrogen"
label="Browse Hydrogen documentation"
/>
<DocsButton url="/graphql" label="Open the GraphiQL explorer" />
<DocsButton
url="https://github.com/Shopify/hydrogen-examples"
label="Explore Hydrogen examples"
/>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
<StorefrontInfo
shopName={shopName}
totalProducts={totalProducts}
totalCollections={totalCollections}
/>
<TemplateLinks
firstProductPath={firstProduct}
firstCollectionPath={firstCollection}
/>
</div>
</div>
);
}
To:
return (
<>
<div className="text-gray-900 pt-16 rounded-[40px] my-16 px-4 xl:px-12 -mx-4 xl:-mx-12">
<div>
<Suspense fallback={<BoxFallback />}>
<StorefrontInfo />
</Suspense>
</div>
</div>
</>
);
Scroll up again and remove the unneeded component functions that we recently deleted on the return function.
What needed to be removed (ExternalIcon, DocsButton, and TemplateLinks):
function ExternalIcon() {
return (
<svg
className="ml-3"
width="15"
height="14"
viewBox="0 0 15 14"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<path d="M8.11963 0.000976562C7.56734 0.000976562 7.11963 0.448692 7.11963 1.00098C7.11963 1.55326 7.56734 2.00098 8.11963 2.00098H10.7054L4.41252 8.29387C4.022 8.68439 4.022 9.31756 4.41252 9.70808C4.80305 10.0986 5.43621 10.0986 5.82674 9.70808L12.1196 3.41519V6.00098C12.1196 6.55326 12.5673 7.00098 13.1196 7.00098C13.6719 7.00098 14.1196 6.55326 14.1196 6.00098V1.00098C14.1196 0.448692 13.6719 0.000976562 13.1196 0.000976562H8.11963Z" />
<path d="M2.11963 2.00098C1.01506 2.00098 0.119629 2.89641 0.119629 4.00098V12.001C0.119629 13.1055 1.01506 14.001 2.11963 14.001H10.1196C11.2242 14.001 12.1196 13.1055 12.1196 12.001V9.00098C12.1196 8.44869 11.6719 8.00098 11.1196 8.00098C10.5673 8.00098 10.1196 8.44869 10.1196 9.00098V12.001H2.11963V4.00098L5.11963 4.00098C5.67191 4.00098 6.11963 3.55326 6.11963 3.00098C6.11963 2.44869 5.67191 2.00098 5.11963 2.00098H2.11963Z" />
</svg>
);
}
function DocsButton({url, label}) {
return (
<a
href={url}
target="_blank"
className="bg-white shadow py-2 px-5 rounded-full inline-flex items-center hover:opacity-80"
rel="noreferrer"
>
{label}
<ExternalIcon />
</a>
);
}
function TemplateLinks({firstProductPath, firstCollectionPath}) {
return (
<div className="bg-white p-12 md:p-12 shadow-xl rounded-xl text-gray-900">
<p className="text-md font-medium uppercase mb-4">
Explore the templates
</p>
<ul>
<li className="mb-4">
<Link
to={`/collections/${firstCollectionPath}`}
className="text-md font-medium text-blue-700 hover:underline"
>
Collection template
</Link>
</li>
<li className="mb-4">
<Link
to={`/products/${firstProductPath}`}
className="text-md font-medium text-blue-700 hover:underline"
>
Product template
</Link>
</li>
<li>
<Link
to="/error-page"
className="text-md font-medium text-blue-700 hover:underline"
>
404 template
</Link>
</li>
</ul>
</div>
);
}
Remove all of those above from the Welcome.server.jsx.
After cleaning up, let's start to modify our StoreFrontInfo component. In our StoreFrontInfo, we want to create a section to bring out our shop's main strengths. For this section, our items would be centered in three grids.
In the StorefrontInfo function, rewrite the code like the code snippet below.
function StorefrontInfo() {
return (
<div className="bg-white p-8 rounded-xl text-gray-900">
<div className='flex justify-around grid-cols-3 gap-4 place-items-center'>
<div className='place-items-center text-center'>
<p className="text-md font-medium uppercase mt-2">Free Shipping</p>
<p className="text-md">
For all order under $99.99
</p>
</div>
<div className='place-items-center text-center'>
<p className="text-md font-medium uppercase mt-2">Delivery On Time</p>
<p className="text-md">
Deliver immediately
</p>
</div>
<div className='place-items-center text-center'>
<p className="text-md font-medium uppercase mt-2">Secure Payment</p>
<p className="text-md">
100% secure payment
</p>
</div>
</div>
</div>
);
}
5. Modify Product List
The next component that we want to modify is the featured product list, right below the hero. We won't change too much of the list, only rearranging the item placement, changing the font color, and adding a big title on top of the component.
Go to FeaturedProductsBox function in src/pages/index.server.js, change to the code snippet below
return (
<div className="text-white m-50 p-12 mb-10">
<div className='font-title text-m5xl sm:text-3xl xl:text-4xl 2xl:text-5xl 2xl:max-w-5xl text-center items-center ml-8 px-4 pb-12'>
<p>Featured Products</p>
</div>
{featuredProductsCollection ? (
<>
<div className="flex justify-between items-center mb-8 text-md font-medium">
<span className="text-white uppercase">
{featuredProductsCollection.title}
</span>
<span className="hidden md:inline-flex">
<Link
to={`/collections/${featuredProductsCollection.handle}`}
className="text-primary hover:underline"
>
Shop all
</Link>
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-8">
{featuredProducts.map((product) => (
<div key={product.id}>
<ProductCard product={product} />
</div>
))}
</div>
<div className="md:hidden text-center">
<Link
to={`/collections/${featuredProductsCollection.handle}`}
className="text-primary"
>
Shop all
</Link>
</div>
</>
) : null}
</div>
);
That's all for the minimal rearranging for our product list. We're done with this section.
6. Modify Product Collections Page
The Snowdevil has two collections that we can see on the navigation header. When we click one of those two collections, the shop will direct us to a page that is called the product collections page.
Because we have changed the background color of all pages to blue (bg-secondary on our case), making the original font color hard to see (black). For this part and another after this, we would only change the font color for this component to white to make it easy to read.
Go to /src/pages/collections/[handle].server.jsx. Change all the text colors from gray to white. Your code will look like this.
return (
<Layout>
<h1 className="font-bold text-4xl md:text-5xl text-white mb-6 mt-6">
{collection.title}
</h1>
<RawHtml string={collection.descriptionHtml} className="text-white text-lg" />
<p className="text-sm text-white mt-5 mb-5">
{products.length} {products.length > 1 ? 'products' : 'product'}
</p>
<ul className="text-white grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
{products.map((product) => (
<li key={product.id}>
<ProductCard product={product} />
</li>
))}
</ul>
{hasNextPage && (
<LoadMoreProducts startingCount={collectionProductCount} />
)}
</Layout>
);
We can see after we change the color, there are still black texts on the component. That’s because of the ProductCard component, we haven’t yet incorporated the change. Go to src/components/ProductCard.jsx, change the return function to the code snippet below
return (
<div className="text-md mb-4 relative">
<Link to={`/products/${product.handle}`}>
<div className="rounded-lg border-2 border-gray-200 mb-2 relative flex items-center justify-center overflow-hidden object-cover h-96">
{selectedVariant.image ? (
<Image
className="bg-white absolute w-full h-full transition-all duration-500 ease-in-out transform bg-center bg-cover object-center object-contain hover:scale-110"
image={selectedVariant.image}
/>
) : null}
{!selectedVariant?.availableForSale && (
<div className="absolute top-3 left-3 rounded-3xl text-xs bg-black text-white py-3 px-4">
Out of stock
</div>
)}
</div>
<span className="text-white font-semibold mb-0.5">{product.title}</span>
{product.vendor && (
<p className="text-white font-medium text-sm mb-0.5">
{product.vendor}
</p>
)}
<div className="flex ">
{selectedVariant.compareAtPriceV2 && (
<MoneyCompareAtPrice money={selectedVariant.compareAtPriceV2} />
)}
<MoneyPrice money={selectedVariant.priceV2} />
</div>
</Link>
</div>
);
There are still black texts on the price of the product card. We can change the color value on MoneyComparedAtPrice and MoneyPriceComponent.
Go to src/components/MoneyCompateAtPrice.jsx, change the function to:
export default function MoneyCompareAtPrice({money}) {
return (
<Money money={money}>
{({amount, currencyNarrowSymbol}) => (
<span className="line-through text-lg mr-2.5 text-white">
{currencyNarrowSymbol}
{amount}
</span>
)}
</Money>
);
}
Go to src/components/MoneyPrice.client.jsx, also change to:
export default function MoneyPrice({money}) {
return (
<Money className="text-white text-md" money={money}>
{({amount, currencyNarrowSymbol, currencyCode}) => (
<>
{currencyCode}
{currencyNarrowSymbol}
{amount}
</>
)}
</Money>
);
}
7. Modify Product Details Page
Similar to the case before, to make the text that was hard to see before because it has a dark color that makes it blend to the background. We will modify the text from black to white, just like the previous product collections page.
Go to src/components/ProductDetails.client.jsx, we will modify the text from black to white, just like the previous collections page. Rewrite the code to the snippet below to change the corresponding text to white.
import {Product, flattenConnection, useProduct} from '@shopify/hydrogen/client';
import ProductOptions from './ProductOptions.client';
import Gallery from './Gallery.client';
import Seo from './Seo.client';
import {
BUTTON_PRIMARY_CLASSES,
BUTTON_SECONDARY_CLASSES,
} from './Button.client';
/**
* A client component that displays detailed information about a product to allow buyers to make informed decisions
*/
function ProductPriceMarkup() {
return (
<div className="flex md:flex-col items-end font-semibold text-lg md:items-start md:mb-4">
<Product.SelectedVariant.Price
priceType="compareAt"
className="text-white line-through text-lg mr-2.5"
>
{({amount, currencyNarrowSymbol}) => `${currencyNarrowSymbol}${amount}`}
</Product.SelectedVariant.Price>
<Product.SelectedVariant.Price className="text-white">
{({currencyCode, amount, currencyNarrowSymbol}) =>
`${currencyCode} ${currencyNarrowSymbol}${amount}`
}
</Product.SelectedVariant.Price>
<Product.SelectedVariant.UnitPrice className="text-white">
{({currencyCode, amount, currencyNarrowSymbol, referenceUnit}) =>
`${currencyCode} ${currencyNarrowSymbol}${amount}/${referenceUnit}`
}
</Product.SelectedVariant.UnitPrice>
</div>
);
}
function AddToCartMarkup() {
const {selectedVariant} = useProduct();
const isOutOfStock = !selectedVariant.availableForSale;
return (
<div className="space-y-2 mb-8">
<Product.SelectedVariant.AddToCartButton
className={BUTTON_PRIMARY_CLASSES}
disabled={isOutOfStock}
>
{isOutOfStock ? 'Out of stock' : 'Add to bag'}
</Product.SelectedVariant.AddToCartButton>
{isOutOfStock ? (
<p className="text-white text-center">Available in 2-3 weeks</p>
) : (
<Product.SelectedVariant.BuyNowButton
className={BUTTON_SECONDARY_CLASSES}
>
Buy it now
</Product.SelectedVariant.BuyNowButton>
)}
</div>
);
}
function SizeChart() {
return (
<>
<h3
className="text-xl text-white font-semibold mt-8 mb-4"
id="size-chart"
>
Size Chart
</h3>
<table className="min-w-full table-fixed text-sm text-center bg-white">
<thead>
<tr className="bg-black text-white">
<th className="w-1/4 py-2 px-4 font-normal">Board Size</th>
<th className="w-1/4 py-2 px-4 font-normal">154</th>
<th className="w-1/4 py-2 px-4 font-normal">158</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-3 border border-black">Weight Range</td>
<td className="p-3 border border-black">120-180 lbs. /54-82kg</td>
<td className="p-3 border border-black">150-200 lbs. /68-91 kg</td>
</tr>
<tr>
<td className="p-3 border border-black">Waist Width</td>
<td className="p-3 border border-black">246mm</td>
<td className="p-3 border border-black">255mm</td>
</tr>
<tr>
<td className="p-3 border border-black">Stance Width</td>
<td className="p-3 border border-black">-40</td>
<td className="p-3 border border-black">-40</td>
</tr>
<tr>
<td className="p-3 border border-black">Binding Sizes</td>
<td className="p-3 border border-black">
Men’s S/M, Women’s S/M
</td>
<td className="p-3 border border-black">
Men’s L, Women’s L
</td>
</tr>
</tbody>
</table>
</>
);
}
export default function ProductDetails({product}) {
const initialVariant = flattenConnection(product.variants)[0];
return (
<>
<Seo product={product} />
<Product product={product} initialVariantId={initialVariant.id}>
<div className="grid grid-cols-1 md:grid-cols-[2fr,1fr] gap-x-8 my-16">
<div className="md:hidden mt-5 mb-8">
<Product.Title
as="h1"
className="text-4xl font-bold text-white mb-4"
/>
{product.vendor && (
<div className="text-sm font-medium mb-2 text-white">
{product.vendor}
</div>
)}
<span />
<div className="flex justify-between md:block">
<ProductPriceMarkup />
</div>
</div>
<Gallery />
<div>
<div className="hidden md:block">
<Product.Title
as="h1"
className="text-5xl font-bold text-white mb-4"
/>
{product.vendor && (
<div className="text-sm font-medium mb-2 text-white">
{product.vendor}
</div>
)}
<ProductPriceMarkup />
</div>
{/* Product Options */}
<div className="mt-8">
<ProductOptions />
<Product.Metafield namespace="my_fields" keyName="size_chart">
{({value}) => {
return value ? (
<a
href="#size-chart"
className="block underline text-white text-sm tracking-wide my-4"
>
Size Chart
</a>
) : null;
}}
</Product.Metafield>
<AddToCartMarkup />
<div className="flex items space-x-4">
<Product.Metafield namespace="my_fields" keyName="sustainable">
{({value}) => {
return value ? (
<span className="flex items-center mb-8">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="stroke-current text-blue-600 mr-3"
>
<path
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364-.7071-.7071M6.34315 6.34315l-.70711-.70711m12.72796.00005-.7071.70711M6.3432 17.6569l-.70711.7071M16 12c0 2.2091-1.7909 4-4 4-2.20914 0-4-1.7909-4-4 0-2.20914 1.79086-4 4-4 2.2091 0 4 1.79086 4 4Z"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className="text-sm text-white font-medium">
Sustainable Material
</span>
</span>
) : null;
}}
</Product.Metafield>
<Product.Metafield
namespace="my_fields"
keyName="lifetime_warranty"
>
{({value}) => {
return value ? (
<span className="flex items-center mb-8">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="stroke-current text-blue-600 mr-3"
>
<path
d="M9 12L11 14L15 10M20.6179 5.98434C20.4132 5.99472 20.2072 5.99997 20 5.99997C16.9265 5.99997 14.123 4.84453 11.9999 2.94434C9.87691 4.84446 7.07339 5.99985 4 5.99985C3.79277 5.99985 3.58678 5.9946 3.38213 5.98422C3.1327 6.94783 3 7.95842 3 9.00001C3 14.5915 6.82432 19.2898 12 20.622C17.1757 19.2898 21 14.5915 21 9.00001C21 7.95847 20.8673 6.94791 20.6179 5.98434Z"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className="text-sm text-white font-medium">
Lifetime Warranty
</span>
</span>
) : null;
}}
</Product.Metafield>
</div>
</div>
{/* Product Description */}
<Product.Description className="text-white border-t border-gray-200 pt-6 text-md" />
<Product.Metafield namespace="my_fields" keyName="size_chart">
{({value}) => {
return value ? (
<div className="border-t border-gray-200">
<SizeChart />
</div>
) : null;
}}
</Product.Metafield>
</div>
</div>
</Product>
</>
);
}
7.1. Modify Product Options
Like the previous components, we can see that there are still some black fonts. It originated from the ProductOptions component. Let’s change the text from black to white in src/components/ProductOptions.client.jsx, by rewriting the return function to this.
return (
<>
{options.map(({name, values}) => {
return (
<fieldset key={name} className="mt-8">
<legend className="mb-4 text-xl font-medium text-white">
{name}
</legend>
<div className="flex items-center flex-wrap gap-4">
{values.map((value) => {
const checked = selectedOptions[name] === value;
const id = `option-${name}-${value}`;
return (
<label key={id} htmlFor={id}>
<input
className="sr-only"
type="radio"
id={id}
name={`option[${name}]`}
value={value}
checked={checked}
onChange={() => setSelectedOption(name, value)}
/>
<div
className={`p-2 border cursor-pointer rounded text-sm md:text-md ${
checked ? 'bg-gray-900 text-white' : 'text-gray-900'
}`}
>
{value}
</div>
</label>
);
})}
</div>
</fieldset>
);
})}
</>
);
8. Assembling index.server.jsx
We are almost done with the shop's appearance. The next thing to do is assemble the components and layout on the index page. We haven't yet added our hero component to the index, and there are still some clean-ups to do.
Let's get back to the index page at src/pages/index.server.jsx.
8.1. Import the Hero Component
Import our recently made hero component on the top of the file, with the other file imports.
import Hero from '../components/Hero.client';
Scroll down to the return function, then add the Hero component on top of the Welcome component, like this snippet below.
return (
<Layout hero={<GradientBackground />}>
<Hero />
<div className="relative mb-12">
<Welcome />
8.2. Modify the Layout
In this step, we'll do some clean-ups. As you can see from the original Snowdevil, the shop has a green to purple gradient background. We won't need this feature, because we have set up the background of our shop to blue.
To remove this feature, go back to the top of the file and see the GradientBackground function attached to the Layout component. Remove the unneeded gradient background. Delete the “bg-gradient-to-t from-gray-50 z-10” from <div className="absolute w-full h-full bg-gradient-to-t from-gray-50 z-10" />
Then we need to uncomment or delete the items that are below this div:
<div className="absolute w-full h-full" />
Remove all of these items below.
<svg
viewBox="0 0 960 743"
fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
className="filter blur-[30px]"
aria-hidden="true"
>
<defs>
<path fill="#fff" d="M0 0h960v540H0z" id="reuse-0" />
</defs>
<g clipPath="url(#a)">
<use xlinkHref="#reuse-0" />
<path d="M960 0H0v743h960V0Z" fill="#7CFBEE" />
<path
d="M831 380c200.48 0 363-162.521 363-363s-162.52-363-363-363c-200.479 0-363 162.521-363 363s162.521 363 363 363Z"
fill="#4F98D0"
/>
<path
d="M579 759c200.479 0 363-162.521 363-363S779.479 33 579 33 216 195.521 216 396s162.521 363 363 363Z"
fill="#7CFBEE"
/>
<path
d="M178 691c200.479 0 363-162.521 363-363S378.479-35 178-35c-200.4794 0-363 162.521-363 363s162.5206 363 363 363Z"
fill="#4F98D0"
/>
<path
d="M490 414c200.479 0 363-162.521 363-363S690.479-312 490-312 127-149.479 127 51s162.521 363 363 363Z"
fill="#4F98D0"
/>
<path
d="M354 569c200.479 0 363-162.521 363-363 0-200.47937-162.521-363-363-363S-9 5.52063-9 206c0 200.479 162.521 363 363 363Z"
fill="#7CFBEE"
/>
<path
d="M630 532c200.479 0 363-162.521 363-363 0-200.4794-162.521-363-363-363S267-31.4794 267 169c0 200.479 162.521 363 363 363Z"
fill="#4F98D0"
/>
</g>
<path fill="#fff" d="M0 540h960v203H0z" />
<defs>
<clipPath id="a">
<use xlinkHref="#reuse-0" />
</clipPath>
</defs>
</svg>
9. Modifying the Footer Minimally
For the footer, we won't change it too much. We will only overwrite the most left column. This column contains the information for the community on GitHub and discord, and we will change it to every product collection available for this shop.
Go to src/components/Footer.server.jsx. See line 12. It contains heading 2 with the title of Community. Change it to Collections instead.
From:
<h2 className="text-md font-medium uppercase mb-4">Community</h2>
To:
<h2 className="text-md font-medium uppercase mb-4">Collections</h2>
We've done changing the title. Next is changing the links and text from Github, Discord, et cetera to the collection title. We won't write the title manually one by one but iterate from the GraphQL query passed to the component.
On the export default function Footer, add another props beside product, named collections like this code snippet below.
export default function Footer({collection, product, collections}) {
On line 13, inside the unordered list, we'll iterate the collections and show a list of every collection's title that navigates to their page.
From the original code:
<ul className="mt-8 space-y-4">
<li className="text-sm font-medium text-gray-600 hover:text-gray-900">
<a
href="https://github.com/Shopify/hydrogen/discussions"
target="_blank"
rel="noreferrer"
className="flex items-center"
>
<svg
aria-hidden="true"
className="fill-current text-gray-600 mr-3"
width="26"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.1319 0.000976562C4.60874 0.000976562 0.135254 4.58917 0.135254 10.2539C0.135254 14.7908 2.99679 18.6229 6.97045 19.9814C7.47028 20.0711 7.65772 19.7635 7.65772 19.4944C7.65772 19.2509 7.64522 18.4434 7.64522 17.5848C5.13357 18.059 4.48379 16.9568 4.28385 16.38C4.17139 16.0853 3.68406 15.1753 3.2592 14.9318C2.90932 14.7396 2.40949 14.2654 3.2467 14.2526C4.03394 14.2397 4.59625 14.9959 4.78369 15.3035C5.68338 16.8542 7.1204 16.4185 7.6952 16.1494C7.78267 15.4829 8.04508 15.0343 8.33249 14.778C6.10824 14.5217 3.78402 13.6374 3.78402 9.71564C3.78402 8.60063 4.17139 7.67786 4.80868 6.96016C4.70871 6.70383 4.35883 5.65291 4.90864 4.24313C4.90864 4.24313 5.74586 3.97399 7.65772 5.29406C8.45745 5.06336 9.30716 4.94802 10.1569 4.94802C11.0066 4.94802 11.8563 5.06336 12.656 5.29406C14.5679 3.96117 15.4051 4.24313 15.4051 4.24313C15.9549 5.65291 15.605 6.70383 15.5051 6.96016C16.1424 7.67786 16.5297 8.58781 16.5297 9.71564C16.5297 13.6502 14.193 14.5217 11.9688 14.778C12.3311 15.0984 12.6435 15.7136 12.6435 16.6748C12.6435 18.0461 12.631 19.1483 12.631 19.4944C12.631 19.7635 12.8185 20.0839 13.3183 19.9814C15.3028 19.2943 17.0273 17.9861 18.2489 16.2411C19.4706 14.4962 20.128 12.4022 20.1285 10.2539C20.1285 4.58917 15.655 0.000976562 10.1319 0.000976562Z"
/>
</svg>
Github discussions
</a>
</li>
<li className="text-sm font-medium text-gray-600 hover:text-gray-900">
<a
href="https://discord.gg/ppSbThrFaS"
target="_blank"
rel="noreferrer"
className="flex items-center"
>
<svg
aria-hidden="true"
className="fill-current text-gray-600 mr-3"
width="26"
height="20"
viewBox="0 0 26 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M21.3103 1.67597C19.691 0.893476 17.9595 0.324791 16.1494 0.000976562C15.9271 0.416095 15.6673 0.974439 15.4883 1.4186C13.564 1.11972 11.6574 1.11972 9.76855 1.4186C9.58952 0.974439 9.3239 0.416095 9.0996 0.000976562C7.28746 0.324791 5.55403 0.895566 3.93472 1.68012C0.668559 6.77767 -0.216844 11.7486 0.225859 16.649C2.39215 18.3198 4.49155 19.3348 6.55551 19.9989C7.06512 19.2745 7.51962 18.5045 7.91116 17.693C7.16546 17.4003 6.45123 17.0392 5.77638 16.6199C5.95541 16.4829 6.13054 16.3397 6.29973 16.1923C10.4159 18.1807 14.8882 18.1807 18.9551 16.1923C19.1263 16.3397 19.3014 16.4829 19.4785 16.6199C18.8016 17.0412 18.0855 17.4024 17.3398 17.6951C17.7313 18.5045 18.1838 19.2766 18.6954 20.001C20.7614 19.3368 22.8627 18.3219 25.029 16.649C25.5484 10.9682 24.1416 6.04292 21.3103 1.67597ZM8.47192 13.6353C7.2363 13.6353 6.22299 12.4439 6.22299 10.9931C6.22299 9.5423 7.21466 8.34886 8.47192 8.34886C9.72922 8.34886 10.7425 9.54021 10.7209 10.9931C10.7228 12.4439 9.72922 13.6353 8.47192 13.6353ZM16.7829 13.6353C15.5473 13.6353 14.534 12.4439 14.534 10.9931C14.534 9.5423 15.5256 8.34886 16.7829 8.34886C18.0402 8.34886 19.0535 9.54021 19.0319 10.9931C19.0319 12.4439 18.0402 13.6353 16.7829 13.6353Z" />
</svg>
Discord
</a>
</li>
</ul>
Change it to:
<ul className='mt-8 space-y-4'>
{collections.map((collection) => (
<>
<li key={collections.id}>
<Link
to={`/collections/${collection.handle}`}
className="flex items-center text-sm font-medium text-gray-600 hover:text-gray-900"
>
{collection.title}
</Link>
</li>
</>
))}
</ul>
We're not finished yet. We need to pass the collections to our Layout component. Go back to src/components/Layout.server.jsx and scroll to line 61.
From:
<Footer collection={collections[0]} product={produccts[0]} />
To:
<Footer collection={collections[0]} product={products[0]} collections={collections}/>
10. Modifying 404 Not Found Page
We’re still not quite done yet. We still need to change the font in the not found page. When we misspell or go to a path that it’s not available, the 404 page will be shown. Let’s change our font from black to white Go to src/components/NotFound.server.jsx
On the file, there are two components that are on the NotFound file that we need to modify. The NotFoundHero and the NotFound function.
Scroll onto the NotFoundHero() function and change the function to this code snippet below.
function NotFoundHero() {
return (
<div className="py-10 border-b border-gray-200">
<div className="max-w-3xl text-center mx-4 md:mx-auto">
<h1 className="font-bold text-4xl md:text-5xl text-white mb-6 mt-6">
We've lost this page
</h1>
<p className="text-lg m-8 text-white">
We couldn’t find the page you’re looking for. Try checking the URL or
heading back to the home page.
</p>
<Button
className="w-full md:mx-auto md:w-96"
url="/"
label="Take me to the home page"
/>
</div>
</div>
);
}
Next find the export default NotFound and give a minimal change like this.
export default function NotFound({country = {isoCode: 'US'}}) {
const {data} = useShopQuery({
query: QUERY,
variables: {
country: country.isoCode,
numProductMetafields: 0,
numProductVariants: 250,
numProductMedia: 0,
numProductVariantMetafields: 0,
numProductVariantSellingPlanAllocations: 0,
numProductSellingPlanGroups: 0,
numProductSellingPlans: 0,
},
});
const products = data ? flattenConnection(data.products) : [];
return (
<Layout>
<NotFoundHero />
<div className="my-8">
<p className="mb-8 text-lg text-white font-medium uppercase">
Products you might like
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
{products.map((product) => (
<div key={product.id}>
<ProductCard product={product} />
</div>
))}
</div>
</div>
</Layout>
);
}
That's all for this post's introduction to Shopify Hydrogen. I hope that you find it useful. For part 2 of this post, we'll create a custom checkout with Hydrogen. Stay tuned!
Shopify App Development using React based Hydrogen Framework Hands-On Videos: https://youtube.com/playlist?list=PLucDsTg_UqT-fF-hxs21QtYcb7gI16iGa
Post a Comment