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