@hanzo/commerce 6.0.0 → 6.1.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-category-item-card.tsx +2 -1
- package/components/buy/_to_deprecate_select-category-item-panel.tsx +4 -2
- package/components/buy/add-to-cart-widget.tsx +5 -3
- package/components/buy/buy-card.tsx +184 -86
- package/components/buy/buy-drawer.tsx +18 -7
- package/components/cart/cart-panel/cart-line-item.tsx +22 -32
- package/components/cart/cart-panel/index.tsx +9 -0
- package/components/index.ts +1 -1
- package/components/item/item-media.tsx +11 -18
- package/components/item/product-card.tsx +20 -49
- package/components/{facet-values-widget → level-nodes-widget}/index.tsx +13 -13
- package/components/level-nodes-widget/node-image.tsx +31 -0
- package/components/select/carousel-selector.tsx +71 -23
- package/components/select/image-selector.tsx +93 -56
- package/components/select/radio-selector.tsx +86 -27
- package/package.json +2 -2
- package/service/impls/standalone/actual-line-item.ts +30 -16
- package/service/impls/standalone/standalone-service.ts +21 -3
- package/types/buy-ui-spec.ts +8 -0
- package/types/category.ts +6 -4
- package/types/commerce-service.ts +1 -0
- package/types/index.ts +1 -0
- package/types/item-selector.ts +25 -4
- package/types/line-item.ts +2 -0
- package/types/product.ts +5 -6
- package/util/buy-ui-conf.ts +34 -0
- package/util/index.ts +5 -2
- package/util/obs-string-mutator.ts +22 -0
- package/components/facet-values-widget/facet-image.tsx +0 -41
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import Image from 'next/image'
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
Card,
|
|
@@ -13,9 +12,9 @@ import { cn } from '@hanzo/ui/util'
|
|
|
13
12
|
|
|
14
13
|
import { formatCurrencyValue } from '../../util'
|
|
15
14
|
import type { LineItem } from '../../types'
|
|
16
|
-
import { Icons } from '../Icons'
|
|
17
15
|
|
|
18
16
|
import AddToCartWidget from '../buy/add-to-cart-widget'
|
|
17
|
+
import ItemMedia from './item-media'
|
|
19
18
|
|
|
20
19
|
interface ProductCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
21
20
|
item: LineItem
|
|
@@ -25,53 +24,25 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
25
24
|
item,
|
|
26
25
|
className,
|
|
27
26
|
...props
|
|
28
|
-
}) =>
|
|
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
|
+
)
|
|
29
46
|
|
|
30
|
-
const ProductImage: React.FC<{className?: string}> = ({className=''}) => (
|
|
31
|
-
<Image
|
|
32
|
-
src={item.img!}
|
|
33
|
-
alt={item.title}
|
|
34
|
-
className={className}
|
|
35
|
-
loading='lazy'
|
|
36
|
-
// width={700}
|
|
37
|
-
// height={700}
|
|
38
|
-
fill
|
|
39
|
-
style={{
|
|
40
|
-
objectFit: 'contain',
|
|
41
|
-
}}
|
|
42
|
-
/>
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<Card
|
|
47
|
-
className={cn('max-h-[360px] lg:min-w-[200px] max-w-[260px] overflow-hidden', className)}
|
|
48
|
-
{...props}
|
|
49
|
-
>
|
|
50
|
-
<CardHeader className='w-full border-b p-6 min-h-[180px] max-h-[240px] relative'>
|
|
51
|
-
{item.img ? (
|
|
52
|
-
<ProductImage className='p-6' />
|
|
53
|
-
) : (
|
|
54
|
-
<div
|
|
55
|
-
aria-label='Placeholder'
|
|
56
|
-
role='img'
|
|
57
|
-
aria-roledescription='placeholder'
|
|
58
|
-
className='flex h-full items-center justify-center bg-background'
|
|
59
|
-
>
|
|
60
|
-
<Icons.barcode className='h-9 w-9 text-muted' aria-hidden='true' />
|
|
61
|
-
</div>
|
|
62
|
-
)}
|
|
63
|
-
</CardHeader>
|
|
64
|
-
<CardContent className='grid gap-2.5 p-4'>
|
|
65
|
-
<CardTitle className='text-sm sm:text-base flex flex-col justify-start items-center line-clap-3'>
|
|
66
|
-
{item.title.split(', ').map((e, i) => (<p key={i}>{e}</p>))}
|
|
67
|
-
<p className='mt-1 font-semibold'>{formatCurrencyValue(item.price)}</p>
|
|
68
|
-
</CardTitle>
|
|
69
|
-
</CardContent>
|
|
70
|
-
<CardFooter className='p-4 flex flex-row justify-center'>
|
|
71
|
-
<AddToCartWidget item={item}/>
|
|
72
|
-
</CardFooter>
|
|
73
|
-
</Card>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
47
|
|
|
77
48
|
export default ProductCard
|
|
@@ -5,10 +5,10 @@ import { ToggleGroup, ToggleGroupItem} from "@hanzo/ui/primitives"
|
|
|
5
5
|
import { cn } from '@hanzo/ui/util'
|
|
6
6
|
|
|
7
7
|
import type { ProductTreeNode, StringMutator, StringArrayMutator } from '../../types'
|
|
8
|
-
import
|
|
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
|
|
@@ -18,7 +18,7 @@ const FacetValuesWidget: React.FC<{
|
|
|
18
18
|
tabSize?: string
|
|
19
19
|
show?: 'image' | 'label' | 'image-and-label'
|
|
20
20
|
}> = ({
|
|
21
|
-
|
|
21
|
+
levelNodes,
|
|
22
22
|
mutator,
|
|
23
23
|
multiple=false,
|
|
24
24
|
buttonClx='',
|
|
@@ -61,25 +61,25 @@ const FacetValuesWidget: React.FC<{
|
|
|
61
61
|
className={className}
|
|
62
62
|
{...roundedToSpread}
|
|
63
63
|
>
|
|
64
|
-
{
|
|
64
|
+
{levelNodes.map((treeNode, index) => {
|
|
65
65
|
const roundedToSpread: any = {}
|
|
66
66
|
if (!multiple) {
|
|
67
67
|
roundedToSpread.rounded = 'none'
|
|
68
68
|
if (index === 0) { roundedToSpread.rounded = 'llg' }
|
|
69
|
-
else if (index ===
|
|
69
|
+
else if (index === levelNodes.length - 1) { roundedToSpread.rounded = 'rlg' }
|
|
70
70
|
}
|
|
71
71
|
return (
|
|
72
72
|
<ToggleGroupItem
|
|
73
|
-
key={
|
|
74
|
-
value={
|
|
75
|
-
disabled={(last && last ===
|
|
76
|
-
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}`}
|
|
77
77
|
{...roundedToSpread}
|
|
78
78
|
className={buttonClx}
|
|
79
79
|
>
|
|
80
80
|
<span className={cn('flex flex-row justify-center gap-1 h-6 items-center', itemClx)} >
|
|
81
|
-
{!(show === 'label') && (<
|
|
82
|
-
{(!(show === 'image') || !
|
|
81
|
+
{!(show === 'label') && (<NodeImage treeNode={treeNode} />) }
|
|
82
|
+
{(!(show === 'image') || !treeNode.img) && (<span className='whitespace-nowrap'>{treeNode.label}</span>)}
|
|
83
83
|
</span>
|
|
84
84
|
</ToggleGroupItem>
|
|
85
85
|
)
|
|
@@ -88,4 +88,4 @@ const FacetValuesWidget: React.FC<{
|
|
|
88
88
|
)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
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
|
|
@@ -1,54 +1,102 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useCallback, useEffect, useRef } from 'react'
|
|
3
|
+
import { reaction } from 'mobx'
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
2
5
|
|
|
3
|
-
import Spline from '@splinetool/react-spline'
|
|
4
|
-
|
|
5
|
-
import type { Dimensions } from '@hanzo/ui/types'
|
|
6
6
|
import { cn } from '@hanzo/ui/util'
|
|
7
7
|
import ItemMedia from '../item/item-media'
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
type CarouselOptionsType,
|
|
11
|
+
type CarouselApi,
|
|
11
12
|
Carousel,
|
|
12
13
|
CarouselContent,
|
|
13
14
|
CarouselItem,
|
|
14
15
|
CarouselPrevious,
|
|
15
|
-
CarouselNext
|
|
16
|
+
CarouselNext,
|
|
17
|
+
ApplyTypography
|
|
16
18
|
} from '@hanzo/ui/primitives'
|
|
17
19
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
ImageBlockComponent,
|
|
21
|
-
type ImageBlock,
|
|
22
|
-
type Block,
|
|
23
|
-
type VideoBlock
|
|
24
|
-
} from '@hanzo/ui/blocks'
|
|
25
|
-
|
|
26
|
-
import type { ItemSelectorProps } from '../../types'
|
|
20
|
+
import type { ItemSelectorProps, LineItem } from '../../types'
|
|
21
|
+
import { formatCurrencyValue } from '../../util'
|
|
27
22
|
|
|
28
23
|
interface CarouselItemSelectorPropsExt {
|
|
29
|
-
constrainTo:
|
|
24
|
+
constrainTo: {w: number, h: number}
|
|
30
25
|
options?: CarouselOptionsType
|
|
31
|
-
|
|
26
|
+
/** Do not show Category and / or Item title and Price */
|
|
27
|
+
imageOnly?: boolean
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
const CarouselItemSelector: React.FC<ItemSelectorProps> = ({
|
|
30
|
+
const CarouselItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
35
31
|
items,
|
|
32
|
+
selectSku,
|
|
33
|
+
selectedItemRef: itemRef,
|
|
34
|
+
scrollList, // ignored
|
|
36
35
|
clx='',
|
|
37
36
|
itemClx='',
|
|
37
|
+
showCategory=false,
|
|
38
38
|
ext={
|
|
39
|
-
options: {},
|
|
40
|
-
constrainTo: {w: 250, h: 250}
|
|
39
|
+
options: {loop: true},
|
|
40
|
+
constrainTo: {w: 250, h: 250},
|
|
41
|
+
imageOnly: false
|
|
41
42
|
} satisfies CarouselItemSelectorPropsExt
|
|
42
43
|
}) => {
|
|
43
44
|
|
|
44
|
-
const { options, constrainTo} = ext
|
|
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
|
+
)
|
|
45
92
|
|
|
46
93
|
return (
|
|
47
|
-
<Carousel options={options} className={cn('px-2', clx)} >
|
|
94
|
+
<Carousel options={options} className={cn('w-full px-2', clx)} onSelect={onSelect} setApi={setApi}>
|
|
48
95
|
<CarouselContent>
|
|
49
96
|
{items.map((item, index) => (
|
|
50
|
-
<CarouselItem key={index} className={cn('p-2 flex flex-
|
|
97
|
+
<CarouselItem key={index} className={cn('p-2 flex flex-col justify-center items-center', itemClx)}>
|
|
51
98
|
<ItemMedia item={item} constrainTo={constrainTo} clx='' />
|
|
99
|
+
{!imageOnly && (<ItemInfo item={item} clx=''/>)}
|
|
52
100
|
</CarouselItem>
|
|
53
101
|
))}
|
|
54
102
|
</CarouselContent>
|
|
@@ -56,7 +104,7 @@ const CarouselItemSelector: React.FC<ItemSelectorProps> = ({
|
|
|
56
104
|
<CarouselNext className='right-1'/>
|
|
57
105
|
</Carousel>
|
|
58
106
|
)
|
|
59
|
-
}
|
|
107
|
+
})
|
|
60
108
|
|
|
61
109
|
export {
|
|
62
110
|
type CarouselItemSelectorPropsExt,
|
|
@@ -1,50 +1,33 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import Image from 'next/image'
|
|
4
3
|
import { observer } from 'mobx-react-lite'
|
|
5
4
|
|
|
6
5
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Image,
|
|
9
|
+
Label,
|
|
10
|
+
RadioGroup,
|
|
11
|
+
ScrollArea
|
|
12
|
+
} from '@hanzo/ui/primitives'
|
|
13
|
+
import { cn } from '@hanzo/ui/util'
|
|
14
|
+
|
|
8
15
|
import type { ItemSelectorProps, LineItem } from '../../types'
|
|
9
16
|
import { formatCurrencyValue } from '../../util'
|
|
10
|
-
import { cn } from '@hanzo/ui/util'
|
|
11
|
-
import type { Dimensions } from '@hanzo/ui/types'
|
|
12
17
|
|
|
13
18
|
const ImageRadioGroupItem = React.forwardRef<
|
|
14
19
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
15
20
|
Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>, 'value' | 'id'> & {
|
|
16
21
|
item: LineItem,
|
|
17
|
-
|
|
22
|
+
constrainTo: {w: number, h: number}
|
|
18
23
|
}
|
|
19
24
|
>(({
|
|
20
25
|
item,
|
|
21
|
-
|
|
26
|
+
constrainTo,
|
|
22
27
|
className,
|
|
23
28
|
...props
|
|
24
29
|
}, ref) => {
|
|
25
30
|
|
|
26
|
-
let dim: Dimensions
|
|
27
|
-
if (item.imgAR) {
|
|
28
|
-
if (item.imgAR >= 1) {
|
|
29
|
-
dim = {
|
|
30
|
-
w: imgSizePx,
|
|
31
|
-
h: imgSizePx / item.imgAR
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
dim = {
|
|
36
|
-
w: imgSizePx * item.imgAR,
|
|
37
|
-
h: imgSizePx
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
dim = {
|
|
43
|
-
w: imgSizePx,
|
|
44
|
-
h: imgSizePx
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
return (
|
|
49
32
|
<RadioGroupPrimitive.Item
|
|
50
33
|
ref={ref}
|
|
@@ -56,10 +39,10 @@ const ImageRadioGroupItem = React.forwardRef<
|
|
|
56
39
|
id={item.sku}
|
|
57
40
|
value={item.sku}
|
|
58
41
|
>
|
|
59
|
-
{
|
|
60
|
-
<Image
|
|
42
|
+
{item.img ? (
|
|
43
|
+
<Image def={item.img} constrainTo={constrainTo} preload className=''/>
|
|
61
44
|
) : ( // placeholder so things align
|
|
62
|
-
<div style={{height:
|
|
45
|
+
<div style={{height: constrainTo.h, width: constrainTo.w}}/>
|
|
63
46
|
)}
|
|
64
47
|
</RadioGroupPrimitive.Item>
|
|
65
48
|
)
|
|
@@ -73,52 +56,106 @@ const ImageItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
|
73
56
|
clx='',
|
|
74
57
|
itemClx='',
|
|
75
58
|
soleItemClx='',
|
|
76
|
-
|
|
77
|
-
showQuantity=
|
|
59
|
+
showCategory=false,
|
|
60
|
+
showQuantity=false,
|
|
61
|
+
scrollList=false
|
|
78
62
|
}) => {
|
|
79
63
|
|
|
80
|
-
const
|
|
64
|
+
const LabelText: React.FC<{item: LineItem}> = ({item}) => (
|
|
65
|
+
(showCategory ? (item.categoryTitle + ', ' + item.optionLabel) : item.optionLabel) +
|
|
66
|
+
(showCategory ? ': ' : ', ') + formatCurrencyValue(item.price)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const ItemAndPrice: React.FC<{
|
|
81
70
|
item: LineItem
|
|
71
|
+
listBoxMode: boolean
|
|
72
|
+
selected: boolean
|
|
82
73
|
className?: string
|
|
83
74
|
}> = ({
|
|
84
75
|
item,
|
|
76
|
+
listBoxMode,
|
|
77
|
+
selected,
|
|
85
78
|
className=''
|
|
86
79
|
}) => (
|
|
87
|
-
<div className={cn(
|
|
88
|
-
|
|
89
|
-
|
|
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>
|
|
90
96
|
</div>
|
|
91
97
|
)
|
|
92
|
-
|
|
93
|
-
const
|
|
98
|
+
|
|
99
|
+
const Item: React.FC<{
|
|
94
100
|
item: LineItem
|
|
95
|
-
|
|
101
|
+
selected: boolean
|
|
102
|
+
listBoxMode: boolean
|
|
103
|
+
clx?: string
|
|
96
104
|
}> = ({
|
|
97
105
|
item,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}
|
|
104
133
|
|
|
105
134
|
return items.length > 1 ? (
|
|
106
135
|
<RadioGroup
|
|
107
|
-
className={cn('gap-0',
|
|
136
|
+
className={cn('flex flex-col gap-0',
|
|
137
|
+
(scrollList ? 'shrink min-h-0' : ''),
|
|
138
|
+
clx,
|
|
139
|
+
)}
|
|
108
140
|
onValueChange={selectSku}
|
|
109
141
|
value={itemRef.item ? itemRef.item.sku : ''}
|
|
110
142
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
</>)}
|
|
119
154
|
</RadioGroup>
|
|
120
155
|
) : (
|
|
121
|
-
<
|
|
156
|
+
<div className={soleItemClx}>
|
|
157
|
+
<LabelText item={items[0]} />
|
|
158
|
+
</div>
|
|
122
159
|
)
|
|
123
160
|
})
|
|
124
161
|
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { observer } from 'mobx-react-lite'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Label,
|
|
7
|
+
RadioGroup,
|
|
8
|
+
RadioGroupItem,
|
|
9
|
+
ScrollArea
|
|
10
|
+
} from '@hanzo/ui/primitives'
|
|
11
|
+
import { cn } from '@hanzo/ui/util'
|
|
12
|
+
|
|
6
13
|
import type { ItemSelectorProps, LineItem } from '../../types'
|
|
7
14
|
import { formatCurrencyValue } from '../../util'
|
|
8
|
-
import { cn } from '@hanzo/ui/util'
|
|
9
15
|
|
|
10
16
|
const RadioItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
11
17
|
items,
|
|
@@ -14,52 +20,105 @@ const RadioItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
|
14
20
|
clx='',
|
|
15
21
|
itemClx='',
|
|
16
22
|
soleItemClx='',
|
|
17
|
-
|
|
18
|
-
showQuantity=
|
|
23
|
+
showCategory=false,
|
|
24
|
+
showQuantity=false,
|
|
25
|
+
scrollList=false
|
|
19
26
|
}) => {
|
|
20
27
|
|
|
21
|
-
const
|
|
28
|
+
const LabelText: React.FC<{item: LineItem}> = ({item}) => (
|
|
29
|
+
(showCategory ? (item.categoryTitle + ', ' + item.optionLabel) : item.optionLabel) +
|
|
30
|
+
(showCategory ? ': ' : ', ') + formatCurrencyValue(item.price)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const ItemAndPrice: React.FC<{
|
|
22
34
|
item: LineItem
|
|
35
|
+
listBoxMode: boolean
|
|
36
|
+
selected: boolean
|
|
23
37
|
className?: string
|
|
24
38
|
}> = ({
|
|
25
39
|
item,
|
|
40
|
+
listBoxMode,
|
|
41
|
+
selected,
|
|
26
42
|
className=''
|
|
27
43
|
}) => (
|
|
28
|
-
<div className={cn(
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
<div className={cn(
|
|
45
|
+
'flex items-center',
|
|
46
|
+
className,
|
|
47
|
+
itemClx,
|
|
48
|
+
)}>
|
|
49
|
+
<RadioGroupItem
|
|
50
|
+
value={item.sku}
|
|
51
|
+
larger
|
|
52
|
+
id={item.sku}
|
|
53
|
+
className={'mr-2 ' + (listBoxMode ? 'hidden' : '')}
|
|
54
|
+
/>
|
|
55
|
+
<Label htmlFor={item.sku} className={selected ? 'text-accent' : ''}>
|
|
56
|
+
<LabelText item={item} />
|
|
57
|
+
</Label>
|
|
31
58
|
</div>
|
|
32
59
|
)
|
|
33
|
-
|
|
34
|
-
const
|
|
60
|
+
|
|
61
|
+
const Item: React.FC<{
|
|
35
62
|
item: LineItem
|
|
36
|
-
|
|
63
|
+
selected: boolean
|
|
64
|
+
listBoxMode: boolean
|
|
65
|
+
clx?: string
|
|
37
66
|
}> = ({
|
|
38
67
|
item,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
selected,
|
|
69
|
+
listBoxMode,
|
|
70
|
+
clx=''
|
|
71
|
+
}) => {
|
|
72
|
+
|
|
73
|
+
const outClx = [
|
|
74
|
+
'h-10',
|
|
75
|
+
(listBoxMode ? 'border-b border-muted-3 py-1 pl-2' : 'mb-3'),
|
|
76
|
+
(selected && listBoxMode ? 'border border-foreground rounded-sm' : ''),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
return (showQuantity ? (
|
|
80
|
+
<div key={item.sku} className={cn('flex flex-row items-center', ...outClx, clx )}>
|
|
81
|
+
<ItemAndPrice item={item} selected={selected} listBoxMode={listBoxMode} className='grow'/>
|
|
82
|
+
<div className='grow-0 shrink-0 font-semibold text-sm leading-none px-2'>{ item.quantity > 0 ? `(${item.quantity})` : ' '}</div>
|
|
83
|
+
</div>
|
|
84
|
+
) : (
|
|
85
|
+
<ItemAndPrice
|
|
86
|
+
key={item.sku}
|
|
87
|
+
item={item}
|
|
88
|
+
selected={selected}
|
|
89
|
+
listBoxMode={listBoxMode}
|
|
90
|
+
className={cn(...outClx,
|
|
91
|
+
listBoxMode ? '' : 'mb-1',
|
|
92
|
+
clx)}
|
|
93
|
+
/>
|
|
94
|
+
))
|
|
95
|
+
}
|
|
45
96
|
|
|
46
97
|
return items.length > 1 ? (
|
|
47
98
|
<RadioGroup
|
|
48
|
-
className={cn('gap-0',
|
|
99
|
+
className={cn('flex flex-col gap-0',
|
|
100
|
+
(scrollList ? 'shrink min-h-0' : ''),
|
|
101
|
+
clx,
|
|
102
|
+
)}
|
|
49
103
|
onValueChange={selectSku}
|
|
50
104
|
value={itemRef.item ? itemRef.item.sku : ''}
|
|
51
105
|
>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
106
|
+
{scrollList ? (
|
|
107
|
+
<ScrollArea className='mt-2 w-full h-full py-0 border border-muted-2 rounded-sm '>
|
|
108
|
+
{items.map((item) => (
|
|
109
|
+
<Item item={item} listBoxMode={true} selected={itemRef.item?.sku === item.sku}/>
|
|
110
|
+
))}
|
|
111
|
+
</ScrollArea>
|
|
112
|
+
) : (<>
|
|
113
|
+
{items.map((item) => (
|
|
114
|
+
<Item item={item} listBoxMode={false} selected={itemRef.item?.sku === item.sku}/>
|
|
115
|
+
))}
|
|
116
|
+
</>)}
|
|
60
117
|
</RadioGroup>
|
|
61
118
|
) : (
|
|
62
|
-
<
|
|
119
|
+
<div className={soleItemClx}>
|
|
120
|
+
<LabelText item={items[0]} />
|
|
121
|
+
</div>
|
|
63
122
|
)
|
|
64
123
|
})
|
|
65
124
|
|