Perf changes

This commit is contained in:
Bel Curcio 2021-06-08 11:31:48 -03:00
parent 1a729f9e72
commit 7ef0e2273e
9 changed files with 326 additions and 229 deletions

View File

@ -1,51 +1,50 @@
import { Swatch } from '@components/product' import { Swatch } from '@components/product'
import type { ProductOption } from '@commerce/types/product' import type { ProductOption } from '@commerce/types/product'
import { SelectedOptions } from '../helpers' import { SelectedOptions } from '../helpers'
import React from 'react'
interface ProductOptionsProps { interface ProductOptionsProps {
options: ProductOption[] options: ProductOption[]
selectedOptions: SelectedOptions selectedOptions: SelectedOptions
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>> setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
} }
const ProductOptions: React.FC<ProductOptionsProps> = ({ const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
options, ({ options, selectedOptions, setSelectedOptions }) => {
selectedOptions, return (
setSelectedOptions, <div>
}) => { {options.map((opt) => (
return ( <div className="pb-4" key={opt.displayName}>
<div> <h2 className="uppercase font-medium text-sm tracking-wide">
{options.map((opt) => ( {opt.displayName}
<div className="pb-4" key={opt.displayName}> </h2>
<h2 className="uppercase font-medium text-sm tracking-wide"> <div className="flex flex-row py-4">
{opt.displayName} {opt.values.map((v, i: number) => {
</h2> const active = selectedOptions[opt.displayName.toLowerCase()]
<div className="flex flex-row py-4"> return (
{opt.values.map((v, i: number) => { <Swatch
const active = selectedOptions[opt.displayName.toLowerCase()] key={`${opt.id}-${i}`}
return ( active={v.label.toLowerCase() === active}
<Swatch variant={opt.displayName}
key={`${opt.id}-${i}`} color={v.hexColors ? v.hexColors[0] : ''}
active={v.label.toLowerCase() === active} label={v.label}
variant={opt.displayName} onClick={() => {
color={v.hexColors ? v.hexColors[0] : ''} setSelectedOptions((selectedOptions) => {
label={v.label} return {
onClick={() => { ...selectedOptions,
setSelectedOptions((selectedOptions) => { [opt.displayName.toLowerCase()]:
return { v.label.toLowerCase(),
...selectedOptions, }
[opt.displayName.toLowerCase()]: v.label.toLowerCase(), })
} }}
}) />
}} )
/> })}
) </div>
})}
</div> </div>
</div> ))}
))} </div>
</div> )
) }
} )
export default ProductOptions export default ProductOptions

View 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;
}
}

View 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

View File

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

View File

@ -2,65 +2,27 @@ import cn from 'classnames'
import Image from 'next/image' import Image from 'next/image'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
import s from './ProductView.module.css' import s from './ProductView.module.css'
import { FC, useEffect, useState } from 'react' import { 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 usePrice from '@framework/product/use-price'
import {
getProductVariant,
selectDefaultOptionFromProduct,
SelectedOptions,
} from '../helpers'
import { useAddItem } from '@framework/cart'
import { WishlistButton } from '@components/wishlist' import { WishlistButton } from '@components/wishlist'
import { ProductSlider, ProductCard, ProductOptions } from '@components/product' import { ProductSlider, ProductCard } from '@components/product'
import { import { Container, Text } from '@components/ui'
Button, import ProductSidebar from '../ProductSidebar'
Container,
Text,
useUI,
Rating,
Collapse,
} from '@components/ui'
interface ProductViewProps { interface ProductViewProps {
product: Product product: Product
className?: string
relatedProducts: Product[] relatedProducts: Product[]
children?: React.ReactNode
} }
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => { 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({ const { price } = usePrice({
amount: product.price.value, amount: product.price.value,
baseAmount: product.price.retailPrice, baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!, 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 ( return (
<Container className="max-w-none w-full" clean> <>
<NextSeo <NextSeo
title={product.name} title={product.name}
description={product.description} description={product.description}
@ -78,110 +40,73 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
], ],
}} }}
/> />
<div className={cn(s.root, 'fit')}> <Container className="max-w-none w-full" clean>
<div className={cn(s.main, 'fit')}> <div className={cn(s.root, 'fit')}>
<div className={s.header}> <div className={cn(s.main, 'fit')}>
<h3 className={s.name}> <div className={s.header}>
<span>{product.name}</span> <h3 className={s.name}>
</h3> <span>{product.name}</span>
<div className={s.price}> </h3>
{`${price} ${product.price?.currencyCode}`} <div className={s.price}>
{`${price} ${product.price?.currencyCode}`}
</div>
</div> </div>
</div>
<div className={s.sliderContainer}> <div className={s.sliderContainer}>
<ProductSlider key={product.id}> <ProductSlider key={product.id}>
{product.images.map((image, i) => ( {product.images.map((image, i) => (
<div key={image.url} className={s.imageContainer}> <div key={image.url} className={s.imageContainer}>
<Image <Image
className={s.img} className={s.img}
src={image.url!} src={image.url!}
alt={image.alt || 'Product Image'} alt={image.alt || 'Product Image'}
width={600} width={600}
height={600} height={600}
priority={i === 0} priority={i === 0}
quality="85" quality="85"
/> />
</div> </div>
))} ))}
</ProductSlider> </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> </div>
</div> {process.env.COMMERCE_WISHLIST_ENABLED && (
<div> <WishlistButton
<Button className={s.wishlistButton}
aria-label="Add to Cart" productId={product.id}
type="button" variant={product.variants[0]}
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,
}}
/> />
</div> )}
))} </div>
<div className={s.sidebar}>
<ProductSidebar product={product} />
</div>
</div> </div>
</section> <hr className="mt-7 border-accent-2" />
</Container> <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>
</>
) )
} }

View File

@ -1,5 +1,5 @@
import cn from 'classnames' import cn from 'classnames'
import { FC } from 'react' import React from 'react'
import s from './Swatch.module.css' import s from './Swatch.module.css'
import { Check } from '@components/icons' import { Check } from '@components/icons'
import Button, { ButtonProps } from '@components/ui/Button' import Button, { ButtonProps } from '@components/ui/Button'
@ -13,48 +13,50 @@ interface SwatchProps {
label?: string | null label?: string | null
} }
const Swatch: FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({ const Swatch: React.FC<Omit<ButtonProps, 'variant'> & SwatchProps> = React.memo(
className, ({
color = '', active,
label = null, className,
variant = 'size', color = '',
active, label = null,
...props variant = 'size',
}) => { ...props
variant = variant?.toLowerCase() }) => {
variant = variant?.toLowerCase()
if (label) { if (label) {
label = label?.toLowerCase() 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 export default Swatch

View File

@ -10,7 +10,7 @@ export interface CollapseProps {
children: ReactNode children: ReactNode
} }
const Collapse: FC<CollapseProps> = ({ title, children }) => { const Collapse: FC<CollapseProps> = React.memo(({ title, children }) => {
const [isActive, setActive] = useState(false) const [isActive, setActive] = useState(false)
const [ref, { height: viewHeight }] = useMeasure() const [ref, { height: viewHeight }] = useMeasure()
@ -41,6 +41,6 @@ const Collapse: FC<CollapseProps> = ({ title, children }) => {
</a.div> </a.div>
</div> </div>
) )
} })
export default Collapse export default Collapse

View File

@ -7,7 +7,7 @@ export interface RatingProps {
value: number value: number
} }
const Quantity: FC<RatingProps> = ({ value = 5 }) => { const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
return ( return (
<div className="flex flex-row py-6 text-accent-9"> <div className="flex flex-row py-6 text-accent-9">
{rangeMap(5, (i) => ( {rangeMap(5, (i) => (
@ -22,6 +22,6 @@ const Quantity: FC<RatingProps> = ({ value = 5 }) => {
))} ))}
</div> </div>
) )
} })
export default Quantity export default Quantity

View File

@ -12,7 +12,7 @@ export async function getStaticProps({
}: GetStaticPropsContext) { }: GetStaticPropsContext) {
const config = { locale, locales } const config = { locale, locales }
const { products } = await commerce.getAllProducts({ const { products } = await commerce.getAllProducts({
variables: { first: 12 }, variables: { first: 6 },
config, config,
preview, preview,
}) })
@ -69,7 +69,7 @@ export default function Home({
))} ))}
</Grid> </Grid>
<Marquee> <Marquee>
{products.slice(0, 3).map((product, i) => ( {products.slice(3).map((product, i) => (
<ProductCard key={product.id} product={product} variant="slim" /> <ProductCard key={product.id} product={product} variant="slim" />
))} ))}
</Marquee> </Marquee>