Update product image & price based on variant, and fixes

This commit is contained in:
Catalin Pinte 2022-11-09 12:37:00 +02:00
parent c75b0fc001
commit 180f815071
22 changed files with 405 additions and 279 deletions

View File

@ -8,6 +8,17 @@ import type { definitions } from '../api/definitions/store-content'
import getSlug from './get-slug'
const normalizePrice = (prices: ProductNode['prices']) => ({
value: prices?.price.value || 0,
currencyCode: prices?.price.currencyCode || 'USD',
...(prices?.salePrice?.value && {
salePrice: prices.salePrice.value,
}),
...(prices?.retailPrice?.value && {
retailPrice: prices.retailPrice.value,
}),
})
function normalizeProductOption(productOption: any) {
const {
node: { entityId, values: { edges = [] } = {}, ...rest },
@ -20,43 +31,64 @@ function normalizeProductOption(productOption: any) {
}
}
export function normalizeProduct(productNode: ProductNode): Product {
const {
entityId: id,
productOptions,
prices,
path,
images,
variants,
} = productNode
return {
id: String(id),
name: productNode.name,
description: productNode.description,
images:
images.edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
const normalizeImages = (productNode: ProductNode) => {
const output =
productNode.images.edges?.map(
({ node: { urlOriginal, altText, ...rest } }: any) => ({
url: urlOriginal,
alt: altText,
...rest,
})) || [],
path: `/${getSlug(path)}`,
variants:
})
) || []
/**
* Add the variants images to the product images, because the variants images are not included in the product images
*/
productNode.variants.edges?.forEach(({ node: variant }: any) => {
if (variant.defaultImage?.urlOriginal) {
output.push({
url: variant.defaultImage.urlOriginal,
alt: variant.defaultImage.altText,
})
}
})
return output
}
const normalizeVariants = (variants: any) =>
variants.edges?.map(
({ node: { entityId, productOptions, ...rest } }: any) => ({
({
node: { entityId, productOptions, prices, defaultImage, ...rest },
}: any) => ({
id: String(entityId),
...(defaultImage && {
image: {
url: defaultImage.urlOriginal,
alt: defaultImage.altText,
},
}),
...(prices && { price: normalizePrice(prices) }),
options: productOptions?.edges
? productOptions.edges.map(normalizeProductOption)
: [],
...rest,
})
) || [],
) || []
export function normalizeProduct(productNode: ProductNode): Product {
const { entityId: id, productOptions, prices, path, variants } = productNode
return {
id: String(id),
name: productNode.name,
description: productNode.description,
images: normalizeImages(productNode),
path: `/${getSlug(path)}`,
variants: normalizeVariants(variants),
options: productOptions?.edges?.map(normalizeProductOption) || [],
slug: path?.replace(/^\/+|\/+$/g, ''),
price: {
value: prices?.price.value,
currencyCode: prices?.price.currencyCode,
},
price: normalizePrice(prices),
}
}

View File

@ -44,7 +44,9 @@ export const transformRequest = (req: NextApiRequest, path: string) => {
body = JSON.stringify(req.body)
}
return new NextRequest(`https://${req.headers.host}/api/commerce/${path}`, {
const url = new URL(req.url || '/', `https://${req.headers.host}`)
return new NextRequest(url, {
headers,
method: req.method,
body,

View File

@ -19,7 +19,9 @@ import type {
import { colorMap } from './colors'
const money = ({ amount, currencyCode }: MoneyV2) => {
type MoneyProps = MoneyV2 & { retailPrice?: string | number }
const money = ({ amount, currencyCode }: MoneyProps) => {
return {
value: +amount,
currencyCode,
@ -67,6 +69,7 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
selectedOptions,
sku,
title,
image,
priceV2,
compareAtPriceV2,
requiresShipping,
@ -77,7 +80,8 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
id,
name: title,
sku,
price: +priceV2.amount,
image,
price: money({ ...priceV2, retailPrice: compareAtPriceV2?.amount }),
listPrice: +compareAtPriceV2?.amount,
requiresShipping,
availableForSale,

View File

@ -34,6 +34,13 @@ const getProductQuery = /* GraphQL */ `
id
title
sku
image {
id
altText
url
width
height
}
availableForSale
requiresShipping
selectedOptions {

View File

@ -7,7 +7,7 @@
.root:hover {
& .productImage {
transform: scale(1.2625);
transform: scale(1.1);
}
& .header .name span,
@ -47,23 +47,25 @@
}
.header .name {
@apply pt-0 max-w-full w-full leading-extra-loose
@apply pt-1 max-w-full w-full leading-extra-loose
transition-colors ease-in-out duration-500;
font-size: 2rem;
font-size: 1rem;
line-height: 1;
letter-spacing: 0.4px;
}
.header .name span {
@apply py-4 px-6 bg-primary text-primary font-bold
@apply py-2 px-3 bg-primary text-primary font-bold
transition-colors ease-in-out duration-500;
font-size: inherit;
letter-spacing: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
line-height: 1 !important;
}
.header .price {
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
@apply pt-1.5 pb-2 px-3 text-sm bg-primary text-accent-9
font-semibold inline-block tracking-wide
transition-colors ease-in-out duration-500;
}
@ -77,8 +79,7 @@
}
.imageContainer .productImage {
@apply transform transition-transform duration-500
object-cover scale-120;
@apply transform transition-transform duration-500 object-cover;
}
.root .wishlistButton {
@ -87,7 +88,7 @@
/* Variant Simple */
.simple .header .name {
@apply pt-2 text-lg leading-10 -mt-1;
@apply pt-1 text-base leading-6;
}
.simple .header .price {
@ -101,11 +102,11 @@
}
.slim .header {
@apply absolute inset-0 flex items-center justify-end mr-8 z-20;
@apply absolute inset-0 flex text-center items-center justify-end mr-8 z-20;
}
.slim span {
@apply bg-accent-9 text-accent-0 inline-block p-3
@apply bg-accent-9 text-accent-0 inline-block px-3 py-2.5
font-bold text-xl break-words;
}

View File

@ -0,0 +1,56 @@
.root {
@apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
min-height: auto;
}
.main {
@apply relative px-0 pb-0 box-border flex flex-col col-span-1;
min-height: 500px;
}
.sidebar {
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
}
.sliderContainer {
@apply flex flex-1 overflow-x-hidden bg-violet items-center justify-center;
}
.imageContainer {
@apply text-center h-full relative flex-shrink-0;
}
.imageContainer > img {
@apply mx-auto;
}
.sliderContainer .img {
@apply w-full h-full max-h-full object-cover transition duration-500 ease-in-out;
}
.button {
width: 100%;
}
.wishlistButton {
@apply absolute z-30 top-0 right-0;
}
@screen lg {
.root {
@apply grid-cols-12;
}
.main {
@apply mx-0 col-span-8;
min-height: 782px;
}
.sidebar {
@apply col-span-4 py-6;
}
.imageContainer {
max-height: 600px;
}
}

View File

@ -0,0 +1,47 @@
import cn from 'clsx'
import { useProduct } from '../context'
import { WishlistButton } from '@components/wishlist'
import Image from 'next/image'
import ProductTag from '../ProductTag'
import ProductSlider from '../ProductSlider'
import ProductSidebar from '../ProductSidebar'
import s from './ProductDetails.module.css'
const ProductDetails = () => {
const { product, variant, price } = useProduct()
return (
<div className={cn(s.root, 'fit')}>
<div className={cn(s.main, 'fit')}>
<ProductTag name={product.name} price={price} fontSize={32} />
<div className={s.sliderContainer}>
<ProductSlider key={product.id}>
{product.images.map((image, i) => (
<div key={image.url} className={s.imageContainer}>
<Image
src={image.url!}
alt={image.alt || 'Product Image'}
width={600}
height={600}
quality="85"
priority={i === 0}
/>
</div>
))}
</ProductSlider>
</div>
{process.env.COMMERCE_WISHLIST_ENABLED && variant && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={variant}
/>
)}
</div>
<ProductSidebar key={product.id} className={s.sidebar} />
</div>
)
}
export default ProductDetails

View File

@ -0,0 +1 @@
export { default } from './ProductDetails'

View File

@ -1,27 +1,18 @@
import { memo } from 'react'
import { Swatch } from '@components/product'
import type { ProductOption } from '@commerce/types/product'
import { SelectedOptions } from '../helpers'
import { useProduct } from '../context'
interface ProductOptionsProps {
options: ProductOption[]
selectedOptions: SelectedOptions
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
}
const ProductOptions: React.FC = () => {
const { product, selectedOptions, setSelectedOptions } = useProduct()
const ProductOptions: React.FC<ProductOptionsProps> = ({
options,
selectedOptions,
setSelectedOptions,
}) => {
return (
<div>
{options.map((opt) => (
{product.options.map((opt) => (
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium text-sm tracking-wide">
{opt.displayName}
</h2>
<div role="listbox" className="flex flex-row py-4">
<div role="listbox" className="flex flex-row flex-wrap gap-3 py-4">
{opt.values.map((v, i: number) => {
const active = selectedOptions[opt.displayName.toLowerCase()]
return (

View File

@ -1,63 +1,49 @@
import s from './ProductSidebar.module.css'
import { useAddItem } from '@framework/cart'
import { FC, useEffect, useState } from 'react'
import { FC, useState } from 'react'
import { ProductOptions } from '@components/product'
import type { Product } from '@commerce/types/product'
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
import {
getProductVariant,
selectDefaultOptionFromProduct,
SelectedOptions,
} from '../helpers'
import ErrorMessage from '@components/ui/ErrorMessage'
import { useProduct } from '../context'
interface ProductSidebarProps {
product: Product
className?: string
}
const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
const ProductSidebar: FC<ProductSidebarProps> = ({ className }) => {
const addItem = useAddItem()
const { product, variant, price } = useProduct()
const { openSidebar, setSidebarView } = useUI()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<null | Error>(null)
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
const [error, setError] = useState<string | null>(null)
useEffect(() => {
selectDefaultOptionFromProduct(product, setSelectedOptions)
}, [product])
const variant = getProductVariant(product, selectedOptions)
const addToCart = async () => {
setLoading(true)
setError(null)
try {
if (variant) {
await addItem({
productId: String(product.id),
variantId: String(variant ? variant.id : product.variants[0]?.id),
variantId: String(variant.id),
})
setSidebarView('CART_VIEW')
openSidebar()
} else {
throw new Error('The variant selected is not available')
}
setLoading(false)
} catch (err) {
console.error(err)
setLoading(false)
if (err instanceof Error) {
console.error(err)
setError({
...err,
message: 'Could not add item to cart. Please try again.',
})
setError(err.message)
}
}
}
return (
<div className={className}>
<ProductOptions
options={product.options}
selectedOptions={selectedOptions}
setSelectedOptions={setSelectedOptions}
/>
<ProductOptions />
<Text
className="pb-4 break-words w-full max-w-xl"
html={product.descriptionHtml || product.description}
@ -67,7 +53,6 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
</div>
<div>
{error && <ErrorMessage error={error} className="my-5" />}
{process.env.COMMERCE_CART_ENABLED && (
<Button
aria-label="Add to Cart"
@ -75,13 +60,19 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
className={s.button}
onClick={addToCart}
loading={loading}
disabled={variant?.availableForSale === false}
disabled={loading || !variant || variant.availableForSale === false}
>
{variant?.availableForSale === false
? 'Not Available'
: 'Add To Cart'}
{!variant || variant.availableForSale === false ? (
'Not Available'
) : (
<>Add To Cart - {price}</>
)}
</Button>
)}
{error && (
<div className="text-red py-3 text-sm font-bold">{error}</div>
)}
</div>
<div className="mt-6">
<Collapse title="Care">

View File

@ -1,29 +1,24 @@
.root {
@apply relative w-full h-full select-none;
@apply flex flex-col relative select-none w-full;
overflow: hidden;
}
.slider {
@apply relative h-full transition-opacity duration-150;
opacity: 0;
}
.slider.show {
opacity: 1;
@apply flex-1;
}
.thumb {
@apply overflow-hidden inline-block cursor-pointer h-full;
@apply overflow-hidden inline-block cursor-pointer h-full transition duration-300 md:hover:scale-105;
width: 125px;
width: calc(100% / 3);
}
.thumb.selected {
@apply bg-white;
@apply bg-white/30;
}
.thumb img {
height: 85% !important;
@apply w-full h-full max-h-full object-cover;
}
.album {
@ -44,10 +39,6 @@
}
@screen md {
.thumb:hover {
transform: scale(1.02);
}
.album {
height: 182px;
}

View File

@ -10,6 +10,8 @@ import cn from 'clsx'
import { a } from '@react-spring/web'
import s from './ProductSlider.module.css'
import ProductSliderControl from '../ProductSliderControl'
import { useProduct } from '../context'
import { Image as ProductImage } from '@commerce/types/common'
interface ProductSliderProps {
children?: React.ReactNode[]
@ -20,19 +22,18 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
children,
className = '',
}) => {
const { product, variant } = useProduct()
const [currentSlide, setCurrentSlide] = useState(0)
const [isMounted, setIsMounted] = useState(false)
const sliderContainerRef = useRef<HTMLDivElement>(null)
const thumbsContainerRef = useRef<HTMLDivElement>(null)
const [ref, slider] = useKeenSlider<HTMLDivElement>({
loop: true,
slides: { perView: 1 },
created: () => setIsMounted(true),
slideChanged(s) {
const slideNumber = s.track.details.rel
setCurrentSlide(slideNumber)
if (thumbsContainerRef.current) {
const $el = document.getElementById(`thumb-${slideNumber}`)
if (slideNumber >= 3) {
@ -44,6 +45,18 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
},
})
useEffect(() => {
const index = product.images.findIndex((image: ProductImage) => {
return image.url === variant?.image?.url
})
if (index !== -1) {
slider.current?.moveToIdx(index, false, {
duration: 0,
})
}
}, [variant, product, slider])
// Stop the history navigation gesture on touch devices
useEffect(() => {
const preventNavigation = (event: TouchEvent) => {
@ -74,15 +87,17 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
}
}, [])
const onPrev = React.useCallback(() => slider.current?.prev(), [slider])
const onNext = React.useCallback(() => slider.current?.next(), [slider])
const onPrev = React.useCallback(() => {
slider.current?.prev()
}, [slider])
const onNext = React.useCallback(() => {
slider.current?.next()
}, [slider])
return (
<div className={cn(s.root, className)} ref={sliderContainerRef}>
<div
ref={ref}
className={cn(s.slider, { [s.show]: isMounted }, 'keen-slider')}
>
<div ref={ref} className={cn(s.slider, 'keen-slider')}>
{slider && <ProductSliderControl onPrev={onPrev} onNext={onNext} />}
{Children.map(children, (child) => {
// Add the keen-slider__slide className to children
@ -109,6 +124,8 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
...child,
props: {
...child.props,
width: 132,
height: 82,
className: cn(child.props.className, s.thumb, {
[s.selected]: currentSlide === idx,
}),

View File

@ -4,10 +4,10 @@
}
.root .name {
@apply pt-0 max-w-full w-full leading-extra-loose;
@apply pt-0 max-w-full w-full;
font-size: 2rem;
letter-spacing: 0.4px;
line-height: 2.1em;
line-height: 1.5em;
}
.root .name span {

View File

@ -1,59 +1,3 @@
.root {
@apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
min-height: auto;
}
.main {
@apply relative px-0 pb-0 box-border flex flex-col col-span-1;
min-height: 500px;
}
.sidebar {
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
}
.sliderContainer {
@apply flex items-center justify-center overflow-x-hidden bg-violet;
}
.imageContainer {
@apply text-center h-full relative;
}
.imageContainer > span {
height: 100% !important;
}
.sliderContainer .img {
@apply w-full h-full max-h-full object-cover;
}
.button {
width: 100%;
}
.wishlistButton {
@apply absolute z-30 top-0 right-0;
}
.relatedProductsGrid {
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7;
}
@screen lg {
.root {
@apply grid-cols-12;
}
.main {
@apply mx-0 col-span-8;
}
.sidebar {
@apply col-span-4 py-6;
}
.imageContainer {
max-height: 600px;
}
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7 m-0;
}

View File

@ -1,88 +1,44 @@
import cn from 'clsx'
import Image from 'next/image'
import s from './ProductView.module.css'
import { FC } from 'react'
import type { FC } from 'react'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import { WishlistButton } from '@components/wishlist'
import { ProductSlider, ProductCard } from '@components/product'
import { Container, Text } from '@components/ui'
import { SEO } from '@components/common'
import ProductSidebar from '../ProductSidebar'
import ProductTag from '../ProductTag'
import { ProductProvider } from '../context'
import { Container, Text } from '@components/ui'
import { ProductCard } from '@components/product'
import ProductDetails from '../ProductDetails/ProductDetails'
interface ProductViewProps {
product: Product
relatedProducts: Product[]
}
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!,
})
return (
<>
<Container className="max-w-none w-full" clean>
<div className={cn(s.root, 'fit')}>
<div className={cn(s.main, 'fit')}>
<ProductTag
name={product.name}
price={`${price} ${product.price?.currencyCode}`}
fontSize={32}
/>
<div className={s.sliderContainer}>
<ProductSlider key={product.id}>
{product.images.map((image, i) => (
<div key={image.url} className={s.imageContainer}>
<Image
className={s.img}
src={image.url!}
alt={image.alt || 'Product Image'}
width={600}
height={600}
priority={i === 0}
quality="85"
/>
</div>
))}
</ProductSlider>
</div>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]}
/>
)}
</div>
<ProductProvider product={product}>
<ProductDetails />
</ProductProvider>
<ProductSidebar
key={product.id}
product={product}
className={s.sidebar}
/>
</div>
<hr className="mt-7 border-accent-2" />
<section className="py-12 px-6 mb-10">
<Text variant="sectionHeading">Related Products</Text>
<div className={s.relatedProductsGrid}>
{relatedProducts.map((p) => (
<div
key={p.path}
className="animated fadeIn bg-accent-0 border border-accent-2"
className="bg-accent-0 border border-accent-2 flex flex-1 h-full"
>
<ProductCard
className="animated fadeIn"
noNameTag
product={p}
key={p.path}
variant="simple"
className="animated fadeIn"
imgProps={{
width: 300,
height: 300,
}}
/>
</div>
))}
@ -99,7 +55,7 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
images: [
{
url: product.images[0]?.url!,
width: '800',
width: '600',
height: '600',
alt: product.name,
},

View File

@ -1,9 +1,9 @@
.swatch {
box-sizing: border-box;
composes: root from '@components/ui/Button/Button.module.css';
@apply h-10 w-10 bg-primary text-primary rounded-full mr-3 inline-flex
@apply h-10 w-10 bg-primary text-primary rounded-full inline-flex
items-center justify-center cursor-pointer transition duration-150 ease-in-out
p-0 shadow-none border-accent-3 border box-border select-none;
p-0 shadow-none border-accent-3 border box-border select-none !mr-0;
margin-right: calc(0.75rem - 1px);
overflow: hidden;
width: 48px;

View File

@ -18,7 +18,7 @@ const Swatch: React.FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({
className,
color = '',
label = null,
variant = 'size',
variant,
...props
}) => {
variant = variant?.toLowerCase()

View File

@ -0,0 +1,71 @@
import { useMemo, useState, useEffect, useContext, createContext } from 'react'
import type { FC, ReactNode, Dispatch, SetStateAction } from 'react'
import type { SelectedOptions } from './helpers'
import type { Product, ProductVariant } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import { getProductVariant, selectDefaultOptionFromProduct } from './helpers'
export interface ProductContextValue {
product: Product
price: string
variant?: ProductVariant
selectedOptions: SelectedOptions
setSelectedOptions: Dispatch<SetStateAction<SelectedOptions>>
}
export const ProductContext = createContext<ProductContextValue | null>(null)
ProductContext.displayName = 'ProductContext'
type ProductProviderProps = {
product: Product
children?: ReactNode
}
export const ProductProvider: FC<ProductProviderProps> = ({
product,
children,
}) => {
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
useEffect(
() => selectDefaultOptionFromProduct(product, setSelectedOptions),
[product]
)
const variant = useMemo(
() => getProductVariant(product, selectedOptions),
[product, selectedOptions]
)
const { price } = usePrice({
amount: variant?.price?.value || product.price.value,
baseAmount: variant?.price?.retailPrice || product.price.retailPrice,
currencyCode: variant?.price?.currencyCode || product.price.currencyCode!,
})
const value = useMemo(
() => ({
price,
product,
variant,
selectedOptions,
setSelectedOptions,
}),
[price, product, selectedOptions, variant]
)
return (
<ProductContext.Provider value={value}>{children}</ProductContext.Provider>
)
}
export const useProduct = () => {
const context = useContext(ProductContext) as ProductContextValue
if (context === undefined) {
throw new Error(`useProduct must be used within a ProductProvider`)
}
return context
}

View File

@ -1,4 +1,4 @@
import type { Product } from '@commerce/types/product'
import type { Product } from '@vercel/commerce/types/product'
export type SelectedOptions = Record<string, string | null>
import { Dispatch, SetStateAction } from 'react'
@ -22,11 +22,19 @@ export function selectDefaultOptionFromProduct(
product: Product,
updater: Dispatch<SetStateAction<SelectedOptions>>
) {
// Selects the default option
product.variants[0]?.options?.forEach((v) => {
updater((choices) => ({
...choices,
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
}))
})
// Get the first available option or the first option
const variant =
product.variants.find((variant) => variant.availableForSale) ||
product.variants[0]
// Reset the selectedOptions and set the default option from the available variant
const newValue: SelectedOptions = {}
if (variant) {
for (const c of variant.options) {
newValue[c.displayName.toLowerCase()] = c.values[0].label.toLowerCase()
}
}
updater(newValue)
}

View File

@ -273,7 +273,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
{/* Products */}
<div className="col-span-8 order-3 lg:order-none">
{(q || activeCategory || activeBrand) && (
<div className="mb-12 transition ease-in duration-75">
<div className="mt-4">
{data ? (
<>
<span
@ -316,7 +316,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
</div>
)}
{data ? (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-4">
{data.products.map((product: Product) => (
<ProductCard
variant="simple"
@ -331,7 +331,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
))}
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-4">
{rangeMap(12, (i) => (
<Skeleton key={i}>
<div className="w-60 h-60" />

View File

@ -14,6 +14,7 @@ export async function getStaticProps({
locales,
preview,
}: GetStaticPropsContext<{ slug: string }>) {
try {
const config = { locale, locales }
const pagesPromise = commerce.getAllPages({ config, preview })
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
@ -46,6 +47,12 @@ export async function getStaticProps({
},
revalidate: 200,
}
} catch (error) {
console.log(error)
return {
notFound: true,
}
}
}
export async function getStaticPaths({ locales }: GetStaticPathsContext) {

View File

@ -23,8 +23,8 @@
"@components/*": ["components/*"],
"@commerce": ["../packages/commerce/src"],
"@commerce/*": ["../packages/commerce/src/*"],
"@framework": ["../packages/local/src"],
"@framework/*": ["../packages/local/src/*"]
"@framework": ["../packages/shopify/src"],
"@framework/*": ["../packages/shopify/src/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],