@rpg-engine/long-bow 0.8.69 → 0.8.71
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/components/Store/Store.d.ts +6 -0
- package/dist/components/Store/StoreItemRow.d.ts +1 -0
- package/dist/components/Store/sections/StoreItemsSection.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +158 -132
- 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 +158 -132
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Store/CartView.tsx +11 -0
- package/src/components/Store/Store.tsx +32 -8
- package/src/components/Store/StoreCharacterSkinRow.tsx +37 -125
- package/src/components/Store/StoreItemRow.tsx +33 -3
- package/src/components/Store/__test__/MetadataCollector.spec.tsx +2 -1
- package/src/components/Store/__test__/useStoreMetadata.spec.tsx +10 -10
- package/src/components/Store/sections/StoreItemsSection.tsx +18 -3
- package/src/stories/Features/store/Store.stories.tsx +4 -0
package/package.json
CHANGED
|
@@ -111,6 +111,9 @@ export const CartView: React.FC<ICartViewProps> = ({
|
|
|
111
111
|
</ItemIconContainer>
|
|
112
112
|
<ItemDetails>
|
|
113
113
|
<ItemName>{cartItem.item.name}</ItemName>
|
|
114
|
+
{cartItem.metadata?.inputValue && (
|
|
115
|
+
<CartMeta>{cartItem.metadata.inputValue}</CartMeta>
|
|
116
|
+
)}
|
|
114
117
|
<ItemInfo>
|
|
115
118
|
<span>${formatPrice(cartItem.item.price)}</span>
|
|
116
119
|
<span>×</span>
|
|
@@ -339,3 +342,11 @@ const MetadataValue = styled.div`
|
|
|
339
342
|
text-overflow: ellipsis;
|
|
340
343
|
white-space: nowrap;
|
|
341
344
|
`;
|
|
345
|
+
|
|
346
|
+
const CartMeta = styled.div`
|
|
347
|
+
font-family: 'Press Start 2P', cursive;
|
|
348
|
+
font-size: 0.75rem;
|
|
349
|
+
color: #ffffff;
|
|
350
|
+
opacity: 0.8;
|
|
351
|
+
margin-top: 0.25rem;
|
|
352
|
+
`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IItemPack, IPurchase, IStoreItem, ItemRarities, ItemSubType, ItemType, UserAccountTypes } from '@rpg-engine/shared';
|
|
2
|
-
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import React, { ReactNode, useMemo, useState } from 'react';
|
|
3
3
|
import { FaHistory, FaShoppingCart } from 'react-icons/fa';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { uiColors } from '../../constants/uiColors';
|
|
@@ -14,6 +14,9 @@ import { StoreItemsSection } from './sections/StoreItemsSection';
|
|
|
14
14
|
import { StorePacksSection } from './sections/StorePacksSection';
|
|
15
15
|
import { StoreItemDetails } from './StoreItemDetails';
|
|
16
16
|
|
|
17
|
+
// Define TabId union type for tab identifiers
|
|
18
|
+
type TabId = 'premium' | 'packs' | 'items';
|
|
19
|
+
|
|
17
20
|
// Define IStoreProps locally as a workaround
|
|
18
21
|
export interface IStoreProps {
|
|
19
22
|
items: IStoreItem[];
|
|
@@ -26,6 +29,10 @@ export interface IStoreProps {
|
|
|
26
29
|
loading?: boolean;
|
|
27
30
|
error?: string;
|
|
28
31
|
onClose?: () => void;
|
|
32
|
+
hidePremiumTab?: boolean;
|
|
33
|
+
tabOrder?: TabId[];
|
|
34
|
+
defaultActiveTab?: TabId;
|
|
35
|
+
textInputItemKeys?: string[];
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export const Store: React.FC<IStoreProps> = ({
|
|
@@ -39,9 +46,19 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
39
46
|
loading = false,
|
|
40
47
|
error,
|
|
41
48
|
onClose,
|
|
49
|
+
hidePremiumTab = false,
|
|
50
|
+
tabOrder,
|
|
51
|
+
defaultActiveTab,
|
|
52
|
+
textInputItemKeys = [],
|
|
42
53
|
}) => {
|
|
43
54
|
const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
|
|
44
|
-
const [activeTab, setActiveTab] = useState(
|
|
55
|
+
const [activeTab, setActiveTab] = useState<TabId>(() => {
|
|
56
|
+
const initialTabs = (tabOrder ?? ['premium', 'packs', 'items']).filter(id => !(hidePremiumTab && id === 'premium'));
|
|
57
|
+
if (defaultActiveTab && initialTabs.includes(defaultActiveTab)) {
|
|
58
|
+
return defaultActiveTab;
|
|
59
|
+
}
|
|
60
|
+
return hidePremiumTab ? 'items' : 'premium';
|
|
61
|
+
});
|
|
45
62
|
const {
|
|
46
63
|
cartItems,
|
|
47
64
|
handleAddToCart,
|
|
@@ -136,8 +153,12 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
136
153
|
return <ErrorMessage>{error}</ErrorMessage>;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
// Build tabs dynamically based on props
|
|
157
|
+
const tabIds: TabId[] = tabOrder ?? ['premium', 'packs', 'items'];
|
|
158
|
+
const availableTabIds: TabId[] = tabIds.filter(id => !(hidePremiumTab && id === 'premium'));
|
|
159
|
+
|
|
160
|
+
const tabsMap: Record<TabId, { id: TabId; title: string; content: ReactNode }> = {
|
|
161
|
+
premium: {
|
|
141
162
|
id: 'premium',
|
|
142
163
|
title: 'Premium',
|
|
143
164
|
content: (
|
|
@@ -148,7 +169,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
148
169
|
/>
|
|
149
170
|
),
|
|
150
171
|
},
|
|
151
|
-
{
|
|
172
|
+
packs: {
|
|
152
173
|
id: 'packs',
|
|
153
174
|
title: 'Packs',
|
|
154
175
|
content: (
|
|
@@ -159,7 +180,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
159
180
|
/>
|
|
160
181
|
),
|
|
161
182
|
},
|
|
162
|
-
{
|
|
183
|
+
items: {
|
|
163
184
|
id: 'items',
|
|
164
185
|
title: 'Items',
|
|
165
186
|
content: (
|
|
@@ -169,10 +190,13 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
169
190
|
atlasJSON={atlasJSON}
|
|
170
191
|
atlasIMG={atlasIMG}
|
|
171
192
|
userAccountType={userAccountType}
|
|
193
|
+
textInputItemKeys={textInputItemKeys}
|
|
172
194
|
/>
|
|
173
195
|
),
|
|
174
196
|
},
|
|
175
|
-
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const tabs = availableTabIds.map(id => tabsMap[id]);
|
|
176
200
|
|
|
177
201
|
return (
|
|
178
202
|
<DraggableContainer
|
|
@@ -243,7 +267,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
243
267
|
borderColor="#f59e0b"
|
|
244
268
|
hoverColor="#fef3c7"
|
|
245
269
|
activeTab={activeTab}
|
|
246
|
-
onTabChange={setActiveTab}
|
|
270
|
+
onTabChange={(tabId: string) => setActiveTab(tabId as TabId)}
|
|
247
271
|
/>
|
|
248
272
|
</MainContent>
|
|
249
273
|
{cartItems.length > 0 && (
|
|
@@ -4,7 +4,6 @@ import { FaCartPlus } from 'react-icons/fa';
|
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { SelectArrow } from '../Arrow/SelectArrow';
|
|
6
6
|
import { ICharacterProps } from '../Character/CharacterSelection';
|
|
7
|
-
import { ErrorBoundary } from '../Item/Inventory/ErrorBoundary';
|
|
8
7
|
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
9
8
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
10
9
|
|
|
@@ -23,7 +22,6 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
|
|
|
23
22
|
onAddToCart,
|
|
24
23
|
userAccountType,
|
|
25
24
|
}) => {
|
|
26
|
-
const [quantity, setQuantity] = useState(1);
|
|
27
25
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
28
26
|
|
|
29
27
|
// Get available characters from metadata
|
|
@@ -40,32 +38,14 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
|
|
|
40
38
|
setCurrentIndex(0);
|
|
41
39
|
}, [item._id]);
|
|
42
40
|
|
|
43
|
-
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
44
|
-
const value = parseInt(e.target.value) || 1;
|
|
45
|
-
setQuantity(Math.min(Math.max(1, value), 99));
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const handleBlur = () => {
|
|
49
|
-
if (quantity < 1) setQuantity(1);
|
|
50
|
-
if (quantity > 99) setQuantity(99);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const incrementQuantity = () => {
|
|
54
|
-
setQuantity(prev => Math.min(prev + 1, 99));
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const decrementQuantity = () => {
|
|
58
|
-
setQuantity(prev => Math.max(1, prev - 1));
|
|
59
|
-
};
|
|
60
|
-
|
|
61
41
|
const handlePreviousSkin = () => {
|
|
62
|
-
setCurrentIndex((prevIndex) =>
|
|
42
|
+
setCurrentIndex((prevIndex) =>
|
|
63
43
|
prevIndex === 0 ? availableCharacters.length - 1 : prevIndex - 1
|
|
64
44
|
);
|
|
65
45
|
};
|
|
66
46
|
|
|
67
47
|
const handleNextSkin = () => {
|
|
68
|
-
setCurrentIndex((prevIndex) =>
|
|
48
|
+
setCurrentIndex((prevIndex) =>
|
|
69
49
|
prevIndex === availableCharacters.length - 1 ? 0 : prevIndex + 1
|
|
70
50
|
);
|
|
71
51
|
};
|
|
@@ -77,17 +57,15 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
|
|
|
77
57
|
const handleAddToCart = () => {
|
|
78
58
|
if (!hasRequiredAccount) return;
|
|
79
59
|
|
|
80
|
-
//
|
|
60
|
+
// Always use a quantity of 1
|
|
81
61
|
if (availableCharacters.length > 0 && currentCharacter) {
|
|
82
|
-
onAddToCart(item,
|
|
62
|
+
onAddToCart(item, 1, {
|
|
83
63
|
selectedSkinName: currentCharacter.name,
|
|
84
64
|
selectedSkinTextureKey: currentCharacter.textureKey
|
|
85
65
|
});
|
|
86
66
|
} else {
|
|
87
|
-
onAddToCart(item,
|
|
67
|
+
onAddToCart(item, 1);
|
|
88
68
|
}
|
|
89
|
-
|
|
90
|
-
setQuantity(1); // Reset quantity after adding to cart
|
|
91
69
|
};
|
|
92
70
|
|
|
93
71
|
const getSpriteKey = (textureKey: string) => {
|
|
@@ -99,34 +77,16 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
|
|
|
99
77
|
return (
|
|
100
78
|
<ItemWrapper>
|
|
101
79
|
<ItemIconContainer>
|
|
102
|
-
{
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<SpriteFromAtlas
|
|
113
|
-
atlasJSON={entityAtlasJSON}
|
|
114
|
-
atlasIMG={entityAtlasIMG}
|
|
115
|
-
spriteKey={getSpriteKey(currentCharacter.textureKey)}
|
|
116
|
-
width={32}
|
|
117
|
-
height={32}
|
|
118
|
-
imgScale={2}
|
|
119
|
-
centered
|
|
120
|
-
/>
|
|
121
|
-
</ErrorBoundary>
|
|
122
|
-
</SpriteContainer>
|
|
123
|
-
|
|
124
|
-
<NavArrow
|
|
125
|
-
direction="right"
|
|
126
|
-
onPointerDown={handleNextSkin}
|
|
127
|
-
size={24}
|
|
128
|
-
/>
|
|
129
|
-
</CharacterSkinPreviewContainer>
|
|
80
|
+
{entityAtlasJSON && entityAtlasIMG && currentCharacter ? (
|
|
81
|
+
<SpriteFromAtlas
|
|
82
|
+
atlasJSON={entityAtlasJSON}
|
|
83
|
+
atlasIMG={entityAtlasIMG}
|
|
84
|
+
spriteKey={getSpriteKey(currentCharacter.textureKey)}
|
|
85
|
+
width={32}
|
|
86
|
+
height={32}
|
|
87
|
+
imgScale={2}
|
|
88
|
+
centered
|
|
89
|
+
/>
|
|
130
90
|
) : (
|
|
131
91
|
<SpriteFromAtlas
|
|
132
92
|
atlasJSON={atlasJSON}
|
|
@@ -141,38 +101,20 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
|
|
|
141
101
|
</ItemIconContainer>
|
|
142
102
|
|
|
143
103
|
<ItemDetails>
|
|
144
|
-
<
|
|
104
|
+
<Header>
|
|
105
|
+
<ItemName>{item.name}</ItemName>
|
|
106
|
+
</Header>
|
|
145
107
|
{availableCharacters.length > 0 && currentCharacter && (
|
|
146
|
-
<
|
|
108
|
+
<SelectedSkinNav>
|
|
109
|
+
<SelectedSkin>Selected:</SelectedSkin>
|
|
110
|
+
<SkinNavArrow direction="left" onPointerDown={handlePreviousSkin} size={24} />
|
|
111
|
+
<SelectedSkin>{currentCharacter.name}</SelectedSkin>
|
|
112
|
+
<SkinNavArrow direction="right" onPointerDown={handleNextSkin} size={24} />
|
|
113
|
+
</SelectedSkinNav>
|
|
147
114
|
)}
|
|
148
115
|
<ItemPrice>${item.price}</ItemPrice>
|
|
149
116
|
</ItemDetails>
|
|
150
|
-
|
|
151
117
|
<Controls>
|
|
152
|
-
<ArrowsContainer>
|
|
153
|
-
<SelectArrow
|
|
154
|
-
direction="left"
|
|
155
|
-
onPointerDown={decrementQuantity}
|
|
156
|
-
size={24}
|
|
157
|
-
/>
|
|
158
|
-
|
|
159
|
-
<QuantityInput
|
|
160
|
-
type="number"
|
|
161
|
-
value={quantity}
|
|
162
|
-
onChange={handleQuantityChange}
|
|
163
|
-
onBlur={handleBlur}
|
|
164
|
-
min={1}
|
|
165
|
-
max={99}
|
|
166
|
-
className="rpgui-input"
|
|
167
|
-
/>
|
|
168
|
-
|
|
169
|
-
<SelectArrow
|
|
170
|
-
direction="right"
|
|
171
|
-
onPointerDown={incrementQuantity}
|
|
172
|
-
size={24}
|
|
173
|
-
/>
|
|
174
|
-
</ArrowsContainer>
|
|
175
|
-
|
|
176
118
|
<CTAButton
|
|
177
119
|
icon={<FaCartPlus />}
|
|
178
120
|
label="Add"
|
|
@@ -197,7 +139,8 @@ const ItemWrapper = styled.div`
|
|
|
197
139
|
`;
|
|
198
140
|
|
|
199
141
|
const ItemIconContainer = styled.div`
|
|
200
|
-
|
|
142
|
+
width: 32px;
|
|
143
|
+
height: 32px;
|
|
201
144
|
display: flex;
|
|
202
145
|
align-items: center;
|
|
203
146
|
justify-content: center;
|
|
@@ -205,28 +148,6 @@ const ItemIconContainer = styled.div`
|
|
|
205
148
|
padding: 4px;
|
|
206
149
|
`;
|
|
207
150
|
|
|
208
|
-
const CharacterSkinPreviewContainer = styled.div`
|
|
209
|
-
position: relative;
|
|
210
|
-
display: flex;
|
|
211
|
-
align-items: center;
|
|
212
|
-
width: 140px;
|
|
213
|
-
height: 42px;
|
|
214
|
-
justify-content: space-between;
|
|
215
|
-
`;
|
|
216
|
-
|
|
217
|
-
const SpriteContainer = styled.div`
|
|
218
|
-
display: flex;
|
|
219
|
-
align-items: center;
|
|
220
|
-
justify-content: center;
|
|
221
|
-
position: absolute;
|
|
222
|
-
left: 50%;
|
|
223
|
-
transform: translateX(-50%);
|
|
224
|
-
`;
|
|
225
|
-
|
|
226
|
-
const NavArrow = styled(SelectArrow)`
|
|
227
|
-
z-index: 2;
|
|
228
|
-
`;
|
|
229
|
-
|
|
230
151
|
const ItemDetails = styled.div`
|
|
231
152
|
flex: 1;
|
|
232
153
|
display: flex;
|
|
@@ -259,28 +180,19 @@ const Controls = styled.div`
|
|
|
259
180
|
min-width: fit-content;
|
|
260
181
|
`;
|
|
261
182
|
|
|
262
|
-
|
|
263
|
-
|
|
183
|
+
// Styled arrow override for inline nav arrows
|
|
184
|
+
const SkinNavArrow = styled(SelectArrow)`
|
|
185
|
+
position: static;
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
const Header = styled.div`
|
|
264
189
|
display: flex;
|
|
265
190
|
align-items: center;
|
|
266
|
-
|
|
267
|
-
height: 42px;
|
|
268
|
-
justify-content: space-between;
|
|
191
|
+
gap: 0.5rem;
|
|
269
192
|
`;
|
|
270
193
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
font-size: 0.875rem;
|
|
276
|
-
background: rgba(0, 0, 0, 0.2);
|
|
277
|
-
color: #ffffff;
|
|
278
|
-
border: none;
|
|
279
|
-
padding: 0.25rem;
|
|
280
|
-
|
|
281
|
-
&::-webkit-inner-spin-button,
|
|
282
|
-
&::-webkit-outer-spin-button {
|
|
283
|
-
-webkit-appearance: none;
|
|
284
|
-
margin: 0;
|
|
285
|
-
}
|
|
194
|
+
const SelectedSkinNav = styled.div`
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 0.5rem;
|
|
286
198
|
`;
|
|
@@ -12,6 +12,7 @@ interface IStoreItemRowProps {
|
|
|
12
12
|
atlasIMG: string;
|
|
13
13
|
onAddToCart: (item: IStoreItem, quantity: number, metadata?: Record<string, any>) => void;
|
|
14
14
|
userAccountType: UserAccountTypes;
|
|
15
|
+
showTextInput?: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
@@ -20,8 +21,10 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
|
20
21
|
atlasIMG,
|
|
21
22
|
onAddToCart,
|
|
22
23
|
userAccountType,
|
|
24
|
+
showTextInput = false,
|
|
23
25
|
}) => {
|
|
24
26
|
const [quantity, setQuantity] = useState(1);
|
|
27
|
+
const [textInputValue, setTextInputValue] = useState('');
|
|
25
28
|
|
|
26
29
|
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
27
30
|
const value = parseInt(e.target.value) || 1;
|
|
@@ -45,10 +48,15 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
|
45
48
|
!item.requiredAccountType?.length ||
|
|
46
49
|
item.requiredAccountType.includes(userAccountType);
|
|
47
50
|
|
|
48
|
-
const
|
|
51
|
+
const handleAddToCartInternal = () => {
|
|
49
52
|
if (!hasRequiredAccount) return;
|
|
53
|
+
if (showTextInput) {
|
|
54
|
+
onAddToCart(item, 1, { inputValue: textInputValue });
|
|
55
|
+
setTextInputValue('');
|
|
56
|
+
} else {
|
|
50
57
|
onAddToCart(item, quantity);
|
|
51
|
-
|
|
58
|
+
setQuantity(1);
|
|
59
|
+
}
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
return (
|
|
@@ -71,6 +79,16 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
|
71
79
|
</ItemDetails>
|
|
72
80
|
|
|
73
81
|
<Controls>
|
|
82
|
+
{/* Show text input if configured, else show arrows only for stackable items */}
|
|
83
|
+
{showTextInput ? (
|
|
84
|
+
<TextInput
|
|
85
|
+
type="text"
|
|
86
|
+
value={textInputValue}
|
|
87
|
+
placeholder="Enter value"
|
|
88
|
+
onChange={e => setTextInputValue(e.target.value)}
|
|
89
|
+
className="rpgui-input"
|
|
90
|
+
/>
|
|
91
|
+
) : item.isStackable ? (
|
|
74
92
|
<ArrowsContainer>
|
|
75
93
|
<SelectArrow
|
|
76
94
|
direction="left"
|
|
@@ -94,11 +112,12 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
|
94
112
|
size={24}
|
|
95
113
|
/>
|
|
96
114
|
</ArrowsContainer>
|
|
115
|
+
) : null}
|
|
97
116
|
|
|
98
117
|
<CTAButton
|
|
99
118
|
icon={<FaCartPlus />}
|
|
100
119
|
label="Add"
|
|
101
|
-
onClick={
|
|
120
|
+
onClick={handleAddToCartInternal}
|
|
102
121
|
disabled={!hasRequiredAccount}
|
|
103
122
|
/>
|
|
104
123
|
</Controls>
|
|
@@ -179,3 +198,14 @@ const QuantityInput = styled.input`
|
|
|
179
198
|
margin: 0;
|
|
180
199
|
}
|
|
181
200
|
`;
|
|
201
|
+
|
|
202
|
+
const TextInput = styled.input`
|
|
203
|
+
width: 120px;
|
|
204
|
+
text-align: center;
|
|
205
|
+
margin: 0 auto;
|
|
206
|
+
font-size: 0.875rem;
|
|
207
|
+
background: rgba(0, 0, 0, 0.2);
|
|
208
|
+
color: #ffffff;
|
|
209
|
+
border: none;
|
|
210
|
+
padding: 0.25rem;
|
|
211
|
+
`;
|
|
@@ -24,9 +24,10 @@ jest.mock('../../Character/CharacterSkinSelectionModal', () => ({
|
|
|
24
24
|
jest.mock('../MetadataCollector', () => ({
|
|
25
25
|
MetadataCollector: jest.fn(({ metadataType, config, onCollect, onCancel }) => {
|
|
26
26
|
// Set up cleanup function for the useEffect tests
|
|
27
|
+
const React = require('react');
|
|
27
28
|
React.useEffect(() => {
|
|
28
29
|
return () => {
|
|
29
|
-
if (
|
|
30
|
+
if (globalThis.__metadataResolvers) {
|
|
30
31
|
onCancel();
|
|
31
32
|
}
|
|
32
33
|
};
|
|
@@ -20,15 +20,15 @@ jest.mock('../hooks/useStoreMetadata', () => {
|
|
|
20
20
|
|
|
21
21
|
const collectMetadata = async (item) => {
|
|
22
22
|
// If no metadata type or None, return null immediately
|
|
23
|
-
if (!item.metadataType || item.metadataType ===
|
|
23
|
+
if (!item.metadataType || item.metadataType === 'None') {
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Setup for valid metadata types
|
|
28
|
-
|
|
28
|
+
globalThis.__metadataResolvers = {
|
|
29
29
|
resolve: (metadata) => {
|
|
30
30
|
// Clean up
|
|
31
|
-
|
|
31
|
+
globalThis.__metadataResolvers = undefined;
|
|
32
32
|
return metadata;
|
|
33
33
|
},
|
|
34
34
|
item
|
|
@@ -36,7 +36,7 @@ jest.mock('../hooks/useStoreMetadata', () => {
|
|
|
36
36
|
|
|
37
37
|
// Handle the last test case specifically
|
|
38
38
|
if (item.key === 'item-character-skin-metadata-cancel') {
|
|
39
|
-
|
|
39
|
+
globalThis.__metadataResolvers = undefined; // Make sure it's cleaned up
|
|
40
40
|
return Promise.resolve(null);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -139,9 +139,9 @@ describe('useStoreMetadata', () => {
|
|
|
139
139
|
const metadataPromise = hook.collectMetadata(mockItemWithCharacterSkinMetadata);
|
|
140
140
|
|
|
141
141
|
// Setup our mock resolver
|
|
142
|
-
if (
|
|
143
|
-
const originalResolve =
|
|
144
|
-
|
|
142
|
+
if (globalThis.__metadataResolvers) {
|
|
143
|
+
const originalResolve = globalThis.__metadataResolvers.resolve;
|
|
144
|
+
globalThis.__metadataResolvers.resolve = function(metadata) {
|
|
145
145
|
resolveSpy(metadata);
|
|
146
146
|
return originalResolve(metadata);
|
|
147
147
|
};
|
|
@@ -149,8 +149,8 @@ describe('useStoreMetadata', () => {
|
|
|
149
149
|
|
|
150
150
|
// Resolve the metadata
|
|
151
151
|
const mockMetadata = { selectedSkin: 'test-skin' };
|
|
152
|
-
if (
|
|
153
|
-
|
|
152
|
+
if (globalThis.__metadataResolvers) {
|
|
153
|
+
globalThis.__metadataResolvers.resolve(mockMetadata);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Wait for the promise to resolve
|
|
@@ -159,7 +159,7 @@ describe('useStoreMetadata', () => {
|
|
|
159
159
|
// Assertions
|
|
160
160
|
expect(result).toEqual(mockMetadata);
|
|
161
161
|
expect(resolveSpy).toHaveBeenCalledWith(mockMetadata);
|
|
162
|
-
expect(
|
|
162
|
+
expect(globalThis.__metadataResolvers).toBeUndefined(); // Should be cleaned up
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('should clean up window.__metadataResolvers when collection is cancelled', async () => {
|
|
@@ -11,6 +11,7 @@ interface IStoreItemsSectionProps {
|
|
|
11
11
|
atlasJSON: Record<string, any>;
|
|
12
12
|
atlasIMG: string;
|
|
13
13
|
userAccountType?: UserAccountTypes;
|
|
14
|
+
textInputItemKeys?: string[];
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
@@ -19,6 +20,7 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
|
19
20
|
atlasJSON,
|
|
20
21
|
atlasIMG,
|
|
21
22
|
userAccountType,
|
|
23
|
+
textInputItemKeys = [],
|
|
22
24
|
}) => {
|
|
23
25
|
const [searchQuery, setSearchQuery] = useState('');
|
|
24
26
|
|
|
@@ -27,7 +29,7 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
|
27
29
|
);
|
|
28
30
|
|
|
29
31
|
const renderStoreItem = (item: IStoreItem) => {
|
|
30
|
-
//
|
|
32
|
+
// Prefer a specialized character skin row when needed
|
|
31
33
|
if (item.metadataType === MetadataType.CharacterSkin) {
|
|
32
34
|
return (
|
|
33
35
|
<StoreCharacterSkinRow
|
|
@@ -40,8 +42,21 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
|
40
42
|
/>
|
|
41
43
|
);
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// Render text input row when configured for this item key
|
|
46
|
+
if (textInputItemKeys.includes(item.key) || textInputItemKeys.includes(item._id)) {
|
|
47
|
+
return (
|
|
48
|
+
<StoreItemRow
|
|
49
|
+
key={item._id}
|
|
50
|
+
item={item}
|
|
51
|
+
atlasJSON={atlasJSON}
|
|
52
|
+
atlasIMG={atlasIMG}
|
|
53
|
+
onAddToCart={onAddToCart}
|
|
54
|
+
userAccountType={userAccountType || UserAccountTypes.Free}
|
|
55
|
+
showTextInput
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
// Fallback to standard arrow-based row
|
|
45
60
|
return (
|
|
46
61
|
<StoreItemRow
|
|
47
62
|
key={item._id}
|
|
@@ -246,6 +246,10 @@ export const Default: Story = {
|
|
|
246
246
|
onClose={() => console.log('Store closed')}
|
|
247
247
|
atlasJSON={itemsAtlasJSON}
|
|
248
248
|
atlasIMG={itemsAtlasIMG}
|
|
249
|
+
hidePremiumTab={true}
|
|
250
|
+
tabOrder={['items', 'packs']}
|
|
251
|
+
defaultActiveTab="items"
|
|
252
|
+
textInputItemKeys={['original-greater-life-potion-2', 'original-angelic-sword-1']}
|
|
249
253
|
/>
|
|
250
254
|
),
|
|
251
255
|
};
|