mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Update product image & price based on variant, and fixes
This commit is contained in:
parent
c75b0fc001
commit
180f815071
@ -8,6 +8,17 @@ import type { definitions } from '../api/definitions/store-content'
|
|||||||
|
|
||||||
import getSlug from './get-slug'
|
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) {
|
function normalizeProductOption(productOption: any) {
|
||||||
const {
|
const {
|
||||||
node: { entityId, values: { edges = [] } = {}, ...rest },
|
node: { entityId, values: { edges = [] } = {}, ...rest },
|
||||||
@ -20,43 +31,64 @@ function normalizeProductOption(productOption: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeImages = (productNode: ProductNode) => {
|
||||||
|
const output =
|
||||||
|
productNode.images.edges?.map(
|
||||||
|
({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||||
|
url: urlOriginal,
|
||||||
|
alt: altText,
|
||||||
|
...rest,
|
||||||
|
})
|
||||||
|
) || []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, 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 {
|
export function normalizeProduct(productNode: ProductNode): Product {
|
||||||
const {
|
const { entityId: id, productOptions, prices, path, variants } = productNode
|
||||||
entityId: id,
|
|
||||||
productOptions,
|
|
||||||
prices,
|
|
||||||
path,
|
|
||||||
images,
|
|
||||||
variants,
|
|
||||||
} = productNode
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(id),
|
id: String(id),
|
||||||
name: productNode.name,
|
name: productNode.name,
|
||||||
description: productNode.description,
|
description: productNode.description,
|
||||||
images:
|
images: normalizeImages(productNode),
|
||||||
images.edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
|
||||||
url: urlOriginal,
|
|
||||||
alt: altText,
|
|
||||||
...rest,
|
|
||||||
})) || [],
|
|
||||||
path: `/${getSlug(path)}`,
|
path: `/${getSlug(path)}`,
|
||||||
variants:
|
variants: normalizeVariants(variants),
|
||||||
variants.edges?.map(
|
|
||||||
({ node: { entityId, productOptions, ...rest } }: any) => ({
|
|
||||||
id: String(entityId),
|
|
||||||
options: productOptions?.edges
|
|
||||||
? productOptions.edges.map(normalizeProductOption)
|
|
||||||
: [],
|
|
||||||
...rest,
|
|
||||||
})
|
|
||||||
) || [],
|
|
||||||
options: productOptions?.edges?.map(normalizeProductOption) || [],
|
options: productOptions?.edges?.map(normalizeProductOption) || [],
|
||||||
slug: path?.replace(/^\/+|\/+$/g, ''),
|
slug: path?.replace(/^\/+|\/+$/g, ''),
|
||||||
price: {
|
price: normalizePrice(prices),
|
||||||
value: prices?.price.value,
|
|
||||||
currencyCode: prices?.price.currencyCode,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,9 @@ export const transformRequest = (req: NextApiRequest, path: string) => {
|
|||||||
body = JSON.stringify(req.body)
|
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,
|
headers,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
body,
|
body,
|
||||||
|
@ -19,7 +19,9 @@ import type {
|
|||||||
|
|
||||||
import { colorMap } from './colors'
|
import { colorMap } from './colors'
|
||||||
|
|
||||||
const money = ({ amount, currencyCode }: MoneyV2) => {
|
type MoneyProps = MoneyV2 & { retailPrice?: string | number }
|
||||||
|
|
||||||
|
const money = ({ amount, currencyCode }: MoneyProps) => {
|
||||||
return {
|
return {
|
||||||
value: +amount,
|
value: +amount,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
@ -67,6 +69,7 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
|
|||||||
selectedOptions,
|
selectedOptions,
|
||||||
sku,
|
sku,
|
||||||
title,
|
title,
|
||||||
|
image,
|
||||||
priceV2,
|
priceV2,
|
||||||
compareAtPriceV2,
|
compareAtPriceV2,
|
||||||
requiresShipping,
|
requiresShipping,
|
||||||
@ -77,7 +80,8 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
|
|||||||
id,
|
id,
|
||||||
name: title,
|
name: title,
|
||||||
sku,
|
sku,
|
||||||
price: +priceV2.amount,
|
image,
|
||||||
|
price: money({ ...priceV2, retailPrice: compareAtPriceV2?.amount }),
|
||||||
listPrice: +compareAtPriceV2?.amount,
|
listPrice: +compareAtPriceV2?.amount,
|
||||||
requiresShipping,
|
requiresShipping,
|
||||||
availableForSale,
|
availableForSale,
|
||||||
|
@ -34,6 +34,13 @@ const getProductQuery = /* GraphQL */ `
|
|||||||
id
|
id
|
||||||
title
|
title
|
||||||
sku
|
sku
|
||||||
|
image {
|
||||||
|
id
|
||||||
|
altText
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
availableForSale
|
availableForSale
|
||||||
requiresShipping
|
requiresShipping
|
||||||
selectedOptions {
|
selectedOptions {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
.root:hover {
|
.root:hover {
|
||||||
& .productImage {
|
& .productImage {
|
||||||
transform: scale(1.2625);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .header .name span,
|
& .header .name span,
|
||||||
@ -47,23 +47,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header .name {
|
.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;
|
transition-colors ease-in-out duration-500;
|
||||||
font-size: 2rem;
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .name span {
|
.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;
|
transition-colors ease-in-out duration-500;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
letter-spacing: inherit;
|
letter-spacing: inherit;
|
||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
-webkit-box-decoration-break: clone;
|
-webkit-box-decoration-break: clone;
|
||||||
|
line-height: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .price {
|
.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
|
font-semibold inline-block tracking-wide
|
||||||
transition-colors ease-in-out duration-500;
|
transition-colors ease-in-out duration-500;
|
||||||
}
|
}
|
||||||
@ -77,8 +79,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.imageContainer .productImage {
|
.imageContainer .productImage {
|
||||||
@apply transform transition-transform duration-500
|
@apply transform transition-transform duration-500 object-cover;
|
||||||
object-cover scale-120;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.root .wishlistButton {
|
.root .wishlistButton {
|
||||||
@ -87,7 +88,7 @@
|
|||||||
|
|
||||||
/* Variant Simple */
|
/* Variant Simple */
|
||||||
.simple .header .name {
|
.simple .header .name {
|
||||||
@apply pt-2 text-lg leading-10 -mt-1;
|
@apply pt-1 text-base leading-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple .header .price {
|
.simple .header .price {
|
||||||
@ -101,11 +102,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slim .header {
|
.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 {
|
.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;
|
font-bold text-xl break-words;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
47
site/components/product/ProductDetails/ProductDetails.tsx
Normal file
47
site/components/product/ProductDetails/ProductDetails.tsx
Normal 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
|
1
site/components/product/ProductDetails/index.ts
Normal file
1
site/components/product/ProductDetails/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ProductDetails'
|
@ -1,27 +1,18 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Swatch } from '@components/product'
|
import { Swatch } from '@components/product'
|
||||||
import type { ProductOption } from '@commerce/types/product'
|
import { useProduct } from '../context'
|
||||||
import { SelectedOptions } from '../helpers'
|
|
||||||
|
|
||||||
interface ProductOptionsProps {
|
const ProductOptions: React.FC = () => {
|
||||||
options: ProductOption[]
|
const { product, selectedOptions, setSelectedOptions } = useProduct()
|
||||||
selectedOptions: SelectedOptions
|
|
||||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductOptions: React.FC<ProductOptionsProps> = ({
|
|
||||||
options,
|
|
||||||
selectedOptions,
|
|
||||||
setSelectedOptions,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{options.map((opt) => (
|
{product.options.map((opt) => (
|
||||||
<div className="pb-4" key={opt.displayName}>
|
<div className="pb-4" key={opt.displayName}>
|
||||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||||
{opt.displayName}
|
{opt.displayName}
|
||||||
</h2>
|
</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) => {
|
{opt.values.map((v, i: number) => {
|
||||||
const active = selectedOptions[opt.displayName.toLowerCase()]
|
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||||
return (
|
return (
|
||||||
|
@ -1,63 +1,49 @@
|
|||||||
import s from './ProductSidebar.module.css'
|
import s from './ProductSidebar.module.css'
|
||||||
import { useAddItem } from '@framework/cart'
|
import { useAddItem } from '@framework/cart'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { ProductOptions } from '@components/product'
|
import { ProductOptions } from '@components/product'
|
||||||
import type { Product } from '@commerce/types/product'
|
|
||||||
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
|
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
|
||||||
import {
|
import { useProduct } from '../context'
|
||||||
getProductVariant,
|
|
||||||
selectDefaultOptionFromProduct,
|
|
||||||
SelectedOptions,
|
|
||||||
} from '../helpers'
|
|
||||||
import ErrorMessage from '@components/ui/ErrorMessage'
|
|
||||||
|
|
||||||
interface ProductSidebarProps {
|
interface ProductSidebarProps {
|
||||||
product: Product
|
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
const ProductSidebar: FC<ProductSidebarProps> = ({ className }) => {
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
|
|
||||||
|
const { product, variant, price } = useProduct()
|
||||||
const { openSidebar, setSidebarView } = useUI()
|
const { openSidebar, setSidebarView } = useUI()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<null | Error>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
|
||||||
}, [product])
|
|
||||||
|
|
||||||
const variant = getProductVariant(product, selectedOptions)
|
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
await addItem({
|
if (variant) {
|
||||||
productId: String(product.id),
|
await addItem({
|
||||||
variantId: String(variant ? variant.id : product.variants[0]?.id),
|
productId: String(product.id),
|
||||||
})
|
variantId: String(variant.id),
|
||||||
setSidebarView('CART_VIEW')
|
})
|
||||||
openSidebar()
|
setSidebarView('CART_VIEW')
|
||||||
|
openSidebar()
|
||||||
|
} else {
|
||||||
|
throw new Error('The variant selected is not available')
|
||||||
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
console.error(err)
|
setError(err.message)
|
||||||
setError({
|
|
||||||
...err,
|
|
||||||
message: 'Could not add item to cart. Please try again.',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<ProductOptions
|
<ProductOptions />
|
||||||
options={product.options}
|
|
||||||
selectedOptions={selectedOptions}
|
|
||||||
setSelectedOptions={setSelectedOptions}
|
|
||||||
/>
|
|
||||||
<Text
|
<Text
|
||||||
className="pb-4 break-words w-full max-w-xl"
|
className="pb-4 break-words w-full max-w-xl"
|
||||||
html={product.descriptionHtml || product.description}
|
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 className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{error && <ErrorMessage error={error} className="my-5" />}
|
|
||||||
{process.env.COMMERCE_CART_ENABLED && (
|
{process.env.COMMERCE_CART_ENABLED && (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Add to Cart"
|
aria-label="Add to Cart"
|
||||||
@ -75,13 +60,19 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
|||||||
className={s.button}
|
className={s.button}
|
||||||
onClick={addToCart}
|
onClick={addToCart}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={variant?.availableForSale === false}
|
disabled={loading || !variant || variant.availableForSale === false}
|
||||||
>
|
>
|
||||||
{variant?.availableForSale === false
|
{!variant || variant.availableForSale === false ? (
|
||||||
? 'Not Available'
|
'Not Available'
|
||||||
: 'Add To Cart'}
|
) : (
|
||||||
|
<>Add To Cart - {price}</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red py-3 text-sm font-bold">{error}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Collapse title="Care">
|
<Collapse title="Care">
|
||||||
|
@ -1,29 +1,24 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply relative w-full h-full select-none;
|
@apply flex flex-col relative select-none w-full;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
@apply relative h-full transition-opacity duration-150;
|
@apply flex-1;
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider.show {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.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: 125px;
|
||||||
width: calc(100% / 3);
|
width: calc(100% / 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb.selected {
|
.thumb.selected {
|
||||||
@apply bg-white;
|
@apply bg-white/30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb img {
|
.thumb img {
|
||||||
height: 85% !important;
|
@apply w-full h-full max-h-full object-cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album {
|
.album {
|
||||||
@ -44,10 +39,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
.thumb:hover {
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.album {
|
.album {
|
||||||
height: 182px;
|
height: 182px;
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ import cn from 'clsx'
|
|||||||
import { a } from '@react-spring/web'
|
import { a } from '@react-spring/web'
|
||||||
import s from './ProductSlider.module.css'
|
import s from './ProductSlider.module.css'
|
||||||
import ProductSliderControl from '../ProductSliderControl'
|
import ProductSliderControl from '../ProductSliderControl'
|
||||||
|
import { useProduct } from '../context'
|
||||||
|
import { Image as ProductImage } from '@commerce/types/common'
|
||||||
|
|
||||||
interface ProductSliderProps {
|
interface ProductSliderProps {
|
||||||
children?: React.ReactNode[]
|
children?: React.ReactNode[]
|
||||||
@ -20,19 +22,18 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
|
const { product, variant } = useProduct()
|
||||||
const [currentSlide, setCurrentSlide] = useState(0)
|
const [currentSlide, setCurrentSlide] = useState(0)
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
|
||||||
const sliderContainerRef = useRef<HTMLDivElement>(null)
|
const sliderContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const thumbsContainerRef = useRef<HTMLDivElement>(null)
|
const thumbsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [ref, slider] = useKeenSlider<HTMLDivElement>({
|
const [ref, slider] = useKeenSlider<HTMLDivElement>({
|
||||||
loop: true,
|
loop: true,
|
||||||
slides: { perView: 1 },
|
slides: { perView: 1 },
|
||||||
created: () => setIsMounted(true),
|
|
||||||
slideChanged(s) {
|
slideChanged(s) {
|
||||||
const slideNumber = s.track.details.rel
|
const slideNumber = s.track.details.rel
|
||||||
setCurrentSlide(slideNumber)
|
setCurrentSlide(slideNumber)
|
||||||
|
|
||||||
if (thumbsContainerRef.current) {
|
if (thumbsContainerRef.current) {
|
||||||
const $el = document.getElementById(`thumb-${slideNumber}`)
|
const $el = document.getElementById(`thumb-${slideNumber}`)
|
||||||
if (slideNumber >= 3) {
|
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
|
// Stop the history navigation gesture on touch devices
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const preventNavigation = (event: TouchEvent) => {
|
const preventNavigation = (event: TouchEvent) => {
|
||||||
@ -74,15 +87,17 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onPrev = React.useCallback(() => slider.current?.prev(), [slider])
|
const onPrev = React.useCallback(() => {
|
||||||
const onNext = React.useCallback(() => slider.current?.next(), [slider])
|
slider.current?.prev()
|
||||||
|
}, [slider])
|
||||||
|
|
||||||
|
const onNext = React.useCallback(() => {
|
||||||
|
slider.current?.next()
|
||||||
|
}, [slider])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(s.root, className)} ref={sliderContainerRef}>
|
<div className={cn(s.root, className)} ref={sliderContainerRef}>
|
||||||
<div
|
<div ref={ref} className={cn(s.slider, 'keen-slider')}>
|
||||||
ref={ref}
|
|
||||||
className={cn(s.slider, { [s.show]: isMounted }, 'keen-slider')}
|
|
||||||
>
|
|
||||||
{slider && <ProductSliderControl onPrev={onPrev} onNext={onNext} />}
|
{slider && <ProductSliderControl onPrev={onPrev} onNext={onNext} />}
|
||||||
{Children.map(children, (child) => {
|
{Children.map(children, (child) => {
|
||||||
// Add the keen-slider__slide className to children
|
// Add the keen-slider__slide className to children
|
||||||
@ -109,6 +124,8 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
...child,
|
...child,
|
||||||
props: {
|
props: {
|
||||||
...child.props,
|
...child.props,
|
||||||
|
width: 132,
|
||||||
|
height: 82,
|
||||||
className: cn(child.props.className, s.thumb, {
|
className: cn(child.props.className, s.thumb, {
|
||||||
[s.selected]: currentSlide === idx,
|
[s.selected]: currentSlide === idx,
|
||||||
}),
|
}),
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root .name {
|
.root .name {
|
||||||
@apply pt-0 max-w-full w-full leading-extra-loose;
|
@apply pt-0 max-w-full w-full;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
line-height: 2.1em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root .name span {
|
.root .name span {
|
||||||
|
@ -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 {
|
.relatedProductsGrid {
|
||||||
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7;
|
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7 m-0;
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,88 +1,44 @@
|
|||||||
import cn from 'clsx'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
import { FC } from 'react'
|
|
||||||
|
import type { FC } from 'react'
|
||||||
import type { Product } from '@commerce/types/product'
|
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 { SEO } from '@components/common'
|
||||||
import ProductSidebar from '../ProductSidebar'
|
import { ProductProvider } from '../context'
|
||||||
import ProductTag from '../ProductTag'
|
import { Container, Text } from '@components/ui'
|
||||||
|
import { ProductCard } from '@components/product'
|
||||||
|
|
||||||
|
import ProductDetails from '../ProductDetails/ProductDetails'
|
||||||
|
|
||||||
interface ProductViewProps {
|
interface ProductViewProps {
|
||||||
product: Product
|
product: Product
|
||||||
relatedProducts: Product[]
|
relatedProducts: Product[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
||||||
const { price } = usePrice({
|
|
||||||
amount: product.price.value,
|
|
||||||
baseAmount: product.price.retailPrice,
|
|
||||||
currencyCode: product.price.currencyCode!,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container className="max-w-none w-full" clean>
|
<Container className="max-w-none w-full" clean>
|
||||||
<div className={cn(s.root, 'fit')}>
|
<ProductProvider product={product}>
|
||||||
<div className={cn(s.main, 'fit')}>
|
<ProductDetails />
|
||||||
<ProductTag
|
</ProductProvider>
|
||||||
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>
|
|
||||||
|
|
||||||
<ProductSidebar
|
|
||||||
key={product.id}
|
|
||||||
product={product}
|
|
||||||
className={s.sidebar}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr className="mt-7 border-accent-2" />
|
<hr className="mt-7 border-accent-2" />
|
||||||
|
|
||||||
<section className="py-12 px-6 mb-10">
|
<section className="py-12 px-6 mb-10">
|
||||||
<Text variant="sectionHeading">Related Products</Text>
|
<Text variant="sectionHeading">Related Products</Text>
|
||||||
<div className={s.relatedProductsGrid}>
|
<div className={s.relatedProductsGrid}>
|
||||||
{relatedProducts.map((p) => (
|
{relatedProducts.map((p) => (
|
||||||
<div
|
<div
|
||||||
key={p.path}
|
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
|
<ProductCard
|
||||||
|
className="animated fadeIn"
|
||||||
noNameTag
|
noNameTag
|
||||||
product={p}
|
product={p}
|
||||||
key={p.path}
|
key={p.path}
|
||||||
variant="simple"
|
variant="simple"
|
||||||
className="animated fadeIn"
|
|
||||||
imgProps={{
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -99,7 +55,7 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
|||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: product.images[0]?.url!,
|
url: product.images[0]?.url!,
|
||||||
width: '800',
|
width: '600',
|
||||||
height: '600',
|
height: '600',
|
||||||
alt: product.name,
|
alt: product.name,
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
.swatch {
|
.swatch {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
composes: root from '@components/ui/Button/Button.module.css';
|
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
|
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);
|
margin-right: calc(0.75rem - 1px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
@ -18,7 +18,7 @@ const Swatch: React.FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({
|
|||||||
className,
|
className,
|
||||||
color = '',
|
color = '',
|
||||||
label = null,
|
label = null,
|
||||||
variant = 'size',
|
variant,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
variant = variant?.toLowerCase()
|
variant = variant?.toLowerCase()
|
||||||
|
71
site/components/product/context.tsx
Normal file
71
site/components/product/context.tsx
Normal 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
|
||||||
|
}
|
@ -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>
|
export type SelectedOptions = Record<string, string | null>
|
||||||
import { Dispatch, SetStateAction } from 'react'
|
import { Dispatch, SetStateAction } from 'react'
|
||||||
|
|
||||||
@ -22,11 +22,19 @@ export function selectDefaultOptionFromProduct(
|
|||||||
product: Product,
|
product: Product,
|
||||||
updater: Dispatch<SetStateAction<SelectedOptions>>
|
updater: Dispatch<SetStateAction<SelectedOptions>>
|
||||||
) {
|
) {
|
||||||
// Selects the default option
|
// Get the first available option or the first option
|
||||||
product.variants[0]?.options?.forEach((v) => {
|
const variant =
|
||||||
updater((choices) => ({
|
product.variants.find((variant) => variant.availableForSale) ||
|
||||||
...choices,
|
product.variants[0]
|
||||||
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
|
|
||||||
}))
|
// 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)
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
|||||||
{/* Products */}
|
{/* Products */}
|
||||||
<div className="col-span-8 order-3 lg:order-none">
|
<div className="col-span-8 order-3 lg:order-none">
|
||||||
{(q || activeCategory || activeBrand) && (
|
{(q || activeCategory || activeBrand) && (
|
||||||
<div className="mb-12 transition ease-in duration-75">
|
<div className="mt-4">
|
||||||
{data ? (
|
{data ? (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
@ -316,7 +316,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data ? (
|
{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) => (
|
{data.products.map((product: Product) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
variant="simple"
|
variant="simple"
|
||||||
@ -331,7 +331,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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) => (
|
{rangeMap(12, (i) => (
|
||||||
<Skeleton key={i}>
|
<Skeleton key={i}>
|
||||||
<div className="w-60 h-60" />
|
<div className="w-60 h-60" />
|
||||||
|
@ -14,37 +14,44 @@ export async function getStaticProps({
|
|||||||
locales,
|
locales,
|
||||||
preview,
|
preview,
|
||||||
}: GetStaticPropsContext<{ slug: string }>) {
|
}: GetStaticPropsContext<{ slug: string }>) {
|
||||||
const config = { locale, locales }
|
try {
|
||||||
const pagesPromise = commerce.getAllPages({ config, preview })
|
const config = { locale, locales }
|
||||||
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
const pagesPromise = commerce.getAllPages({ config, preview })
|
||||||
const productPromise = commerce.getProduct({
|
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
||||||
variables: { slug: params!.slug },
|
const productPromise = commerce.getProduct({
|
||||||
config,
|
variables: { slug: params!.slug },
|
||||||
preview,
|
config,
|
||||||
})
|
preview,
|
||||||
const allProductsPromise = commerce.getAllProducts({
|
})
|
||||||
variables: { first: 4 },
|
const allProductsPromise = commerce.getAllProducts({
|
||||||
config,
|
variables: { first: 4 },
|
||||||
preview,
|
config,
|
||||||
})
|
preview,
|
||||||
|
})
|
||||||
|
|
||||||
const { pages } = await pagesPromise
|
const { pages } = await pagesPromise
|
||||||
const { categories } = await siteInfoPromise
|
const { categories } = await siteInfoPromise
|
||||||
const { product } = await productPromise
|
const { product } = await productPromise
|
||||||
const { products: relatedProducts } = await allProductsPromise
|
const { products: relatedProducts } = await allProductsPromise
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new Error(`Product with slug '${params!.slug}' not found`)
|
throw new Error(`Product with slug '${params!.slug}' not found`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
pages,
|
pages,
|
||||||
product,
|
product,
|
||||||
relatedProducts,
|
relatedProducts,
|
||||||
categories,
|
categories,
|
||||||
},
|
},
|
||||||
revalidate: 200,
|
revalidate: 200,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
"@components/*": ["components/*"],
|
"@components/*": ["components/*"],
|
||||||
"@commerce": ["../packages/commerce/src"],
|
"@commerce": ["../packages/commerce/src"],
|
||||||
"@commerce/*": ["../packages/commerce/src/*"],
|
"@commerce/*": ["../packages/commerce/src/*"],
|
||||||
"@framework": ["../packages/local/src"],
|
"@framework": ["../packages/shopify/src"],
|
||||||
"@framework/*": ["../packages/local/src/*"]
|
"@framework/*": ["../packages/shopify/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user