mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 20:26:49 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
@@ -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}
|
||||
|
||||
{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
|
||||
|
50
components/product/ProductOptions/ProductOptions.tsx
Normal file
50
components/product/ProductOptions/ProductOptions.tsx
Normal 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
|
1
components/product/ProductOptions/index.ts
Normal file
1
components/product/ProductOptions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './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;
|
||||
}
|
||||
}
|
87
components/product/ProductSidebar/ProductSidebar.tsx
Normal file
87
components/product/ProductSidebar/ProductSidebar.tsx
Normal 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
|
1
components/product/ProductSidebar/index.ts
Normal file
1
components/product/ProductSidebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './ProductSidebar'
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
)
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -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
|
1
components/product/ProductSliderControl/index.ts
Normal file
1
components/product/ProductSliderControl/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './ProductSliderControl'
|
30
components/product/ProductTag/ProductTag.module.css
Normal file
30
components/product/ProductTag/ProductTag.module.css
Normal 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;
|
||||
}
|
36
components/product/ProductTag/ProductTag.tsx
Normal file
36
components/product/ProductTag/ProductTag.tsx
Normal 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
|
1
components/product/ProductTag/index.ts
Normal file
1
components/product/ProductTag/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './ProductTag'
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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(),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
@@ -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'
|
||||
|
Reference in New Issue
Block a user