@hanzo/commerce 7.3.2 → 7.3.4
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/Icons.tsx +34 -34
- package/components/add-to-cart-widget.tsx +183 -183
- package/components/buy/buy-card.tsx +259 -259
- package/components/buy/carousel-buy-card.tsx +242 -242
- package/components/buy/multi-family/all-variants-carousel.tsx +261 -261
- package/components/buy/multi-family/family-carousel/index.tsx +77 -77
- package/components/buy/multi-family/family-carousel/slide.tsx +83 -83
- package/components/buy/multi-family/family-carousel/state.ts +87 -87
- package/components/buy/multi-family/index.ts +2 -2
- package/components/buy/single-family-selector.tsx +90 -90
- package/components/buy/title-and-byline.tsx +25 -25
- package/components/cart/cart-panel/cart-line-item.tsx +76 -76
- package/components/cart/cart-panel/index.tsx +154 -154
- package/components/cart/cart-panel/promo-code.tsx +106 -106
- package/components/cart/cart-panel/total-area.tsx +60 -60
- package/components/checkout/payment-step-form/card-icon-row.tsx +26 -26
- package/components/checkout/payment-step-form/card-icons/amex.tsx +32 -32
- package/components/checkout/payment-step-form/card-icons/diners-club.tsx +13 -13
- package/components/checkout/payment-step-form/card-icons/discover.tsx +25 -25
- package/components/checkout/payment-step-form/card-icons/jcb.tsx +26 -26
- package/components/checkout/payment-step-form/card-icons/mastercard.tsx +27 -27
- package/components/checkout/payment-step-form/card-icons/visa.tsx +25 -25
- package/components/checkout/payment-step-form/cc-button.tsx +17 -17
- package/components/checkout/payment-step-form/contact-form.tsx +49 -49
- package/components/checkout/payment-step-form/crypto-icons/btc.tsx +11 -11
- package/components/checkout/payment-step-form/crypto-icons/eth.tsx +20 -20
- package/components/checkout/payment-step-form/crypto-icons/usdt.tsx +13 -13
- package/components/checkout/payment-step-form/index.tsx +122 -122
- package/components/checkout/payment-step-form/methods/bank-transfer.tsx +79 -79
- package/components/checkout/payment-step-form/methods/card.tsx +232 -232
- package/components/checkout/payment-step-form/methods/crypto.tsx +227 -227
- package/components/checkout/payment-step-form/methods/index.ts +22 -22
- package/components/checkout/shipping-step-form.tsx +172 -172
- package/components/index.ts +11 -11
- package/components/item/product-card.tsx +48 -48
- package/components/item-selector/button.tsx +188 -188
- package/components/item-selector/carousel/index.tsx +197 -197
- package/components/item-selector/carousel/slider.tsx +40 -40
- package/components/item-selector/index.ts +5 -5
- package/components/item-selector/quantity-indicator.tsx +48 -48
- package/components/node-tabs/index.tsx +91 -91
- package/components/node-tabs/node-image.tsx +31 -31
- package/index.ts +12 -12
- package/package.json +60 -60
- package/service/context.tsx +45 -45
- package/service/debug.ts +41 -41
- package/service/impls/standalone/actual-line-item.ts +135 -135
- package/service/impls/standalone/get-instance.ts +64 -64
- package/service/impls/standalone/index.ts +579 -579
- package/service/impls/standalone/order/firebase.ts +14 -14
- package/service/impls/standalone/order/index.ts +128 -128
- package/service/impls/standalone/persistence.ts +33 -33
- package/service/path-utils.ts +25 -25
- package/service/sep.ts +6 -6
- package/tsconfig.json +10 -10
- package/types/README.md +1 -1
- package/types/category-node.ts +49 -49
- package/types/checkout.ts +46 -46
- package/types/commerce-config.ts +13 -13
- package/types/commerce-service.ts +127 -127
- package/types/family.ts +25 -25
- package/types/index.ts +15 -15
- package/types/item-selector.ts +96 -96
- package/types/line-item.ts +29 -29
- package/types/multi-family-selector-props.ts +19 -19
- package/types/product.ts +20 -20
- package/types/promo.ts +9 -9
- package/types/selection-ui-specifier.ts +51 -51
- package/types/string-mutator.ts +13 -13
- package/types/token-separators.ts +7 -7
- package/util/analytics.ts +20 -20
- package/util/countries.ts +195 -195
- package/util/error.ts +34 -34
- package/util/index.ts +70 -70
- package/util/item-selector-options-accessor.ts +34 -34
- package/util/line-item-ref.ts +23 -23
- package/util/multi-family-selector-options-accessor.ts +14 -14
- package/util/obs-string-mutator.ts +22 -22
- package/util/product-media-accessor.ts +58 -58
- package/util/promo-codes.ts +108 -108
- package/util/selection-ui-specifiers.ts +29 -29
- package/util/square-payment.ts +42 -42
- package/util/use-sync-sku-param-w-current-item.ts +88 -88
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
|
-
import { reaction } from 'mobx'
|
|
4
|
-
import { observer } from 'mobx-react-lite'
|
|
5
|
-
|
|
6
|
-
import { cn } from '@hanzo/ui/util'
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
ApplyTypography,
|
|
10
|
-
type CarouselApi,
|
|
11
|
-
Carousel,
|
|
12
|
-
CarouselContent,
|
|
13
|
-
CarouselItem,
|
|
14
|
-
CarouselPrevious,
|
|
15
|
-
CarouselNext,
|
|
16
|
-
MediaStack,
|
|
17
|
-
} from '@hanzo/ui/primitives'
|
|
18
|
-
|
|
19
|
-
import type { MultiFamilySelectorProps, LineItem } from '../../../types'
|
|
20
|
-
import {
|
|
21
|
-
formatCurrencyValue,
|
|
22
|
-
accessItemOptions,
|
|
23
|
-
accessMultiSelectorOptions
|
|
24
|
-
} from '../../../util'
|
|
25
|
-
|
|
26
|
-
import QuantityIndicator from '../../item-selector/quantity-indicator'
|
|
27
|
-
import { ButtonItemSelector, useCommerce } from '../../..'
|
|
28
|
-
|
|
29
|
-
const debugBorder = (c: 'r' | 'g' | 'b', disable: boolean = true): string => {
|
|
30
|
-
|
|
31
|
-
if (disable) {
|
|
32
|
-
return ''
|
|
33
|
-
}
|
|
34
|
-
switch (c) {
|
|
35
|
-
case 'r': return ' border border-[#ffaaaa] '
|
|
36
|
-
case 'g': return ' border border-[#aaffaa] '
|
|
37
|
-
case 'b': return ' border border-[#aaaaff] '
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const DEFAULT_CONSTRAINT = {w: 250, h: 250}
|
|
42
|
-
|
|
43
|
-
const AllVariantsCarousel: React.FC<MultiFamilySelectorProps> = ({
|
|
44
|
-
families,
|
|
45
|
-
parent,
|
|
46
|
-
clx='',
|
|
47
|
-
itemClx='',
|
|
48
|
-
itemOptions,
|
|
49
|
-
selectorOptions,
|
|
50
|
-
mediaConstraint=DEFAULT_CONSTRAINT,
|
|
51
|
-
mobile=false, // not relavant to any children
|
|
52
|
-
}) => {
|
|
53
|
-
|
|
54
|
-
const cmmc = useCommerce()
|
|
55
|
-
const r = useRef<{
|
|
56
|
-
api: CarouselApi | undefined
|
|
57
|
-
items: LineItem[]
|
|
58
|
-
dontRespond: boolean
|
|
59
|
-
initialIndex: number
|
|
60
|
-
} | undefined>(undefined)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const [changeMeToRerender, setChangeMeToRerender] = useState<boolean>(false)
|
|
64
|
-
// Safe, since Carousel is only render (and this method passed), once the ref is initialized.
|
|
65
|
-
const setApi = (api: CarouselApi) => { r.current!.api = api }
|
|
66
|
-
|
|
67
|
-
const onSelect = useCallback((emblaApi: CarouselApi) => {
|
|
68
|
-
if (r.current?.dontRespond) {
|
|
69
|
-
r.current.dontRespond = false
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
const index = emblaApi.selectedScrollSnap()
|
|
73
|
-
if (index !== -1) {
|
|
74
|
-
const item = r.current?.items[index]
|
|
75
|
-
cmmc.setCurrentItem(item?.sku) // (sets currentFamily as well)
|
|
76
|
-
}
|
|
77
|
-
if (r.current) {
|
|
78
|
-
r.current.dontRespond = false
|
|
79
|
-
}
|
|
80
|
-
}, [])
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
|
|
84
|
-
const items = families.map((fam) => (fam.products as LineItem[])).flat()
|
|
85
|
-
const foundIndex = cmmc.currentItem ? items.findIndex((item) => (cmmc.currentItem!.sku == item.sku)) : -1
|
|
86
|
-
r.current = {
|
|
87
|
-
items,
|
|
88
|
-
initialIndex: foundIndex === -1 ? 0 : foundIndex,
|
|
89
|
-
dontRespond: false,
|
|
90
|
-
api: undefined
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
setChangeMeToRerender(!changeMeToRerender)
|
|
94
|
-
|
|
95
|
-
// This responds to the swatch clicks
|
|
96
|
-
return reaction(
|
|
97
|
-
() => (cmmc.currentItem),
|
|
98
|
-
(item) => {
|
|
99
|
-
if (r.current && r.current.api) {
|
|
100
|
-
const index = r.current.items.findIndex((_item: LineItem) => (_item.sku === item?.sku))
|
|
101
|
-
if (index && index !== -1) {
|
|
102
|
-
// no need to sync family, since ui only allows selecting within a family
|
|
103
|
-
r.current.dontRespond = true
|
|
104
|
-
r.current.api.scrollTo(index)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
)
|
|
109
|
-
}, [])
|
|
110
|
-
|
|
111
|
-
const Header: React.FC<{clx?: string}> = observer(({
|
|
112
|
-
clx=''
|
|
113
|
-
}) => {
|
|
114
|
-
|
|
115
|
-
const { showParentTitle, parentByline: parentBylineMode } = accessMultiSelectorOptions(selectorOptions)
|
|
116
|
-
const { familyTitle, showFamilyByline } = accessItemOptions(itemOptions)
|
|
117
|
-
|
|
118
|
-
const parentTitleDisplay = showParentTitle ? parent.label : undefined
|
|
119
|
-
const parentBylineDisplay = (parentBylineMode === 'own-line') ? parent.subNodesLabel : ''
|
|
120
|
-
|
|
121
|
-
const title = (familyTitle === 'none' ?
|
|
122
|
-
undefined
|
|
123
|
-
:
|
|
124
|
-
(familyTitle === 'long' ?
|
|
125
|
-
cmmc.currentFamily?.title
|
|
126
|
-
:
|
|
127
|
-
(cmmc.currentFamily?.titleShort ?? cmmc.currentFamily?.title)
|
|
128
|
-
)
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
let titleLinePrefix = (parentBylineMode === 'none' || parentBylineMode === 'own-line') ? '' : (parent.subNodesLabel ?? '')
|
|
132
|
-
if (titleLinePrefix.length > 0 && title && parentBylineMode === 'comma-sep') {
|
|
133
|
-
titleLinePrefix += ', '
|
|
134
|
-
}
|
|
135
|
-
else if (titleLinePrefix.length > 0 && title && parentBylineMode === 'colon-sep') {
|
|
136
|
-
titleLinePrefix += ': '
|
|
137
|
-
}
|
|
138
|
-
const titleDisplay = title ? (titleLinePrefix + title) : undefined
|
|
139
|
-
const bylineDisplay = (familyTitle !== 'none') && showFamilyByline ? cmmc.currentFamily?.byline : undefined
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<ApplyTypography className={cn('flex flex-col items-center !gap-0 [&>*]:!m-0 ', clx)} >
|
|
143
|
-
{parentTitleDisplay && <h4>{parentTitleDisplay}</h4>}
|
|
144
|
-
{parentBylineDisplay && <h4>{parentBylineDisplay}</h4>}
|
|
145
|
-
{titleDisplay && <h4>{titleDisplay}</h4>}
|
|
146
|
-
{bylineDisplay && (<h6 >{bylineDisplay}</h6>)}
|
|
147
|
-
</ApplyTypography>
|
|
148
|
-
)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
const ItemInfo: React.FC<{
|
|
152
|
-
clx?: string
|
|
153
|
-
labelClx?: string
|
|
154
|
-
}> = observer(({
|
|
155
|
-
clx='',
|
|
156
|
-
labelClx=''
|
|
157
|
-
}) => {
|
|
158
|
-
|
|
159
|
-
const {
|
|
160
|
-
showPrice,
|
|
161
|
-
showQuantity,
|
|
162
|
-
showFamilyInOption,
|
|
163
|
-
showByline,
|
|
164
|
-
} = accessItemOptions(itemOptions)
|
|
165
|
-
|
|
166
|
-
const optionLabel = () => (
|
|
167
|
-
showFamilyInOption ?
|
|
168
|
-
(cmmc.currentItem!.familyTitle + ', ' + cmmc.currentItem!.optionLabel)
|
|
169
|
-
:
|
|
170
|
-
cmmc.currentItem!.optionLabel
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
return (cmmc.currentItem && (
|
|
174
|
-
<ApplyTypography className={cn('flex flex-col items-center [&>*]:!m-0 !gap-1 ', clx)}>
|
|
175
|
-
<div className={
|
|
176
|
-
'flex items-center gap-1 [&>*]:!m-0 ' +
|
|
177
|
-
debugBorder('g') +
|
|
178
|
-
(showFamilyInOption ? 'flex-col' : 'flex-row')
|
|
179
|
-
}>
|
|
180
|
-
<h6 className={cn('font-semibold', labelClx)}>
|
|
181
|
-
{optionLabel() + (showPrice && !showFamilyInOption ? ',' : '')}
|
|
182
|
-
</h6>
|
|
183
|
-
<div className={
|
|
184
|
-
'flex items-center gap-1 [&>*]:!m-0 flex-row ' + debugBorder('b') +
|
|
185
|
-
(showFamilyInOption ? 'w-full justify-between' : '')
|
|
186
|
-
}>
|
|
187
|
-
{showPrice && (<p>{formatCurrencyValue(cmmc.currentItem.price)}</p>)}
|
|
188
|
-
{showQuantity && (
|
|
189
|
-
<QuantityIndicator
|
|
190
|
-
item={cmmc.currentItem}
|
|
191
|
-
clx='h-[22px] ml-4'
|
|
192
|
-
iconClx='fill-foreground'
|
|
193
|
-
digitClx='not-typography font-semibold text-primary-fg leading-none font-sans text-xs'
|
|
194
|
-
/>
|
|
195
|
-
)}
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
{showByline && cmmc.currentItem.byline && (<p>{cmmc.currentItem.byline}</p>)}
|
|
199
|
-
</ApplyTypography>
|
|
200
|
-
))
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
const Swatches: React.FC<{
|
|
204
|
-
clx?: string
|
|
205
|
-
}> = observer(({
|
|
206
|
-
clx=''
|
|
207
|
-
}) => {
|
|
208
|
-
const { showItemSwatches } = accessMultiSelectorOptions(selectorOptions)
|
|
209
|
-
if (
|
|
210
|
-
!showItemSwatches ||
|
|
211
|
-
!cmmc.currentFamily
|
|
212
|
-
//|| cmmc.currentFamily.products.length === 1
|
|
213
|
-
) {
|
|
214
|
-
return null
|
|
215
|
-
}
|
|
216
|
-
return <ButtonItemSelector
|
|
217
|
-
items={cmmc.currentFamily.products as LineItem[]}
|
|
218
|
-
selectedItemRef={cmmc}
|
|
219
|
-
selectSku={cmmc.setCurrentItem.bind(cmmc)}
|
|
220
|
-
clx={clx}
|
|
221
|
-
options={{
|
|
222
|
-
buttonType: 'image',
|
|
223
|
-
horizButtons: true
|
|
224
|
-
}}
|
|
225
|
-
/>
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
return (
|
|
229
|
-
<div className={cn('w-full flex flex-col items-center', clx)}>
|
|
230
|
-
<Header />
|
|
231
|
-
{r.current && /* Only render once we've set the ref fields, or else bad things! */ (
|
|
232
|
-
<Carousel
|
|
233
|
-
options={{loop: true, startIndex: r.current.initialIndex}}
|
|
234
|
-
className={'w-full px-2' + debugBorder('r')}
|
|
235
|
-
onCarouselSelect={onSelect}
|
|
236
|
-
setApi={setApi}
|
|
237
|
-
>
|
|
238
|
-
<CarouselContent>
|
|
239
|
-
{r.current.items.map((item) => (
|
|
240
|
-
<CarouselItem key={item.sku} className={cn('p-2 flex flex-col justify-center items-center', itemClx)}>
|
|
241
|
-
<MediaStack media={item} constrainTo={mediaConstraint} clx='my-4'/>
|
|
242
|
-
</CarouselItem>
|
|
243
|
-
))}
|
|
244
|
-
</CarouselContent>
|
|
245
|
-
{r.current.items.length > 1 && (<>
|
|
246
|
-
<CarouselPrevious className='left-1'/>
|
|
247
|
-
<CarouselNext className='right-1'/>
|
|
248
|
-
</>)}
|
|
249
|
-
</Carousel>
|
|
250
|
-
)}
|
|
251
|
-
<div className='flex flex-col items-center justify-start'>
|
|
252
|
-
<ItemInfo labelClx='!text-base font-medium'/>
|
|
253
|
-
<Swatches clx='mt-2'/>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export {
|
|
260
|
-
AllVariantsCarousel as default
|
|
261
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { reaction } from 'mobx'
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@hanzo/ui/util'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ApplyTypography,
|
|
10
|
+
type CarouselApi,
|
|
11
|
+
Carousel,
|
|
12
|
+
CarouselContent,
|
|
13
|
+
CarouselItem,
|
|
14
|
+
CarouselPrevious,
|
|
15
|
+
CarouselNext,
|
|
16
|
+
MediaStack,
|
|
17
|
+
} from '@hanzo/ui/primitives'
|
|
18
|
+
|
|
19
|
+
import type { MultiFamilySelectorProps, LineItem } from '../../../types'
|
|
20
|
+
import {
|
|
21
|
+
formatCurrencyValue,
|
|
22
|
+
accessItemOptions,
|
|
23
|
+
accessMultiSelectorOptions
|
|
24
|
+
} from '../../../util'
|
|
25
|
+
|
|
26
|
+
import QuantityIndicator from '../../item-selector/quantity-indicator'
|
|
27
|
+
import { ButtonItemSelector, useCommerce } from '../../..'
|
|
28
|
+
|
|
29
|
+
const debugBorder = (c: 'r' | 'g' | 'b', disable: boolean = true): string => {
|
|
30
|
+
|
|
31
|
+
if (disable) {
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
switch (c) {
|
|
35
|
+
case 'r': return ' border border-[#ffaaaa] '
|
|
36
|
+
case 'g': return ' border border-[#aaffaa] '
|
|
37
|
+
case 'b': return ' border border-[#aaaaff] '
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_CONSTRAINT = {w: 250, h: 250}
|
|
42
|
+
|
|
43
|
+
const AllVariantsCarousel: React.FC<MultiFamilySelectorProps> = ({
|
|
44
|
+
families,
|
|
45
|
+
parent,
|
|
46
|
+
clx='',
|
|
47
|
+
itemClx='',
|
|
48
|
+
itemOptions,
|
|
49
|
+
selectorOptions,
|
|
50
|
+
mediaConstraint=DEFAULT_CONSTRAINT,
|
|
51
|
+
mobile=false, // not relavant to any children
|
|
52
|
+
}) => {
|
|
53
|
+
|
|
54
|
+
const cmmc = useCommerce()
|
|
55
|
+
const r = useRef<{
|
|
56
|
+
api: CarouselApi | undefined
|
|
57
|
+
items: LineItem[]
|
|
58
|
+
dontRespond: boolean
|
|
59
|
+
initialIndex: number
|
|
60
|
+
} | undefined>(undefined)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const [changeMeToRerender, setChangeMeToRerender] = useState<boolean>(false)
|
|
64
|
+
// Safe, since Carousel is only render (and this method passed), once the ref is initialized.
|
|
65
|
+
const setApi = (api: CarouselApi) => { r.current!.api = api }
|
|
66
|
+
|
|
67
|
+
const onSelect = useCallback((emblaApi: CarouselApi) => {
|
|
68
|
+
if (r.current?.dontRespond) {
|
|
69
|
+
r.current.dontRespond = false
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
const index = emblaApi.selectedScrollSnap()
|
|
73
|
+
if (index !== -1) {
|
|
74
|
+
const item = r.current?.items[index]
|
|
75
|
+
cmmc.setCurrentItem(item?.sku) // (sets currentFamily as well)
|
|
76
|
+
}
|
|
77
|
+
if (r.current) {
|
|
78
|
+
r.current.dontRespond = false
|
|
79
|
+
}
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
|
|
84
|
+
const items = families.map((fam) => (fam.products as LineItem[])).flat()
|
|
85
|
+
const foundIndex = cmmc.currentItem ? items.findIndex((item) => (cmmc.currentItem!.sku == item.sku)) : -1
|
|
86
|
+
r.current = {
|
|
87
|
+
items,
|
|
88
|
+
initialIndex: foundIndex === -1 ? 0 : foundIndex,
|
|
89
|
+
dontRespond: false,
|
|
90
|
+
api: undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setChangeMeToRerender(!changeMeToRerender)
|
|
94
|
+
|
|
95
|
+
// This responds to the swatch clicks
|
|
96
|
+
return reaction(
|
|
97
|
+
() => (cmmc.currentItem),
|
|
98
|
+
(item) => {
|
|
99
|
+
if (r.current && r.current.api) {
|
|
100
|
+
const index = r.current.items.findIndex((_item: LineItem) => (_item.sku === item?.sku))
|
|
101
|
+
if (index && index !== -1) {
|
|
102
|
+
// no need to sync family, since ui only allows selecting within a family
|
|
103
|
+
r.current.dontRespond = true
|
|
104
|
+
r.current.api.scrollTo(index)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
111
|
+
const Header: React.FC<{clx?: string}> = observer(({
|
|
112
|
+
clx=''
|
|
113
|
+
}) => {
|
|
114
|
+
|
|
115
|
+
const { showParentTitle, parentByline: parentBylineMode } = accessMultiSelectorOptions(selectorOptions)
|
|
116
|
+
const { familyTitle, showFamilyByline } = accessItemOptions(itemOptions)
|
|
117
|
+
|
|
118
|
+
const parentTitleDisplay = showParentTitle ? parent.label : undefined
|
|
119
|
+
const parentBylineDisplay = (parentBylineMode === 'own-line') ? parent.subNodesLabel : ''
|
|
120
|
+
|
|
121
|
+
const title = (familyTitle === 'none' ?
|
|
122
|
+
undefined
|
|
123
|
+
:
|
|
124
|
+
(familyTitle === 'long' ?
|
|
125
|
+
cmmc.currentFamily?.title
|
|
126
|
+
:
|
|
127
|
+
(cmmc.currentFamily?.titleShort ?? cmmc.currentFamily?.title)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
let titleLinePrefix = (parentBylineMode === 'none' || parentBylineMode === 'own-line') ? '' : (parent.subNodesLabel ?? '')
|
|
132
|
+
if (titleLinePrefix.length > 0 && title && parentBylineMode === 'comma-sep') {
|
|
133
|
+
titleLinePrefix += ', '
|
|
134
|
+
}
|
|
135
|
+
else if (titleLinePrefix.length > 0 && title && parentBylineMode === 'colon-sep') {
|
|
136
|
+
titleLinePrefix += ': '
|
|
137
|
+
}
|
|
138
|
+
const titleDisplay = title ? (titleLinePrefix + title) : undefined
|
|
139
|
+
const bylineDisplay = (familyTitle !== 'none') && showFamilyByline ? cmmc.currentFamily?.byline : undefined
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<ApplyTypography className={cn('flex flex-col items-center !gap-0 [&>*]:!m-0 ', clx)} >
|
|
143
|
+
{parentTitleDisplay && <h4>{parentTitleDisplay}</h4>}
|
|
144
|
+
{parentBylineDisplay && <h4>{parentBylineDisplay}</h4>}
|
|
145
|
+
{titleDisplay && <h4>{titleDisplay}</h4>}
|
|
146
|
+
{bylineDisplay && (<h6 >{bylineDisplay}</h6>)}
|
|
147
|
+
</ApplyTypography>
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const ItemInfo: React.FC<{
|
|
152
|
+
clx?: string
|
|
153
|
+
labelClx?: string
|
|
154
|
+
}> = observer(({
|
|
155
|
+
clx='',
|
|
156
|
+
labelClx=''
|
|
157
|
+
}) => {
|
|
158
|
+
|
|
159
|
+
const {
|
|
160
|
+
showPrice,
|
|
161
|
+
showQuantity,
|
|
162
|
+
showFamilyInOption,
|
|
163
|
+
showByline,
|
|
164
|
+
} = accessItemOptions(itemOptions)
|
|
165
|
+
|
|
166
|
+
const optionLabel = () => (
|
|
167
|
+
showFamilyInOption ?
|
|
168
|
+
(cmmc.currentItem!.familyTitle + ', ' + cmmc.currentItem!.optionLabel)
|
|
169
|
+
:
|
|
170
|
+
cmmc.currentItem!.optionLabel
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return (cmmc.currentItem && (
|
|
174
|
+
<ApplyTypography className={cn('flex flex-col items-center [&>*]:!m-0 !gap-1 ', clx)}>
|
|
175
|
+
<div className={
|
|
176
|
+
'flex items-center gap-1 [&>*]:!m-0 ' +
|
|
177
|
+
debugBorder('g') +
|
|
178
|
+
(showFamilyInOption ? 'flex-col' : 'flex-row')
|
|
179
|
+
}>
|
|
180
|
+
<h6 className={cn('font-semibold', labelClx)}>
|
|
181
|
+
{optionLabel() + (showPrice && !showFamilyInOption ? ',' : '')}
|
|
182
|
+
</h6>
|
|
183
|
+
<div className={
|
|
184
|
+
'flex items-center gap-1 [&>*]:!m-0 flex-row ' + debugBorder('b') +
|
|
185
|
+
(showFamilyInOption ? 'w-full justify-between' : '')
|
|
186
|
+
}>
|
|
187
|
+
{showPrice && (<p>{formatCurrencyValue(cmmc.currentItem.price)}</p>)}
|
|
188
|
+
{showQuantity && (
|
|
189
|
+
<QuantityIndicator
|
|
190
|
+
item={cmmc.currentItem}
|
|
191
|
+
clx='h-[22px] ml-4'
|
|
192
|
+
iconClx='fill-foreground'
|
|
193
|
+
digitClx='not-typography font-semibold text-primary-fg leading-none font-sans text-xs'
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
{showByline && cmmc.currentItem.byline && (<p>{cmmc.currentItem.byline}</p>)}
|
|
199
|
+
</ApplyTypography>
|
|
200
|
+
))
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const Swatches: React.FC<{
|
|
204
|
+
clx?: string
|
|
205
|
+
}> = observer(({
|
|
206
|
+
clx=''
|
|
207
|
+
}) => {
|
|
208
|
+
const { showItemSwatches } = accessMultiSelectorOptions(selectorOptions)
|
|
209
|
+
if (
|
|
210
|
+
!showItemSwatches ||
|
|
211
|
+
!cmmc.currentFamily
|
|
212
|
+
//|| cmmc.currentFamily.products.length === 1
|
|
213
|
+
) {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
return <ButtonItemSelector
|
|
217
|
+
items={cmmc.currentFamily.products as LineItem[]}
|
|
218
|
+
selectedItemRef={cmmc}
|
|
219
|
+
selectSku={cmmc.setCurrentItem.bind(cmmc)}
|
|
220
|
+
clx={clx}
|
|
221
|
+
options={{
|
|
222
|
+
buttonType: 'image',
|
|
223
|
+
horizButtons: true
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div className={cn('w-full flex flex-col items-center', clx)}>
|
|
230
|
+
<Header />
|
|
231
|
+
{r.current && /* Only render once we've set the ref fields, or else bad things! */ (
|
|
232
|
+
<Carousel
|
|
233
|
+
options={{loop: true, startIndex: r.current.initialIndex}}
|
|
234
|
+
className={'w-full px-2' + debugBorder('r')}
|
|
235
|
+
onCarouselSelect={onSelect}
|
|
236
|
+
setApi={setApi}
|
|
237
|
+
>
|
|
238
|
+
<CarouselContent>
|
|
239
|
+
{r.current.items.map((item) => (
|
|
240
|
+
<CarouselItem key={item.sku} className={cn('p-2 flex flex-col justify-center items-center', itemClx)}>
|
|
241
|
+
<MediaStack media={item} constrainTo={mediaConstraint} clx='my-4'/>
|
|
242
|
+
</CarouselItem>
|
|
243
|
+
))}
|
|
244
|
+
</CarouselContent>
|
|
245
|
+
{r.current.items.length > 1 && (<>
|
|
246
|
+
<CarouselPrevious className='left-1'/>
|
|
247
|
+
<CarouselNext className='right-1'/>
|
|
248
|
+
</>)}
|
|
249
|
+
</Carousel>
|
|
250
|
+
)}
|
|
251
|
+
<div className='flex flex-col items-center justify-start'>
|
|
252
|
+
<ItemInfo labelClx='!text-base font-medium'/>
|
|
253
|
+
<Swatches clx='mt-2'/>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export {
|
|
260
|
+
AllVariantsCarousel as default
|
|
261
|
+
}
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React, { useCallback, useRef } from 'react'
|
|
3
|
-
|
|
4
|
-
import { cn } from '@hanzo/ui/util'
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
type CarouselApi,
|
|
8
|
-
Carousel,
|
|
9
|
-
CarouselContent,
|
|
10
|
-
CarouselItem,
|
|
11
|
-
CarouselPrevious,
|
|
12
|
-
CarouselNext,
|
|
13
|
-
} from '@hanzo/ui/primitives'
|
|
14
|
-
|
|
15
|
-
import type { MultiFamilySelectorProps } from '../../../../types'
|
|
16
|
-
|
|
17
|
-
import FamilySlide from './slide'
|
|
18
|
-
import { FamilyCarouselState } from './state'
|
|
19
|
-
|
|
20
|
-
import { useCommerce } from '../../../..'
|
|
21
|
-
|
|
22
|
-
const FamilyCarousel: React.FC<MultiFamilySelectorProps> = ({
|
|
23
|
-
families,
|
|
24
|
-
clx='',
|
|
25
|
-
itemClx='',
|
|
26
|
-
itemOptions,
|
|
27
|
-
selectorOptions,
|
|
28
|
-
mediaConstraint={w: 250, h: 250},
|
|
29
|
-
mobile=false,
|
|
30
|
-
}) => {
|
|
31
|
-
|
|
32
|
-
const cmmc = useCommerce()
|
|
33
|
-
const stateRef = useRef<FamilyCarouselState>(
|
|
34
|
-
new FamilyCarouselState(families, cmmc.setCurrentItem.bind(cmmc))
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
const onSelect = useCallback((emblaApi: CarouselApi) => {
|
|
38
|
-
const index = emblaApi.selectedScrollSnap()
|
|
39
|
-
if (index !== -1) {
|
|
40
|
-
stateRef.current.setCurrentFamily(families[index].id)
|
|
41
|
-
}
|
|
42
|
-
}, [stateRef.current])
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<Carousel
|
|
46
|
-
className={cn('px-2', clx)}
|
|
47
|
-
options={{loop: false}}
|
|
48
|
-
onCarouselSelect={onSelect}
|
|
49
|
-
>
|
|
50
|
-
<CarouselContent>
|
|
51
|
-
{families.map((family) => {
|
|
52
|
-
const slideState = stateRef.current.getSlideState(family.id)
|
|
53
|
-
if (!slideState) {
|
|
54
|
-
throw new Error(`FamilyCarousel: no SlideState for family '${family.id}'!`)
|
|
55
|
-
}
|
|
56
|
-
return (
|
|
57
|
-
<CarouselItem key={family.id} className='p-2 flex flex-col justify-center items-center'>
|
|
58
|
-
<FamilySlide
|
|
59
|
-
family={family}
|
|
60
|
-
selectedItemRef={slideState}
|
|
61
|
-
selectSku={slideState.selectSku.bind(slideState)}
|
|
62
|
-
mediaConstraint={mediaConstraint}
|
|
63
|
-
options={itemOptions}
|
|
64
|
-
mobile={mobile}
|
|
65
|
-
clx={itemClx}
|
|
66
|
-
/>
|
|
67
|
-
</CarouselItem>
|
|
68
|
-
)}
|
|
69
|
-
)}
|
|
70
|
-
</CarouselContent>
|
|
71
|
-
<CarouselPrevious className='left-1'/>
|
|
72
|
-
<CarouselNext className='right-1'/>
|
|
73
|
-
</Carousel>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export default FamilyCarousel
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useCallback, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '@hanzo/ui/util'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type CarouselApi,
|
|
8
|
+
Carousel,
|
|
9
|
+
CarouselContent,
|
|
10
|
+
CarouselItem,
|
|
11
|
+
CarouselPrevious,
|
|
12
|
+
CarouselNext,
|
|
13
|
+
} from '@hanzo/ui/primitives'
|
|
14
|
+
|
|
15
|
+
import type { MultiFamilySelectorProps } from '../../../../types'
|
|
16
|
+
|
|
17
|
+
import FamilySlide from './slide'
|
|
18
|
+
import { FamilyCarouselState } from './state'
|
|
19
|
+
|
|
20
|
+
import { useCommerce } from '../../../..'
|
|
21
|
+
|
|
22
|
+
const FamilyCarousel: React.FC<MultiFamilySelectorProps> = ({
|
|
23
|
+
families,
|
|
24
|
+
clx='',
|
|
25
|
+
itemClx='',
|
|
26
|
+
itemOptions,
|
|
27
|
+
selectorOptions,
|
|
28
|
+
mediaConstraint={w: 250, h: 250},
|
|
29
|
+
mobile=false,
|
|
30
|
+
}) => {
|
|
31
|
+
|
|
32
|
+
const cmmc = useCommerce()
|
|
33
|
+
const stateRef = useRef<FamilyCarouselState>(
|
|
34
|
+
new FamilyCarouselState(families, cmmc.setCurrentItem.bind(cmmc))
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const onSelect = useCallback((emblaApi: CarouselApi) => {
|
|
38
|
+
const index = emblaApi.selectedScrollSnap()
|
|
39
|
+
if (index !== -1) {
|
|
40
|
+
stateRef.current.setCurrentFamily(families[index].id)
|
|
41
|
+
}
|
|
42
|
+
}, [stateRef.current])
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Carousel
|
|
46
|
+
className={cn('px-2', clx)}
|
|
47
|
+
options={{loop: false}}
|
|
48
|
+
onCarouselSelect={onSelect}
|
|
49
|
+
>
|
|
50
|
+
<CarouselContent>
|
|
51
|
+
{families.map((family) => {
|
|
52
|
+
const slideState = stateRef.current.getSlideState(family.id)
|
|
53
|
+
if (!slideState) {
|
|
54
|
+
throw new Error(`FamilyCarousel: no SlideState for family '${family.id}'!`)
|
|
55
|
+
}
|
|
56
|
+
return (
|
|
57
|
+
<CarouselItem key={family.id} className='p-2 flex flex-col justify-center items-center'>
|
|
58
|
+
<FamilySlide
|
|
59
|
+
family={family}
|
|
60
|
+
selectedItemRef={slideState}
|
|
61
|
+
selectSku={slideState.selectSku.bind(slideState)}
|
|
62
|
+
mediaConstraint={mediaConstraint}
|
|
63
|
+
options={itemOptions}
|
|
64
|
+
mobile={mobile}
|
|
65
|
+
clx={itemClx}
|
|
66
|
+
/>
|
|
67
|
+
</CarouselItem>
|
|
68
|
+
)}
|
|
69
|
+
)}
|
|
70
|
+
</CarouselContent>
|
|
71
|
+
<CarouselPrevious className='left-1'/>
|
|
72
|
+
<CarouselNext className='right-1'/>
|
|
73
|
+
</Carousel>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default FamilyCarousel
|