New Release (#371)

* Custom Checkout Progress

* Updates to Checkout

* Custom Checkout Progress

* Adding tabs

* Adding Collapse

* Adding Collapse

* Improving Sidebar Scroll

* Modif footer

* Changes

* More design updates

* sidebar cart

* More design updates

* More design updates

* More design updates

* More design updates

* Types

* Types

* Design Updates

* More changes

* More changes

* More changes

* Changes

* Changes

* Changes

* New tailwind required changes

* Sidebar Styling issues with Mobile

* Latest changes - Normalizing cart

* Styling Fixes

* New changes

* Changes

* latest

* Refactor and Renaming some UI Props

* Adding Quantity Component

* Adding Rating Component

* Rating Component

* More updates

* User Select disabled, plus hidding horizontal scroll bars

* Changes

* Adding ProductOptions Component and more helpers

* Styling updates

* Styling updates

* Fix for slim tags

* Missmatch with RightArrow

* Footer updates and some styles

* Latest Updates

* Latest Updates

* Latest Updates

* Removing Portal, since it's not needed. We might add it later I'd rather not to.

* Removing Portal, since it's not needed. We might add it later I'd rather not to.

* Sam backdrop filter

* General UI Improvements

* General UI Improvements

* Search now with Geist Colors

* Now with Geist Colors

* Changes

* Scroll for Mobile on IOs devises

* LoadingDots Working (:

* Changes

* More Changes

* Perf changes

* More perf changes

* Fade to the Nametags in the ProductCard

* changes

* Search issue ui

* Search issue ui

* Make sure to only refresh navbar and modals when required

* Index revalidate

* Fixed image issue

* hide album scroll on windows

* Fix scrollbar

* Changing

* Adding 404 with Layout

* Removing Toast

* Adding Assets

* Adding Assets

* Progress with LocalProvider

* New productTag

* Only images for the drop

* changes

* Empty SWRhooks

* Adding Local Provider

* Working local

* Working view of a LocalProvider

* More updates

* Changes

* Removed react-ticker

* default to local if no env available

* default to local if no env available

* add missing `@` to css import

* rewrite search rewrites to multiple pages

* allow requests in getStaticProps to execute in parallel

* make type import explicit

* add a tsconfig.js file

* use local provider in tsconfig.js

* avoid a circular dependency

* Saleor was not in the providers list

* avoid circular dependency in bigcommerce

* Adding more to the Local Provider (#366)

* Adding more data

* Adding more data

* optimize assets (#370)

* Optimize assets (#372)

* optimize assets

* remove assets

* remove assets

* cart enabled

* Adding saleor

* Changes with Webpack

* Changes

Co-authored-by: Luis Alvarez <luis@vercel.com>
Co-authored-by: Tobias Koppers <tobias.koppers@googlemail.com>
Co-authored-by: Shu Ding <g@shud.in>
This commit is contained in:
B
2021-06-15 20:23:17 -03:00
committed by GitHub
parent 3c9b90f453
commit 78cc378a72
202 changed files with 15406 additions and 2201 deletions

View File

@@ -1,136 +1,114 @@
.root {
@apply relative max-h-full w-full box-border overflow-hidden
bg-no-repeat bg-center bg-cover transition-transform
ease-linear cursor-pointer;
ease-linear cursor-pointer inline-block bg-accent-1;
height: 100% !important;
}
&:hover {
& .squareBg:before {
transform: scale(0.98);
}
& .productImage {
transform: scale(1.2625);
}
& .productTitle > span,
& .productPrice,
& .wishlistButton {
@apply bg-secondary text-secondary;
}
&:nth-child(6n + 1) .productTitle > span,
&:nth-child(6n + 1) .productPrice,
&:nth-child(6n + 1) .wishlistButton {
@apply bg-violet text-white;
}
&:nth-child(6n + 5) .productTitle > span,
&:nth-child(6n + 5) .productPrice,
&:nth-child(6n + 5) .wishlistButton {
@apply bg-blue text-white;
}
&:nth-child(6n + 3) .productTitle > span,
&:nth-child(6n + 3) .productPrice,
&:nth-child(6n + 3) .wishlistButton {
@apply bg-pink text-white;
}
&:nth-child(6n + 6) .productTitle > span,
&:nth-child(6n + 6) .productPrice,
&:nth-child(6n + 6) .wishlistButton {
@apply bg-cyan text-white;
}
.root:hover {
& .productImage {
transform: scale(1.2625);
}
&:nth-child(6n + 1) .squareBg {
@apply bg-violet;
& .header .name span,
& .header .price,
& .wishlistButton {
@apply bg-secondary text-secondary;
}
&:nth-child(6n + 5) .squareBg {
@apply bg-blue;
&:nth-child(6n + 1) .header .name span,
&:nth-child(6n + 1) .header .price,
&:nth-child(6n + 1) .wishlistButton {
@apply bg-violet text-white;
}
&:nth-child(6n + 3) .squareBg {
@apply bg-pink;
&:nth-child(6n + 5) .header .name span,
&:nth-child(6n + 5) .header .price,
&:nth-child(6n + 5) .wishlistButton {
@apply bg-blue text-white;
}
&:nth-child(6n + 6) .squareBg {
@apply bg-cyan;
&:nth-child(6n + 3) .header .name span,
&:nth-child(6n + 3) .header .price,
&:nth-child(6n + 3) .wishlistButton {
@apply bg-pink text-white;
}
&:nth-child(6n + 6) .header .name span,
&:nth-child(6n + 6) .header .price,
&:nth-child(6n + 6) .wishlistButton {
@apply bg-cyan text-white;
}
}
.squareBg,
.productTitle > span,
.productPrice,
.wishlistButton {
@apply transition-colors ease-in-out duration-500;
.header {
@apply transition-colors ease-in-out duration-500
absolute top-0 left-0 z-20 pr-16;
}
.squareBg {
@apply transition-colors absolute inset-0 z-0;
background-color: #212529;
}
.squareBg:before {
@apply transition ease-in-out duration-500 bg-repeat-space w-full h-full block;
background-image: url('/bg-products.svg');
content: '';
}
.simple {
& .squareBg {
@apply bg-accents-0 !important;
background-image: url('/bg-products.svg');
}
& .productTitle {
@apply pt-2;
font-size: 1rem;
& span {
@apply leading-extra-loose;
}
}
& .productPrice {
@apply text-sm;
}
}
.productTitle {
@apply pt-0 max-w-full w-full leading-extra-loose;
.header .name {
@apply pt-0 max-w-full w-full leading-extra-loose
transition-colors ease-in-out duration-500;
font-size: 2rem;
letter-spacing: 0.4px;
& 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;
}
}
.productPrice {
@apply py-4 px-6 bg-primary text-primary font-semibold inline-block text-sm leading-6;
letter-spacing: 0.4px;
.header .name span {
@apply py-4 px-6 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;
}
.wishlistButton {
@apply w-10 h-10 flex ml-auto items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer;
.header .price {
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
font-semibold inline-block tracking-wide
transition-colors ease-in-out duration-500;
}
.imageContainer {
@apply flex items-center justify-center;
overflow: hidden;
& > div {
min-width: 100%;
}
@apply flex items-center justify-center overflow-hidden;
}
.productImage {
@apply transform transition-transform duration-500 object-cover scale-120;
.imageContainer > div {
min-width: 100%;
}
.imageContainer .productImage {
@apply transform transition-transform duration-500
object-cover scale-120;
}
.root .wishlistButton {
@apply top-0 right-0 z-30 absolute;
}
/* Variant Simple */
.simple .header .name {
@apply pt-2 text-lg leading-10 -mt-1;
}
.simple .header .price {
@apply text-sm;
}
/* Variant Slim */
.slim {
@apply bg-transparent relative overflow-hidden
box-border;
}
.slim .header {
@apply absolute inset-0 flex items-center justify-end mr-8 z-20;
}
.slim span {
@apply bg-accent-9 text-accent-0 inline-block p-3
font-bold text-xl break-words;
}
.root:global(.secondary) .header span {
@apply bg-accent-0 text-accent-9;
}

View File

@@ -5,58 +5,98 @@ import type { Product } from '@commerce/types/product'
import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/product/use-price'
import ProductTag from '../ProductTag'
interface Props {
className?: string
product: Product
variant?: 'slim' | 'simple'
imgProps?: Omit<any, 'src'>
noNameTag?: boolean
imgProps?: Omit<ImageProps, 'src'>
variant?: 'default' | 'slim' | 'simple'
}
const placeholderImg = '/product-img-placeholder.svg'
const ProductCard: FC<Props> = ({
className,
product,
variant,
imgProps,
className,
noNameTag = false,
variant = 'default',
...props
}) => (
<Link href={`/product/${product.slug}`} {...props}>
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
{variant === 'slim' ? (
<div className="relative overflow-hidden box-border">
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
{product.name}
</span>
</div>
{product?.images && (
<Image
quality="85"
src={product.images[0]?.url || placeholderImg}
alt={product.name || 'Product Image'}
height={320}
width={320}
layout="fixed"
{...imgProps}
/>
)}
</div>
) : (
<>
<div className={s.squareBg} />
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
<div className="absolute top-0 left-0 pr-16 max-w-full">
<h3 className={s.productTitle}>
<span>{product.name}</span>
</h3>
<span className={s.productPrice}>
{product.price.value}
&nbsp;
{product.price.currencyCode}
</span>
}) => {
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!,
})
const rootClassName = cn(
s.root,
{ [s.slim]: variant === 'slim', [s.simple]: variant === 'simple' },
className
)
return (
<Link href={`/product/${product.slug}`} {...props}>
<a className={rootClassName}>
{variant === 'slim' && (
<>
<div className={s.header}>
<span>{product.name}</span>
</div>
{product?.images && (
<Image
quality="85"
src={product.images[0]?.url || placeholderImg}
alt={product.name || 'Product Image'}
height={320}
width={320}
layout="fixed"
{...imgProps}
/>
)}
</>
)}
{variant === 'simple' && (
<>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]}
/>
)}
{!noNameTag && (
<div className={s.header}>
<h3 className={s.name}>
<span>{product.name}</span>
</h3>
<div className={s.price}>
{`${price} ${product.price?.currencyCode}`}
</div>
</div>
)}
<div className={s.imageContainer}>
{product?.images && (
<Image
alt={product.name || 'Product Image'}
className={s.productImage}
src={product.images[0].url || placeholderImg}
height={540}
width={540}
quality="85"
layout="responsive"
{...imgProps}
/>
)}
</div>
</>
)}
{variant === 'default' && (
<>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
@@ -64,25 +104,29 @@ const ProductCard: FC<Props> = ({
variant={product.variants[0] as any}
/>
)}
</div>
<div className={s.imageContainer}>
{product?.images && (
<Image
alt={product.name || 'Product Image'}
className={s.productImage}
src={product.images[0]?.url || placeholderImg}
height={540}
width={540}
quality="85"
layout="responsive"
{...imgProps}
/>
)}
</div>
</>
)}
</a>
</Link>
)
<ProductTag
name={product.name}
price={`${price} ${product.price?.currencyCode}`}
/>
<div className={s.imageContainer}>
{product?.images && (
<Image
alt={product.name || 'Product Image'}
className={s.productImage}
src={product.images[0]?.url || placeholderImg}
height={540}
width={540}
quality="85"
layout="responsive"
{...imgProps}
/>
)}
</div>
</>
)}
</a>
</Link>
)
}
export default ProductCard

View File

@@ -0,0 +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> = 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>
)
}
)
export default ProductOptions

View File

@@ -0,0 +1 @@
export { default } from './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,87 @@
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
className?: string
}
const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
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 (
<div className={className}>
<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={4} />
<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>
</div>
)
}
export default ProductSidebar

View File

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

View File

@@ -1,82 +1,64 @@
.root {
@apply relative w-full h-full;
@apply relative w-full h-full select-none;
overflow: hidden;
}
.slider {
@apply relative h-full transition-opacity duration-150;
opacity: 0;
}
.slider.show {
opacity: 1;
}
.thumb {
@apply transition-transform transition-colors
ease-linear duration-75 overflow-hidden inline-block
cursor-pointer h-full;
width: 125px;
width: calc(100% / 3);
}
.thumb.selected {
@apply bg-white;
}
.thumb img {
height: 85% !important;
}
.album {
width: 100%;
height: 100%;
@apply bg-violet-dark;
box-sizing: content-box;
overflow-y: hidden;
overflow-x: auto;
white-space: nowrap;
height: 125px;
scrollbar-width: none;
}
.leftControl,
.rightControl {
@apply absolute top-1/2 -translate-x-1/2 z-20 w-16 h-16 flex items-center justify-center bg-hover-1 rounded-full;
/* Hide scrollbar for Chrome, Safari and Opera */
.album::-webkit-scrollbar {
display: none;
}
.leftControl:hover,
.rightControl:hover {
@apply bg-hover-2;
}
@screen md {
.thumb:hover {
transform: scale(1.02);
background-color: rgba(255, 255, 255, 0.08);
}
.leftControl:hover,
.rightControl:hover {
@apply outline-none shadow-outline-normal;
}
.thumb.selected {
@apply bg-white;
}
.leftControl {
@apply bg-cover left-10;
background-image: url('public/cursor-left.png');
@screen md {
@apply left-6;
.album {
height: 182px;
}
.thumb {
width: 235px;
}
}
.rightControl {
@apply bg-cover right-10;
background-image: url('public/cursor-right.png');
@screen md {
@apply right-6;
}
}
.control {
@apply opacity-0 transition duration-150;
}
.root:hover .control {
@apply opacity-100;
}
.positionIndicatorsContainer {
@apply hidden;
@screen sm {
@apply block absolute bottom-6 left-1/2;
transform: translateX(-50%);
}
}
.positionIndicator {
@apply rounded-full p-2;
}
.dot {
@apply bg-hover-1 transition w-3 h-3 rounded-full;
}
.positionIndicator:hover .dot {
@apply bg-hover-2;
}
.positionIndicator:focus {
@apply outline-none;
}
.positionIndicator:focus .dot {
@apply shadow-outline-normal;
}
.positionIndicatorActive .dot {
@apply bg-white;
}
.positionIndicatorActive:hover .dot {
@apply bg-white;
}

View File

@@ -8,20 +8,42 @@ import React, {
useEffect,
} from 'react'
import cn from 'classnames'
import { a } from '@react-spring/web'
import s from './ProductSlider.module.css'
import ProductSliderControl from '../ProductSliderControl'
const ProductSlider: FC = ({ children }) => {
interface ProductSliderProps {
children: React.ReactNode[]
className?: string
}
const ProductSlider: React.FC<ProductSliderProps> = ({
children,
className = '',
}) => {
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,
slidesPerView: 1,
mounted: () => setIsMounted(true),
slideChanged(s) {
setCurrentSlide(s.details().relativeSlide)
const slideNumber = s.details().relativeSlide
setCurrentSlide(slideNumber)
if (thumbsContainerRef.current) {
const $el = document.getElementById(
`thumb-${s.details().relativeSlide}`
)
if (slideNumber >= 3) {
thumbsContainerRef.current.scrollLeft = $el!.offsetLeft
} else {
thumbsContainerRef.current.scrollLeft = 0
}
}
},
})
@@ -59,23 +81,16 @@ const ProductSlider: FC = ({ children }) => {
}
}, [])
const onPrev = React.useCallback(() => slider.prev(), [slider])
const onNext = React.useCallback(() => slider.next(), [slider])
return (
<div className={s.root} ref={sliderContainerRef}>
<button
className={cn(s.leftControl, s.control)}
onClick={slider?.prev}
aria-label="Previous Product Image"
/>
<button
className={cn(s.rightControl, s.control)}
onClick={slider?.next}
aria-label="Next Product Image"
/>
<div className={cn(s.root, className)} ref={sliderContainerRef}>
<div
ref={ref}
className="keen-slider h-full transition-opacity duration-150"
style={{ opacity: isMounted ? 1 : 0 }}
className={cn(s.slider, { [s.show]: isMounted }, 'keen-slider')}
>
{slider && <ProductSliderControl onPrev={onPrev} onNext={onNext} />}
{Children.map(children, (child) => {
// Add the keen-slider__slide className to children
if (isValidElement(child)) {
@@ -92,26 +107,28 @@ const ProductSlider: FC = ({ children }) => {
return child
})}
</div>
{slider && (
<div className={cn(s.positionIndicatorsContainer)}>
{[...Array(slider.details().size).keys()].map((idx) => {
return (
<button
aria-label="Position indicator"
key={idx}
className={cn(s.positionIndicator, {
[s.positionIndicatorActive]: currentSlide === idx,
})}
onClick={() => {
slider.moveToSlideRelative(idx)
}}
>
<div className={s.dot} />
</button>
)
<a.div className={s.album} ref={thumbsContainerRef}>
{slider &&
Children.map(children, (child, idx) => {
if (isValidElement(child)) {
return {
...child,
props: {
...child.props,
className: cn(child.props.className, s.thumb, {
[s.selected]: currentSlide === idx,
}),
id: `thumb-${idx}`,
onClick: () => {
slider.moveToSlideRelative(idx)
},
},
}
}
return child
})}
</div>
)}
</a.div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
.control {
@apply bg-violet absolute bottom-10 right-10 flex flex-row
border-accent-0 border text-accent-0 z-30 shadow-xl select-none;
height: 48px;
}
.leftControl,
.rightControl {
@apply px-9 cursor-pointer;
transition: background-color 0.2s ease;
}
.leftControl:hover,
.rightControl:hover {
background-color: var(--violet-dark);
}
.leftControl:focus,
.rightControl:focus {
@apply outline-none;
}
.rightControl {
@apply border-l border-accent-0;
}
.leftControl {
margin-right: -1px;
}

View File

@@ -0,0 +1,31 @@
import cn from 'classnames'
import React from 'react'
import s from './ProductSliderControl.module.css'
import { ArrowLeft, ArrowRight } from '@components/icons'
interface ProductSliderControl {
onPrev: React.MouseEventHandler<HTMLButtonElement>
onNext: React.MouseEventHandler<HTMLButtonElement>
}
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
({ onPrev, onNext }) => (
<div className={s.control}>
<button
className={cn(s.leftControl)}
onClick={onPrev}
aria-label="Previous Product Image"
>
<ArrowLeft />
</button>
<button
className={cn(s.rightControl)}
onClick={onNext}
aria-label="Next Product Image"
>
<ArrowRight />
</button>
</div>
)
)
export default ProductSliderControl

View File

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

View File

@@ -0,0 +1,30 @@
.root {
@apply transition-colors ease-in-out duration-500
absolute top-0 left-0 z-20 pr-16;
}
.root .name {
@apply pt-0 max-w-full w-full leading-extra-loose;
font-size: 2rem;
letter-spacing: 0.4px;
line-height: 2.2em;
}
.root .name span {
@apply py-4 px-6 bg-primary text-primary font-bold;
min-height: 70px;
font-size: inherit;
letter-spacing: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.root .name span.fontsizing {
display: flex;
padding-top: 1.5rem;
}
.root .price {
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
font-semibold inline-block tracking-wide;
}

View File

@@ -0,0 +1,36 @@
import cn from 'classnames'
import { inherits } from 'util'
import s from './ProductTag.module.css'
interface ProductTagProps {
className?: string
name: string
price: string
fontSize?: number
}
const ProductTag: React.FC<ProductTagProps> = ({
name,
price,
className = '',
fontSize = 32,
}) => {
return (
<div className={cn(s.root, className)}>
<h3 className={s.name}>
<span
className={cn({ [s.fontsizing]: fontSize < 32 })}
style={{
fontSize: `${fontSize}px`,
lineHeight: `${fontSize}px`,
}}
>
{name}
</span>
</h3>
<div className={s.price}>{price}</div>
</div>
)
}
export default ProductTag

View File

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

View File

@@ -1,96 +1,60 @@
.root {
@apply relative grid items-start gap-8 grid-cols-1 overflow-x-hidden;
@screen lg {
@apply grid-cols-12;
}
@apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
min-height: auto;
}
.productDisplay {
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet;
min-height: 600px;
@screen md {
min-height: 700px;
}
@screen lg {
margin-right: -2rem;
margin-left: -2rem;
@apply mx-0 col-span-6;
min-height: 100%;
height: 100%;
}
}
.squareBg {
@apply absolute inset-0 bg-violet z-0 h-full;
}
.nameBox {
@apply absolute top-6 left-0 z-20 pr-16;
@screen lg {
@apply left-6 pr-16;
}
& .name {
@apply px-6 py-2 bg-primary text-primary font-bold;
font-size: 2rem;
letter-spacing: 0.4px;
}
& .price {
@apply px-6 py-2 pb-4 bg-primary text-primary font-bold inline-block tracking-wide;
}
@screen lg {
& .name,
& .price {
@apply bg-violet-light text-white;
}
}
.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 w-full h-full;
@screen lg {
@apply col-span-6 py-24 justify-between;
}
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
}
.sliderContainer {
@apply absolute z-10 inset-0 flex items-center justify-center overflow-x-hidden;
@apply flex items-center justify-center overflow-x-hidden bg-violet;
}
.imageContainer {
& > div {
@apply h-full;
& > div {
@apply h-full;
}
}
@apply text-center;
}
.img {
.imageContainer > div,
.imageContainer > div > div {
@apply h-full;
}
.sliderContainer .img {
@apply w-full h-auto max-h-full object-cover;
}
.button {
text-align: center;
width: 100%;
max-width: 300px;
@screen sm {
min-width: 300px;
}
}
.wishlistButton {
@apply absolute z-30 top-6 right-0 bg-primary text-primary w-10 h-10 flex items-center justify-center font-semibold leading-6 cursor-pointer;
@apply absolute z-30 top-0 right-0;
}
@screen lg {
@apply right-12 text-white bg-violet;
.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

@@ -1,64 +1,90 @@
import cn from 'classnames'
import Image from 'next/image'
import { NextSeo } from 'next-seo'
import { FC, useEffect, useState } from 'react'
import s from './ProductView.module.css'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text, useUI } from '@components/ui'
import { FC } from 'react'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import { useAddItem } from '@framework/cart'
import { getVariant, SelectedOptions } from '../helpers'
import WishlistButton from '@components/wishlist/WishlistButton'
interface Props {
children?: any
import { WishlistButton } from '@components/wishlist'
import { ProductSlider, ProductCard } from '@components/product'
import { Container, Text } from '@components/ui'
import ProductSidebar from '../ProductSidebar'
import ProductTag from '../ProductTag'
interface ProductViewProps {
product: Product
className?: string
relatedProducts: Product[]
}
const ProductView: FC<Props> = ({ product }) => {
// TODO: fix this missing argument issue
/* @ts-ignore */
const addItem = useAddItem()
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!,
})
const { openSidebar } = useUI()
const [loading, setLoading] = useState(false)
const [choices, setChoices] = useState<SelectedOptions>({})
useEffect(() => {
// Selects the default option
const options = product.variants[0].options || []
options.forEach((v) => {
setChoices((choices) => ({
...choices,
[v.displayName.toLowerCase()]: v.values[0]?.label.toLowerCase(),
}))
})
}, [])
const variant = getVariant(product, choices)
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>
<>
<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>
<ProductSidebar 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"
>
<ProductCard
noNameTag
product={p}
key={p.path}
variant="simple"
className="animated fadeIn"
imgProps={{
width: 300,
height: 300,
}}
/>
</div>
))}
</div>
</section>
</Container>
<NextSeo
title={product.name}
description={product.description}
@@ -76,97 +102,7 @@ const ProductView: FC<Props> = ({ product }) => {
],
}}
/>
<div className={cn(s.root, 'fit')}>
<div className={cn(s.productDisplay, 'fit')}>
<div className={s.nameBox}>
<h1 className={s.name}>{product.name}</h1>
<div className={s.price}>
{price}
{` `}
{product.price?.currencyCode}
</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={1050}
height={1050}
priority={i === 0}
quality="85"
/>
</div>
))}
</ProductSlider>
</div>
</div>
<div className={s.sidebar}>
<section>
{product.options?.map((opt) => (
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium">{opt.displayName}</h2>
<div className="flex flex-row py-4">
{opt.values.map((v, i: number) => {
const active = (choices as any)[
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={() => {
setChoices((choices) => {
return {
...choices,
[opt.displayName.toLowerCase()]:
v.label.toLowerCase(),
}
})
}}
/>
)
})}
</div>
</div>
))}
<div className="pb-14 break-words w-full max-w-xl">
<Text html={product.descriptionHtml || product.description} />
</div>
</section>
<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>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]! as any}
/>
)}
</div>
</Container>
</>
)
}

View File

@@ -1,11 +1,13 @@
.swatch {
box-sizing: border-box;
composes: root from 'components/ui/Button/Button.module.css';
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
composes: root from '@components/ui/Button/Button.module.css';
@apply h-10 w-10 bg-primary text-primary rounded-full mr-3 inline-flex
items-center justify-center cursor-pointer transition duration-150 ease-in-out
p-0 shadow-none border-gray-200 border box-border;
p-0 shadow-none border-accent-3 border box-border select-none;
margin-right: calc(0.75rem - 1px);
overflow: hidden;
width: 48px;
height: 48px;
}
.swatch::before,
@@ -35,7 +37,7 @@
}
.active {
@apply border-accents-9 border-2;
@apply border-accent-9 border-2;
padding-right: 1px;
padding-left: 1px;
}
@@ -46,7 +48,7 @@
}
.active.textLabel {
@apply border-accents-9 border-2;
@apply border-accent-9 border-2;
padding-right: calc(1rem - 1px);
padding-left: calc(1rem - 1px);
}

View File

@@ -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

View File

@@ -1,7 +1,8 @@
import type { Product } from '@commerce/types/product'
export type SelectedOptions = Record<string, string | null>
import { Dispatch, SetStateAction } from 'react'
export function getVariant(product: Product, opts: SelectedOptions) {
export function getProductVariant(product: Product, opts: SelectedOptions) {
const variant = product.variants.find((variant) => {
return Object.entries(opts).every(([key, value]) =>
variant.options.find((option) => {
@@ -16,3 +17,16 @@ export function getVariant(product: Product, opts: SelectedOptions) {
})
return variant
}
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(),
}))
})
}

View File

@@ -2,3 +2,4 @@ export { default as Swatch } from './Swatch'
export { default as ProductView } from './ProductView'
export { default as ProductCard } from './ProductCard'
export { default as ProductSlider } from './ProductSlider'
export { default as ProductOptions } from './ProductOptions'