@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.
Files changed (55) hide show
  1. package/components/buy/_to_deprecate_select-family-item-card.tsx +3 -4
  2. package/components/buy/_to_deprecate_select-family-item-panel.tsx +4 -5
  3. package/components/buy/add-to-cart-widget.tsx +43 -32
  4. package/components/buy/buy-button.tsx +1 -1
  5. package/components/buy/buy-card.tsx +17 -26
  6. package/components/buy/buy-drawer.tsx +6 -25
  7. package/components/buy/carousel-buy-card.tsx +145 -194
  8. package/components/buy/foo.txt +27 -0
  9. package/components/cart/cart-panel/cart-line-item.tsx +1 -1
  10. package/components/checkout/payment-step-form/contact-form.tsx +2 -2
  11. package/components/checkout/payment-step-form/methods/crypto.tsx +1 -2
  12. package/components/checkout/shipping-step-form.tsx +5 -5
  13. package/components/index.ts +1 -3
  14. package/components/item-selector/button.tsx +175 -0
  15. package/components/item-selector/carousel.tsx +162 -0
  16. package/components/item-selector/index.ts +5 -0
  17. package/components/quantity-indicator.tsx +48 -0
  18. package/components/select-family/family-carousel/index.tsx +47 -38
  19. package/components/select-family/family-carousel/slide.tsx +83 -0
  20. package/components/select-family/family-carousel/state.ts +83 -0
  21. package/components/select-family/level-nodes-widget/index.tsx +2 -2
  22. package/components/select-family/level-nodes-widget/node-image.tsx +2 -2
  23. package/index.ts +7 -7
  24. package/package.json +7 -4
  25. package/service/context.tsx +8 -7
  26. package/service/debug.ts +41 -0
  27. package/service/get-instance.ts +3 -0
  28. package/service/impls/standalone/actual-line-item.ts +5 -7
  29. package/service/impls/standalone/index.ts +11 -3
  30. package/service/impls/standalone/standalone-service.ts +133 -38
  31. package/service/path-utils.ts +26 -0
  32. package/service/sep.ts +7 -0
  33. package/types/category-node.ts +38 -0
  34. package/types/commerce-service.ts +44 -12
  35. package/types/family.ts +4 -1
  36. package/types/index.ts +3 -2
  37. package/types/item-selector.ts +29 -14
  38. package/types/product.ts +5 -1
  39. package/types/selection-ui-specifier.ts +36 -0
  40. package/types/token-separators.ts +7 -0
  41. package/util/error.ts +34 -0
  42. package/util/index.ts +4 -1
  43. package/util/product-media-accessor.ts +58 -0
  44. package/util/promo-codes.ts +1 -2
  45. package/util/selection-ui-specifiers.ts +34 -0
  46. package/util/use-sync-sku-param-w-current-item.ts +2 -2
  47. package/components/select-family/family-carousel/family-slide.tsx +0 -36
  48. package/components/select-product/carousel-selector.tsx +0 -112
  49. package/components/select-product/image-selector.tsx +0 -162
  50. package/components/select-product/radio-selector.tsx +0 -125
  51. package/service/impls/index.ts +0 -3
  52. package/types/buy-ui-spec.ts +0 -8
  53. package/types/tree-node.ts +0 -24
  54. package/util/buy-ui-conf.ts +0 -35
  55. package/util/sep.ts +0 -5
@@ -0,0 +1,175 @@
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
+ import type { Dimensions } from '@hanzo/ui/types'
15
+
16
+ import type { ItemSelectorProps, LineItem } from '../../types'
17
+ import { formatCurrencyValue } from '../../util'
18
+
19
+ import QuantityIndicator from '../quantity-indicator'
20
+
21
+ const DEFAULT_CONSTRAINT = {h: 36, w: 72} // // Apple suggest 42px for clickability
22
+
23
+ const ImageRadioGroupItem = React.forwardRef<
24
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
25
+ Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>, 'value' | 'id'> & {
26
+ item: LineItem,
27
+ constrainTo: Dimensions
28
+ }
29
+ >(({
30
+ item,
31
+ constrainTo,
32
+ className,
33
+ ...props
34
+ }, ref) => {
35
+
36
+ const img = item.optionImg ? item.optionImg : item.img
37
+
38
+ return (
39
+ <RadioGroupPrimitive.Item
40
+ ref={ref}
41
+ className={cn(
42
+ 'ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring',
43
+ 'focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
44
+ className,
45
+ 'overflow-hidden',
46
+ img?.rounded ? `rounded-${img.rounded}` : 'rounded-sm'
47
+ )}
48
+ {...props}
49
+ id={item.sku}
50
+ value={item.sku}
51
+ >
52
+ {img ? (
53
+ <Image def={img} constrainTo={constrainTo} preload className=''/>
54
+ ) : ( // placeholder so things align
55
+ <div style={{height: constrainTo.h, width: constrainTo.w}}/>
56
+ )}
57
+ </RadioGroupPrimitive.Item>
58
+ )
59
+ })
60
+ ImageRadioGroupItem.displayName = 'ImageRadioGroupItem'
61
+
62
+ const ButtonItemSelector: React.FC<ItemSelectorProps> = observer(({
63
+ items,
64
+ selectedItemRef: itemRef,
65
+ selectSku,
66
+ clx='',
67
+ itemClx='',
68
+ soleItemClx='',
69
+ scrollable=false,
70
+ mobile=false,
71
+ options={}
72
+ }) => {
73
+
74
+ const imageButtons = 'imageButtons' in options ? options.imageButtons : false
75
+ const showFamily = 'showFamily' in options ? options.showFamily : false
76
+ const showPrice = 'showPrice' in options ? options.showPrice : true
77
+ const showQuantity = 'showQuantity' in options ? options.showQuantity : false
78
+ const horizButtons = 'horizButtons' in options ? options.horizButtons : false
79
+
80
+ const labelAndPrice = (item : LineItem) => (
81
+ (showFamily ? (item.familyTitle + ', ' + item.optionLabel) : item.optionLabel) +
82
+ (showPrice ? ((showFamily ? ': ' : ', ') + formatCurrencyValue(item.price)) : '')
83
+ )
84
+
85
+ const Item: React.FC<{
86
+ item: LineItem
87
+ selected: boolean
88
+ }> = observer(({
89
+ item,
90
+ selected
91
+ }) => {
92
+
93
+ const textClx = (selected) ? 'text-accent ' : 'text-muted'
94
+ const cursorClx = (selected) ? 'hover:cursor-default' : 'hover:cursor-pointer'
95
+ const justifyClx = (!imageButtons) ? 'justify-center ' : ''
96
+ const paddingClx = (imageButtons) ? (scrollable ? 'px-4' : 'px-2' ) : ''
97
+ let bgClx = ''
98
+ let borderClx = ''
99
+ if (scrollable) {
100
+ borderClx += (selected) ? 'border-foreground border ' : 'border-b border-muted-2 '
101
+ }
102
+ else if (!imageButtons) {
103
+ borderClx += 'border rounded-lg '
104
+ borderClx += (selected) ? 'border-foreground ' : 'border-muted-2 '
105
+ bgClx += 'hover:bg-level-2'
106
+ }
107
+
108
+ const outerClx = ['h-10 py-2 flex items-center', justifyClx, paddingClx, bgClx, borderClx]
109
+
110
+ return (
111
+ <div className={cn(...outerClx, itemClx )}>
112
+ <ImageRadioGroupItem
113
+ item={item}
114
+ constrainTo={DEFAULT_CONSTRAINT}
115
+ className={cn(
116
+ cursorClx,
117
+ (scrollable ? '' : 'border-transparent border-2 data-[state=checked]:border-foreground'),
118
+ imageButtons ? 'mr-2' : 'hidden'
119
+ )}
120
+ />
121
+ <Label htmlFor={item.sku} className={cn(
122
+ showQuantity ? 'flex items-center' : 'block',
123
+ textClx,
124
+ cursorClx,
125
+ )}>
126
+ <div className={showQuantity ? 'grow' : ''}>{labelAndPrice(item)}</div>
127
+ {showQuantity && (
128
+ <QuantityIndicator
129
+ item={item}
130
+ clx='grow-0 shrink-0 h-[20px] ml-2'
131
+ iconClx={selected ? 'fill-foreground' : 'fill-muted'}
132
+ digitClx='font-semibold text-primary-fg leading-none font-sans text-xxs'
133
+ />
134
+ )}
135
+ </Label>
136
+ </div>
137
+ )
138
+ })
139
+
140
+ return items.length > 1 ? (
141
+ <RadioGroup
142
+ className={cn('flex',
143
+ (scrollable ? 'shrink min-h-0 gap-0' : (mobile ? 'gap-3' : 'gap-1')),
144
+ (horizButtons ? 'flex-row gap-0' : 'flex-col'),
145
+ (mobile && !imageButtons) ? 'min-w-pr-50' : '',
146
+ clx,
147
+ )}
148
+ onValueChange={selectSku}
149
+ value={itemRef.item ? itemRef.item.sku : ''}
150
+ >
151
+ {scrollable ? (
152
+ <ScrollArea className='mt-2 w-full h-full py-0 border border-muted-2' data-vaul-no-drag>
153
+ {items.map((item) => (
154
+ <Item item={item} selected={itemRef.item?.sku === item.sku} key={item.sku}/>
155
+ ))}
156
+ </ScrollArea>
157
+ ) : (<>
158
+ {items.map((item) => (
159
+ <Item item={item} selected={itemRef.item?.sku === item.sku} key={item.sku}/>
160
+ ))}
161
+ </>)}
162
+ </RadioGroup>
163
+ ) : (
164
+ <div className={cn(showQuantity ? 'flex items-center' : 'block', soleItemClx )}>
165
+ <div className={showQuantity ? 'grow' : ''}>
166
+ {labelAndPrice(items[0])}
167
+ </div>
168
+ {showQuantity && (
169
+ <QuantityIndicator item={items[0]} clx='grow-0 shrink-0 font-semibold text-sm leading-none px-2' />
170
+ )}
171
+ </div>
172
+ )
173
+ })
174
+
175
+ export default ButtonItemSelector
@@ -0,0 +1,162 @@
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 type { Dimensions } from '@hanzo/ui/types'
8
+
9
+ import {
10
+ type CarouselOptions,
11
+ type CarouselApi,
12
+ Carousel,
13
+ CarouselContent,
14
+ CarouselItem,
15
+ CarouselPrevious,
16
+ CarouselNext,
17
+ ApplyTypography,
18
+ MediaStack
19
+ } from '@hanzo/ui/primitives'
20
+
21
+ import type { ItemSelectorProps, LineItem } from '../../types'
22
+ import { formatCurrencyValue } from '../../util'
23
+
24
+ import QuantityIndicator from '../quantity-indicator'
25
+
26
+ const DEFAULT_CONSTRAINT = {w: 250, h: 250}
27
+
28
+ interface CarouselItemSelectorPropsExt {
29
+ constrainTo: Dimensions
30
+ options?: CarouselOptions
31
+ /** Do not show Family and / or Item title and Price */
32
+ imageOnly?: boolean
33
+ }
34
+
35
+ const ItemSlide: React.FC<{
36
+ item: LineItem,
37
+ constrainTo: Dimensions
38
+ imageOnly: boolean
39
+ showFamily: boolean
40
+ showPrice: boolean
41
+ showQuantity: boolean
42
+ clx?: string
43
+ }> = ({
44
+ item,
45
+ constrainTo,
46
+ showFamily,
47
+ imageOnly,
48
+ showPrice,
49
+ showQuantity,
50
+ clx=''
51
+ }) => {
52
+
53
+ return (
54
+ <CarouselItem className={cn('p-2 flex flex-col justify-center items-center', clx)}>
55
+ {!imageOnly && (
56
+ <ApplyTypography className='flex flex-col items-center !gap-2 [&>*]:!m-0'>
57
+ {showFamily && (<h4>{item.familyTitle}</h4>)}
58
+ <h6>{item.optionLabel}</h6>
59
+ {item.byline && (<p>{item.byline}</p>)}
60
+ </ApplyTypography>
61
+ )}
62
+ <MediaStack media={item} constrainTo={constrainTo} clx='my-4' />
63
+ {!imageOnly && (showPrice || showQuantity) && (
64
+ <ApplyTypography className='flex flex-row items-center !gap-2 [&>*]:!m-0'>
65
+ {showPrice && (<p>{formatCurrencyValue(item.price)}</p>)}
66
+ {showQuantity && (
67
+ <QuantityIndicator
68
+ item={item}
69
+ clx='h-[26px] ml-1'
70
+ iconClx='fill-foreground'
71
+ digitClx='not-typography font-semibold text-primary-fg leading-none font-sans text-xs'
72
+ />
73
+ )}
74
+ </ApplyTypography>
75
+ )}
76
+ </CarouselItem>
77
+ )
78
+ }
79
+
80
+ const CarouselItemSelector: React.FC<ItemSelectorProps> = observer(({
81
+ items,
82
+ selectSku,
83
+ selectedItemRef: itemRef,
84
+ scrollable, // ignored
85
+ clx='',
86
+ itemClx='',
87
+ options={},
88
+ ext={
89
+ options: {loop: true},
90
+ constrainTo: DEFAULT_CONSTRAINT,
91
+ imageOnly: false
92
+ } satisfies CarouselItemSelectorPropsExt
93
+ }) => {
94
+
95
+ const showFamily = 'showFamily' in options ? options.showFamily! : false
96
+ const showPrice = 'showPrice' in options ? options.showPrice! : true
97
+ const showQuantity = 'showQuantity' in options ? options.showQuantity! : false
98
+
99
+ const carouselOptions = 'options' in ext ? ext.options : undefined
100
+ const constrainTo = 'constrainTo' in ext ? ext.constrainTo : DEFAULT_CONSTRAINT
101
+ const imageOnly = 'imageOnly' in ext ? ext.imageOnly : false
102
+
103
+ const elbaApiRef = useRef<CarouselApi | undefined>(undefined)
104
+ const dontRespondRef = useRef<boolean>(false)
105
+
106
+ const setApi = (api: CarouselApi) => {elbaApiRef.current = api}
107
+
108
+ const onSelect = useCallback((emblaApi: CarouselApi) => {
109
+ if (dontRespondRef.current) {
110
+ dontRespondRef.current = false
111
+ return
112
+ }
113
+ const index = emblaApi.selectedScrollSnap()
114
+ if (index !== -1) {
115
+ selectSku(items[index].sku)
116
+ }
117
+ dontRespondRef.current = false
118
+ }, [])
119
+
120
+ useEffect(() => {
121
+ return reaction(
122
+ () => ({item: itemRef.item}),
123
+ ({item}) => {
124
+ if (elbaApiRef.current) {
125
+ const index = items.findIndex((el) => (el.sku === item?.sku))
126
+ if (index !== -1) {
127
+ dontRespondRef.current = true
128
+ elbaApiRef.current.scrollTo(index)
129
+ }
130
+ }
131
+ }
132
+ )
133
+ }, [])
134
+
135
+ return (
136
+ <Carousel options={carouselOptions} className={cn('w-full px-2', clx)} onCarouselSelect={onSelect} setApi={setApi}>
137
+ <CarouselContent>
138
+ {items.map((item) => (
139
+ <ItemSlide
140
+ key={item.sku}
141
+ item={item}
142
+ {...{
143
+ showFamily,
144
+ showPrice,
145
+ showQuantity,
146
+ constrainTo,
147
+ imageOnly
148
+ }}
149
+ clx={itemClx}
150
+ />
151
+ ))}
152
+ </CarouselContent>
153
+ <CarouselPrevious className='left-1'/>
154
+ <CarouselNext className='right-1'/>
155
+ </Carousel>
156
+ )
157
+ })
158
+
159
+ export {
160
+ type CarouselItemSelectorPropsExt,
161
+ CarouselItemSelector as default
162
+ }
@@ -0,0 +1,5 @@
1
+ export { default as ButtonItemSelector } from './button'
2
+ export {
3
+ default as CarouselItemSelector,
4
+ type CarouselItemSelectorPropsExt
5
+ } from './carousel'
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+ import React from 'react'
3
+ import { observer } from 'mobx-react-lite'
4
+
5
+ import { type LucideProps } from 'lucide-react'
6
+
7
+ import { cn } from '@hanzo/ui/util'
8
+ import type { LineItem } from '../types'
9
+
10
+ // Generalize this.
11
+ const BagIcon: React.FC<LucideProps> = (props: LucideProps) => (
12
+ <svg fill="currentColor" viewBox='0 0 20 23' {...props}>
13
+ <path fillRule="evenodd" d="M5 5a5 5 0 0 1 10 0v1h-2V5a3 3 0 1 0-6 0v1H5V5Zm0 1v4h2V6h6v4h2V6h3.5a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1h-17a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1H5Z" clipRule="evenodd"/>
14
+ </svg>
15
+ )
16
+
17
+ const QuantityIndicator: React.FC<{
18
+ item: LineItem
19
+ clx?: string
20
+ iconClx?: string
21
+ digitClx?: string
22
+ }> = observer(({
23
+ item,
24
+ clx='',
25
+ iconClx='',
26
+ digitClx=''
27
+ }) => {
28
+
29
+ if (!item.isInCart) {
30
+ return <div />
31
+ }
32
+
33
+ return (
34
+ <div className={cn('relative aspect-square flex items-center justify-center', clx)} >
35
+ <div className={cn(
36
+ 'z-above-content flex flex-col justify-center items-center',
37
+ 'absolute left-0 right-0 top-0 bottom-0',
38
+ digitClx
39
+ )}>
40
+ <div className='h-[1px] w-full' />
41
+ <div style={{color: 'black' /* tailwind bug */}}>{item.quantity}</div>
42
+ </div>
43
+ <BagIcon className={cn('relative -top-[12%] h-full ', iconClx, )} aria-hidden="true" />
44
+ </div>
45
+ )
46
+ }) // -top-[3px]
47
+
48
+ export default QuantityIndicator
@@ -1,78 +1,87 @@
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
+ import React, { useCallback, useRef } from 'react'
5
3
 
6
4
  import { cn } from '@hanzo/ui/util'
7
5
 
8
6
  import {
9
- type CarouselOptions,
10
7
  type CarouselApi,
11
8
  Carousel,
12
9
  CarouselContent,
13
10
  CarouselItem,
14
11
  CarouselPrevious,
15
12
  CarouselNext,
16
- ApplyTypography,
17
- MediaStack
18
13
  } from '@hanzo/ui/primitives'
14
+ import type { Dimensions } from '@hanzo/ui/types'
19
15
 
20
- import type { Family, LineItem, ProductTreeNode } from '../../../types'
21
- import { formatCurrencyValue } from '../../../util'
16
+ import type { Family, FamilyCarouselSlideOptions } from '../../../types'
22
17
 
18
+ import FamilySlide from './slide'
19
+ import {
20
+ SlideState,
21
+ FamilyCarouselState
22
+ } from './state'
23
+
24
+ import { useCommerce } from '../../..'
23
25
 
24
26
  const FamilyCarousel: React.FC<{
25
- skuPath: string
27
+ families: Family[]
26
28
  clx?: string
27
29
  itemClx?: string
28
- options?: CarouselOptions
29
- mediaConstraint?: {w: number, h: number}
30
- getBylineText?: (c: Family) => string
31
- variantOrientation?: 'vert' | 'horiz'
32
- showVariantImage?: boolean
33
- showVariantPrice?: boolean
34
- onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
30
+ slideOptions?: FamilyCarouselSlideOptions
31
+ mediaConstraint?: Dimensions
35
32
  mobile?: boolean
36
- }> = observer(({
37
- skuPath,
33
+ }> = ({
34
+ families,
38
35
  clx='',
39
36
  itemClx='',
40
- options={loop: true},
37
+ slideOptions,
41
38
  mediaConstraint={w: 250, h: 250},
42
- variantOrientation='vert',
43
- showVariantImage=true,
44
- showVariantPrice=true,
45
- onQuantityChanged,
46
39
  mobile=false,
47
40
  }) => {
48
41
 
49
- const elbaApiRef = useRef<CarouselApi | undefined>(undefined)
50
- const setApi = (api: CarouselApi) => {elbaApiRef.current = api}
42
+ const cmmc = useCommerce()
43
+ const stateRef = useRef<FamilyCarouselState>(
44
+ new FamilyCarouselState(families, cmmc.setCurrentItem.bind(cmmc))
45
+ )
51
46
 
52
47
  const onSelect = useCallback((emblaApi: CarouselApi) => {
53
48
  const index = emblaApi.selectedScrollSnap()
54
49
  if (index !== -1) {
55
- //selectSku(items[index].sku)
56
- // cmmc.selectPath
50
+ stateRef.current.setCurrentFamily(families[index].id)
57
51
  }
58
- }, [])
59
-
60
- const TEST = ["one", "two", "three"]
52
+ }, [stateRef.current])
61
53
 
62
54
  return (
63
- <Carousel options={options} className={cn('w-full px-2', clx)} onCarouselSelect={onSelect} setApi={setApi}>
55
+ <Carousel
56
+ className={cn('px-2', clx)}
57
+ options={{loop: false}}
58
+ onCarouselSelect={onSelect}
59
+ >
64
60
  <CarouselContent>
65
-
66
- {/*items.map((item, index) => (
67
- <CarouselItem key={index} className={cn('p-2 flex flex-col justify-center items-center', itemClx)}>
68
- <FamilySlide cat
69
- </CarouselItem>
70
- ))*/}
61
+ {families.map((family) => {
62
+ const slideState = stateRef.current.getSlideState(family.id)
63
+ if (!slideState) {
64
+ throw new Error(`FamilyCarousel: no SlideState for family '${family.id}'!`)
65
+ }
66
+ return (
67
+ <CarouselItem key={family.id} className='p-2 flex flex-col justify-center items-center'>
68
+ <FamilySlide
69
+ family={family}
70
+ selectedItemRef={slideState}
71
+ selectSku={slideState.selectSku.bind(slideState)}
72
+ mediaConstraint={mediaConstraint}
73
+ options={slideOptions}
74
+ mobile={mobile}
75
+ clx={itemClx}
76
+ />
77
+ </CarouselItem>
78
+ )}
79
+ )}
71
80
  </CarouselContent>
72
81
  <CarouselPrevious className='left-1'/>
73
82
  <CarouselNext className='right-1'/>
74
83
  </Carousel>
75
84
  )
76
- })
85
+ }
77
86
 
78
87
  export default FamilyCarousel
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+ import React from 'react'
3
+
4
+ import {
5
+ ApplyTypography,
6
+ MediaStack
7
+ } from '@hanzo/ui/primitives'
8
+ import type { Dimensions } from '@hanzo/ui/types'
9
+
10
+ import type { FamilyCarouselSlideOptions, Family, LineItem, ItemSelector } from '../../../types'
11
+ import { ButtonItemSelector } from '../..'
12
+ import { cn } from '@hanzo/ui/util'
13
+
14
+ const FamilySlide: React.FC<Omit<ItemSelector, 'items'> & {
15
+ family: Family
16
+ mediaConstraint: Dimensions
17
+ clx?: string
18
+ mobile?: boolean,
19
+ options?: FamilyCarouselSlideOptions
20
+ }> = ({
21
+ family,
22
+ selectedItemRef,
23
+ selectSku,
24
+ mediaConstraint: cnst = {w: 200, h: 200},
25
+ clx='',
26
+ mobile=false,
27
+ options={}
28
+ }) => {
29
+
30
+ const titleSpec = 'title' in options ? options.title : 'short'
31
+ const showByline = 'showByline' in options ? options.showByline : true
32
+ const title = titleSpec === 'long' ?
33
+ family.title
34
+ :
35
+ (
36
+ titleSpec === 'short' ?
37
+ (family.titleShort ? family.titleShort : family.title )
38
+ :
39
+ undefined
40
+ )
41
+
42
+ const byline = (showByline && family.byline) ? family.byline : undefined
43
+ /*
44
+ let byline: string | undefined = undefined
45
+ if (showByline) {
46
+ if (family.byline) {
47
+ // if byline names another field thats a function, call it
48
+ if (family.byline in family && typeof (family as any)[family.byline] === 'function') {
49
+ byline = (family as any)[family.byline]() as string
50
+ }
51
+ else {
52
+ byline = family.byline
53
+ }
54
+ }
55
+ }
56
+ */
57
+
58
+ return (
59
+ <div className={cn('flex flex-col items-center gap-4', clx)}>
60
+ {title && (
61
+ <ApplyTypography className='flex flex-col items-center !gap-1 [&>*]:!m-0'>
62
+ <h6 className='font-bold text-lg'>{title}</h6>
63
+ {byline && (<p className={''}>{byline}</p>)}
64
+ </ApplyTypography>
65
+ )}
66
+ <MediaStack
67
+ media={selectedItemRef.item!}
68
+ constrainTo={cnst}
69
+ />
70
+ <ButtonItemSelector
71
+ items={family.products as LineItem[]}
72
+ selectedItemRef={selectedItemRef}
73
+ selectSku={selectSku}
74
+ scrollable={false}
75
+ mobile={mobile}
76
+ options={options}
77
+ itemClx='text-sm'
78
+ />
79
+ </div>
80
+ )
81
+ }
82
+
83
+ export default FamilySlide
@@ -0,0 +1,83 @@
1
+ import type { Family, LineItem, ObsLineItemRef, Product } from '../../../types'
2
+ import { observable, action, computed, makeObservable } from 'mobx'
3
+
4
+ class SlideState implements ObsLineItemRef {
5
+
6
+ _fam: Family
7
+ _item: Product
8
+ _syncSku: (sku: string) => void
9
+ _selected: boolean
10
+
11
+ constructor(fam: Family, initiallySelected: boolean, sync: (sku: string) => void) {
12
+ this._fam = fam
13
+ this._item = fam.products[0]
14
+ this._selected = initiallySelected
15
+ this._syncSku = sync
16
+
17
+ makeObservable(this, {
18
+ _item: observable,
19
+ item: computed,
20
+ selectSku: action
21
+ })
22
+ }
23
+
24
+ selectSku = (sku: string) => {
25
+ if (this._item.sku === sku) return;
26
+ const found = this._fam.products.find((i) => (i.sku === sku))
27
+ if (found) {
28
+ this._item = found
29
+ if (this._selected) {
30
+ this._syncSku(sku)
31
+ }
32
+ }
33
+ else {
34
+ throw new Error('SlideState.selectSku(): no item found!')
35
+ }
36
+ }
37
+
38
+ setSelected = (b: boolean) => {this._selected = b}
39
+
40
+ get id(): string {return (this._fam.id)}
41
+
42
+ get item(): LineItem | undefined {
43
+ return this._item as LineItem
44
+ }
45
+ }
46
+
47
+ class FamilyCarouselState {
48
+
49
+ _map = new Map<string, SlideState>()
50
+ _syncSku: (sku: string) => void
51
+
52
+ constructor(fams: Family[], syncSkuToCurrentSlide: (sku: string) => void) {
53
+
54
+ fams.forEach((f, index) => {this._map.set(f.id, new SlideState(
55
+ f,
56
+ (index === 0),
57
+ syncSkuToCurrentSlide
58
+ ))})
59
+ this._syncSku = syncSkuToCurrentSlide
60
+ }
61
+
62
+ setCurrentFamily = (id: string) => {
63
+
64
+ this._map.forEach((s) => {
65
+ s.setSelected(s.id === id)
66
+ })
67
+
68
+ const slideState = this._map.get(id)
69
+ if (slideState) {
70
+ this._syncSku(slideState.item!.sku)
71
+ }
72
+ else {
73
+ throw new Error(`FamilyCarouselState.setCurrentFamily: no state object for id '${id}'!`)
74
+ }
75
+ }
76
+
77
+ getSlideState = (familyId: string): SlideState | undefined => (this._map.get(familyId))
78
+ }
79
+
80
+ export {
81
+ SlideState,
82
+ FamilyCarouselState
83
+ }
@@ -4,11 +4,11 @@ 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 { ProductTreeNode, StringMutator, StringArrayMutator } from '../../../types'
7
+ import type { CategoryNode, StringMutator, StringArrayMutator } from '../../../types'
8
8
  import NodeImage from './node-image'
9
9
 
10
10
  const LevelNodesWidget: React.FC<{
11
- levelNodes: ProductTreeNode[]
11
+ levelNodes: CategoryNode[]
12
12
  mutator: StringMutator | StringArrayMutator
13
13
  multiple?: boolean
14
14
  className?: string