@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/dist/hooks/useCharacterSkinNavigation.d.ts +7 -0
- package/dist/hooks/usePackFiltering.d.ts +7 -0
- package/dist/hooks/useQuantityControl.d.ts +10 -0
- package/dist/hooks/useStoreFiltering.d.ts +11 -0
- package/dist/long-bow.cjs.development.js +211 -75
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +211 -75
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Store/StoreCharacterSkinRow.tsx +63 -45
- package/src/components/Store/StoreItemRow.tsx +51 -53
- package/src/components/Store/sections/StoreItemsSection.tsx +72 -21
- package/src/components/Store/sections/StorePacksSection.tsx +5 -10
- package/src/hooks/useCharacterSkinNavigation.ts +34 -0
- package/src/hooks/usePackFiltering.ts +20 -0
- package/src/hooks/useQuantityControl.ts +41 -0
- package/src/hooks/useStoreFiltering.ts +51 -0
- package/src/mocks/dailyTasks.mocks.ts +6 -6
- package/src/stories/Features/store/Store.stories.tsx +35 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpg-engine/long-bow",
|
|
3
|
-
"version": "0.8.
|
|
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.
|
|
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 {
|
|
2
|
-
|
|
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: (
|
|
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
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
107
|
+
<SkinNavArrow
|
|
108
|
+
direction="left"
|
|
109
|
+
onPointerDown={handlePreviousSkin}
|
|
110
|
+
size={24}
|
|
111
|
+
/>
|
|
111
112
|
<SelectedSkin>{currentCharacter.name}</SelectedSkin>
|
|
112
|
-
<SkinNavArrow
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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: (
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
180
|
+
gap: 0.5rem;
|
|
183
181
|
min-width: fit-content;
|
|
184
182
|
`;
|
|
185
183
|
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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: (
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
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
|
+
};
|