@rpg-engine/long-bow 0.8.72 → 0.8.73

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.72",
3
+ "version": "0.8.73",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -84,7 +84,7 @@
84
84
  "dependencies": {
85
85
  "@capacitor/core": "^6.1.0",
86
86
  "@rollup/plugin-image": "^2.1.1",
87
- "@rpg-engine/shared": "^0.10.10",
87
+ "@rpg-engine/shared": "^0.10.14",
88
88
  "dayjs": "^1.11.2",
89
89
  "font-awesome": "^4.7.0",
90
90
  "fs-extra": "^10.1.0",
@@ -1,17 +1,26 @@
1
- import { IProductBlueprint, MetadataType, UserAccountTypes } from '@rpg-engine/shared';
2
- import React, { useEffect, useState } from 'react';
1
+ import {
2
+ IProductBlueprint,
3
+ MetadataType,
4
+ UserAccountTypes,
5
+ } from '@rpg-engine/shared';
6
+ import React from 'react';
3
7
  import { FaCartPlus } from 'react-icons/fa';
4
8
  import styled from 'styled-components';
5
9
  import { SelectArrow } from '../Arrow/SelectArrow';
6
10
  import { ICharacterProps } from '../Character/CharacterSelection';
7
11
  import { CTAButton } from '../shared/CTAButton/CTAButton';
8
12
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
13
+ import { useCharacterSkinNavigation } from '../../hooks/useCharacterSkinNavigation';
9
14
 
10
15
  interface IStoreCharacterSkinRowProps {
11
16
  item: IProductBlueprint;
12
17
  atlasJSON: Record<string, any>;
13
18
  atlasIMG: string;
14
- onAddToCart: (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => void;
19
+ onAddToCart: (
20
+ item: IProductBlueprint,
21
+ quantity: number,
22
+ metadata?: Record<string, any>
23
+ ) => void;
15
24
  userAccountType: UserAccountTypes;
16
25
  }
17
26
 
@@ -22,33 +31,21 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
22
31
  onAddToCart,
23
32
  userAccountType,
24
33
  }) => {
25
- const [currentIndex, setCurrentIndex] = useState(0);
26
-
27
34
  // Get available characters from metadata
28
- const availableCharacters: ICharacterProps[] =
29
- item.metadataType === MetadataType.CharacterSkin &&
30
- item.metadataConfig?.availableCharacters || [];
31
-
35
+ const availableCharacters: ICharacterProps[] =
36
+ (item.metadataType === MetadataType.CharacterSkin &&
37
+ item.metadataConfig?.availableCharacters) ||
38
+ [];
39
+
32
40
  // Get the active character entity atlas info
33
41
  const entityAtlasJSON = item.metadataConfig?.atlasJSON;
34
42
  const entityAtlasIMG = item.metadataConfig?.atlasIMG;
35
-
36
- // Effect to reset currentIndex when switching items
37
- useEffect(() => {
38
- setCurrentIndex(0);
39
- }, [item.key]);
40
-
41
- const handlePreviousSkin = () => {
42
- setCurrentIndex((prevIndex) =>
43
- prevIndex === 0 ? availableCharacters.length - 1 : prevIndex - 1
44
- );
45
- };
46
43
 
47
- const handleNextSkin = () => {
48
- setCurrentIndex((prevIndex) =>
49
- prevIndex === availableCharacters.length - 1 ? 0 : prevIndex + 1
50
- );
51
- };
44
+ const {
45
+ currentCharacter,
46
+ handlePreviousSkin,
47
+ handleNextSkin,
48
+ } = useCharacterSkinNavigation(availableCharacters, item.key);
52
49
 
53
50
  const hasRequiredAccount =
54
51
  !item.requiredAccountType?.length ||
@@ -56,26 +53,24 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
56
53
 
57
54
  const handleAddToCart = () => {
58
55
  if (!hasRequiredAccount) return;
59
-
56
+
60
57
  // Always use a quantity of 1
61
58
  if (availableCharacters.length > 0 && currentCharacter) {
62
- onAddToCart(item, 1, {
59
+ onAddToCart(item, 1, {
63
60
  selectedSkinName: currentCharacter.name,
64
- selectedSkinTextureKey: currentCharacter.textureKey
61
+ selectedSkinTextureKey: currentCharacter.textureKey,
65
62
  });
66
63
  } else {
67
64
  onAddToCart(item, 1);
68
65
  }
69
66
  };
70
-
67
+
71
68
  const getSpriteKey = (textureKey: string) => {
72
69
  return textureKey + '/down/standing/0.png';
73
70
  };
74
-
75
- const currentCharacter = availableCharacters[currentIndex];
76
71
 
77
72
  return (
78
- <ItemWrapper>
73
+ <ItemWrapper $isHighlighted={item.store?.isHighlighted || false}>
79
74
  <ItemIconContainer>
80
75
  {entityAtlasJSON && entityAtlasIMG && currentCharacter ? (
81
76
  <SpriteFromAtlas
@@ -87,7 +82,7 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
87
82
  imgScale={2}
88
83
  centered
89
84
  />
90
- ) : (
85
+ ) : item.texturePath ? (
91
86
  <SpriteFromAtlas
92
87
  atlasJSON={atlasJSON}
93
88
  atlasIMG={atlasIMG}
@@ -97,19 +92,29 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
97
92
  imgScale={2}
98
93
  centered
99
94
  />
95
+ ) : (
96
+ <DefaultIcon>👤</DefaultIcon>
100
97
  )}
101
98
  </ItemIconContainer>
102
99
 
103
100
  <ItemDetails>
104
101
  <Header>
105
- <ItemName>{item.name}</ItemName>
102
+ <ItemName>{item.name}</ItemName>
106
103
  </Header>
107
104
  {availableCharacters.length > 0 && currentCharacter && (
108
105
  <SelectedSkinNav>
109
106
  <SelectedSkin>Selected:</SelectedSkin>
110
- <SkinNavArrow direction="left" onPointerDown={handlePreviousSkin} size={24} />
107
+ <SkinNavArrow
108
+ direction="left"
109
+ onPointerDown={handlePreviousSkin}
110
+ size={24}
111
+ />
111
112
  <SelectedSkin>{currentCharacter.name}</SelectedSkin>
112
- <SkinNavArrow direction="right" onPointerDown={handleNextSkin} size={24} />
113
+ <SkinNavArrow
114
+ direction="right"
115
+ onPointerDown={handleNextSkin}
116
+ size={24}
117
+ />
113
118
  </SelectedSkinNav>
114
119
  )}
115
120
  <ItemPrice>${item.price}</ItemPrice>
@@ -126,12 +131,16 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
126
131
  );
127
132
  };
128
133
 
129
- const ItemWrapper = styled.div`
134
+ const ItemWrapper = styled.div<{ $isHighlighted: boolean }>`
130
135
  display: flex;
131
136
  align-items: center;
132
- gap: 1rem;
133
- padding: 1rem;
137
+ gap: 0.75rem;
138
+ padding: 0.5rem 1rem;
134
139
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
140
+ background: ${props =>
141
+ props.$isHighlighted ? 'rgba(255, 215, 0, 0.1)' : 'transparent'};
142
+ border-left: ${props =>
143
+ props.$isHighlighted ? '3px solid #ffd700' : '3px solid transparent'};
135
144
 
136
145
  &:last-child {
137
146
  border-bottom: none;
@@ -152,31 +161,31 @@ const ItemDetails = styled.div`
152
161
  flex: 1;
153
162
  display: flex;
154
163
  flex-direction: column;
155
- gap: 0.5rem;
164
+ gap: 0.25rem;
156
165
  `;
157
166
 
158
167
  const ItemName = styled.div`
159
168
  font-family: 'Press Start 2P', cursive;
160
- font-size: 0.875rem;
169
+ font-size: 0.75rem;
161
170
  color: #ffffff;
162
171
  `;
163
172
 
164
173
  const SelectedSkin = styled.div`
165
174
  font-family: 'Press Start 2P', cursive;
166
- font-size: 0.65rem;
175
+ font-size: 0.5rem;
167
176
  color: #fef08a;
168
177
  `;
169
178
 
170
179
  const ItemPrice = styled.div`
171
180
  font-family: 'Press Start 2P', cursive;
172
- font-size: 0.75rem;
181
+ font-size: 0.625rem;
173
182
  color: #fef08a;
174
183
  `;
175
184
 
176
185
  const Controls = styled.div`
177
186
  display: flex;
178
187
  align-items: center;
179
- gap: 1rem;
188
+ gap: 0.5rem;
180
189
  min-width: fit-content;
181
190
  `;
182
191
 
@@ -195,4 +204,13 @@ const SelectedSkinNav = styled.div`
195
204
  display: flex;
196
205
  align-items: center;
197
206
  gap: 0.5rem;
198
- `;
207
+ `;
208
+
209
+ const DefaultIcon = styled.div`
210
+ font-size: 1.5rem;
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ width: 32px;
215
+ height: 32px;
216
+ `;
@@ -5,12 +5,17 @@ import styled from 'styled-components';
5
5
  import { SelectArrow } from '../Arrow/SelectArrow';
6
6
  import { CTAButton } from '../shared/CTAButton/CTAButton';
7
7
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
8
+ import { useQuantityControl } from '../../hooks/useQuantityControl';
8
9
 
9
10
  interface IStoreItemRowProps {
10
11
  item: IProductBlueprint;
11
12
  atlasJSON: Record<string, any>;
12
13
  atlasIMG: string;
13
- onAddToCart: (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => void;
14
+ onAddToCart: (
15
+ item: IProductBlueprint,
16
+ quantity: number,
17
+ metadata?: Record<string, any>
18
+ ) => void;
14
19
  userAccountType: UserAccountTypes;
15
20
  showTextInput?: boolean;
16
21
  textInputPlaceholder?: string;
@@ -25,26 +30,15 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
25
30
  showTextInput = false,
26
31
  textInputPlaceholder = item.inputPlaceholder,
27
32
  }) => {
28
- const [quantity, setQuantity] = useState(1);
29
33
  const [textInputValue, setTextInputValue] = useState('');
30
-
31
- const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
32
- const value = parseInt(e.target.value) || 1;
33
- setQuantity(Math.min(Math.max(1, value), 99));
34
- };
35
-
36
- const handleBlur = () => {
37
- if (quantity < 1) setQuantity(1);
38
- if (quantity > 99) setQuantity(99);
39
- };
40
-
41
- const incrementQuantity = () => {
42
- setQuantity(prev => Math.min(prev + 1, 99));
43
- };
44
-
45
- const decrementQuantity = () => {
46
- setQuantity(prev => Math.max(1, prev - 1));
47
- };
34
+ const {
35
+ quantity,
36
+ handleQuantityChange,
37
+ handleBlur,
38
+ incrementQuantity,
39
+ decrementQuantity,
40
+ resetQuantity,
41
+ } = useQuantityControl();
48
42
 
49
43
  const hasRequiredAccount =
50
44
  !item.requiredAccountType?.length ||
@@ -56,13 +50,13 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
56
50
  onAddToCart(item, 1, { inputValue: textInputValue });
57
51
  setTextInputValue('');
58
52
  } else {
59
- onAddToCart(item, quantity);
60
- setQuantity(1);
53
+ onAddToCart(item, quantity);
54
+ resetQuantity();
61
55
  }
62
56
  };
63
57
 
64
58
  return (
65
- <ItemWrapper>
59
+ <ItemWrapper $isHighlighted={item.store?.isHighlighted || false}>
66
60
  <ItemIconContainer>
67
61
  <SpriteFromAtlas
68
62
  atlasJSON={atlasJSON}
@@ -92,29 +86,29 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
92
86
  className="rpgui-input"
93
87
  />
94
88
  ) : item.isStackable ? (
95
- <ArrowsContainer>
96
- <SelectArrow
97
- direction="left"
98
- onPointerDown={decrementQuantity}
99
- size={24}
100
- />
101
-
102
- <QuantityInput
103
- type="number"
104
- value={quantity}
105
- onChange={handleQuantityChange}
106
- onBlur={handleBlur}
107
- min={1}
108
- max={99}
109
- className="rpgui-input"
110
- />
111
-
112
- <SelectArrow
113
- direction="right"
114
- onPointerDown={incrementQuantity}
115
- size={24}
116
- />
117
- </ArrowsContainer>
89
+ <ArrowsContainer>
90
+ <SelectArrow
91
+ direction="left"
92
+ onPointerDown={decrementQuantity}
93
+ size={24}
94
+ />
95
+
96
+ <QuantityInput
97
+ type="number"
98
+ value={quantity}
99
+ onChange={handleQuantityChange}
100
+ onBlur={handleBlur}
101
+ min={1}
102
+ max={99}
103
+ className="rpgui-input"
104
+ />
105
+
106
+ <SelectArrow
107
+ direction="right"
108
+ onPointerDown={incrementQuantity}
109
+ size={24}
110
+ />
111
+ </ArrowsContainer>
118
112
  ) : null}
119
113
 
120
114
  <CTAButton
@@ -128,12 +122,16 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
128
122
  );
129
123
  };
130
124
 
131
- const ItemWrapper = styled.div`
125
+ const ItemWrapper = styled.div<{ $isHighlighted: boolean }>`
132
126
  display: flex;
133
127
  align-items: center;
134
- gap: 1rem;
135
- padding: 1rem;
128
+ gap: 0.75rem;
129
+ padding: 0.5rem 1rem;
136
130
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
131
+ background: ${props =>
132
+ props.$isHighlighted ? 'rgba(255, 215, 0, 0.1)' : 'transparent'};
133
+ border-left: ${props =>
134
+ props.$isHighlighted ? '3px solid #ffd700' : '3px solid transparent'};
137
135
 
138
136
  &:last-child {
139
137
  border-bottom: none;
@@ -154,18 +152,18 @@ const ItemDetails = styled.div`
154
152
  flex: 1;
155
153
  display: flex;
156
154
  flex-direction: column;
157
- gap: 0.5rem;
155
+ gap: 0.25rem;
158
156
  `;
159
157
 
160
158
  const ItemName = styled.div`
161
159
  font-family: 'Press Start 2P', cursive;
162
- font-size: 0.875rem;
160
+ font-size: 0.75rem;
163
161
  color: #ffffff;
164
162
  `;
165
163
 
166
164
  const ItemPrice = styled.div`
167
165
  font-family: 'Press Start 2P', cursive;
168
- font-size: 0.75rem;
166
+ font-size: 0.625rem;
169
167
  color: #fef08a;
170
168
  `;
171
169
 
@@ -179,7 +177,7 @@ const ItemDescription = styled.div`
179
177
  const Controls = styled.div`
180
178
  display: flex;
181
179
  align-items: center;
182
- gap: 1rem;
180
+ gap: 0.5rem;
183
181
  min-width: fit-content;
184
182
  `;
185
183
 
@@ -1,13 +1,25 @@
1
- import { IProductBlueprint, MetadataType, UserAccountTypes } from '@rpg-engine/shared';
2
- import React, { useState } from 'react';
1
+ import {
2
+ IProductBlueprint,
3
+ MetadataType,
4
+ UserAccountTypes,
5
+ ItemType,
6
+ } from '@rpg-engine/shared';
7
+ import React from 'react';
8
+ import styled from 'styled-components';
3
9
  import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
4
10
  import { StoreCharacterSkinRow } from '../StoreCharacterSkinRow';
5
11
  import { StoreItemRow } from '../StoreItemRow';
6
-
12
+ import { Dropdown } from '../../Dropdown';
13
+ import { SearchBar } from '../../shared/SearchBar/SearchBar';
14
+ import { useStoreFiltering } from '../../../hooks/useStoreFiltering';
7
15
 
8
16
  interface IStoreItemsSectionProps {
9
17
  items: IProductBlueprint[];
10
- onAddToCart: (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => void;
18
+ onAddToCart: (
19
+ item: IProductBlueprint,
20
+ quantity: number,
21
+ metadata?: Record<string, any>
22
+ ) => void;
11
23
  atlasJSON: Record<string, any>;
12
24
  atlasIMG: string;
13
25
  userAccountType?: UserAccountTypes;
@@ -22,11 +34,13 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
22
34
  userAccountType,
23
35
  textInputItemKeys = [],
24
36
  }) => {
25
- const [searchQuery, setSearchQuery] = useState('');
26
-
27
- const filteredItems = items.filter(item =>
28
- item.name.toLowerCase().includes(searchQuery.toLowerCase())
29
- );
37
+ const {
38
+ searchQuery,
39
+ setSearchQuery,
40
+ setSelectedCategory,
41
+ categoryOptions,
42
+ filteredItems,
43
+ } = useStoreFiltering(items);
30
44
 
31
45
  const renderStoreItem = (item: IProductBlueprint) => {
32
46
  // Prefer a specialized character skin row when needed
@@ -70,17 +84,54 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
70
84
  };
71
85
 
72
86
  return (
73
- <ScrollableContent
74
- items={filteredItems}
75
- renderItem={renderStoreItem}
76
- emptyMessage="No items available."
77
- searchOptions={{
78
- value: searchQuery,
79
- onChange: setSearchQuery,
80
- placeholder: 'Search items...',
81
- }}
82
- layout="list"
83
- maxHeight="400px"
84
- />
87
+ <StoreContainer>
88
+ <SearchHeader>
89
+ <SearchBarContainer>
90
+ <SearchBar
91
+ value={searchQuery}
92
+ onChange={setSearchQuery}
93
+ placeholder="Search items..."
94
+ />
95
+ </SearchBarContainer>
96
+ <DropdownContainer>
97
+ <Dropdown
98
+ options={categoryOptions}
99
+ onChange={value => setSelectedCategory(value as ItemType | 'all')}
100
+ width="100%"
101
+ />
102
+ </DropdownContainer>
103
+ </SearchHeader>
104
+
105
+ <ScrollableContent
106
+ items={filteredItems}
107
+ renderItem={renderStoreItem}
108
+ emptyMessage="No items match your filters."
109
+ layout="list"
110
+ maxHeight="350px"
111
+ />
112
+ </StoreContainer>
85
113
  );
86
114
  };
115
+
116
+ const StoreContainer = styled.div`
117
+ display: flex;
118
+ flex-direction: column;
119
+ height: 100%;
120
+ gap: 0.5rem;
121
+ `;
122
+
123
+ const SearchHeader = styled.div`
124
+ display: flex;
125
+ gap: 0.5rem;
126
+ align-items: center;
127
+ padding-top: 0.25rem;
128
+ `;
129
+
130
+ const SearchBarContainer = styled.div`
131
+ flex: 0.75;
132
+ `;
133
+
134
+ const DropdownContainer = styled.div`
135
+ flex: 0.25;
136
+ min-width: 140px;
137
+ `;
@@ -1,10 +1,11 @@
1
1
  import { IItemPack } from '@rpg-engine/shared';
2
- import React, { useCallback, useMemo, useState } from 'react';
2
+ import React, { useCallback } from 'react';
3
3
  import { FaCartPlus } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { CTAButton } from '../../shared/CTAButton/CTAButton';
6
6
  import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
7
7
  import { ShoppingCardHorizontal } from '../../shared/ShoppingCart/CartCardHorizontal';
8
+ import { usePackFiltering } from '../../../hooks/usePackFiltering';
8
9
 
9
10
  interface IStorePacksSectionProps {
10
11
  packs: IItemPack[];
@@ -17,7 +18,9 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
17
18
  onAddToCart,
18
19
  onSelectPack,
19
20
  }) => {
20
- const [searchQuery, setSearchQuery] = useState('');
21
+ const { searchQuery, setSearchQuery, filteredPacks } = usePackFiltering(
22
+ packs
23
+ );
21
24
 
22
25
  const renderPackFooter = useCallback(
23
26
  (pack: IItemPack) => (
@@ -50,14 +53,6 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
50
53
  [onSelectPack, renderPackFooter]
51
54
  );
52
55
 
53
- const filteredPacks = useMemo(
54
- () =>
55
- packs.filter(pack =>
56
- pack.title.toLowerCase().includes(searchQuery.toLowerCase())
57
- ),
58
- [packs, searchQuery]
59
- );
60
-
61
56
  return (
62
57
  <ScrollableContent
63
58
  items={filteredPacks}
@@ -0,0 +1,34 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { ICharacterProps } from '../components/Character/CharacterSelection';
3
+
4
+ export const useCharacterSkinNavigation = (
5
+ availableCharacters: ICharacterProps[],
6
+ itemKey: string
7
+ ) => {
8
+ const [currentIndex, setCurrentIndex] = useState(0);
9
+
10
+ useEffect(() => {
11
+ setCurrentIndex(0);
12
+ }, [itemKey]);
13
+
14
+ const handlePreviousSkin = () => {
15
+ setCurrentIndex(prevIndex =>
16
+ prevIndex === 0 ? availableCharacters.length - 1 : prevIndex - 1
17
+ );
18
+ };
19
+
20
+ const handleNextSkin = () => {
21
+ setCurrentIndex(prevIndex =>
22
+ prevIndex === availableCharacters.length - 1 ? 0 : prevIndex + 1
23
+ );
24
+ };
25
+
26
+ const currentCharacter = availableCharacters[currentIndex];
27
+
28
+ return {
29
+ currentIndex,
30
+ currentCharacter,
31
+ handlePreviousSkin,
32
+ handleNextSkin,
33
+ };
34
+ };
@@ -0,0 +1,20 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { IItemPack } from '@rpg-engine/shared';
3
+
4
+ export const usePackFiltering = (packs: IItemPack[]) => {
5
+ const [searchQuery, setSearchQuery] = useState('');
6
+
7
+ const filteredPacks = useMemo(
8
+ () =>
9
+ packs.filter(pack =>
10
+ pack.title.toLowerCase().includes(searchQuery.toLowerCase())
11
+ ),
12
+ [packs, searchQuery]
13
+ );
14
+
15
+ return {
16
+ searchQuery,
17
+ setSearchQuery,
18
+ filteredPacks,
19
+ };
20
+ };
@@ -0,0 +1,41 @@
1
+ import { useState } from 'react';
2
+
3
+ export const useQuantityControl = (
4
+ initialQuantity: number = 1,
5
+ min: number = 1,
6
+ max: number = 99
7
+ ) => {
8
+ const [quantity, setQuantity] = useState(initialQuantity);
9
+
10
+ const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
11
+ const value = parseInt(e.target.value) || min;
12
+ setQuantity(Math.min(Math.max(min, value), max));
13
+ };
14
+
15
+ const handleBlur = () => {
16
+ if (quantity < min) setQuantity(min);
17
+ if (quantity > max) setQuantity(max);
18
+ };
19
+
20
+ const incrementQuantity = () => {
21
+ setQuantity(prev => Math.min(prev + 1, max));
22
+ };
23
+
24
+ const decrementQuantity = () => {
25
+ setQuantity(prev => Math.max(min, prev - 1));
26
+ };
27
+
28
+ const resetQuantity = () => {
29
+ setQuantity(initialQuantity);
30
+ };
31
+
32
+ return {
33
+ quantity,
34
+ setQuantity,
35
+ handleQuantityChange,
36
+ handleBlur,
37
+ incrementQuantity,
38
+ decrementQuantity,
39
+ resetQuantity,
40
+ };
41
+ };