mirror of
https://github.com/vercel/commerce.git
synced 2025-06-08 01:06:59 +00:00
Perf changes
This commit is contained in:
parent
1a729f9e72
commit
7ef0e2273e
@ -1,51 +1,50 @@
|
||||
import { Swatch } from '@components/product'
|
||||
import type { ProductOption } from '@commerce/types/product'
|
||||
import { SelectedOptions } from '../helpers'
|
||||
|
||||
import React from 'react'
|
||||
interface ProductOptionsProps {
|
||||
options: ProductOption[]
|
||||
selectedOptions: SelectedOptions
|
||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
||||
}
|
||||
|
||||
const ProductOptions: React.FC<ProductOptionsProps> = ({
|
||||
options,
|
||||
selectedOptions,
|
||||
setSelectedOptions,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{options.map((opt) => (
|
||||
<div className="pb-4" key={opt.displayName}>
|
||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||
{opt.displayName}
|
||||
</h2>
|
||||
<div className="flex flex-row py-4">
|
||||
{opt.values.map((v, i: number) => {
|
||||
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||
return (
|
||||
<Swatch
|
||||
key={`${opt.id}-${i}`}
|
||||
active={v.label.toLowerCase() === active}
|
||||
variant={opt.displayName}
|
||||
color={v.hexColors ? v.hexColors[0] : ''}
|
||||
label={v.label}
|
||||
onClick={() => {
|
||||
setSelectedOptions((selectedOptions) => {
|
||||
return {
|
||||
...selectedOptions,
|
||||
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
||||
({ options, selectedOptions, setSelectedOptions }) => {
|
||||
return (
|
||||
<div>
|
||||
{options.map((opt) => (
|
||||
<div className="pb-4" key={opt.displayName}>
|
||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||
{opt.displayName}
|
||||
</h2>
|
||||
<div className="flex flex-row py-4">
|
||||
{opt.values.map((v, i: number) => {
|
||||
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||
return (
|
||||
<Swatch
|
||||
key={`${opt.id}-${i}`}
|
||||
active={v.label.toLowerCase() === active}
|
||||
variant={opt.displayName}
|
||||
color={v.hexColors ? v.hexColors[0] : ''}
|
||||
label={v.label}
|
||||
onClick={() => {
|
||||
setSelectedOptions((selectedOptions) => {
|
||||
return {
|
||||
...selectedOptions,
|
||||
[opt.displayName.toLowerCase()]:
|
||||
v.label.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ProductOptions
|
||||
|
84
components/product/ProductSidebar/ProductSidebar.module.css
Normal file
84
components/product/ProductSidebar/ProductSidebar.module.css
Normal file
@ -0,0 +1,84 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply transition-colors ease-in-out duration-500
|
||||
absolute top-0 left-0 z-20 pr-16;
|
||||
}
|
||||
|
||||
.header .name {
|
||||
@apply pt-0 max-w-full w-full leading-extra-loose;
|
||||
font-size: 2rem;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.header .name span {
|
||||
@apply py-4 px-6 bg-primary text-primary font-bold;
|
||||
font-size: inherit;
|
||||
letter-spacing: inherit;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.header .price {
|
||||
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
|
||||
font-semibold inline-block tracking-wide;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.imageContainer > div,
|
||||
.imageContainer > div > div {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.sliderContainer .img {
|
||||
@apply w-full h-auto 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;
|
||||
}
|
||||
}
|
86
components/product/ProductSidebar/ProductSidebar.tsx
Normal file
86
components/product/ProductSidebar/ProductSidebar.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import s from './ProductSidebar.module.css'
|
||||
import { useAddItem } from '@framework/cart'
|
||||
import { FC, useEffect, 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'
|
||||
|
||||
interface ProductSidebarProps {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const ProductSidebar: FC<ProductSidebarProps> = ({ product }) => {
|
||||
const addItem = useAddItem()
|
||||
const { openSidebar } = useUI()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
|
||||
|
||||
useEffect(() => {
|
||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||
}, [])
|
||||
|
||||
const variant = getProductVariant(product, selectedOptions)
|
||||
const addToCart = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await addItem({
|
||||
productId: String(product.id),
|
||||
variantId: String(variant ? variant.id : product.variants[0].id),
|
||||
})
|
||||
openSidebar()
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductOptions
|
||||
options={product.options}
|
||||
selectedOptions={selectedOptions}
|
||||
setSelectedOptions={setSelectedOptions}
|
||||
/>
|
||||
<Text
|
||||
className="pb-4 break-words w-full max-w-xl"
|
||||
html={product.descriptionHtml || product.description}
|
||||
/>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<Rating value={2} />
|
||||
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
aria-label="Add to Cart"
|
||||
type="button"
|
||||
className={s.button}
|
||||
onClick={addToCart}
|
||||
loading={loading}
|
||||
disabled={variant?.availableForSale === false}
|
||||
>
|
||||
{variant?.availableForSale === false
|
||||
? 'Not Available'
|
||||
: 'Add To Cart'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Collapse title="Care">
|
||||
This is a limited edition production run. Printing starts when the
|
||||
drop ends.
|
||||
</Collapse>
|
||||
<Collapse title="Details">
|
||||
This is a limited edition production run. Printing starts when the
|
||||
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days due
|
||||
to COVID-19.
|
||||
</Collapse>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductSidebar
|
1
components/product/ProductSidebar/index.ts
Normal file
1
components/product/ProductSidebar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './ProductSidebar'
|
@ -2,65 +2,27 @@ import cn from 'classnames'
|
||||
import Image from 'next/image'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import s from './ProductView.module.css'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC } from 'react'
|
||||
import type { Product } from '@commerce/types/product'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import {
|
||||
getProductVariant,
|
||||
selectDefaultOptionFromProduct,
|
||||
SelectedOptions,
|
||||
} from '../helpers'
|
||||
import { useAddItem } from '@framework/cart'
|
||||
import { WishlistButton } from '@components/wishlist'
|
||||
import { ProductSlider, ProductCard, ProductOptions } from '@components/product'
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Text,
|
||||
useUI,
|
||||
Rating,
|
||||
Collapse,
|
||||
} from '@components/ui'
|
||||
|
||||
import { ProductSlider, ProductCard } from '@components/product'
|
||||
import { Container, Text } from '@components/ui'
|
||||
import ProductSidebar from '../ProductSidebar'
|
||||
interface ProductViewProps {
|
||||
product: Product
|
||||
className?: string
|
||||
relatedProducts: Product[]
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
||||
const { openSidebar } = useUI()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
|
||||
const addItem = useAddItem()
|
||||
const { price } = usePrice({
|
||||
amount: product.price.value,
|
||||
baseAmount: product.price.retailPrice,
|
||||
currencyCode: product.price.currencyCode!,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||
}, [])
|
||||
|
||||
const variant = getProductVariant(product, selectedOptions)
|
||||
const addToCart = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await addItem({
|
||||
productId: String(product.id),
|
||||
variantId: String(variant ? variant.id : product.variants[0].id),
|
||||
})
|
||||
openSidebar()
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="max-w-none w-full" clean>
|
||||
<>
|
||||
<NextSeo
|
||||
title={product.name}
|
||||
description={product.description}
|
||||
@ -78,110 +40,73 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div className={cn(s.root, 'fit')}>
|
||||
<div className={cn(s.main, 'fit')}>
|
||||
<div className={s.header}>
|
||||
<h3 className={s.name}>
|
||||
<span>{product.name}</span>
|
||||
</h3>
|
||||
<div className={s.price}>
|
||||
{`${price} ${product.price?.currencyCode}`}
|
||||
<Container className="max-w-none w-full" clean>
|
||||
<div className={cn(s.root, 'fit')}>
|
||||
<div className={cn(s.main, 'fit')}>
|
||||
<div className={s.header}>
|
||||
<h3 className={s.name}>
|
||||
<span>{product.name}</span>
|
||||
</h3>
|
||||
<div className={s.price}>
|
||||
{`${price} ${product.price?.currencyCode}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div className={s.sidebar}>
|
||||
<ProductOptions
|
||||
options={product.options}
|
||||
selectedOptions={selectedOptions}
|
||||
setSelectedOptions={setSelectedOptions}
|
||||
/>
|
||||
<Text
|
||||
className="pb-4 break-words w-full max-w-xl"
|
||||
html={product.descriptionHtml || product.description}
|
||||
/>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<Rating value={2} />
|
||||
<div className="text-accent-6 pr-1 font-medium text-sm">
|
||||
36 reviews
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
aria-label="Add to Cart"
|
||||
type="button"
|
||||
className={s.button}
|
||||
onClick={addToCart}
|
||||
loading={loading}
|
||||
disabled={variant?.availableForSale === false}
|
||||
>
|
||||
{variant?.availableForSale === false
|
||||
? 'Not Available'
|
||||
: 'Add To Cart'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Collapse title="Care">
|
||||
This is a limited edition production run. Printing starts when the
|
||||
drop ends.
|
||||
</Collapse>
|
||||
<Collapse title="Details">
|
||||
This is a limited edition production run. Printing starts when the
|
||||
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days
|
||||
due to COVID-19.
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<ProductCard
|
||||
noNameTag
|
||||
product={p}
|
||||
key={p.path}
|
||||
variant="simple"
|
||||
className="animated fadeIn"
|
||||
imgProps={{
|
||||
width: 300,
|
||||
height: 300,
|
||||
}}
|
||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
<WishlistButton
|
||||
className={s.wishlistButton}
|
||||
productId={product.id}
|
||||
variant={product.variants[0]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
<div className={s.sidebar}>
|
||||
<ProductSidebar product={product} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
<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"
|
||||
>
|
||||
<ProductCard
|
||||
noNameTag
|
||||
product={p}
|
||||
key={p.path}
|
||||
variant="simple"
|
||||
className="animated fadeIn"
|
||||
imgProps={{
|
||||
width: 300,
|
||||
height: 300,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import cn from 'classnames'
|
||||
import { FC } from 'react'
|
||||
import React from 'react'
|
||||
import s from './Swatch.module.css'
|
||||
import { Check } from '@components/icons'
|
||||
import Button, { ButtonProps } from '@components/ui/Button'
|
||||
@ -13,48 +13,50 @@ interface SwatchProps {
|
||||
label?: string | null
|
||||
}
|
||||
|
||||
const Swatch: FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({
|
||||
className,
|
||||
color = '',
|
||||
label = null,
|
||||
variant = 'size',
|
||||
active,
|
||||
...props
|
||||
}) => {
|
||||
variant = variant?.toLowerCase()
|
||||
const Swatch: React.FC<Omit<ButtonProps, 'variant'> & SwatchProps> = React.memo(
|
||||
({
|
||||
active,
|
||||
className,
|
||||
color = '',
|
||||
label = null,
|
||||
variant = 'size',
|
||||
...props
|
||||
}) => {
|
||||
variant = variant?.toLowerCase()
|
||||
|
||||
if (label) {
|
||||
label = label?.toLowerCase()
|
||||
if (label) {
|
||||
label = label?.toLowerCase()
|
||||
}
|
||||
|
||||
const swatchClassName = cn(
|
||||
s.swatch,
|
||||
{
|
||||
[s.color]: color,
|
||||
[s.active]: active,
|
||||
[s.size]: variant === 'size',
|
||||
[s.dark]: color ? isDark(color) : false,
|
||||
[s.textLabel]: !color && label && label.length > 3,
|
||||
},
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="Variant Swatch"
|
||||
className={swatchClassName}
|
||||
{...(label && color && { title: label })}
|
||||
style={color ? { backgroundColor: color } : {}}
|
||||
{...props}
|
||||
>
|
||||
{color && active && (
|
||||
<span>
|
||||
<Check />
|
||||
</span>
|
||||
)}
|
||||
{!color ? label : null}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const swatchClassName = cn(
|
||||
s.swatch,
|
||||
{
|
||||
[s.active]: active,
|
||||
[s.size]: variant === 'size',
|
||||
[s.color]: color,
|
||||
[s.dark]: color ? isDark(color) : false,
|
||||
[s.textLabel]: !color && label && label.length > 3,
|
||||
},
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={swatchClassName}
|
||||
style={color ? { backgroundColor: color } : {}}
|
||||
aria-label="Variant Swatch"
|
||||
{...(label && color && { title: label })}
|
||||
{...props}
|
||||
>
|
||||
{color && active && (
|
||||
<span>
|
||||
<Check />
|
||||
</span>
|
||||
)}
|
||||
{!color ? label : null}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default Swatch
|
||||
|
@ -10,7 +10,7 @@ export interface CollapseProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Collapse: FC<CollapseProps> = ({ title, children }) => {
|
||||
const Collapse: FC<CollapseProps> = React.memo(({ title, children }) => {
|
||||
const [isActive, setActive] = useState(false)
|
||||
const [ref, { height: viewHeight }] = useMeasure()
|
||||
|
||||
@ -41,6 +41,6 @@ const Collapse: FC<CollapseProps> = ({ title, children }) => {
|
||||
</a.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Collapse
|
||||
|
@ -7,7 +7,7 @@ export interface RatingProps {
|
||||
value: number
|
||||
}
|
||||
|
||||
const Quantity: FC<RatingProps> = ({ value = 5 }) => {
|
||||
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
|
||||
return (
|
||||
<div className="flex flex-row py-6 text-accent-9">
|
||||
{rangeMap(5, (i) => (
|
||||
@ -22,6 +22,6 @@ const Quantity: FC<RatingProps> = ({ value = 5 }) => {
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Quantity
|
||||
|
@ -12,7 +12,7 @@ export async function getStaticProps({
|
||||
}: GetStaticPropsContext) {
|
||||
const config = { locale, locales }
|
||||
const { products } = await commerce.getAllProducts({
|
||||
variables: { first: 12 },
|
||||
variables: { first: 6 },
|
||||
config,
|
||||
preview,
|
||||
})
|
||||
@ -69,7 +69,7 @@ export default function Home({
|
||||
))}
|
||||
</Grid>
|
||||
<Marquee>
|
||||
{products.slice(0, 3).map((product, i) => (
|
||||
{products.slice(3).map((product, i) => (
|
||||
<ProductCard key={product.id} product={product} variant="slim" />
|
||||
))}
|
||||
</Marquee>
|
||||
|
Loading…
x
Reference in New Issue
Block a user