@rpg-engine/long-bow 0.8.69 → 0.8.70

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.69",
3
+ "version": "0.8.70",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -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,9 @@ export interface IStoreProps {
26
29
  loading?: boolean;
27
30
  error?: string;
28
31
  onClose?: () => void;
32
+ hidePremiumTab?: boolean;
33
+ tabOrder?: TabId[];
34
+ defaultActiveTab?: TabId;
29
35
  }
30
36
 
31
37
  export const Store: React.FC<IStoreProps> = ({
@@ -39,9 +45,18 @@ export const Store: React.FC<IStoreProps> = ({
39
45
  loading = false,
40
46
  error,
41
47
  onClose,
48
+ hidePremiumTab = false,
49
+ tabOrder,
50
+ defaultActiveTab,
42
51
  }) => {
43
52
  const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
44
- const [activeTab, setActiveTab] = useState('premium');
53
+ const [activeTab, setActiveTab] = useState<TabId>(() => {
54
+ const initialTabs = (tabOrder ?? ['premium', 'packs', 'items']).filter(id => !(hidePremiumTab && id === 'premium'));
55
+ if (defaultActiveTab && initialTabs.includes(defaultActiveTab)) {
56
+ return defaultActiveTab;
57
+ }
58
+ return hidePremiumTab ? 'items' : 'premium';
59
+ });
45
60
  const {
46
61
  cartItems,
47
62
  handleAddToCart,
@@ -136,8 +151,12 @@ export const Store: React.FC<IStoreProps> = ({
136
151
  return <ErrorMessage>{error}</ErrorMessage>;
137
152
  }
138
153
 
139
- const tabs = [
140
- {
154
+ // Build tabs dynamically based on props
155
+ const tabIds: TabId[] = tabOrder ?? ['premium', 'packs', 'items'];
156
+ const availableTabIds: TabId[] = tabIds.filter(id => !(hidePremiumTab && id === 'premium'));
157
+
158
+ const tabsMap: Record<TabId, { id: TabId; title: string; content: ReactNode }> = {
159
+ premium: {
141
160
  id: 'premium',
142
161
  title: 'Premium',
143
162
  content: (
@@ -148,7 +167,7 @@ export const Store: React.FC<IStoreProps> = ({
148
167
  />
149
168
  ),
150
169
  },
151
- {
170
+ packs: {
152
171
  id: 'packs',
153
172
  title: 'Packs',
154
173
  content: (
@@ -159,7 +178,7 @@ export const Store: React.FC<IStoreProps> = ({
159
178
  />
160
179
  ),
161
180
  },
162
- {
181
+ items: {
163
182
  id: 'items',
164
183
  title: 'Items',
165
184
  content: (
@@ -172,7 +191,9 @@ export const Store: React.FC<IStoreProps> = ({
172
191
  />
173
192
  ),
174
193
  },
175
- ];
194
+ };
195
+
196
+ const tabs = availableTabIds.map(id => tabsMap[id]);
176
197
 
177
198
  return (
178
199
  <DraggableContainer
@@ -243,7 +264,7 @@ export const Store: React.FC<IStoreProps> = ({
243
264
  borderColor="#f59e0b"
244
265
  hoverColor="#fef3c7"
245
266
  activeTab={activeTab}
246
- onTabChange={setActiveTab}
267
+ onTabChange={(tabId: string) => setActiveTab(tabId as TabId)}
247
268
  />
248
269
  </MainContent>
249
270
  {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
- // If we have character skins, add the selected skin to the purchase
60
+ // Always use a quantity of 1
81
61
  if (availableCharacters.length > 0 && currentCharacter) {
82
- onAddToCart(item, quantity, {
62
+ onAddToCart(item, 1, {
83
63
  selectedSkinName: currentCharacter.name,
84
64
  selectedSkinTextureKey: currentCharacter.textureKey
85
65
  });
86
66
  } else {
87
- onAddToCart(item, quantity);
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
- {availableCharacters.length > 0 && currentCharacter && entityAtlasJSON && entityAtlasIMG ? (
103
- <CharacterSkinPreviewContainer>
104
- <NavArrow
105
- direction="left"
106
- onPointerDown={handlePreviousSkin}
107
- size={24}
108
- />
109
-
110
- <SpriteContainer>
111
- <ErrorBoundary>
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
- <ItemName>{item.name}</ItemName>
104
+ <Header>
105
+ <ItemName>{item.name}</ItemName>
106
+ </Header>
145
107
  {availableCharacters.length > 0 && currentCharacter && (
146
- <SelectedSkin>Selected: {currentCharacter.name}</SelectedSkin>
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
- min-width: 140px;
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
- const ArrowsContainer = styled.div`
263
- position: relative;
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
- width: 120px;
267
- height: 42px;
268
- justify-content: space-between;
191
+ gap: 0.5rem;
269
192
  `;
270
193
 
271
- const QuantityInput = styled.input`
272
- width: 40px;
273
- text-align: center;
274
- margin: 0 auto;
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
  `;
@@ -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 (window.__metadataResolvers) {
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 === MetadataType.None) {
23
+ if (!item.metadataType || item.metadataType === 'None') {
24
24
  return null;
25
25
  }
26
26
 
27
27
  // Setup for valid metadata types
28
- window.__metadataResolvers = {
28
+ globalThis.__metadataResolvers = {
29
29
  resolve: (metadata) => {
30
30
  // Clean up
31
- window.__metadataResolvers = undefined;
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
- window.__metadataResolvers = undefined; // Make sure it's cleaned up
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 (window.__metadataResolvers) {
143
- const originalResolve = window.__metadataResolvers.resolve;
144
- window.__metadataResolvers.resolve = function(metadata) {
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 (window.__metadataResolvers) {
153
- window.__metadataResolvers.resolve(mockMetadata);
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(window.__metadataResolvers).toBeUndefined(); // Should be cleaned up
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 () => {
@@ -246,6 +246,9 @@ 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"
249
252
  />
250
253
  ),
251
254
  };