@hanzo/commerce 1.0.10 → 1.0.12

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.
@@ -1,17 +1,16 @@
1
1
  'use client'
2
2
  import React, { type PropsWithChildren, useState } from 'react'
3
+ import { useRouter } from 'next/navigation'
3
4
  import { observer } from 'mobx-react-lite'
4
5
 
5
6
  import { Button } from '@hanzo/ui/primitives'
6
7
  import { cn } from '@hanzo/ui/util'
8
+ import { useAuth } from '@hanzo/auth/service'
7
9
 
8
10
  import { persistCart, useCommerce } from '../service'
9
11
  import { formatPrice } from '../util'
10
12
 
11
13
  import CartLineItem from './cart-line-item'
12
- import { useRouter } from 'next/navigation'
13
- import { useAuth } from '@hanzo/auth/service'
14
-
15
14
 
16
15
  const Cart: React.FC<PropsWithChildren & {
17
16
  className?: string
@@ -24,14 +23,14 @@ const Cart: React.FC<PropsWithChildren & {
24
23
  hideCheckout=false
25
24
  }) => {
26
25
  const [loadingCheckout, setLoadingCheckout] = useState(false)
27
- const c = useCommerce()
26
+ const cmmc = useCommerce()
28
27
  const router = useRouter()
29
28
  const auth = useAuth()
30
29
 
31
30
  const checkout = async () => {
32
31
  setLoadingCheckout(true)
33
32
  if (auth.user) {
34
- await persistCart(c.cartItems, auth.user.email)
33
+ await persistCart(cmmc.cartItems, auth.user.email)
35
34
  }
36
35
  router.push('/checkout')
37
36
  }
@@ -41,14 +40,14 @@ const Cart: React.FC<PropsWithChildren & {
41
40
  {children}
42
41
  <div className='mt-2'>
43
42
  {!!children && <div className='h-[1px] w-pr-80 mx-auto bg-muted-3'/>}
44
- {c.cartItems.length === 0 ? (
43
+ {cmmc.cartItems.length === 0 ? (
45
44
  <p className='text-center mt-4'>No items in cart</p>
46
45
  ) : (<>
47
- {c.cartItems.map((item, i) => (<CartLineItem isMobile={isMobile} item={item} key={item.sku} className='mb-4 sm:mb-2'/>))}
48
- <p className='mt-6 text-right border-t pt-1'>TOTAL: {c.cartTotal === 0 ? '0' : formatPrice(c.cartTotal)}</p>
46
+ {cmmc.cartItems.map((item, i) => (<CartLineItem isMobile={isMobile} item={item} key={item.sku} className='mb-4 sm:mb-2'/>))}
47
+ <p className='mt-6 text-right border-t pt-1'>TOTAL: {cmmc.cartTotal === 0 ? '0' : formatPrice(cmmc.cartTotal)}</p>
49
48
  </>)}
50
49
  </div>
51
- {c.cartItems.length > 0 && !hideCheckout && (
50
+ {cmmc.cartItems.length > 0 && !hideCheckout && (
52
51
  <>
53
52
  {!auth.loggedIn ? (
54
53
  <Button size='lg' variant='secondary' rounded='lg' className='mt-12 mx-auto' onClick={() => router.push('/login?redirectUrl=checkout')}>Login</Button>
@@ -31,15 +31,15 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
31
31
  ...props
32
32
  }) => {
33
33
 
34
- const waiting = (): boolean => (isLoading || !category)
34
+ const waiting = (): boolean => (isLoading) // keep for convenience for now
35
35
 
36
36
  const CategoryImage: React.FC<{ className?: string }> = ({ className = '' }) => {
37
37
 
38
38
  if (waiting()) {
39
39
  // deliberately not Skeleton to have a better overall pulse effect.
40
40
  return <div className={cn(
41
- 'bg-level-1 rounded-xl aspect-square ' +
42
- ' min-h-[100px] sm:min-h-[200px] lg:aspect-auto lg:h-[300px] lg:w-[200px] 2xl:w-auto 2xl:aspect-square',
41
+ 'bg-level-1 rounded-xl aspect-square w-full min-h-1 ', // +
42
+ //' min-h-[100px] sm:min-h-[200px] lg:aspect-auto 2xl:w-auto 2xl:aspect-square',
43
43
  className)} />
44
44
  }
45
45
 
@@ -49,7 +49,7 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
49
49
  aria-label='Placeholder'
50
50
  role='img'
51
51
  aria-roledescription='placeholder'
52
- className={cn('h-60 flex items-center justify-center', className)}
52
+ className={cn('w-full flex items-center justify-center aspect-square' , className)}
53
53
  >
54
54
  <Icons.barcode className='h-9 w-9 text-muted' aria-hidden='true' />
55
55
  </div>
@@ -83,13 +83,13 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
83
83
  <Skeleton className={'min-h-[120px] w-pr-60 mx-auto ' + className} />
84
84
  ) : (
85
85
  <div /* id='CV_AVAIL_AMOUNTS' */ className={cn(
86
- 'w-full md:w-auto flex flex-col justify-start items-center',
86
+ 'sm:w-pr-80 sm:mx-auto md:w-full flex flex-col justify-start items-center',
87
87
  (soleOption ? 'gap-2' : mobilePicker ? 'gap-4' : 'gap-8'),
88
88
  className
89
89
  )}>
90
90
  <div className='w-full flex flex-col justify-start items-center'>
91
91
  <h6 className='text-center font-semibold'>{`Available size${soleOption ? '' : 's'}`}</h6>
92
- <div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-70') } />
92
+ <div className={'h-[1px] bg-muted-3 ' + (mobilePicker ? 'w-pr-55' : 'w-pr-80') } />
93
93
  </div>
94
94
  {soleOption ? (
95
95
  <p>{category.products[0].titleAsOption}</p>
@@ -107,8 +107,8 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
107
107
  products={category.products}
108
108
  selectedSku={lineItemRef.item?.sku ?? undefined}
109
109
  onValueChange={handleItemSelected}
110
- groupClx='block xs:columns-2 xs:px-3 columns-3 gap-2 lg:columns-2 lg:gap-6 w-full lg:auto'
111
- itemClx='flex flex-row gap-2 items-center mb-3 xs:mb-5'
110
+ groupClx='grid grid-cols-2 gap-0 gap-y-3 gap-x-8 '
111
+ itemClx='flex flex-row gap-2 items-center'
112
112
  />
113
113
  )
114
114
  )}
@@ -128,11 +128,11 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
128
128
  waiting() ? (<Skeleton className={'h-12 w-pr-80 mx-auto ' + className} />) : (
129
129
 
130
130
  <div className={cn('flex flex-col justify-start items-center mb-6', className)}>
131
- <h3 className='text-lg lg:text-xl font-heading text-center'>
132
- <span className='xs:inline sm:hidden md:inline lg:hidden'>
131
+ <h3 className='text-base md:text-lg lg:text-2xl md:mt-[2vw] font-nav text-center'>
132
+ <span className='xs:inline md:hidden'>
133
133
  {category.title.split(', ').map((s, i) => (<p key={i}>{s}</p>))}
134
134
  </span>
135
- <span className='xs:hidden sm:inline md:hidden lg:inline'>
135
+ <span className='xs:hidden md:inline '>
136
136
  {category.title}
137
137
  </span>
138
138
  </h3>
@@ -174,12 +174,17 @@ const SelectItemInCategoryView: React.FC<React.HTMLAttributes<HTMLDivElement> &
174
174
  ) : (
175
175
  <div /* id='CV_OUTERMOST' */>
176
176
  <div /* id='CV_OUTER' */ className={cn('w-full flex flex-row justify-between items-stretch gap-6 sm:gap-4', className)} {...props}>
177
- <div /* id='CV_IMAGE_COL' */ className={'relative ' + (!isLoading ? 'w-pr-33' : '')}>
178
- <CategoryImage className='' />
177
+ <div /* id='CV_IMAGE_COL_SEP' */ className={'relative ' + (!isLoading ? 'md:hidden sm:min-w-[185px] xl:flex xl:w-pr-40' : '')}>
178
+ <CategoryImage className='w-full' />
179
179
  </div>
180
- <div /* id='CV_CONTENT_COL */ className='w-pr-66'>
180
+ <div /* id='CV_CONTENT_COL */ className='flex flex-col gap-y-6 xl:w-pr-60'>
181
+ <div /* id='CV_MD_IMAGE_AND_TITLE */ className='hidden md:flex xl:hidden flex-row gap-x-3'>
182
+ <CategoryImage className='min-w-pr-30' />
183
+ <TitleArea className='grow' />
184
+ </div>
185
+
181
186
  <div /* id='CV_CONTENT' */ className={'flex flex-col gap-2.5 ' + (isLoading ? 'justify-between h-full' : '')}>
182
- <TitleArea className='' />
187
+ <TitleArea className='md:hidden xl:flex' />
183
188
  <Desc className='' />
184
189
  </div>
185
190
  <div /* id='CV_CTA_AREA_BIG' */ className='hidden lg:flex p-4 flex-col justify-start items-center gap-6'>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/commerce",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Library with shopping cart components.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -33,7 +33,7 @@
33
33
  "react-mobile-picker": "^1.0.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@hanzo/auth": "^1.0.9",
36
+ "@hanzo/auth": "^1.0.12",
37
37
  "@hanzo/ui": "^1.0.8",
38
38
  "firebase": "^10.8.0",
39
39
  "mobx": "^6.12.0",
@@ -30,7 +30,7 @@ interface CommerceService extends ObsLineItemRef {
30
30
  * "current" is unrelated to what is "specified",
31
31
  * ie, facets' values
32
32
  * */
33
- setCurrentItem(sku: string | undefined): void
33
+ setCurrentItem(sku: string | undefined): boolean // was valid sku and was set.
34
34
  /**
35
35
  * For convenience, so widgets can share state.
36
36
  * "current" is unrelated to what is "specified",
@@ -31,7 +31,7 @@ class StandaloneCommerceService
31
31
 
32
32
  private _options : StandaloneServiceOptions
33
33
 
34
- private _currentItemSku: string | undefined = undefined
34
+ private _currentItem: LineItem | undefined = undefined
35
35
 
36
36
  constructor(
37
37
  categories: Category[],
@@ -50,20 +50,20 @@ class StandaloneCommerceService
50
50
  makeObservable<
51
51
  StandaloneCommerceService,
52
52
  '_selectedFacets' |
53
- '_currentItemSku'
53
+ '_currentItem'
54
54
  >(this, {
55
55
  _selectedFacets : observable.deep,
56
- _currentItemSku: observable,
56
+ _currentItem: observable,
57
57
  })
58
58
 
59
59
  makeObservable(this, {
60
60
  cartItems: computed,
61
61
  cartTotal: computed,
62
62
  specifiedItems: computed,
63
+ specifiedCategories: computed,
63
64
  setCurrentItem: action,
64
65
  currentItem: computed,
65
66
  item: computed,
66
- specifiedCategories: computed,
67
67
  facetsValue: computed
68
68
  /* NOT setFacets. It implements it's action mechanism */
69
69
  })
@@ -85,39 +85,46 @@ class StandaloneCommerceService
85
85
  )
86
86
  }
87
87
 
88
- setCurrentItem(sku: string | undefined): void {
89
- this._currentItemSku = sku
88
+ setCurrentItem(skuToFind: string | undefined): boolean {
89
+
90
+ if (skuToFind === undefined || skuToFind.length === 0) {
91
+ this._currentItem = undefined
92
+ return true
93
+ }
94
+
95
+ this._currentItem = ((): LineItem | undefined => {
96
+ const categoriesTried: string[] = []
97
+ if (this.specifiedCategories && this.specifiedCategories.length > 0) {
98
+
99
+ for (let category of this.specifiedCategories) {
100
+ categoriesTried.push(category.id)
101
+ const foundItem =
102
+ (category.products as LineItem[]).find((item) => (item.sku === skuToFind))
103
+ if (foundItem) {
104
+ return foundItem
105
+ }
106
+ }
107
+ }
108
+
109
+ let foundItem: LineItem | undefined = undefined
110
+ this._categoryMap.forEach((category, categoryId) => {
111
+ if (foundItem) return
112
+ if (categoriesTried.includes(categoryId)) return
113
+ foundItem = (category.products as LineItem[]).find((item) => (item.sku === skuToFind))
114
+ })
115
+ })();
116
+
117
+ return !!this._currentItem
90
118
  }
91
119
 
120
+
92
121
  /* ObsLineItemRef */
93
122
  get item(): LineItem | undefined {
94
123
  return this.currentItem
95
124
  }
96
125
 
97
126
  get currentItem(): LineItem | undefined {
98
- if (!this._currentItemSku) return undefined
99
-
100
- const categoriesTried: string[] = []
101
- if (this.specifiedCategories && this.specifiedCategories.length > 0) {
102
-
103
- for (let category of this.specifiedCategories) {
104
- categoriesTried.push(category.id)
105
- const foundItem =
106
- (category.products as LineItem[]).find((item) => (item.sku === this._currentItemSku))
107
- if (foundItem) {
108
- return foundItem
109
- }
110
- }
111
- }
112
-
113
- let foundItem: LineItem | undefined = undefined
114
- this._categoryMap.forEach((category, categoryId) => {
115
- if (foundItem) return
116
- if (categoriesTried.includes(categoryId)) return
117
- foundItem = (category.products as LineItem[]).find((item) => (item.sku === this._currentItemSku))
118
- })
119
-
120
- return foundItem
127
+ return this._currentItem
121
128
  }
122
129
 
123
130
  setFacets(sel: FacetsValue): Category[] {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  useEffect,
3
3
  useRef,
4
+ useState,
4
5
  } from 'react'
5
6
  import { autorun, toJS, type IReactionDisposer } from 'mobx'
6
7
 
@@ -13,6 +14,8 @@ import {
13
14
  import type { Category, FacetsValue, StringMutator } from '../types'
14
15
  import { useCommerce } from '../service'
15
16
 
17
+ const PLEASE_SELECT_FACETS = 'Please select an option from each group above.'
18
+
16
19
  interface GetMutator {
17
20
  (level: 1 | 2): StringMutator
18
21
  }
@@ -24,11 +27,9 @@ const useSkuAndFacetParams = (
24
27
  const cmmc = useCommerce()
25
28
 
26
29
  const encRef = useRef<{
27
- category: Category | undefined
28
30
  usingSkuMode: boolean
29
31
  autoRunDisposer: IReactionDisposer | undefined
30
32
  }>({
31
- category: undefined,
32
33
  usingSkuMode: false,
33
34
  autoRunDisposer: undefined,
34
35
  })
@@ -40,12 +41,15 @@ const useSkuAndFacetParams = (
40
41
  parseAsString.withDefault('').withOptions({ clearOnDefault: true })
41
42
  )
42
43
 
43
- const [skuParam, setSkuParam] = useQueryState('sku')
44
+ const [skuParam, setSkuParam] = useQueryState('sku',
45
+ parseAsString.withDefault('').withOptions({ clearOnDefault: true })
46
+ )
47
+
44
48
  const [addParam, setAddParam] = useQueryState('add',
45
49
  parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
46
50
  )
47
51
 
48
- let message = ''
52
+ const [message, setMessage] = useState<string | undefined>(undefined)
49
53
 
50
54
  const directMutator: GetMutator = (level: 1 | 2): StringMutator => {
51
55
 
@@ -83,70 +87,80 @@ const useSkuAndFacetParams = (
83
87
 
84
88
  const setCurrentCategoryFromSku = (sku: string) => {
85
89
  const toks: string[] = sku.split('-')
86
- const categories = cmmc.setFacets({
90
+ cmmc.setFacets({
87
91
  1: [toks[1]],
88
92
  2: [toks[2]]
89
93
  })
90
- encRef.current.category = categories[0]
91
94
  }
92
95
 
93
96
  if (skuParam) {
94
- cmmc.setCurrentItem(skuParam)
95
- if (addParam && cmmc.currentItem!.quantity === 0) {
96
- cmmc.currentItem!.increment()
97
- setAddParam(false)
97
+ // true if a valid sku
98
+ if (cmmc.setCurrentItem(skuParam)) {
99
+ if (addParam && cmmc.currentItem!.quantity === 0) {
100
+ cmmc.currentItem!.increment()
101
+ setAddParam(false)
102
+ }
103
+ // Marks that we've been here so no need to
104
+ // create an autorun() instance twice.
105
+ if (!encRef.current.usingSkuMode) {
106
+ setCurrentCategoryFromSku(skuParam)
107
+ encRef.current.autoRunDisposer = autorun(() => {
108
+ if (cmmc.currentItem && cmmc.currentItem.sku !== skuParam) {
109
+ setSkuParam(cmmc.currentItem.sku)
110
+ }
111
+ })
112
+ }
113
+ setMessage(undefined)
114
+ encRef.current.usingSkuMode = true
98
115
  }
99
- // Marks that we've been here so no need to
100
- // create an autorun() instance twice.
101
- if (!encRef.current.usingSkuMode) {
102
- setCurrentCategoryFromSku(skuParam)
103
- encRef.current.autoRunDisposer = autorun(() => {
104
- if (cmmc.currentItem && cmmc.currentItem.sku !== skuParam) {
105
- setSkuParam(cmmc.currentItem.sku)
106
- }
107
- })
116
+ else {
117
+ setSkuParam('')
118
+ // if sent here w an invalid sku,
119
+ // it will effectively put us in facet params mode
120
+ setMessage('Invalid sku. ' + PLEASE_SELECT_FACETS)
108
121
  }
109
- encRef.current.usingSkuMode = true
110
- }
111
- else if (encRef.current.category) {
112
- //const catMutators = [{val: level1, set: setLevel1} , {val: level2, set: setLevel2}]
113
- cmmc.setCurrentItem(encRef.current.category.products[0].sku)
114
122
  }
123
+
115
124
  setLoading && setLoading(false)
116
- }, [skuParam, encRef.current.category])
125
+ }, [skuParam])
117
126
 
118
127
  // supposed to be when component unmounts
128
+ /*
119
129
  useEffect(() => (() => {
120
130
  if (encRef.current.autoRunDisposer) {
121
131
  //encRef.current.autoRunDisposer() // no idea why this seems to be called prematurely and so many times <shrug>
122
132
  }
123
133
  }), [])
134
+ */
124
135
 
125
136
 
126
137
  // Level Params Mode
127
138
  useEffect(() => {
128
139
 
129
140
  if (encRef.current.usingSkuMode) return
130
-
141
+
131
142
  const facets: FacetsValue = { }
132
143
  if (level1) { facets[1] = [level1] }
133
144
  if (level2) { facets[2] = [level2] }
145
+ //console.log(`LEV1: ${level1}, LEV2: ${level2}`)
134
146
  if (level1 && level2) {
135
147
  const categories = cmmc.setFacets(facets)
136
- if (categories.length > 1) {
137
- console.error("CAT", categories.map((c) => (c.title)))
138
- throw new Error ( "CategoryContents: More than one specified Category should never be possible with this UI!")
148
+ if (categories && categories.length > 0) {
149
+ cmmc.setCurrentItem(categories?.[0].products[0].sku)
150
+ setMessage(undefined)
151
+ }
152
+ else {
153
+ cmmc.setCurrentItem(undefined)
154
+ setMessage('Unrecognized facets. ' + PLEASE_SELECT_FACETS)
139
155
  }
140
- encRef.current.category = categories[0]
141
156
  }
142
157
  else {
143
- message = 'Please select an option from each group above.'
158
+ setMessage(PLEASE_SELECT_FACETS)
144
159
  }
145
160
  setLoading && setLoading(false)
146
161
  }, [level1 , level2])
147
162
 
148
163
  return {
149
- category: encRef.current.category,
150
164
  message,
151
165
  getMutator: encRef.current.usingSkuMode ?
152
166
  directMutator : paramsMutator