@hanzo/commerce 5.1.3 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/{buy-item/select-category-item-card.tsx → buy/_to_deprecate_select-category-item-card.tsx} +22 -32
- package/components/{select-category-item-panel.tsx → buy/_to_deprecate_select-category-item-panel.tsx} +16 -22
- package/components/{buy-item/buy-item-popup.tsx → buy/_unused_buy-popup.tsx_} +4 -4
- package/components/{add-to-cart-widget.tsx → buy/add-to-cart-widget.tsx} +10 -6
- package/components/buy/buy-button.tsx +31 -0
- package/components/buy/buy-card.tsx +267 -0
- package/components/buy/buy-drawer.tsx +89 -0
- package/components/buy/buy-trigger-wrapper.tsx +20 -0
- package/components/{cart-accordian.tsx → cart/cart-accordian.tsx} +2 -1
- package/components/{cart-panel → cart/cart-panel}/cart-line-item.tsx +27 -14
- package/components/{cart-panel → cart/cart-panel}/index.tsx +12 -3
- package/components/{cart-panel → cart/cart-panel}/promo-code.tsx +3 -3
- package/components/{payment-step-form → checkout/payment-step-form}/contact-form.tsx +1 -1
- package/components/{payment-step-form → checkout/payment-step-form}/index.tsx +3 -3
- package/components/{payment-step-form → checkout/payment-step-form}/methods/bank-transfer.tsx +1 -1
- package/components/{payment-step-form → checkout/payment-step-form}/methods/card.tsx +4 -4
- package/components/{payment-step-form → checkout/payment-step-form}/methods/crypto.tsx +3 -3
- package/components/{payment-step-form → checkout/payment-step-form}/methods/index.ts +1 -1
- package/components/{shipping-step-form.tsx → checkout/shipping-step-form.tsx} +4 -4
- package/components/index.ts +20 -18
- package/components/item/item-media.tsx +51 -0
- package/components/item/product-card.tsx +48 -0
- package/components/{facet-values-widget → level-nodes-widget}/index.tsx +20 -19
- package/components/level-nodes-widget/node-image.tsx +31 -0
- package/components/select/carousel-selector.tsx +112 -0
- package/components/select/image-selector.tsx +162 -0
- package/components/select/radio-selector.tsx +125 -0
- package/package.json +3 -2
- package/service/context.tsx +4 -4
- package/service/impls/standalone/actual-line-item.ts +30 -16
- package/service/impls/standalone/index.ts +4 -4
- package/service/impls/standalone/standalone-service.ts +94 -64
- package/types/buy-ui-spec.ts +8 -0
- package/types/category.ts +6 -4
- package/types/commerce-service.ts +21 -16
- package/types/index.ts +3 -2
- package/types/item-selector.ts +34 -6
- package/types/line-item.ts +2 -0
- package/types/product.ts +5 -5
- package/types/tree-node.ts +24 -0
- package/util/buy-ui-conf.ts +34 -0
- package/util/index.ts +14 -11
- package/util/obs-string-mutator.ts +22 -0
- package/util/use-sync-sku-param-w-current-item.ts +5 -5
- package/components/buy-item/buy-item-button-wrapper.tsx +0 -27
- package/components/buy-item/buy-item-button.tsx +0 -34
- package/components/buy-item/buy-item-card.tsx +0 -92
- package/components/buy-item/buy-item-mobile-drawer.tsx +0 -54
- package/components/buy-item/select-category-and-item-widget.tsx +0 -83
- package/components/category-item-radio-selector.tsx +0 -55
- package/components/category-item-scroll-selector.tsx +0 -40
- package/components/facet-values-widget/facet-image.tsx +0 -41
- package/components/item-carousel.tsx +0 -80
- package/components/product-card.tsx +0 -79
- package/types/facet.ts +0 -35
- /package/components/{cart-icon.tsx → cart/cart-icon.tsx} +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icon-row.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/amex.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/diners-club.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/discover.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/jcb.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/mastercard.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/card-icons/visa.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/cc-button.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/btc.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/eth.tsx +0 -0
- /package/components/{payment-step-form → checkout/payment-step-form}/crypto-icons/usdt.tsx +0 -0
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
AccordionTrigger,
|
|
12
12
|
} from '@hanzo/ui/primitives'
|
|
13
13
|
|
|
14
|
-
import { formatCurrencyValue
|
|
14
|
+
import { formatCurrencyValue } from '../../util'
|
|
15
|
+
import { useCommerce } from '../../service/context'
|
|
15
16
|
|
|
16
17
|
import CartPanel from './cart-panel'
|
|
17
18
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
|
-
import Image from 'next/image'
|
|
5
4
|
import { observer } from 'mobx-react-lite'
|
|
6
5
|
|
|
7
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
import { Image } from '@hanzo/ui/primitives'
|
|
8
8
|
|
|
9
|
-
import type { LineItem } from '
|
|
10
|
-
import { formatCurrencyValue } from '
|
|
11
|
-
import AddToCartWidget from '
|
|
12
|
-
import { useCommerce } from '
|
|
9
|
+
import type { LineItem } from '../../../types'
|
|
10
|
+
import { formatCurrencyValue } from '../../../util'
|
|
11
|
+
import AddToCartWidget from '../../buy/add-to-cart-widget'
|
|
12
|
+
import { useCommerce } from '../../../service/context'
|
|
13
13
|
|
|
14
14
|
const DEF_IMG_SIZE=40
|
|
15
15
|
|
|
@@ -26,25 +26,38 @@ const CartLineItem: React.FC<{
|
|
|
26
26
|
className?: string
|
|
27
27
|
imgSizePx?: number
|
|
28
28
|
showPromoCode?: boolean
|
|
29
|
+
itemClicked?: (item: LineItem) => void
|
|
30
|
+
selected?: boolean
|
|
29
31
|
}> = observer(({
|
|
30
32
|
item,
|
|
31
33
|
className='',
|
|
32
34
|
imgSizePx=DEF_IMG_SIZE,
|
|
33
|
-
showPromoCode
|
|
35
|
+
showPromoCode=false,
|
|
36
|
+
itemClicked,
|
|
37
|
+
selected=false
|
|
34
38
|
}) => {
|
|
35
39
|
|
|
36
40
|
const cmmc = useCommerce()
|
|
37
41
|
const promoPrice = showPromoCode ? cmmc.itemPromoPrice(item) : undefined
|
|
38
|
-
|
|
42
|
+
|
|
39
43
|
return (
|
|
40
44
|
<div className={cn('flex flex-col justify-start items-start text-sm font-sans', className)}>
|
|
41
|
-
<div
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
<div
|
|
46
|
+
className={
|
|
47
|
+
'flex flex-row justify-between items-center gap-1 ' +
|
|
48
|
+
(itemClicked && !selected ? 'cursor-pointer ' : 'cursor-default ')
|
|
49
|
+
}
|
|
50
|
+
onClick={() => {itemClicked && itemClicked(item)}}
|
|
51
|
+
>
|
|
52
|
+
{/* 1px gap between image and border for better emphasis w small images */}
|
|
53
|
+
<div className={selected ? 'border-foreground p-[1px] border rounded-sm' : 'p-[2px]'}>
|
|
54
|
+
{item.img ? (
|
|
55
|
+
<Image def={item.img} constrainTo={{w: imgSizePx, h: imgSizePx}} />
|
|
56
|
+
) : ( // placeholder so things align
|
|
57
|
+
<div style={{height: imgSizePx, width: imgSizePx}} className='bg-level-3'/>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<div className='grow leading-tight ml-1'>{renderTitle(item.title)}</div>
|
|
48
61
|
</div>
|
|
49
62
|
<div className='flex flex-row items-center justify-between w-full'>
|
|
50
63
|
<div className='flex flex-row items-center'>
|
|
@@ -5,12 +5,13 @@ import { observer } from 'mobx-react-lite'
|
|
|
5
5
|
import { Button, ScrollArea } from '@hanzo/ui/primitives'
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
|
|
8
|
-
import { useCommerce } from '
|
|
9
|
-
import { formatCurrencyValue } from '
|
|
10
|
-
import { sendFBEvent, sendGAEvent } from '
|
|
8
|
+
import { useCommerce } from '../../../service/context'
|
|
9
|
+
import { formatCurrencyValue } from '../../../util'
|
|
10
|
+
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
11
11
|
|
|
12
12
|
import CartLineItem from './cart-line-item'
|
|
13
13
|
import PromoCode from './promo-code'
|
|
14
|
+
import type { LineItem } from '../../../types'
|
|
14
15
|
|
|
15
16
|
const CartPanel: React.FC<PropsWithChildren & {
|
|
16
17
|
/** fix size and scroll after 'scrollAfter items in teh cart. */
|
|
@@ -25,6 +26,7 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
25
26
|
buttonClx?: string
|
|
26
27
|
showPromoCode?: boolean
|
|
27
28
|
showShipping?: boolean
|
|
29
|
+
selectItems?: boolean
|
|
28
30
|
/** if not provided, 'checkout' button will be rendered */
|
|
29
31
|
handleCheckout?: () => void
|
|
30
32
|
}> = observer(({
|
|
@@ -41,6 +43,7 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
41
43
|
buttonClx='',
|
|
42
44
|
showPromoCode=false,
|
|
43
45
|
showShipping=false,
|
|
46
|
+
selectItems=false,
|
|
44
47
|
handleCheckout,
|
|
45
48
|
}) => {
|
|
46
49
|
|
|
@@ -74,6 +77,10 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
74
77
|
handleCheckout && handleCheckout()
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
const itemClicked = (item: LineItem) => {
|
|
81
|
+
cmmc.setCurrentItem(item.sku)
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
const Main: React.FC = observer(() => (<>
|
|
78
85
|
{cmmc.cartEmpty ? (
|
|
79
86
|
<p className={cn('text-center mt-4', noItemsClx)}>No items in cart</p>
|
|
@@ -85,6 +92,8 @@ const CartPanel: React.FC<PropsWithChildren & {
|
|
|
85
92
|
item={item}
|
|
86
93
|
className={cn('mb-2', itemClx)}
|
|
87
94
|
showPromoCode={showPromoCode}
|
|
95
|
+
itemClicked={selectItems ? itemClicked : undefined}
|
|
96
|
+
selected={selectItems ? cmmc.currentItem?.sku === item.sku : false}
|
|
88
97
|
/>
|
|
89
98
|
))}
|
|
90
99
|
</>)}
|
|
@@ -10,9 +10,9 @@ import { observer } from 'mobx-react-lite'
|
|
|
10
10
|
|
|
11
11
|
import { Button, Form, FormControl, FormField, FormItem, Input } from '@hanzo/ui/primitives'
|
|
12
12
|
|
|
13
|
-
import { useCommerce } from '
|
|
14
|
-
import type { Promo } from '
|
|
15
|
-
import getPromoFromApi from '
|
|
13
|
+
import { useCommerce } from '../../../service/context'
|
|
14
|
+
import type { Promo } from '../../../types'
|
|
15
|
+
import getPromoFromApi from '../../../util/promo-codes'
|
|
16
16
|
|
|
17
17
|
const formSchema = z.object({
|
|
18
18
|
code: z.string().min(1, 'Invalid code'),
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '@hanzo/ui/primitives/form'
|
|
8
8
|
import { Input } from '@hanzo/ui/primitives'
|
|
9
9
|
|
|
10
|
-
import type { ContactFormType } from '
|
|
10
|
+
import type { ContactFormType } from '../../../types'
|
|
11
11
|
|
|
12
12
|
const ContactForm: React.FC<{
|
|
13
13
|
form: ContactFormType,
|
|
@@ -9,9 +9,9 @@ import { useForm } from 'react-hook-form'
|
|
|
9
9
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@hanzo/ui/primitives'
|
|
10
10
|
import { useAuth } from '@hanzo/auth/service'
|
|
11
11
|
|
|
12
|
-
import { useCommerce } from '
|
|
13
|
-
import { sendFBEvent, sendGAEvent } from '
|
|
14
|
-
import type { CheckoutStepComponentProps, TransactionStatus } from '
|
|
12
|
+
import { useCommerce } from '../../../service/context'
|
|
13
|
+
import { sendFBEvent, sendGAEvent } from '../../../util/analytics'
|
|
14
|
+
import type { CheckoutStepComponentProps, TransactionStatus } from '../../../types'
|
|
15
15
|
|
|
16
16
|
import METHODS from './methods'
|
|
17
17
|
|
package/components/{payment-step-form → checkout/payment-step-form}/methods/bank-transfer.tsx
RENAMED
|
@@ -5,7 +5,7 @@ import { Copy } from 'lucide-react'
|
|
|
5
5
|
|
|
6
6
|
import { Button, toast } from '@hanzo/ui/primitives'
|
|
7
7
|
|
|
8
|
-
import type { PaymentMethodComponentProps } from '
|
|
8
|
+
import type { PaymentMethodComponentProps } from '../../../../types'
|
|
9
9
|
|
|
10
10
|
import ContactForm from '../contact-form'
|
|
11
11
|
|
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
|
|
16
16
|
import { cn } from '@hanzo/ui/util'
|
|
17
17
|
|
|
18
|
-
import { useCommerce } from '
|
|
19
|
-
import { processSquareCardPayment } from '
|
|
20
|
-
import type { PaymentMethodComponentProps } from '
|
|
21
|
-
import { sendFBEvent, sendGAEvent } from '
|
|
18
|
+
import { useCommerce } from '../../../../service/context'
|
|
19
|
+
import { processSquareCardPayment } from '../../../../util'
|
|
20
|
+
import type { PaymentMethodComponentProps } from '../../../../types'
|
|
21
|
+
import { sendFBEvent, sendGAEvent } from '../../../../util/analytics'
|
|
22
22
|
|
|
23
23
|
import ContactInfo from '../contact-form'
|
|
24
24
|
import PaymentMethods from '../card-icon-row'
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
} from '@hanzo/ui/primitives'
|
|
20
20
|
|
|
21
21
|
import Eth from '../crypto-icons/eth'
|
|
22
|
-
import { useCommerce } from '
|
|
23
|
-
import type { PaymentMethodComponentProps } from '
|
|
24
|
-
import { sendFBEvent, sendGAEvent } from '
|
|
22
|
+
import { useCommerce } from '../../../../service/context'
|
|
23
|
+
import type { PaymentMethodComponentProps } from '../../../../types'
|
|
24
|
+
import { sendFBEvent, sendGAEvent } from '../../../../util/analytics'
|
|
25
25
|
|
|
26
26
|
import ContactForm from '../contact-form'
|
|
27
27
|
|
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
SelectValue
|
|
20
20
|
} from '@hanzo/ui/primitives'
|
|
21
21
|
|
|
22
|
-
import { useCommerce } from '
|
|
22
|
+
import { useCommerce } from '../../service/context'
|
|
23
23
|
|
|
24
|
-
import countries from '
|
|
25
|
-
import { sendGAEvent } from '
|
|
24
|
+
import countries from '../../util/countries'
|
|
25
|
+
import { sendGAEvent } from '../../util/analytics'
|
|
26
26
|
|
|
27
|
-
import type { CheckoutStepComponentProps } from '
|
|
27
|
+
import type { CheckoutStepComponentProps } from '../../types'
|
|
28
28
|
|
|
29
29
|
const shippingFormSchema = z.object({
|
|
30
30
|
addressLine1: z.string().min(2, 'Address must be at least 2 characters.'),
|
package/components/index.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
export { default as AddToCartWidget } from './add-to-cart-widget'
|
|
2
|
-
export { default as
|
|
3
|
-
export { default as
|
|
4
|
-
export { default as
|
|
5
|
-
export { default as
|
|
6
|
-
|
|
7
|
-
export { default as
|
|
8
|
-
export { default as
|
|
9
|
-
|
|
10
|
-
export { default as
|
|
11
|
-
|
|
12
|
-
export { default as PaymentStepForm } from './payment-step-form'
|
|
13
|
-
export { default as
|
|
14
|
-
|
|
15
|
-
export { default as
|
|
16
|
-
export {
|
|
17
|
-
|
|
18
|
-
export {
|
|
1
|
+
export { default as AddToCartWidget } from './buy/add-to-cart-widget'
|
|
2
|
+
export { default as BuyTriggerWrapper } from './buy/buy-trigger-wrapper'
|
|
3
|
+
export { default as BuyButton } from './buy/buy-button'
|
|
4
|
+
export { default as BuyCard } from './buy/buy-card'
|
|
5
|
+
export { default as BuyDrawer } from './buy/buy-drawer'
|
|
6
|
+
|
|
7
|
+
export { default as CartAccordian } from './cart/cart-accordian'
|
|
8
|
+
export { default as CartPanel } from './cart/cart-panel'
|
|
9
|
+
|
|
10
|
+
export { default as LevelNodesWidget } from './level-nodes-widget'
|
|
11
|
+
|
|
12
|
+
export { default as PaymentStepForm } from './checkout/payment-step-form'
|
|
13
|
+
export { default as ShippingStepForm } from './checkout/shipping-step-form'
|
|
14
|
+
|
|
15
|
+
export { default as ProductCard } from './item/product-card'
|
|
16
|
+
export { Icons } from './Icons'
|
|
17
|
+
|
|
18
|
+
export { default as ImageItemSelector } from './select/image-selector'
|
|
19
|
+
export { default as RadioItemSelector } from './select/radio-selector'
|
|
20
|
+
export { default as CarouselItemSelector, type CarouselItemSelectorPropsExt } from './select/carousel-selector'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import Spline from '@splinetool/react-spline'
|
|
4
|
+
|
|
5
|
+
import { Image } from '@hanzo/ui/primitives'
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
VideoBlockComponent,
|
|
10
|
+
type Block,
|
|
11
|
+
type VideoBlock
|
|
12
|
+
} from '@hanzo/ui/blocks'
|
|
13
|
+
import type { Product } from '../../types'
|
|
14
|
+
|
|
15
|
+
const ItemMedia: React.FC<{
|
|
16
|
+
item: Product
|
|
17
|
+
constrainTo?: {w: number, h: number}
|
|
18
|
+
clx?: string
|
|
19
|
+
}> = ({
|
|
20
|
+
item: {img, video, animation},
|
|
21
|
+
constrainTo: cnst = {w: 250, h: 250},
|
|
22
|
+
clx=''
|
|
23
|
+
}) => {
|
|
24
|
+
|
|
25
|
+
// Order of precedence: 3D > MP4 > Image
|
|
26
|
+
return animation ? (
|
|
27
|
+
<Spline
|
|
28
|
+
scene={animation}
|
|
29
|
+
className={cn(clx, 'pointer-events-none')}
|
|
30
|
+
style={{
|
|
31
|
+
// // !aspect-[12/10]
|
|
32
|
+
width: (6/5 * (typeof cnst.h === 'number' ? cnst.h as number : parseInt(cnst.h as string)) ),
|
|
33
|
+
height: cnst.h
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
) : video ? (
|
|
37
|
+
<VideoBlockComponent
|
|
38
|
+
constrainTo={cnst}
|
|
39
|
+
block={{blockType: 'video', ...video} satisfies VideoBlock as Block}
|
|
40
|
+
className={clx}
|
|
41
|
+
/>
|
|
42
|
+
) : (img &&
|
|
43
|
+
<Image
|
|
44
|
+
def={img}
|
|
45
|
+
constrainTo={cnst}
|
|
46
|
+
className={cn('m-auto', clx)}
|
|
47
|
+
/>
|
|
48
|
+
) // TODO: return placeholder
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default ItemMedia
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardFooter,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from '@hanzo/ui/primitives'
|
|
10
|
+
|
|
11
|
+
import { cn } from '@hanzo/ui/util'
|
|
12
|
+
|
|
13
|
+
import { formatCurrencyValue } from '../../util'
|
|
14
|
+
import type { LineItem } from '../../types'
|
|
15
|
+
|
|
16
|
+
import AddToCartWidget from '../buy/add-to-cart-widget'
|
|
17
|
+
import ItemMedia from './item-media'
|
|
18
|
+
|
|
19
|
+
interface ProductCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
20
|
+
item: LineItem
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ProductCard: React.FC<ProductCardProps> = ({
|
|
24
|
+
item,
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}) => (
|
|
28
|
+
<Card
|
|
29
|
+
className={cn('max-h-[360px] lg:min-w-[200px] max-w-[260px] overflow-hidden', className)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<CardHeader className='w-full border-b p-6 min-h-[180px] max-h-[240px] relative'>
|
|
33
|
+
<ItemMedia item={item} constrainTo={{w: 700, h:700}} clx='p-6' />
|
|
34
|
+
</CardHeader>
|
|
35
|
+
<CardContent className='grid gap-2.5 p-4'>
|
|
36
|
+
<CardTitle className='text-sm sm:text-base flex flex-col justify-start items-center line-clap-3'>
|
|
37
|
+
{item.title.split(', ').map((e, i) => (<p key={i}>{e}</p>))}
|
|
38
|
+
<p className='mt-1 font-semibold'>{formatCurrencyValue(item.price)}</p>
|
|
39
|
+
</CardTitle>
|
|
40
|
+
</CardContent>
|
|
41
|
+
<CardFooter className='p-4 flex flex-row justify-center'>
|
|
42
|
+
<AddToCartWidget item={item}/>
|
|
43
|
+
</CardFooter>
|
|
44
|
+
</Card>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export default ProductCard
|
|
@@ -4,27 +4,29 @@ import React, { useState } from 'react'
|
|
|
4
4
|
import { ToggleGroup, ToggleGroupItem} from "@hanzo/ui/primitives"
|
|
5
5
|
import { cn } from '@hanzo/ui/util'
|
|
6
6
|
|
|
7
|
-
import type {
|
|
8
|
-
import
|
|
7
|
+
import type { ProductTreeNode, StringMutator, StringArrayMutator } from '../../types'
|
|
8
|
+
import NodeImage from './node-image'
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const LevelNodesWidget: React.FC<{
|
|
11
|
+
levelNodes: ProductTreeNode[]
|
|
12
12
|
mutator: StringMutator | StringArrayMutator
|
|
13
13
|
multiple?: boolean
|
|
14
14
|
className?: string
|
|
15
15
|
buttonClx?: string
|
|
16
16
|
itemClx?: string
|
|
17
|
-
|
|
17
|
+
mobile?: boolean
|
|
18
18
|
tabSize?: string
|
|
19
|
+
show?: 'image' | 'label' | 'image-and-label'
|
|
19
20
|
}> = ({
|
|
20
|
-
|
|
21
|
+
levelNodes,
|
|
21
22
|
mutator,
|
|
22
23
|
multiple=false,
|
|
23
24
|
buttonClx='',
|
|
24
25
|
itemClx='',
|
|
25
26
|
className='',
|
|
26
|
-
|
|
27
|
-
tabSize
|
|
27
|
+
mobile=false,
|
|
28
|
+
tabSize,
|
|
29
|
+
show='image-and-label'
|
|
28
30
|
}) => {
|
|
29
31
|
|
|
30
32
|
const [last, setLast] = useState<string | undefined>(undefined)
|
|
@@ -54,30 +56,30 @@ const FacetValuesWidget: React.FC<{
|
|
|
54
56
|
type={multiple ? 'multiple' : 'single'}
|
|
55
57
|
value={val}
|
|
56
58
|
variant='default'
|
|
57
|
-
size={tabSize ? tabSize : (
|
|
59
|
+
size={tabSize ? tabSize : (mobile ? 'sm' : 'default')}
|
|
58
60
|
onValueChange={multiple ? handleChangeMultiple : handleChangeSingle}
|
|
59
61
|
className={className}
|
|
60
62
|
{...roundedToSpread}
|
|
61
63
|
>
|
|
62
|
-
{
|
|
64
|
+
{levelNodes.map((treeNode, index) => {
|
|
63
65
|
const roundedToSpread: any = {}
|
|
64
66
|
if (!multiple) {
|
|
65
67
|
roundedToSpread.rounded = 'none'
|
|
66
68
|
if (index === 0) { roundedToSpread.rounded = 'llg' }
|
|
67
|
-
else if (index ===
|
|
69
|
+
else if (index === levelNodes.length - 1) { roundedToSpread.rounded = 'rlg' }
|
|
68
70
|
}
|
|
69
71
|
return (
|
|
70
72
|
<ToggleGroupItem
|
|
71
|
-
key={
|
|
72
|
-
value={
|
|
73
|
-
disabled={(last && last ===
|
|
74
|
-
aria-label={`Select ${
|
|
73
|
+
key={treeNode.skuToken}
|
|
74
|
+
value={treeNode.skuToken}
|
|
75
|
+
disabled={(last && last === treeNode.skuToken || treeNode.skuToken === mutator.get())}
|
|
76
|
+
aria-label={`Select ${treeNode.label}`}
|
|
75
77
|
{...roundedToSpread}
|
|
76
78
|
className={buttonClx}
|
|
77
79
|
>
|
|
78
80
|
<span className={cn('flex flex-row justify-center gap-1 h-6 items-center', itemClx)} >
|
|
79
|
-
<
|
|
80
|
-
{(!
|
|
81
|
+
{!(show === 'label') && (<NodeImage treeNode={treeNode} />) }
|
|
82
|
+
{(!(show === 'image') || !treeNode.img) && (<span className='whitespace-nowrap'>{treeNode.label}</span>)}
|
|
81
83
|
</span>
|
|
82
84
|
</ToggleGroupItem>
|
|
83
85
|
)
|
|
@@ -85,6 +87,5 @@ const FacetValuesWidget: React.FC<{
|
|
|
85
87
|
</ToggleGroup>
|
|
86
88
|
)
|
|
87
89
|
}
|
|
88
|
-
// hidden md:block
|
|
89
90
|
|
|
90
|
-
export default
|
|
91
|
+
export default LevelNodesWidget
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Image from 'next/image'
|
|
3
|
+
|
|
4
|
+
import type { ProductTreeNode } from '../../types'
|
|
5
|
+
|
|
6
|
+
const ICON_SIZE = 20
|
|
7
|
+
|
|
8
|
+
const NodeImage: React.FC<{
|
|
9
|
+
treeNode: ProductTreeNode
|
|
10
|
+
}> = ({
|
|
11
|
+
treeNode: {
|
|
12
|
+
img,
|
|
13
|
+
imgAR: ar,
|
|
14
|
+
label
|
|
15
|
+
}
|
|
16
|
+
}) => {
|
|
17
|
+
|
|
18
|
+
if (!img) { return null }
|
|
19
|
+
// treat as URL
|
|
20
|
+
return (typeof img === 'string') ? (
|
|
21
|
+
<Image
|
|
22
|
+
src={img as string}
|
|
23
|
+
alt={`Toggle ${label}`}
|
|
24
|
+
className={'block mr-1 '}
|
|
25
|
+
width={ar ? ar * ICON_SIZE : ICON_SIZE}
|
|
26
|
+
height={ICON_SIZE}
|
|
27
|
+
/>
|
|
28
|
+
) : (img as React.ReactNode)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default NodeImage
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useCallback, useEffect, useRef } from 'react'
|
|
3
|
+
import { reaction } from 'mobx'
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
import ItemMedia from '../item/item-media'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type CarouselOptionsType,
|
|
11
|
+
type CarouselApi,
|
|
12
|
+
Carousel,
|
|
13
|
+
CarouselContent,
|
|
14
|
+
CarouselItem,
|
|
15
|
+
CarouselPrevious,
|
|
16
|
+
CarouselNext,
|
|
17
|
+
ApplyTypography
|
|
18
|
+
} from '@hanzo/ui/primitives'
|
|
19
|
+
|
|
20
|
+
import type { ItemSelectorProps, LineItem } from '../../types'
|
|
21
|
+
import { formatCurrencyValue } from '../../util'
|
|
22
|
+
|
|
23
|
+
interface CarouselItemSelectorPropsExt {
|
|
24
|
+
constrainTo: {w: number, h: number}
|
|
25
|
+
options?: CarouselOptionsType
|
|
26
|
+
/** Do not show Category and / or Item title and Price */
|
|
27
|
+
imageOnly?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CarouselItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
31
|
+
items,
|
|
32
|
+
selectSku,
|
|
33
|
+
selectedItemRef: itemRef,
|
|
34
|
+
scrollList, // ignored
|
|
35
|
+
clx='',
|
|
36
|
+
itemClx='',
|
|
37
|
+
showCategory=false,
|
|
38
|
+
ext={
|
|
39
|
+
options: {loop: true},
|
|
40
|
+
constrainTo: {w: 250, h: 250},
|
|
41
|
+
imageOnly: false
|
|
42
|
+
} satisfies CarouselItemSelectorPropsExt
|
|
43
|
+
}) => {
|
|
44
|
+
|
|
45
|
+
const { options, constrainTo, imageOnly} = ext
|
|
46
|
+
|
|
47
|
+
const elbaApiRef = useRef<CarouselApi | undefined>(undefined)
|
|
48
|
+
const dontRespondRef = useRef<boolean>(false)
|
|
49
|
+
|
|
50
|
+
const setApi = (api: CarouselApi) => {elbaApiRef.current = api}
|
|
51
|
+
|
|
52
|
+
const onSelect = useCallback((emblaApi: CarouselApi) => {
|
|
53
|
+
if (dontRespondRef.current) {
|
|
54
|
+
dontRespondRef.current = false
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const index = emblaApi.selectedScrollSnap()
|
|
58
|
+
if (index !== -1) {
|
|
59
|
+
selectSku(items[index].sku)
|
|
60
|
+
}
|
|
61
|
+
dontRespondRef.current = false
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
return reaction(
|
|
66
|
+
() => ({item: itemRef.item}),
|
|
67
|
+
({item}) => {
|
|
68
|
+
if (elbaApiRef.current) {
|
|
69
|
+
const index = items.findIndex((el) => (el.sku === item?.sku))
|
|
70
|
+
if (index !== -1) {
|
|
71
|
+
dontRespondRef.current = true
|
|
72
|
+
elbaApiRef.current.scrollTo(index)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const ItemInfo: React.FC<{
|
|
80
|
+
item: LineItem
|
|
81
|
+
clx?: string
|
|
82
|
+
}> = ({
|
|
83
|
+
item,
|
|
84
|
+
clx
|
|
85
|
+
}) => (
|
|
86
|
+
<ApplyTypography className={cn(clx, 'flex flex-col items-center !gap-2 [&>*]:!m-0')}>
|
|
87
|
+
{showCategory && (<h4>{item.categoryTitle}</h4>)}
|
|
88
|
+
<p>{item.optionLabel}</p>
|
|
89
|
+
<p>{formatCurrencyValue(item.price)}</p>
|
|
90
|
+
</ApplyTypography>
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Carousel options={options} className={cn('w-full px-2', clx)} onSelect={onSelect} setApi={setApi}>
|
|
95
|
+
<CarouselContent>
|
|
96
|
+
{items.map((item, index) => (
|
|
97
|
+
<CarouselItem key={index} className={cn('p-2 flex flex-col justify-center items-center', itemClx)}>
|
|
98
|
+
<ItemMedia item={item} constrainTo={constrainTo} clx='' />
|
|
99
|
+
{!imageOnly && (<ItemInfo item={item} clx=''/>)}
|
|
100
|
+
</CarouselItem>
|
|
101
|
+
))}
|
|
102
|
+
</CarouselContent>
|
|
103
|
+
<CarouselPrevious className='left-1'/>
|
|
104
|
+
<CarouselNext className='right-1'/>
|
|
105
|
+
</Carousel>
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
type CarouselItemSelectorPropsExt,
|
|
111
|
+
CarouselItemSelector as default
|
|
112
|
+
}
|