@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
|
@@ -7,40 +7,39 @@ import { Skeleton } from '@hanzo/ui/primitives'
|
|
|
7
7
|
|
|
8
8
|
import type { ItemSelector, LineItem } from '../../types'
|
|
9
9
|
|
|
10
|
-
import AddToCartWidget from '
|
|
11
|
-
import
|
|
12
|
-
import CategoryItemScrollSelector from '../category-item-scroll-selector'
|
|
10
|
+
import AddToCartWidget from './add-to-cart-widget'
|
|
11
|
+
import RadioItemSelector from '../select/radio-selector'
|
|
13
12
|
import { formatCurrencyValue } from '../../util'
|
|
14
13
|
|
|
15
14
|
const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & ItemSelector & {
|
|
16
15
|
isLoading?: boolean
|
|
17
16
|
mobile?: boolean
|
|
18
|
-
|
|
17
|
+
title?: string
|
|
19
18
|
onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
|
|
20
19
|
}> = /* NOT observer */({
|
|
21
|
-
|
|
20
|
+
items,
|
|
22
21
|
selectedItemRef: selItemRef,
|
|
23
22
|
selectSku,
|
|
24
23
|
className,
|
|
25
24
|
isLoading = false,
|
|
26
25
|
mobile = false,
|
|
27
|
-
|
|
26
|
+
title,
|
|
28
27
|
onQuantityChanged,
|
|
29
28
|
...props
|
|
30
29
|
}) => {
|
|
31
30
|
|
|
32
|
-
const soleOption =
|
|
31
|
+
const soleOption = items.length === 1
|
|
33
32
|
|
|
34
33
|
const SelectProductComp: React.FC<{ className?: string }> = ({
|
|
35
34
|
className = ''
|
|
36
35
|
}) => {
|
|
37
36
|
|
|
38
|
-
const mobilePicker = (mobile ||
|
|
37
|
+
const mobilePicker = (mobile || items.length > 6)
|
|
39
38
|
if (soleOption) {
|
|
40
|
-
const item =
|
|
39
|
+
const item = items[0] as LineItem
|
|
41
40
|
return (
|
|
42
41
|
<div className={cn('flex flex-col justify-center items-center ' + (mobilePicker ? 'h-[180px] ' : 'h-auto min-h-24'), className)}>
|
|
43
|
-
<p className='text-lg text-center font-semibold'>{item.
|
|
42
|
+
<p className='text-lg text-center font-semibold'>{item.optionLabel + ', ' + formatCurrencyValue(item.price)}</p>
|
|
44
43
|
</div>
|
|
45
44
|
)
|
|
46
45
|
}
|
|
@@ -50,25 +49,16 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
|
|
|
50
49
|
'sm:w-pr-80 sm:mx-auto md:w-full flex flex-col justify-start items-center',
|
|
51
50
|
className
|
|
52
51
|
)}>
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<CategoryItemRadioSelector
|
|
64
|
-
category={category}
|
|
65
|
-
selectedItemRef={selItemRef}
|
|
66
|
-
selectSku={selectSku}
|
|
67
|
-
groupClx='mt-2'
|
|
68
|
-
showQuantity={false}
|
|
69
|
-
itemClx='flex flex-row gap-2.5 items-center'
|
|
70
|
-
/>
|
|
71
|
-
)}
|
|
52
|
+
{title && (<div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } /> )}
|
|
53
|
+
<RadioItemSelector
|
|
54
|
+
items={items}
|
|
55
|
+
selectedItemRef={selItemRef}
|
|
56
|
+
selectSku={selectSku}
|
|
57
|
+
clx='mt-2'
|
|
58
|
+
showQuantity={false}
|
|
59
|
+
scrollList={false}
|
|
60
|
+
itemClx='flex flex-row gap-2.5 items-center'
|
|
61
|
+
/>
|
|
72
62
|
</div>
|
|
73
63
|
)
|
|
74
64
|
}
|
|
@@ -90,7 +80,7 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
|
|
|
90
80
|
isLoading ? (<Skeleton className={'h-8 w-full ' + className} />) : (
|
|
91
81
|
|
|
92
82
|
<div className={cn('text-center flex flex-col justify-start items-center', className)}>
|
|
93
|
-
<p className='font-heading text-center'>{
|
|
83
|
+
<p className='font-heading text-center'>{title}</p>
|
|
94
84
|
</div>
|
|
95
85
|
|
|
96
86
|
)))
|
|
@@ -103,13 +93,13 @@ const SelectCategoryItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & It
|
|
|
103
93
|
)}
|
|
104
94
|
{...props}
|
|
105
95
|
>
|
|
106
|
-
{
|
|
96
|
+
{title && (<TitleArea className='grow pt-3 mb-0' />)}
|
|
107
97
|
<SelectProductComp className='w-pr-65' />
|
|
108
98
|
<AddToCartComp className='w-pr-65' />
|
|
109
99
|
</div>
|
|
110
100
|
) : (
|
|
111
101
|
<div className={cn('', className)} {...props}>
|
|
112
|
-
{
|
|
102
|
+
{title && (<TitleArea className='' />)}
|
|
113
103
|
<div className='flex flex-col justify-start items-center gap-4'>
|
|
114
104
|
<SelectProductComp />
|
|
115
105
|
<AddToCartComp className='' />
|
|
@@ -6,17 +6,20 @@ import { observer } from 'mobx-react-lite'
|
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
import { Skeleton } from '@hanzo/ui/primitives'
|
|
8
8
|
|
|
9
|
-
import type {
|
|
10
|
-
import { formatCurrencyValue } from '
|
|
11
|
-
import { Icons } from '
|
|
9
|
+
import type { Category, ObsLineItemRef, LineItem } from '../../types'
|
|
10
|
+
import { formatCurrencyValue } from '../../util'
|
|
11
|
+
import { Icons } from '../Icons'
|
|
12
12
|
|
|
13
13
|
import AddToCartWidget from './add-to-cart-widget'
|
|
14
|
-
import
|
|
15
|
-
import CategoryItemScrollSelector from './category-item-scroll-selector'
|
|
14
|
+
import RadioItemSelector from '../select/radio-selector'
|
|
16
15
|
|
|
17
16
|
const SelectCategoryItemPanel: React.FC<
|
|
18
|
-
React.HTMLAttributes<HTMLDivElement> &
|
|
17
|
+
React.HTMLAttributes<HTMLDivElement> &
|
|
19
18
|
{
|
|
19
|
+
category: Category
|
|
20
|
+
selectedItemRef: ObsLineItemRef
|
|
21
|
+
selectSku: (sku: string) => void
|
|
22
|
+
showQuantity?: boolean
|
|
20
23
|
isLoading?: boolean
|
|
21
24
|
mobile?: boolean
|
|
22
25
|
}
|
|
@@ -49,7 +52,7 @@ const SelectCategoryItemPanel: React.FC<
|
|
|
49
52
|
<div className={cn('w-full border rounded-xl p-6 ')}>
|
|
50
53
|
<div className={cn('w-full aspect-square relative')}>
|
|
51
54
|
<Image
|
|
52
|
-
src={category.img}
|
|
55
|
+
src={category.img.src}
|
|
53
56
|
fill
|
|
54
57
|
sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, (max-width: 1200px) 50vw, 20vw"
|
|
55
58
|
alt={category.title}
|
|
@@ -88,25 +91,16 @@ const SelectCategoryItemPanel: React.FC<
|
|
|
88
91
|
<h6 className='text-center font-semibold'>Available options</h6>
|
|
89
92
|
<div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-60') } />
|
|
90
93
|
</div>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
category={category}
|
|
94
|
+
<RadioItemSelector
|
|
95
|
+
items={category.products as LineItem[]}
|
|
94
96
|
selectedItemRef={selectedItemRef}
|
|
95
97
|
selectSku={selectSku}
|
|
96
98
|
showQuantity={showQuantity}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
) : (
|
|
101
|
-
<CategoryItemRadioSelector
|
|
102
|
-
category={category}
|
|
103
|
-
selectedItemRef={selectedItemRef}
|
|
104
|
-
selectSku={selectSku}
|
|
105
|
-
showQuantity={showQuantity}
|
|
106
|
-
groupClx='block columns-2 gap-4'
|
|
99
|
+
scrollList={false}
|
|
100
|
+
|
|
101
|
+
clx='block columns-2 gap-4'
|
|
107
102
|
itemClx='flex flex-row gap-2 items-center mb-2.5'
|
|
108
103
|
/>
|
|
109
|
-
)}
|
|
110
104
|
</div>
|
|
111
105
|
)
|
|
112
106
|
})
|
|
@@ -132,7 +126,7 @@ const SelectCategoryItemPanel: React.FC<
|
|
|
132
126
|
</h3>
|
|
133
127
|
{selectedItemRef.item?.sku ? (
|
|
134
128
|
<h6 className='text-center font-semibold'>
|
|
135
|
-
{(soleOption ? '' : (selectedItemRef.item.
|
|
129
|
+
{(soleOption ? '' : (selectedItemRef.item.optionLabel + ': ')) + formatCurrencyValue(selectedItemRef.item.price)}
|
|
136
130
|
</h6>
|
|
137
131
|
) : ''}
|
|
138
132
|
</div>
|
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import { cn } from '@hanzo/ui/util'
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
import BuyCard from './buy-item-card'
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const BuyPopup: React.FC<PropsWithChildren & {
|
|
18
18
|
skuPath: string
|
|
19
19
|
triggerClx?: string
|
|
20
20
|
popupClx?: string
|
|
@@ -42,10 +42,10 @@ const BuyItemPopup: React.FC<PropsWithChildren & {
|
|
|
42
42
|
</PopoverTrigger>
|
|
43
43
|
<PopoverContent className={cn('relative flex flex-col p-0 px-4 pb-4 pt-2', popupClx)}>
|
|
44
44
|
<PopoverClose className='absolute z-20 right-2 top-2 self-end hover:bg-level-3 text-muted hover:text-accent p-1 rounded-full'><X className='w-5 h-5'/></PopoverClose>
|
|
45
|
-
<
|
|
45
|
+
<BuyCard skuPath={skuPath} onQuantityChanged={onQuantityChanged} className={cn("w-full relative ", cardClx)}/>
|
|
46
46
|
</PopoverContent>
|
|
47
47
|
</Popover>
|
|
48
48
|
)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export default
|
|
51
|
+
export default BuyPopup
|
|
@@ -5,9 +5,9 @@ import { observer } from 'mobx-react-lite'
|
|
|
5
5
|
import { Button, toast, type ButtonSizes } from '@hanzo/ui/primitives'
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
|
|
8
|
-
import { Icons } from '
|
|
9
|
-
import type { LineItem } from '
|
|
10
|
-
import { sendFBEvent, sendGAEvent } from '
|
|
8
|
+
import { Icons } from '../Icons'
|
|
9
|
+
import type { LineItem } from '../../types'
|
|
10
|
+
import { sendFBEvent, sendGAEvent } from '../../util/analytics'
|
|
11
11
|
|
|
12
12
|
const AddToCartWidget: React.FC<{
|
|
13
13
|
item: LineItem
|
|
@@ -27,8 +27,8 @@ const AddToCartWidget: React.FC<{
|
|
|
27
27
|
onQuantityChanged
|
|
28
28
|
}) => {
|
|
29
29
|
|
|
30
|
-
const iconClx = ghost ? 'h-4 w-4 md:h-3 md:w-3 text-muted-3 hover:text-foreground' : 'h-5 w-7 px-1'
|
|
31
|
-
const digitClx = ghost ? 'px-2 md:px-0.5 text-foreground ' : 'sm:px-2 font-
|
|
30
|
+
const iconClx = ghost ? 'h-4 w-4 md:h-3 md:w-3 text-muted-3 hover:text-foreground' : 'h-5 w-7 px-1 opacity-50'
|
|
31
|
+
const digitClx = ghost ? 'px-2 md:px-0.5 text-foreground ' : 'sm:px-2 font-semibold text-primary-fg '
|
|
32
32
|
|
|
33
33
|
if (disabled) {
|
|
34
34
|
return (
|
|
@@ -44,12 +44,14 @@ const AddToCartWidget: React.FC<{
|
|
|
44
44
|
if (onQuantityChanged) {
|
|
45
45
|
onQuantityChanged(item.sku, old, old + 1)
|
|
46
46
|
}
|
|
47
|
+
/*
|
|
47
48
|
if (old === 0) {
|
|
48
49
|
toast(`Added ${item.title} to your bag.`)
|
|
49
50
|
}
|
|
50
51
|
else {
|
|
51
52
|
toast(`Changed quantity to ${old + 1} for ${item.title}.`)
|
|
52
53
|
}
|
|
54
|
+
*/
|
|
53
55
|
sendGAEvent('add_to_cart', {
|
|
54
56
|
items: [{
|
|
55
57
|
item_id: item.sku,
|
|
@@ -79,12 +81,14 @@ const AddToCartWidget: React.FC<{
|
|
|
79
81
|
if (onQuantityChanged) {
|
|
80
82
|
onQuantityChanged(item.sku, old, old - 1)
|
|
81
83
|
}
|
|
84
|
+
/*
|
|
82
85
|
if (old === 1) {
|
|
83
86
|
toast(`Removed ${item.title} from your bag.`)
|
|
84
87
|
}
|
|
85
88
|
else {
|
|
86
89
|
toast(`Changed quantity to ${old - 1} for ${item.title}.`)
|
|
87
90
|
}
|
|
91
|
+
*/
|
|
88
92
|
sendGAEvent('remove_from_cart', {
|
|
89
93
|
items: [{
|
|
90
94
|
item_id: item.sku,
|
|
@@ -115,7 +119,7 @@ const AddToCartWidget: React.FC<{
|
|
|
115
119
|
<Icons.trash className={iconClx} aria-hidden='true'/>
|
|
116
120
|
)}
|
|
117
121
|
</Button>
|
|
118
|
-
<div className={'text-sm flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}</div>
|
|
122
|
+
<div className={'text-sm flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}{ghost ? '' : ' in Bag'}</div>
|
|
119
123
|
<Button
|
|
120
124
|
aria-label={'Add another ' + item.title + ' to the cart'}
|
|
121
125
|
size={size}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, {type PropsWithChildren} from 'react'
|
|
3
|
+
|
|
4
|
+
import { type ButtonVariants, type ButtonSizes, Button } from '@hanzo/ui/primitives'
|
|
5
|
+
|
|
6
|
+
import BuyTriggerWrapper from './buy-trigger-wrapper'
|
|
7
|
+
import { cn } from '@hanzo/ui/util'
|
|
8
|
+
|
|
9
|
+
const BuyButton: React.FC<PropsWithChildren & {
|
|
10
|
+
skuPath: string
|
|
11
|
+
variant? : ButtonVariants
|
|
12
|
+
size?: ButtonSizes
|
|
13
|
+
/* rounded?: ButtonRounded // TODO: wait for version bump*/
|
|
14
|
+
className?: string
|
|
15
|
+
mobile?: boolean
|
|
16
|
+
}> = ({
|
|
17
|
+
skuPath,
|
|
18
|
+
variant,
|
|
19
|
+
size,
|
|
20
|
+
children,
|
|
21
|
+
className='',
|
|
22
|
+
mobile=false
|
|
23
|
+
}) => (
|
|
24
|
+
<BuyTriggerWrapper skuPath={skuPath} mobile={mobile}
|
|
25
|
+
trigger={ <Button size={size} variant={variant} className={cn(className, '')}>{children}</Button> }
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export default BuyButton
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useRef, useEffect, type ComponentType } from 'react'
|
|
3
|
+
import { reaction, type IReactionDisposer } from 'mobx'
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
SelectedPaths,
|
|
10
|
+
ItemSelectorProps,
|
|
11
|
+
ItemSelectorCompProps,
|
|
12
|
+
LineItem,
|
|
13
|
+
Category,
|
|
14
|
+
ProductTreeNode,
|
|
15
|
+
} from '../../types'
|
|
16
|
+
|
|
17
|
+
import { useCommerce } from '../../service/context'
|
|
18
|
+
import { getFacetValuesMutator, ObsStringMutator } from '../../util'
|
|
19
|
+
|
|
20
|
+
import LevelNodesWidget from '../level-nodes-widget'
|
|
21
|
+
import AddToCartWidget from './add-to-cart-widget'
|
|
22
|
+
import ItemMedia from '../item/item-media'
|
|
23
|
+
import { ApplyTypography } from '@hanzo/ui/primitives'
|
|
24
|
+
|
|
25
|
+
const BuyCard: React.FC<{
|
|
26
|
+
skuPath: string
|
|
27
|
+
|
|
28
|
+
/** For any selectors that show a list of choices,
|
|
29
|
+
* starting with 'scrollAfter' elements,
|
|
30
|
+
* the list will scroll, and the card will
|
|
31
|
+
* have 'scrollHeightClx' applied to it to define and fix the height.
|
|
32
|
+
* This is most like something like '80vh'
|
|
33
|
+
* defaults are '5' and 'h-[70vh]'
|
|
34
|
+
* */
|
|
35
|
+
scrollAfter?: number
|
|
36
|
+
scrollHeightClx?: string
|
|
37
|
+
|
|
38
|
+
clx?: string
|
|
39
|
+
catWidgetClx?: string
|
|
40
|
+
addWidgetClx?: string
|
|
41
|
+
|
|
42
|
+
selector: ComponentType<ItemSelectorProps>
|
|
43
|
+
selectorProps?: ItemSelectorCompProps
|
|
44
|
+
|
|
45
|
+
/** Show all items from all sibling categories
|
|
46
|
+
* If skuPath defines a Category, the first of it's items will
|
|
47
|
+
* be selected. The facet widget will just select a the first
|
|
48
|
+
* item in the chosen Category without changings the overall set
|
|
49
|
+
* of choices. */
|
|
50
|
+
allVariants?: boolean
|
|
51
|
+
|
|
52
|
+
categoryTabAs?: 'image' | 'label' | 'image-and-label'
|
|
53
|
+
showItemMedia?: boolean
|
|
54
|
+
|
|
55
|
+
onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
|
|
56
|
+
mobile?: boolean
|
|
57
|
+
}> = observer(({
|
|
58
|
+
skuPath,
|
|
59
|
+
scrollAfter=5,
|
|
60
|
+
scrollHeightClx='h-[80vh]',
|
|
61
|
+
clx='',
|
|
62
|
+
catWidgetClx='',
|
|
63
|
+
addWidgetClx='',
|
|
64
|
+
selector: Selector,
|
|
65
|
+
selectorProps={
|
|
66
|
+
clx: '',
|
|
67
|
+
soleItemClx: '',
|
|
68
|
+
itemClx: '',
|
|
69
|
+
ext: {}
|
|
70
|
+
},
|
|
71
|
+
categoryTabAs='image-and-label',
|
|
72
|
+
showItemMedia=true,
|
|
73
|
+
allVariants=false,
|
|
74
|
+
mobile=false,
|
|
75
|
+
onQuantityChanged,
|
|
76
|
+
}) => {
|
|
77
|
+
|
|
78
|
+
const cmmc = useCommerce()
|
|
79
|
+
const inst = useRef<{
|
|
80
|
+
level: number,
|
|
81
|
+
requestedCat : Category | undefined
|
|
82
|
+
requestedNode: ProductTreeNode | undefined
|
|
83
|
+
currentCat: ObsStringMutator
|
|
84
|
+
disposers: IReactionDisposer[]
|
|
85
|
+
} | undefined>(undefined)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
|
|
90
|
+
let requestedCat = cmmc.getCategory(skuPath)
|
|
91
|
+
let requestedNode = requestedCat ? undefined : cmmc.getNodeAtPath(skuPath)
|
|
92
|
+
let initialCat = undefined
|
|
93
|
+
const toks = skuPath.split('-')
|
|
94
|
+
const level = toks.length - 1
|
|
95
|
+
if (level === 0) {
|
|
96
|
+
throw new Error('BuyCard.useEffect(): must specify at least at least one Level in skuPath!')
|
|
97
|
+
}
|
|
98
|
+
const paths: SelectedPaths = {}
|
|
99
|
+
for (let l = 1; l <= level; l++ ) {
|
|
100
|
+
paths[l] = [toks[l]]
|
|
101
|
+
}
|
|
102
|
+
if (requestedCat) {
|
|
103
|
+
if (allVariants && level >= 2) {
|
|
104
|
+
// select all siblings of requestedCat.
|
|
105
|
+
// ie, go one level back
|
|
106
|
+
delete paths[level]
|
|
107
|
+
initialCat = requestedCat
|
|
108
|
+
requestedCat = undefined
|
|
109
|
+
requestedNode = cmmc.getNodeAtPath(toks.slice(0, -1).join('-'))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// select siblings paths
|
|
114
|
+
if (allVariants) {
|
|
115
|
+
paths[level + 1] = [...requestedNode!.subNodes!.map((node) => (node.skuToken))]
|
|
116
|
+
}
|
|
117
|
+
// Actually select the the first cat at the cat level,
|
|
118
|
+
else {
|
|
119
|
+
paths[level + 1] = [requestedNode!.subNodes![0].skuToken]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const selectedCats = cmmc.selectPaths(paths)
|
|
124
|
+
initialCat = initialCat ? initialCat : (requestedCat ? requestedCat : selectedCats[0])
|
|
125
|
+
cmmc.setCurrentItem(initialCat.products[0].sku)
|
|
126
|
+
const currentCat = new ObsStringMutator(initialCat!.id.split('-').pop()!)
|
|
127
|
+
|
|
128
|
+
inst.current = {
|
|
129
|
+
level,
|
|
130
|
+
requestedCat,
|
|
131
|
+
requestedNode,
|
|
132
|
+
currentCat,
|
|
133
|
+
disposers: []
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!allVariants) {
|
|
137
|
+
inst.current?.disposers.push(reaction(
|
|
138
|
+
() => {
|
|
139
|
+
const cats = cmmc.selectedCategories
|
|
140
|
+
return (cats.length > 0) ? cats[0].id : undefined
|
|
141
|
+
},
|
|
142
|
+
(catId) => {
|
|
143
|
+
if (catId && catId !== cmmc.currentItem?.categoryId) {
|
|
144
|
+
const cat = cmmc.getCategory(catId)
|
|
145
|
+
cmmc.setCurrentItem(cat?.products[0].sku)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return () => {
|
|
152
|
+
inst.current?.disposers.forEach((d) => {d()})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
}, [])
|
|
156
|
+
|
|
157
|
+
const setCatPath = (pathValue: string | null) => {
|
|
158
|
+
if (inst.current?.currentCat.get() !== pathValue) {
|
|
159
|
+
inst.current?.currentCat.set(pathValue)
|
|
160
|
+
}
|
|
161
|
+
const found = cmmc.selectedItems.find((item) => (item.categoryId.endsWith(pathValue!)))
|
|
162
|
+
if (found) {
|
|
163
|
+
cmmc.setCurrentItem(found.sku)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const selectSku = (sku: string) => {
|
|
168
|
+
cmmc.setCurrentItem(sku)
|
|
169
|
+
const pathValue = cmmc.currentItem?.categoryId.split('-').pop()!
|
|
170
|
+
if (pathValue !== inst.current?.currentCat.get()) {
|
|
171
|
+
inst.current?.currentCat.set(pathValue)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const catTitle = inst.current?.requestedCat ? inst.current.requestedCat.title : cmmc.selectedCategories?.[0]?.title
|
|
176
|
+
|
|
177
|
+
const itemsToShow = (!cmmc.hasSelection) ? undefined :
|
|
178
|
+
allVariants ? cmmc.selectedItems : cmmc.selectedCategories[0].products as LineItem[]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
const TitleArea: React.FC<{
|
|
182
|
+
title: string
|
|
183
|
+
byline?: string
|
|
184
|
+
clx?: string
|
|
185
|
+
}> = ({
|
|
186
|
+
title,
|
|
187
|
+
byline,
|
|
188
|
+
clx=''
|
|
189
|
+
}) => (
|
|
190
|
+
<ApplyTypography className={clx}>
|
|
191
|
+
<h3>{title}</h3>
|
|
192
|
+
{byline && (<h6>{byline}</h6>)}
|
|
193
|
+
</ApplyTypography>
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const scroll = !!(itemsToShow && itemsToShow.length > scrollAfter)
|
|
197
|
+
return (
|
|
198
|
+
<div className={cn(
|
|
199
|
+
'px-4 md:px-6 pt-3 pb-4 flex flex-col items-center min-h-[60vh]',
|
|
200
|
+
clx,
|
|
201
|
+
scroll ? scrollHeightClx : 'h-auto'
|
|
202
|
+
)} >
|
|
203
|
+
{inst.current?.requestedNode && (<>
|
|
204
|
+
<ApplyTypography className={'mb-2 ' + (scroll ? 'shrink-0' : '')}>
|
|
205
|
+
<h3>{inst.current!.requestedNode.label}</h3>
|
|
206
|
+
{inst.current!.requestedNode.subNodesLabel && (
|
|
207
|
+
<h6 className='!text-center font-bold text-muted mt-3'>{inst.current!.requestedNode.subNodesLabel}</h6>
|
|
208
|
+
)}
|
|
209
|
+
</ApplyTypography>
|
|
210
|
+
<LevelNodesWidget
|
|
211
|
+
className={cn(
|
|
212
|
+
'grid gap-0 ' + `grid-cols-${inst.current.requestedNode.subNodes!.length}`,
|
|
213
|
+
'border-b-2 rounded-lg border-level-3 mb-4 -mr-2 -ml-2 max-w-[460px]',
|
|
214
|
+
(scroll ? 'shrink-0' : ''),
|
|
215
|
+
catWidgetClx
|
|
216
|
+
)}
|
|
217
|
+
mobile={mobile}
|
|
218
|
+
mutator={allVariants ?
|
|
219
|
+
{
|
|
220
|
+
get: () => (inst.current!.currentCat.s),
|
|
221
|
+
set: setCatPath
|
|
222
|
+
}
|
|
223
|
+
:
|
|
224
|
+
getFacetValuesMutator(inst.current.level + 1, cmmc)
|
|
225
|
+
}
|
|
226
|
+
itemClx='flex-col h-auto gap-0 py-1 px-3'
|
|
227
|
+
buttonClx={
|
|
228
|
+
'h-full ' +
|
|
229
|
+
'!border-level-3'
|
|
230
|
+
}
|
|
231
|
+
levelNodes={inst.current.requestedNode.subNodes!}
|
|
232
|
+
show={categoryTabAs}
|
|
233
|
+
/>
|
|
234
|
+
</>)}
|
|
235
|
+
{!inst.current?.requestedNode && catTitle && (
|
|
236
|
+
<TitleArea title={catTitle} clx=''/>
|
|
237
|
+
)}
|
|
238
|
+
{(cmmc.currentItem && showItemMedia) && (
|
|
239
|
+
<ItemMedia item={cmmc.currentItem} constrainTo={{w: 200, h: 200}} clx={(scroll ? 'shrink-0' : '')}/>
|
|
240
|
+
)}
|
|
241
|
+
{itemsToShow && (
|
|
242
|
+
<Selector
|
|
243
|
+
items={itemsToShow}
|
|
244
|
+
selectedItemRef={cmmc}
|
|
245
|
+
selectSku={selectSku}
|
|
246
|
+
{...selectorProps}
|
|
247
|
+
scrollList={scroll}
|
|
248
|
+
showCategory={selectorProps.showCategory ?
|
|
249
|
+
selectorProps.showCategory
|
|
250
|
+
:
|
|
251
|
+
(allVariants && !inst.current?.requestedCat)
|
|
252
|
+
}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
{(cmmc.currentItem) && (
|
|
256
|
+
<AddToCartWidget
|
|
257
|
+
size='default'
|
|
258
|
+
item={cmmc.currentItem}
|
|
259
|
+
onQuantityChanged={onQuantityChanged}
|
|
260
|
+
className={cn('min-w-[160px] mx-auto mt-4', (scroll ? 'shrink-0' : ''), addWidgetClx)}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
</div >
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
export default BuyCard
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useState, type ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import { X as LucideX} from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Drawer,
|
|
8
|
+
DrawerTrigger,
|
|
9
|
+
DrawerContent,
|
|
10
|
+
Button,
|
|
11
|
+
} from '@hanzo/ui/primitives'
|
|
12
|
+
|
|
13
|
+
import { cn } from '@hanzo/ui/util'
|
|
14
|
+
|
|
15
|
+
import { getUISpecFromPath } from '../../util'
|
|
16
|
+
|
|
17
|
+
import BuyCard from './buy-card'
|
|
18
|
+
import RadioItemSelector from '../select/radio-selector'
|
|
19
|
+
import ImageItemSelector from '../select/image-selector'
|
|
20
|
+
import CarouselItemSelector from '../select/carousel-selector'
|
|
21
|
+
|
|
22
|
+
const BuyDrawer: React.FC<{
|
|
23
|
+
skuPath: string
|
|
24
|
+
trigger: ReactNode
|
|
25
|
+
triggerClx?: string
|
|
26
|
+
drawerClx?: string
|
|
27
|
+
cardClx?: string
|
|
28
|
+
mobile?: boolean
|
|
29
|
+
}> = ({
|
|
30
|
+
skuPath,
|
|
31
|
+
trigger,
|
|
32
|
+
triggerClx='',
|
|
33
|
+
drawerClx='',
|
|
34
|
+
cardClx='',
|
|
35
|
+
mobile=false
|
|
36
|
+
}) => {
|
|
37
|
+
|
|
38
|
+
const [open, setOpen] = useState<boolean>(false)
|
|
39
|
+
|
|
40
|
+
const onQuantityChanged = (sku: string, oldV: number, newV: number) => {
|
|
41
|
+
//if (oldV === 0 && newV === 1) {
|
|
42
|
+
// setTimeout(() => {setOpen(false)}, 150)
|
|
43
|
+
//}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const spec = getUISpecFromPath(skuPath) ?? {
|
|
47
|
+
selector: 'radio',
|
|
48
|
+
allVariants: false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const selector = spec.selector === 'radio' ? RadioItemSelector :
|
|
52
|
+
(spec.selector === 'image' ? ImageItemSelector : CarouselItemSelector)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Drawer open={open} onOpenChange={setOpen} >
|
|
56
|
+
<DrawerTrigger asChild className={triggerClx}>
|
|
57
|
+
{trigger}
|
|
58
|
+
</DrawerTrigger>
|
|
59
|
+
<DrawerContent
|
|
60
|
+
className={cn('rounded-t-xl z-[52] mt-6 pb-12 h-auto min-h-[35vh] pt-6 md:max-w-[550px] md:mx-auto', drawerClx)}
|
|
61
|
+
overlayClx='z-[51]'
|
|
62
|
+
>
|
|
63
|
+
<BuyCard
|
|
64
|
+
skuPath={skuPath}
|
|
65
|
+
scrollAfter={spec.selector === 'carousel' ? 999 : undefined}
|
|
66
|
+
mobile={mobile}
|
|
67
|
+
onQuantityChanged={onQuantityChanged}
|
|
68
|
+
clx={cn('w-full', cardClx)}
|
|
69
|
+
selector={selector}
|
|
70
|
+
selectorProps={{soleItemClx:'mb-3'}}
|
|
71
|
+
showItemMedia={spec.selector !== 'carousel'}
|
|
72
|
+
categoryTabAs='label'
|
|
73
|
+
allVariants={spec.allVariants}
|
|
74
|
+
/>
|
|
75
|
+
<Button
|
|
76
|
+
variant='ghost'
|
|
77
|
+
size='icon'
|
|
78
|
+
onClick={() => {setOpen(false)}}
|
|
79
|
+
className={'absolute top-4 right-4 w-8 h-8 group rounded-full p-1 hidden md:flex items-center'}
|
|
80
|
+
>
|
|
81
|
+
<LucideX className='w-6 h-6 text-muted group-hover:text-foreground'/>
|
|
82
|
+
</Button>
|
|
83
|
+
|
|
84
|
+
</DrawerContent>
|
|
85
|
+
</Drawer>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default BuyDrawer
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import BuyDrawer from './buy-drawer'
|
|
5
|
+
|
|
6
|
+
const BuyTriggerWrapper: React.FC<{
|
|
7
|
+
skuPath: string
|
|
8
|
+
trigger: React.ReactNode
|
|
9
|
+
mobileTrigger?: React.ReactNode
|
|
10
|
+
mobile?: boolean
|
|
11
|
+
}> = ({
|
|
12
|
+
skuPath,
|
|
13
|
+
trigger,
|
|
14
|
+
mobileTrigger,
|
|
15
|
+
mobile
|
|
16
|
+
}) => (
|
|
17
|
+
<BuyDrawer skuPath={skuPath} trigger={trigger} mobile={mobile}/>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export default BuyTriggerWrapper
|