@hanzo/commerce 6.1.13 → 6.2.1
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/_to_deprecate_select-family-item-card.tsx +3 -4
- package/components/buy/_to_deprecate_select-family-item-panel.tsx +4 -5
- package/components/buy/add-to-cart-widget.tsx +43 -32
- package/components/buy/buy-button.tsx +1 -1
- package/components/buy/buy-card.tsx +17 -26
- package/components/buy/buy-drawer.tsx +6 -25
- package/components/buy/carousel-buy-card.tsx +145 -194
- package/components/buy/foo.txt +27 -0
- package/components/cart/cart-panel/cart-line-item.tsx +1 -1
- package/components/checkout/payment-step-form/contact-form.tsx +2 -2
- package/components/checkout/payment-step-form/methods/crypto.tsx +1 -2
- package/components/checkout/shipping-step-form.tsx +5 -5
- package/components/index.ts +1 -3
- package/components/item-selector/button.tsx +175 -0
- package/components/item-selector/carousel.tsx +162 -0
- package/components/item-selector/index.ts +5 -0
- package/components/quantity-indicator.tsx +48 -0
- package/components/select-family/family-carousel/index.tsx +47 -38
- package/components/select-family/family-carousel/slide.tsx +83 -0
- package/components/select-family/family-carousel/state.ts +83 -0
- package/components/select-family/level-nodes-widget/index.tsx +2 -2
- package/components/select-family/level-nodes-widget/node-image.tsx +2 -2
- package/index.ts +7 -7
- package/package.json +7 -4
- package/service/context.tsx +8 -7
- package/service/debug.ts +41 -0
- package/service/get-instance.ts +3 -0
- package/service/impls/standalone/actual-line-item.ts +5 -7
- package/service/impls/standalone/index.ts +11 -3
- package/service/impls/standalone/standalone-service.ts +133 -38
- package/service/path-utils.ts +26 -0
- package/service/sep.ts +7 -0
- package/types/category-node.ts +38 -0
- package/types/commerce-service.ts +44 -12
- package/types/family.ts +4 -1
- package/types/index.ts +3 -2
- package/types/item-selector.ts +29 -14
- package/types/product.ts +5 -1
- package/types/selection-ui-specifier.ts +36 -0
- package/types/token-separators.ts +7 -0
- package/util/error.ts +34 -0
- package/util/index.ts +4 -1
- package/util/product-media-accessor.ts +58 -0
- package/util/promo-codes.ts +1 -2
- package/util/selection-ui-specifiers.ts +34 -0
- package/util/use-sync-sku-param-w-current-item.ts +2 -2
- package/components/select-family/family-carousel/family-slide.tsx +0 -36
- package/components/select-product/carousel-selector.tsx +0 -112
- package/components/select-product/image-selector.tsx +0 -162
- package/components/select-product/radio-selector.tsx +0 -125
- package/service/impls/index.ts +0 -3
- package/types/buy-ui-spec.ts +0 -8
- package/types/tree-node.ts +0 -24
- package/util/buy-ui-conf.ts +0 -35
- package/util/sep.ts +0 -5
package/types/family.ts
CHANGED
|
@@ -6,9 +6,12 @@ interface Family {
|
|
|
6
6
|
title: string // Minted Bar, Lux Elite Card
|
|
7
7
|
titleShort?: string // Elite
|
|
8
8
|
parentTitle?: string // Lux Gold, Lux Credit
|
|
9
|
+
byline?: string // literal text, or name of function on 'this' object that returns a string
|
|
9
10
|
desc?: string
|
|
10
11
|
img?: ImageDef,
|
|
11
|
-
// inbound they're Products
|
|
12
|
+
// inbound to the service, they're Products
|
|
13
|
+
// when it's initialized, interally they become LineItems
|
|
14
|
+
// always ok to cast up to LineItem thereafter
|
|
12
15
|
products: Product[]
|
|
13
16
|
}
|
|
14
17
|
|
package/types/index.ts
CHANGED
|
@@ -3,10 +3,11 @@ export type { default as Family } from './family'
|
|
|
3
3
|
export type { default as CommerceService } from './commerce-service'
|
|
4
4
|
export type { default as Product } from './product'
|
|
5
5
|
export type { default as Promo } from './promo'
|
|
6
|
-
export type { default as
|
|
6
|
+
export type { default as TokenSeparators } from './token-separators'
|
|
7
7
|
|
|
8
8
|
export * from './checkout'
|
|
9
9
|
export * from './item-selector'
|
|
10
10
|
export * from './line-item'
|
|
11
|
-
export * from './
|
|
11
|
+
export * from './category-node'
|
|
12
12
|
export * from './string-mutator'
|
|
13
|
+
export * from './selection-ui-specifier'
|
package/types/item-selector.ts
CHANGED
|
@@ -6,31 +6,45 @@ interface ItemSelector {
|
|
|
6
6
|
selectSku: (sku: string) => void
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
clx?: string
|
|
11
|
-
itemClx?: string
|
|
12
|
-
soleItemClx?: string
|
|
13
|
-
/** type-specific props for ItemSelector's.
|
|
14
|
-
* eg, Carousel options.
|
|
15
|
-
*/
|
|
16
|
-
ext?: any
|
|
17
|
-
/** List selectors will scroll. Used internally */
|
|
18
|
-
scrollList: boolean
|
|
9
|
+
type ItemSelectorOptions = {
|
|
19
10
|
/**
|
|
20
11
|
* Whether the item label includes the Family name.
|
|
21
12
|
* eg, 'Minted Bar, 1oz' vs '1oz'
|
|
22
|
-
* default:
|
|
23
|
-
* This overrides
|
|
13
|
+
* default: false.
|
|
24
14
|
*/
|
|
25
15
|
showFamily?: boolean
|
|
26
16
|
/**
|
|
27
17
|
* Show the current item quantity along with title and price.
|
|
28
18
|
* default: false
|
|
29
19
|
*/
|
|
30
|
-
showQuantity?: boolean
|
|
20
|
+
showQuantity?: boolean
|
|
21
|
+
|
|
22
|
+
/** default: true */
|
|
23
|
+
showPrice?: boolean
|
|
24
|
+
|
|
25
|
+
/** default: false */
|
|
26
|
+
imageButtons?: boolean
|
|
27
|
+
|
|
28
|
+
/** default: false */
|
|
29
|
+
horizButtons?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface _ItemSelectorCompProps {
|
|
33
|
+
clx?: string
|
|
34
|
+
itemClx?: string
|
|
35
|
+
soleItemClx?: string
|
|
36
|
+
/** type-specific props for ItemSelector's.
|
|
37
|
+
* eg, Carousel options.
|
|
38
|
+
*/
|
|
39
|
+
ext?: any
|
|
40
|
+
/** List selectors will scroll. Used internally */
|
|
41
|
+
scrollable: boolean
|
|
42
|
+
options?: ItemSelectorOptions
|
|
43
|
+
|
|
44
|
+
mobile?: boolean
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
type ItemSelectorCompProps = Omit<_ItemSelectorCompProps, '
|
|
47
|
+
type ItemSelectorCompProps = Omit<_ItemSelectorCompProps, 'scrollable'>
|
|
34
48
|
|
|
35
49
|
interface ItemSelectorProps extends
|
|
36
50
|
ItemSelector, _ItemSelectorCompProps {}
|
|
@@ -38,5 +52,6 @@ interface ItemSelectorProps extends
|
|
|
38
52
|
export {
|
|
39
53
|
type ItemSelector,
|
|
40
54
|
type ItemSelectorCompProps,
|
|
55
|
+
type ItemSelectorOptions,
|
|
41
56
|
type ItemSelectorProps
|
|
42
57
|
}
|
package/types/product.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MediaStackDef } from '@hanzo/ui/types'
|
|
1
|
+
import type { ImageDef, MediaStackDef } from '@hanzo/ui/types'
|
|
2
2
|
|
|
3
3
|
interface Product extends MediaStackDef {
|
|
4
4
|
id: string // DB index // not a logical aspect of our domain. may not be necessary at all
|
|
@@ -7,8 +7,12 @@ interface Product extends MediaStackDef {
|
|
|
7
7
|
optionLabel: string
|
|
8
8
|
familyId: string // skuPath, eg LXB-AU-B
|
|
9
9
|
familyTitle: string
|
|
10
|
+
byline?: string
|
|
10
11
|
desc?: string
|
|
11
12
|
price: number
|
|
13
|
+
// image to by uses as an image button in a list of choices
|
|
14
|
+
// defaults to (MediaStackDef's) img
|
|
15
|
+
optionImg?: ImageDef
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ItemSelectorOptions } from './item-selector'
|
|
2
|
+
type SelectorType = 'buttons' | 'carousel'
|
|
3
|
+
|
|
4
|
+
interface FamilyCarouselSlideOptions extends ItemSelectorOptions {
|
|
5
|
+
/**
|
|
6
|
+
* default: short
|
|
7
|
+
* 'short': product.shortTitle if defined
|
|
8
|
+
* 'long': always us product.title, even if .shortTitle is defined
|
|
9
|
+
* 'none': do not show title AND BYLINE
|
|
10
|
+
*/
|
|
11
|
+
title?: 'none' | 'long' | 'short'
|
|
12
|
+
/** default: true */
|
|
13
|
+
showByline?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SelectionUISpecifier {
|
|
17
|
+
multiFamily?: {
|
|
18
|
+
familySelector: 'carousel'
|
|
19
|
+
/** default: true */
|
|
20
|
+
showParentTitle: boolean
|
|
21
|
+
slide: {
|
|
22
|
+
type: 'buttons'
|
|
23
|
+
options?: FamilyCarouselSlideOptions
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
singleFamily?: {
|
|
27
|
+
type: SelectorType
|
|
28
|
+
options?: ItemSelectorOptions
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
type SelectionUISpecifier,
|
|
34
|
+
type FamilyCarouselSlideOptions,
|
|
35
|
+
type SelectorType
|
|
36
|
+
}
|
package/util/error.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
// https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript
|
|
3
|
+
|
|
4
|
+
type ErrorWithMessage = {
|
|
5
|
+
message: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
|
|
9
|
+
return (
|
|
10
|
+
typeof error === 'object' &&
|
|
11
|
+
error !== null &&
|
|
12
|
+
'message' in error &&
|
|
13
|
+
typeof (error as Record<string, unknown>).message === 'string'
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const toErrorWithMessage = (maybeError: unknown): ErrorWithMessage => {
|
|
18
|
+
|
|
19
|
+
if (isErrorWithMessage(maybeError)) {
|
|
20
|
+
return maybeError
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return new Error(JSON.stringify(maybeError))
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// fallback in case there's an error stringifying the maybeError
|
|
28
|
+
// like with circular references for example.
|
|
29
|
+
return new Error(String(maybeError))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const getErrorMessage = (error: unknown) => (toErrorWithMessage(error).message)
|
|
34
|
+
|
package/util/index.ts
CHANGED
|
@@ -61,5 +61,8 @@ export const getFacetValuesMutator = (level: number, cmmc: CommerceService): Str
|
|
|
61
61
|
export { default as useSyncSkuParamWithCurrentItem } from './use-sync-sku-param-w-current-item'
|
|
62
62
|
export { default as processSquareCardPayment } from './square-payment'
|
|
63
63
|
export { default as ObsStringMutator } from './obs-string-mutator'
|
|
64
|
+
export { default as ProductMediaAccessor } from './product-media-accessor'
|
|
64
65
|
|
|
65
|
-
export
|
|
66
|
+
export * from './selection-ui-specifiers'
|
|
67
|
+
|
|
68
|
+
export { getErrorMessage } from './error'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AnimationDef, ImageDef, MediaStackDef, VideoDef } from '@hanzo/ui/types'
|
|
2
|
+
|
|
3
|
+
interface FamilyMediaData {
|
|
4
|
+
img?: Record<string, ImageDef>
|
|
5
|
+
video?: Record<string, VideoDef>
|
|
6
|
+
anim?: Record<string, AnimationDef>
|
|
7
|
+
optionImg?: Record<string, ImageDef>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class ProductMediaAccessor {
|
|
11
|
+
|
|
12
|
+
_data: FamilyMediaData
|
|
13
|
+
|
|
14
|
+
constructor(data: FamilyMediaData) {
|
|
15
|
+
this._data = data
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
img = (key: string): ImageDef | undefined => (
|
|
19
|
+
(this._data.img) ? this._data.img[key] : undefined
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
spreadableImg = (key: string): { img?: ImageDef } => (
|
|
23
|
+
(this._data.img) ? {img: this._data.img[key]} : {}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
video = (key: string): VideoDef | undefined => (
|
|
27
|
+
(this._data.video) ? this._data.video[key] : undefined
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
spreadableVideo = (key: string): { video?: VideoDef } => (
|
|
31
|
+
(this._data.video) ? {video: this._data.video[key]} : {}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
anim = (key: string): AnimationDef | undefined => (
|
|
35
|
+
(this._data.anim) ? this._data.anim[key] : undefined
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
spreadableAnim = (key: string): { animation?: AnimationDef } => (
|
|
39
|
+
(this._data.anim) ? {animation: this._data.anim[key]} : {}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
mediaStack = (key: string): MediaStackDef => ({
|
|
43
|
+
...this.spreadableImg(key),
|
|
44
|
+
...this.spreadableVideo(key),
|
|
45
|
+
...this.spreadableAnim(key)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
optionImg = (key: string): ImageDef | undefined => (
|
|
50
|
+
(this._data.optionImg) ? this._data.optionImg[key] : undefined
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
spreadableOptionImg = (key: string): { optionImg?: ImageDef } => {
|
|
54
|
+
return (this._data.optionImg) ? { optionImg: this._data.optionImg[key]} : {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default ProductMediaAccessor
|
package/util/promo-codes.ts
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SelectionUISpecifier } from '../types'
|
|
2
|
+
import sep from '../service/sep'
|
|
3
|
+
|
|
4
|
+
const DEFAULT = {
|
|
5
|
+
multiFamily: {
|
|
6
|
+
familySelector: 'carousel',
|
|
7
|
+
showParentTitle: false,
|
|
8
|
+
slide: {
|
|
9
|
+
type: 'buttons'
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
singleFamily: { type: 'carousel' }
|
|
13
|
+
} satisfies SelectionUISpecifier
|
|
14
|
+
|
|
15
|
+
const map = new Map<string, SelectionUISpecifier>()
|
|
16
|
+
|
|
17
|
+
// key: first two tokens of path
|
|
18
|
+
export const initSelectionUI = (v: Record<string, SelectionUISpecifier>) => {
|
|
19
|
+
|
|
20
|
+
// Might have multiple calls client and server side
|
|
21
|
+
if (map.size > 0) {
|
|
22
|
+
map.clear()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (let [key, value] of Object.entries(v)) {
|
|
26
|
+
map.set(key, value)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const getSelectionUISpecifier = (skuPath: string): SelectionUISpecifier => {
|
|
31
|
+
const key = skuPath.split(sep.tok).slice(0, 2).join(sep.tok)
|
|
32
|
+
const result = map.get(key)
|
|
33
|
+
return result ?? DEFAULT
|
|
34
|
+
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import { useCommerce } from '../service/context'
|
|
12
12
|
import type { SelectedPaths } from '../types'
|
|
13
|
-
import
|
|
13
|
+
import sep from '../service/sep'
|
|
14
14
|
|
|
15
15
|
const PLEASE_SELECT_FACETS = 'Please select an option from each group.'
|
|
16
16
|
|
|
@@ -55,7 +55,7 @@ const useSyncSkuParamWithCurrentItem = (
|
|
|
55
55
|
useEffect(() => {
|
|
56
56
|
|
|
57
57
|
const setCurrentFamilyFromSku = (sku: string) => {
|
|
58
|
-
const toks: string[] = sku.split(
|
|
58
|
+
const toks: string[] = sku.split(sep.tok)
|
|
59
59
|
const fv: SelectedPaths = {}
|
|
60
60
|
// TODO: confirm that extra trailing nonsense tokens won't break selectPaths()
|
|
61
61
|
for (let i = 1; i < familyLevel; i++) {
|
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
|
|
8
|
-
import {
|
|
9
|
-
ApplyTypography,
|
|
10
|
-
MediaStack
|
|
11
|
-
} from '@hanzo/ui/primitives'
|
|
12
|
-
import type { Family } from '../../../types'
|
|
13
|
-
|
|
14
|
-
const FamilySlide: React.FC<{
|
|
15
|
-
family: Family
|
|
16
|
-
clx?: string
|
|
17
|
-
mediaConstraint?: {w: number, h: number}
|
|
18
|
-
getBylineText?: (c: Family) => string
|
|
19
|
-
variantOrientation?: 'vert' | 'horiz'
|
|
20
|
-
showVariantImage?: boolean
|
|
21
|
-
showVariantPrice?: boolean
|
|
22
|
-
}> = ({
|
|
23
|
-
family,
|
|
24
|
-
clx='',
|
|
25
|
-
mediaConstraint,
|
|
26
|
-
getBylineText,
|
|
27
|
-
variantOrientation,
|
|
28
|
-
showVariantImage,
|
|
29
|
-
showVariantPrice
|
|
30
|
-
}) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return <></>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export default FamilySlide
|
|
@@ -1,112 +0,0 @@
|
|
|
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
|
-
|
|
8
|
-
import {
|
|
9
|
-
type CarouselOptions,
|
|
10
|
-
type CarouselApi,
|
|
11
|
-
Carousel,
|
|
12
|
-
CarouselContent,
|
|
13
|
-
CarouselItem,
|
|
14
|
-
CarouselPrevious,
|
|
15
|
-
CarouselNext,
|
|
16
|
-
ApplyTypography,
|
|
17
|
-
MediaStack
|
|
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?: CarouselOptions
|
|
26
|
-
/** Do not show Family 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
|
-
showFamily=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
|
-
{showFamily && (<h4>{item.familyTitle}</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)} onCarouselSelect={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
|
-
<MediaStack media={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
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import { observer } from 'mobx-react-lite'
|
|
4
|
-
|
|
5
|
-
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
Image,
|
|
9
|
-
Label,
|
|
10
|
-
RadioGroup,
|
|
11
|
-
ScrollArea
|
|
12
|
-
} from '@hanzo/ui/primitives'
|
|
13
|
-
import { cn } from '@hanzo/ui/util'
|
|
14
|
-
|
|
15
|
-
import type { ItemSelectorProps, LineItem } from '../../types'
|
|
16
|
-
import { formatCurrencyValue } from '../../util'
|
|
17
|
-
|
|
18
|
-
const ImageRadioGroupItem = React.forwardRef<
|
|
19
|
-
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
20
|
-
Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>, 'value' | 'id'> & {
|
|
21
|
-
item: LineItem,
|
|
22
|
-
constrainTo: {w: number, h: number}
|
|
23
|
-
}
|
|
24
|
-
>(({
|
|
25
|
-
item,
|
|
26
|
-
constrainTo,
|
|
27
|
-
className,
|
|
28
|
-
...props
|
|
29
|
-
}, ref) => {
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<RadioGroupPrimitive.Item
|
|
33
|
-
ref={ref}
|
|
34
|
-
className={cn(
|
|
35
|
-
"ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
36
|
-
className
|
|
37
|
-
)}
|
|
38
|
-
{...props}
|
|
39
|
-
id={item.sku}
|
|
40
|
-
value={item.sku}
|
|
41
|
-
>
|
|
42
|
-
{item.img ? (
|
|
43
|
-
<Image def={item.img} constrainTo={constrainTo} preload className=''/>
|
|
44
|
-
) : ( // placeholder so things align
|
|
45
|
-
<div style={{height: constrainTo.h, width: constrainTo.w}}/>
|
|
46
|
-
)}
|
|
47
|
-
</RadioGroupPrimitive.Item>
|
|
48
|
-
)
|
|
49
|
-
})
|
|
50
|
-
ImageRadioGroupItem.displayName = 'ImageRadioGroupItem'
|
|
51
|
-
|
|
52
|
-
const ImageItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
53
|
-
items,
|
|
54
|
-
selectedItemRef: itemRef,
|
|
55
|
-
selectSku,
|
|
56
|
-
clx='',
|
|
57
|
-
itemClx='',
|
|
58
|
-
soleItemClx='',
|
|
59
|
-
showFamily=false,
|
|
60
|
-
showQuantity=false,
|
|
61
|
-
scrollList=false
|
|
62
|
-
}) => {
|
|
63
|
-
|
|
64
|
-
const LabelText: React.FC<{item: LineItem}> = ({item}) => (
|
|
65
|
-
(showFamily ? (item.familyTitle + ', ' + item.optionLabel) : item.optionLabel) +
|
|
66
|
-
(showFamily ? ': ' : ', ') + formatCurrencyValue(item.price)
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
const ItemAndPrice: React.FC<{
|
|
70
|
-
item: LineItem
|
|
71
|
-
listBoxMode: boolean
|
|
72
|
-
selected: boolean
|
|
73
|
-
className?: string
|
|
74
|
-
}> = ({
|
|
75
|
-
item,
|
|
76
|
-
listBoxMode,
|
|
77
|
-
selected,
|
|
78
|
-
className=''
|
|
79
|
-
}) => (
|
|
80
|
-
<div className={cn(
|
|
81
|
-
'flex items-center',
|
|
82
|
-
className,
|
|
83
|
-
itemClx,
|
|
84
|
-
)}>
|
|
85
|
-
<ImageRadioGroupItem
|
|
86
|
-
item={item}
|
|
87
|
-
constrainTo={{h: 36, w: 72}} // Apple suggest 42px
|
|
88
|
-
className={
|
|
89
|
-
'mr-2 ' + (listBoxMode ? '' :
|
|
90
|
-
'border-transparent border-2 rounded-sm data-[state=checked]:border-foreground')
|
|
91
|
-
}
|
|
92
|
-
/>
|
|
93
|
-
<Label htmlFor={item.sku} className={selected && listBoxMode ? 'text-accent' : ''}>
|
|
94
|
-
<LabelText item={item} />
|
|
95
|
-
</Label>
|
|
96
|
-
</div>
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
const Item: React.FC<{
|
|
100
|
-
item: LineItem
|
|
101
|
-
selected: boolean
|
|
102
|
-
listBoxMode: boolean
|
|
103
|
-
clx?: string
|
|
104
|
-
}> = ({
|
|
105
|
-
item,
|
|
106
|
-
selected,
|
|
107
|
-
listBoxMode,
|
|
108
|
-
clx=''
|
|
109
|
-
}) => {
|
|
110
|
-
|
|
111
|
-
const outClx = [
|
|
112
|
-
(listBoxMode ? 'border-b border-muted-3 py-1' : 'mb-3'),
|
|
113
|
-
(selected && listBoxMode ? 'border border-foreground rounded-sm' : ''),
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
return (showQuantity ? (
|
|
117
|
-
<div key={item.sku} className={cn('flex flex-row items-center', ...outClx, clx )}>
|
|
118
|
-
<ItemAndPrice item={item} selected={selected} listBoxMode={listBoxMode} className='grow'/>
|
|
119
|
-
<div className='grow-0 shrink-0 font-semibold text-sm leading-none px-2'>{ item.quantity > 0 ? `(${item.quantity})` : ' '}</div>
|
|
120
|
-
</div>
|
|
121
|
-
) : (
|
|
122
|
-
<ItemAndPrice
|
|
123
|
-
key={item.sku}
|
|
124
|
-
item={item}
|
|
125
|
-
selected={selected}
|
|
126
|
-
listBoxMode={listBoxMode}
|
|
127
|
-
className={cn(...outClx,
|
|
128
|
-
listBoxMode ? '' : 'mb-1',
|
|
129
|
-
clx)}
|
|
130
|
-
/>
|
|
131
|
-
))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return items.length > 1 ? (
|
|
135
|
-
<RadioGroup
|
|
136
|
-
className={cn('flex flex-col gap-0',
|
|
137
|
-
(scrollList ? 'shrink min-h-0' : ''),
|
|
138
|
-
clx,
|
|
139
|
-
)}
|
|
140
|
-
onValueChange={selectSku}
|
|
141
|
-
value={itemRef.item ? itemRef.item.sku : ''}
|
|
142
|
-
>
|
|
143
|
-
{scrollList ? (
|
|
144
|
-
<ScrollArea className='mt-2 w-full h-full py-0 border border-muted-2 rounded-sm '>
|
|
145
|
-
{items.map((item) => (
|
|
146
|
-
<Item item={item} listBoxMode={true} selected={itemRef.item?.sku === item.sku}/>
|
|
147
|
-
))}
|
|
148
|
-
</ScrollArea>
|
|
149
|
-
) : (<>
|
|
150
|
-
{items.map((item) => (
|
|
151
|
-
<Item item={item} listBoxMode={false} selected={itemRef.item?.sku === item.sku}/>
|
|
152
|
-
))}
|
|
153
|
-
</>)}
|
|
154
|
-
</RadioGroup>
|
|
155
|
-
) : (
|
|
156
|
-
<div className={soleItemClx}>
|
|
157
|
-
<LabelText item={items[0]} />
|
|
158
|
-
</div>
|
|
159
|
-
)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
export default ImageItemSelector
|