@rpg-engine/long-bow 0.8.218 → 0.8.220
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/MetadataCollector.d.ts +2 -2
- package/dist/components/Store/Store.d.ts +10 -21
- package/dist/components/Store/StoreHeader.d.ts +14 -0
- package/dist/components/Store/hooks/useStoreCart.d.ts +2 -0
- package/dist/components/Store/hooks/useStoreMetadata.d.ts +4 -11
- package/dist/components/Store/hooks/useStoreTabs.d.ts +20 -0
- package/dist/components/Store/internal/packToBlueprint.d.ts +2 -0
- package/dist/components/Store/sections/StoreItemsSection.d.ts +5 -3
- package/dist/hooks/useStoreFiltering.d.ts +7 -4
- package/dist/long-bow.cjs.development.js +346 -375
- 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 +348 -377
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Store/CartView.tsx +7 -2
- package/src/components/Store/MetadataCollector.tsx +60 -40
- package/src/components/Store/Store.tsx +75 -270
- package/src/components/Store/StoreHeader.tsx +74 -0
- package/src/components/Store/__test__/MetadataCollector.spec.tsx +94 -164
- package/src/components/Store/__test__/Store.spec.tsx +4 -0
- package/src/components/Store/__test__/useStoreMetadata.spec.tsx +58 -156
- package/src/components/Store/__test__/useStoreTabs.spec.tsx +69 -0
- package/src/components/Store/hooks/useStoreCart.ts +5 -2
- package/src/components/Store/hooks/useStoreMetadata.ts +30 -48
- package/src/components/Store/hooks/useStoreTabs.ts +104 -0
- package/src/components/Store/internal/packToBlueprint.ts +21 -0
- package/src/components/Store/sections/StoreItemsSection.tsx +19 -60
- package/src/components/Store/sections/StorePacksSection.tsx +0 -1
- package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -6
- package/src/hooks/useStoreFiltering.spec.tsx +79 -0
- package/src/hooks/useStoreFiltering.ts +27 -9
package/package.json
CHANGED
|
@@ -260,14 +260,17 @@ const Container = styled.div`
|
|
|
260
260
|
display: flex;
|
|
261
261
|
flex-direction: column;
|
|
262
262
|
width: 100%;
|
|
263
|
-
|
|
263
|
+
height: 100%;
|
|
264
264
|
padding: 1rem;
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
box-sizing: border-box;
|
|
265
267
|
`;
|
|
266
268
|
|
|
267
269
|
const Header = styled.div`
|
|
268
270
|
display: flex;
|
|
269
271
|
justify-content: space-between;
|
|
270
272
|
align-items: center;
|
|
273
|
+
flex-shrink: 0;
|
|
271
274
|
`;
|
|
272
275
|
|
|
273
276
|
const Title = styled.h2`
|
|
@@ -293,8 +296,10 @@ const CartItems = styled.div`
|
|
|
293
296
|
display: flex;
|
|
294
297
|
flex-direction: column;
|
|
295
298
|
gap: 0.5rem;
|
|
299
|
+
flex: 1;
|
|
300
|
+
min-height: 0;
|
|
296
301
|
overflow-y: auto;
|
|
297
|
-
|
|
302
|
+
margin: 1rem 0;
|
|
298
303
|
padding-right: 0.5rem;
|
|
299
304
|
|
|
300
305
|
&::-webkit-scrollbar {
|
|
@@ -1,48 +1,68 @@
|
|
|
1
|
-
import { MetadataType } from
|
|
2
|
-
import React, { useEffect } from
|
|
3
|
-
import { CharacterSkinSelectionModal } from
|
|
1
|
+
import { MetadataType } from '@rpg-engine/shared';
|
|
2
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { CharacterSkinSelectionModal } from '../Character/CharacterSkinSelectionModal';
|
|
4
4
|
|
|
5
5
|
export interface IMetadataCollectorProps {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
metadataType: MetadataType;
|
|
7
|
+
config: Record<string, any>;
|
|
8
|
+
onCollect: (metadata: Record<string, any>) => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const MetadataCollector: React.FC<IMetadataCollectorProps> = ({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
metadataType,
|
|
14
|
+
config,
|
|
15
|
+
onCollect,
|
|
16
|
+
onCancel,
|
|
17
17
|
}) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return null;
|
|
18
|
+
const isPendingRef = useRef(true);
|
|
19
|
+
|
|
20
|
+
const finalize = useCallback((callback: () => void) => {
|
|
21
|
+
if (!isPendingRef.current) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
isPendingRef.current = false;
|
|
26
|
+
callback();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const handleCollect = useCallback((metadata: Record<string, any>) => {
|
|
30
|
+
finalize(() => onCollect(metadata));
|
|
31
|
+
}, [finalize, onCollect]);
|
|
32
|
+
|
|
33
|
+
const handleCancel = useCallback(() => {
|
|
34
|
+
finalize(onCancel);
|
|
35
|
+
}, [finalize, onCancel]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
return () => {
|
|
39
|
+
handleCancel();
|
|
40
|
+
};
|
|
41
|
+
}, [handleCancel]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (metadataType === MetadataType.CharacterSkin) {
|
|
45
|
+
return undefined;
|
|
47
46
|
}
|
|
47
|
+
|
|
48
|
+
const timer = window.setTimeout(handleCancel, 0);
|
|
49
|
+
return () => window.clearTimeout(timer);
|
|
50
|
+
}, [handleCancel, metadataType]);
|
|
51
|
+
|
|
52
|
+
if (metadataType === MetadataType.CharacterSkin) {
|
|
53
|
+
return (
|
|
54
|
+
<CharacterSkinSelectionModal
|
|
55
|
+
isOpen
|
|
56
|
+
onClose={handleCancel}
|
|
57
|
+
onConfirm={(selectedSkin: any) => handleCollect({ selectedSkin })}
|
|
58
|
+
availableCharacters={config.availableCharacters || []}
|
|
59
|
+
atlasJSON={config.atlasJSON}
|
|
60
|
+
atlasIMG={config.atlasIMG}
|
|
61
|
+
initialSelectedSkin={config.initialSelectedSkin}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.warn(`No collector implemented for metadata type: ${metadataType}`);
|
|
67
|
+
return null;
|
|
48
68
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IItemPack, IPurchase, IProductBlueprint,
|
|
1
|
+
import { IItemPack, IPurchase, IProductBlueprint, UserAccountTypes } from '@rpg-engine/shared';
|
|
2
2
|
import { Crown } from 'pixelarticons/react/Crown';
|
|
3
3
|
import { Gift } from 'pixelarticons/react/Gift';
|
|
4
4
|
import { Package } from 'pixelarticons/react/Package';
|
|
@@ -9,22 +9,20 @@ import styled from 'styled-components';
|
|
|
9
9
|
import { uiColors } from '../../constants/uiColors';
|
|
10
10
|
import { DraggableContainer } from '../DraggableContainer';
|
|
11
11
|
import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
|
|
12
|
-
import { Tabs } from '../shared/Tabs';
|
|
13
|
-
import { LabelPill } from '../shared/LabelPill/LabelPill';
|
|
14
12
|
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
13
|
+
import { LabelPill } from '../shared/LabelPill/LabelPill';
|
|
15
14
|
import { CartView } from './CartView';
|
|
16
15
|
import { FeaturedBanner, IFeaturedItem } from './FeaturedBanner';
|
|
16
|
+
import { packToBlueprint } from './internal/packToBlueprint';
|
|
17
17
|
import { useStoreCart } from './hooks/useStoreCart';
|
|
18
|
+
import { TabId, useStoreTabs } from './hooks/useStoreTabs';
|
|
18
19
|
import { MetadataCollector } from './MetadataCollector';
|
|
20
|
+
import { StoreHeader } from './StoreHeader';
|
|
19
21
|
import { StoreItemsSection } from './sections/StoreItemsSection';
|
|
20
22
|
import { StorePacksSection } from './sections/StorePacksSection';
|
|
21
23
|
import { StoreRedeemSection } from './sections/StoreRedeemSection';
|
|
22
24
|
import { StoreItemDetails } from './StoreItemDetails';
|
|
23
25
|
|
|
24
|
-
// Define TabId union type for tab identifiers
|
|
25
|
-
type TabId = 'premium' | 'packs' | 'items' | 'characters' | 'wallet' | 'history' | 'redeem';
|
|
26
|
-
|
|
27
|
-
// Define IStoreProps locally as a workaround
|
|
28
26
|
export interface IStoreProps {
|
|
29
27
|
items: IProductBlueprint[];
|
|
30
28
|
packs?: IItemPack[];
|
|
@@ -47,47 +45,33 @@ export interface IStoreProps {
|
|
|
47
45
|
customCharactersContent?: React.ReactNode;
|
|
48
46
|
customWalletContent?: React.ReactNode;
|
|
49
47
|
customHistoryContent?: React.ReactNode;
|
|
50
|
-
/** When true the store renders full-screen (useful on mobile). */
|
|
51
48
|
fullScreen?: boolean;
|
|
52
|
-
/** Override the DraggableContainer width (e.g. "90vw"). Defaults to "1000px". */
|
|
53
|
-
containerWidth?: string;
|
|
54
|
-
/** Override the DraggableContainer height (e.g. "80vh"). Defaults to "80vh" so the inner scroll area is bounded. */
|
|
55
|
-
containerHeight?: string;
|
|
56
49
|
packsBadge?: string;
|
|
57
50
|
featuredItems?: IFeaturedItem[];
|
|
58
51
|
onQuickBuy?: (item: IProductBlueprint, quantity?: number) => void;
|
|
59
52
|
itemBadges?: Record<string, { badges?: import('./StoreBadges').IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
|
|
60
53
|
packBadges?: Record<string, { badges?: import('./StoreBadges').IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
|
|
61
|
-
/** Fires when an item row becomes visible (on mount). Useful for store_item_viewed analytics. */
|
|
62
54
|
onItemView?: (item: IProductBlueprint, position: number) => void;
|
|
63
|
-
/** Fires when a pack row becomes visible (on mount). Useful for pack_viewed analytics. */
|
|
64
55
|
onPackView?: (pack: IItemPack, position: number) => void;
|
|
65
|
-
/** Fires when the active store tab changes (e.g. 'items', 'packs', 'premium'). */
|
|
66
56
|
onTabChange?: (tab: string, itemsShown: number) => void;
|
|
67
|
-
/** Fires when the category filter changes in the items tab. */
|
|
68
57
|
onCategoryChange?: (category: string, itemsShown: number) => void;
|
|
69
|
-
/** Fires when the cart is opened. */
|
|
70
58
|
onCartOpen?: () => void;
|
|
71
|
-
/** Fires when any item or pack is added to the cart. */
|
|
72
59
|
onAddToCart?: (item: IProductBlueprint, quantity: number) => void;
|
|
73
|
-
/** Fires when an item is removed from the cart. */
|
|
74
60
|
onRemoveFromCart?: (itemKey: string) => void;
|
|
75
|
-
/** Fires when the user taps "Pay" — before the purchase resolves. */
|
|
76
61
|
onCheckoutStart?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
|
|
77
|
-
/** Fires after a successful purchase. */
|
|
78
62
|
onPurchaseSuccess?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
|
|
79
|
-
/** Fires when a purchase fails. */
|
|
80
63
|
onPurchaseError?: (error: string) => void;
|
|
81
|
-
/** Called when the DC nudge in CartView is tapped — open the DC purchase flow. */
|
|
82
64
|
onBuyDC?: () => void;
|
|
83
|
-
/** Currency symbol to display (e.g. "$" for USD, "R$" for BRL). Defaults to "$". */
|
|
84
65
|
currencySymbol?: string;
|
|
85
|
-
/** Callback to redeem a voucher code. When provided, the Redeem tab is shown. */
|
|
86
66
|
onRedeem?: (code: string) => Promise<{ success: boolean; dcAmount?: number; error?: string }>;
|
|
87
|
-
/** Called when the voucher code input gains focus. */
|
|
88
67
|
onRedeemInputFocus?: () => void;
|
|
89
|
-
/** Called when the voucher code input loses focus. */
|
|
90
68
|
onRedeemInputBlur?: () => void;
|
|
69
|
+
/** Override the modal width. Defaults to '1000px'. */
|
|
70
|
+
width?: string;
|
|
71
|
+
/** Override the modal height. Defaults to 'min(85vh, 900px)'. */
|
|
72
|
+
height?: string;
|
|
73
|
+
/** Override the item category filter pills. Auto-derived from item.itemType when omitted. */
|
|
74
|
+
itemCategoryOptions?: Array<{ value: string; label: string }>;
|
|
91
75
|
}
|
|
92
76
|
|
|
93
77
|
export type { IFeaturedItem };
|
|
@@ -114,8 +98,6 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
114
98
|
customWalletContent,
|
|
115
99
|
customHistoryContent,
|
|
116
100
|
fullScreen = false,
|
|
117
|
-
containerWidth,
|
|
118
|
-
containerHeight,
|
|
119
101
|
packsTabLabel = 'Packs',
|
|
120
102
|
packsBadge,
|
|
121
103
|
featuredItems,
|
|
@@ -137,23 +119,12 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
137
119
|
onRedeem,
|
|
138
120
|
onRedeemInputFocus,
|
|
139
121
|
onRedeemInputBlur,
|
|
122
|
+
width = '1000px',
|
|
123
|
+
height = 'min(85vh, 900px)',
|
|
124
|
+
itemCategoryOptions,
|
|
140
125
|
}) => {
|
|
141
|
-
const defaultTabOrder: TabId[] = ['premium', 'packs', 'items'];
|
|
142
126
|
const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
|
|
143
|
-
|
|
144
|
-
const allTabIds: TabId[] = [
|
|
145
|
-
...(tabOrder ?? defaultTabOrder),
|
|
146
|
-
...(customCharactersContent ? ['characters' as TabId] : []),
|
|
147
|
-
...(onRedeem ? ['redeem' as TabId] : []),
|
|
148
|
-
...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
|
|
149
|
-
...((onShowHistory || customHistoryContent) ? ['history' as TabId] : []),
|
|
150
|
-
];
|
|
151
|
-
const validTabs = Array.from(new Set(allTabIds.filter(id => !(hidePremiumTab && id === 'premium'))));
|
|
152
|
-
if (defaultActiveTab && validTabs.includes(defaultActiveTab)) {
|
|
153
|
-
return defaultActiveTab;
|
|
154
|
-
}
|
|
155
|
-
return validTabs[0] ?? (hidePremiumTab ? 'items' : 'premium');
|
|
156
|
-
});
|
|
127
|
+
|
|
157
128
|
const {
|
|
158
129
|
cartItems,
|
|
159
130
|
handleAddToCart,
|
|
@@ -164,9 +135,32 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
164
135
|
getTotalItems,
|
|
165
136
|
getTotalPrice,
|
|
166
137
|
isCartOpen,
|
|
138
|
+
isCollectingMetadata,
|
|
139
|
+
currentMetadataItem,
|
|
140
|
+
resolveMetadata,
|
|
167
141
|
} = useStoreCart();
|
|
168
|
-
|
|
169
|
-
const
|
|
142
|
+
|
|
143
|
+
const filteredItems = useMemo(() => ({
|
|
144
|
+
items: items,
|
|
145
|
+
premium: items.filter(item => (item.requiredAccountType?.length ?? 0) > 0),
|
|
146
|
+
}), [items]);
|
|
147
|
+
|
|
148
|
+
const { availableTabIds, activeTab, handleTabChange } = useStoreTabs({
|
|
149
|
+
tabOrder,
|
|
150
|
+
defaultActiveTab,
|
|
151
|
+
hidePremiumTab,
|
|
152
|
+
hasCharacters: !!customCharactersContent,
|
|
153
|
+
hasRedeem: !!onRedeem,
|
|
154
|
+
hasWallet: !!(onShowWallet || customWalletContent),
|
|
155
|
+
hasHistory: !!(onShowHistory || customHistoryContent),
|
|
156
|
+
onTabChange: onTabChange as ((tab: TabId, itemsShown: number) => void) | undefined,
|
|
157
|
+
getItemCount: (tab) => {
|
|
158
|
+
if (tab === 'items') return filteredItems.items.length;
|
|
159
|
+
if (tab === 'premium') return filteredItems.premium.length;
|
|
160
|
+
if (tab === 'packs') return packs.length;
|
|
161
|
+
return 0;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
170
164
|
|
|
171
165
|
const handleOpenCart = () => {
|
|
172
166
|
openCart();
|
|
@@ -184,87 +178,14 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
184
178
|
};
|
|
185
179
|
|
|
186
180
|
const handleAddPackToCart = (pack: IItemPack, quantity: number = 1) => {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
description: pack.description || '',
|
|
191
|
-
price: pack.priceUSD,
|
|
192
|
-
currency: PaymentCurrency.USD,
|
|
193
|
-
texturePath: pack.image.default || pack.image.src,
|
|
194
|
-
type: PurchaseType.Pack,
|
|
195
|
-
onPurchase: async () => {},
|
|
196
|
-
itemType: ItemType.Consumable,
|
|
197
|
-
itemSubType: ItemSubType.Other,
|
|
198
|
-
rarity: ItemRarities.Common,
|
|
199
|
-
weight: 0,
|
|
200
|
-
isStackable: false,
|
|
201
|
-
maxStackSize: 1,
|
|
202
|
-
isUsable: false,
|
|
203
|
-
};
|
|
204
|
-
handleAddToCart(packItem, quantity);
|
|
205
|
-
onAddToCart?.(packItem, quantity);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const filterItems = (
|
|
209
|
-
itemsToFilter: IProductBlueprint[],
|
|
210
|
-
type: 'items' | 'premium'
|
|
211
|
-
): IProductBlueprint[] => {
|
|
212
|
-
return itemsToFilter.filter(item => {
|
|
213
|
-
if (type === 'premium') {
|
|
214
|
-
return item.requiredAccountType?.length ?? 0 > 0;
|
|
215
|
-
}
|
|
216
|
-
return true;
|
|
217
|
-
});
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const filteredItems = useMemo(
|
|
221
|
-
() => ({
|
|
222
|
-
items: filterItems(items, 'items'),
|
|
223
|
-
premium: filterItems(items, 'premium'),
|
|
224
|
-
}),
|
|
225
|
-
[items]
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
const handleMetadataCollected = (metadata: Record<string, any>) => {
|
|
229
|
-
if (currentMetadataItem && window.__metadataResolvers) {
|
|
230
|
-
// Resolve the promise in the useStoreMetadata hook
|
|
231
|
-
window.__metadataResolvers.resolve(metadata);
|
|
232
|
-
|
|
233
|
-
// Reset the metadata collection state
|
|
234
|
-
setCurrentMetadataItem(null);
|
|
235
|
-
// Removed unused setPendingMetadataQuantity call
|
|
236
|
-
}
|
|
181
|
+
const bp = packToBlueprint(pack);
|
|
182
|
+
handleAddToCart(bp, quantity);
|
|
183
|
+
onAddToCart?.(bp, quantity);
|
|
237
184
|
};
|
|
238
185
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
window.__metadataResolvers.resolve(null);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Reset the metadata collection state
|
|
246
|
-
setCurrentMetadataItem(null);
|
|
247
|
-
// Removed unused setPendingMetadataQuantity call
|
|
248
|
-
setIsCollectingMetadata(false);
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
if (loading) {
|
|
252
|
-
return <LoadingMessage>Loading...</LoadingMessage>;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (error) {
|
|
256
|
-
return <ErrorMessage>{error}</ErrorMessage>;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Build tabs dynamically based on props
|
|
260
|
-
const tabIds: TabId[] = [
|
|
261
|
-
...(tabOrder ?? defaultTabOrder),
|
|
262
|
-
...(customCharactersContent ? ['characters' as TabId] : []),
|
|
263
|
-
...(onRedeem ? ['redeem' as TabId] : []),
|
|
264
|
-
...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
|
|
265
|
-
...((onShowHistory || customHistoryContent) ? ['history' as TabId] : [])
|
|
266
|
-
];
|
|
267
|
-
const availableTabIds: TabId[] = Array.from(new Set(tabIds.filter(id => !(hidePremiumTab && id === 'premium'))));
|
|
186
|
+
const makePackQuickBuy = onQuickBuy
|
|
187
|
+
? (pack: IItemPack, qty?: number) => onQuickBuy(packToBlueprint(pack), qty)
|
|
188
|
+
: undefined;
|
|
268
189
|
|
|
269
190
|
const tabsMap: Record<string, { id: TabId; title: ReactNode; icon: ReactNode; content: ReactNode }> = {
|
|
270
191
|
premium: {
|
|
@@ -275,26 +196,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
275
196
|
<StorePacksSection
|
|
276
197
|
packs={packs.filter(pack => pack.priceUSD >= 9.99)}
|
|
277
198
|
onAddToCart={handleAddPackToCart}
|
|
278
|
-
onQuickBuy={
|
|
279
|
-
const bp: IProductBlueprint = {
|
|
280
|
-
key: pack.key,
|
|
281
|
-
name: pack.title,
|
|
282
|
-
description: pack.description || '',
|
|
283
|
-
price: pack.priceUSD,
|
|
284
|
-
currency: PaymentCurrency.USD,
|
|
285
|
-
texturePath: pack.image.default || pack.image.src,
|
|
286
|
-
type: PurchaseType.Pack,
|
|
287
|
-
onPurchase: async () => {},
|
|
288
|
-
itemType: ItemType.Consumable,
|
|
289
|
-
itemSubType: ItemSubType.Other,
|
|
290
|
-
rarity: ItemRarities.Common,
|
|
291
|
-
weight: 0,
|
|
292
|
-
isStackable: false,
|
|
293
|
-
maxStackSize: 1,
|
|
294
|
-
isUsable: false,
|
|
295
|
-
};
|
|
296
|
-
onQuickBuy(bp, qty);
|
|
297
|
-
} : undefined}
|
|
199
|
+
onQuickBuy={makePackQuickBuy}
|
|
298
200
|
onSelectPack={setSelectedPack}
|
|
299
201
|
atlasJSON={atlasJSON}
|
|
300
202
|
atlasIMG={atlasIMG}
|
|
@@ -317,26 +219,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
317
219
|
<StorePacksSection
|
|
318
220
|
packs={hidePremiumTab ? packs : packs.filter(pack => pack.priceUSD < 9.99)}
|
|
319
221
|
onAddToCart={handleAddPackToCart}
|
|
320
|
-
onQuickBuy={
|
|
321
|
-
const bp: IProductBlueprint = {
|
|
322
|
-
key: pack.key,
|
|
323
|
-
name: pack.title,
|
|
324
|
-
description: pack.description || '',
|
|
325
|
-
price: pack.priceUSD,
|
|
326
|
-
currency: PaymentCurrency.USD,
|
|
327
|
-
texturePath: pack.image.default || pack.image.src,
|
|
328
|
-
type: PurchaseType.Pack,
|
|
329
|
-
onPurchase: async () => {},
|
|
330
|
-
itemType: ItemType.Consumable,
|
|
331
|
-
itemSubType: ItemSubType.Other,
|
|
332
|
-
rarity: ItemRarities.Common,
|
|
333
|
-
weight: 0,
|
|
334
|
-
isStackable: false,
|
|
335
|
-
maxStackSize: 1,
|
|
336
|
-
isUsable: false,
|
|
337
|
-
};
|
|
338
|
-
onQuickBuy(bp, qty);
|
|
339
|
-
} : undefined}
|
|
222
|
+
onQuickBuy={makePackQuickBuy}
|
|
340
223
|
onSelectPack={setSelectedPack}
|
|
341
224
|
atlasJSON={atlasJSON}
|
|
342
225
|
atlasIMG={atlasIMG}
|
|
@@ -354,7 +237,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
354
237
|
<StoreItemsSection
|
|
355
238
|
items={filteredItems.items}
|
|
356
239
|
onAddToCart={handleAddToCartTracked}
|
|
357
|
-
onQuickBuy={onQuickBuy ? (item, qty
|
|
240
|
+
onQuickBuy={onQuickBuy ? (item, qty) => onQuickBuy(item, qty) : undefined}
|
|
358
241
|
atlasJSON={atlasJSON}
|
|
359
242
|
atlasIMG={atlasIMG}
|
|
360
243
|
userAccountType={userAccountType}
|
|
@@ -363,6 +246,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
363
246
|
onItemView={onItemView}
|
|
364
247
|
onCategoryChange={onCategoryChange}
|
|
365
248
|
currencySymbol={currencySymbol}
|
|
249
|
+
categoryOptions={itemCategoryOptions}
|
|
366
250
|
/>
|
|
367
251
|
),
|
|
368
252
|
},
|
|
@@ -406,33 +290,32 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
406
290
|
},
|
|
407
291
|
};
|
|
408
292
|
|
|
293
|
+
if (loading) return <LoadingMessage>Loading...</LoadingMessage>;
|
|
294
|
+
if (error) return <ErrorMessage>{error}</ErrorMessage>;
|
|
295
|
+
|
|
409
296
|
return (
|
|
410
297
|
<DraggableContainer
|
|
411
|
-
title="Store"
|
|
412
298
|
onCloseButton={onClose}
|
|
413
|
-
width={
|
|
299
|
+
width={width}
|
|
414
300
|
minWidth="700px"
|
|
415
|
-
height={
|
|
301
|
+
height={height}
|
|
416
302
|
type={RPGUIContainerTypes.Framed}
|
|
417
|
-
cancelDrag="
|
|
303
|
+
cancelDrag=".store-scroll-area, [class*='CartView'], [class*='StoreItemDetails'], .close-button"
|
|
418
304
|
isFullScreen={fullScreen}
|
|
419
305
|
>
|
|
420
|
-
{isCollectingMetadata && currentMetadataItem
|
|
306
|
+
{isCollectingMetadata && currentMetadataItem?.metadataType ? (
|
|
421
307
|
<MetadataCollector
|
|
422
308
|
metadataType={currentMetadataItem.metadataType}
|
|
423
309
|
config={currentMetadataItem.metadataConfig || {}}
|
|
424
|
-
onCollect={
|
|
425
|
-
onCancel={
|
|
310
|
+
onCollect={resolveMetadata}
|
|
311
|
+
onCancel={() => resolveMetadata(null)}
|
|
426
312
|
/>
|
|
427
313
|
) : isCartOpen ? (
|
|
428
314
|
<CartView
|
|
429
315
|
cartItems={cartItems}
|
|
430
316
|
onRemoveFromCart={handleRemoveFromCartTracked}
|
|
431
317
|
onClose={closeCart}
|
|
432
|
-
onPurchase={async () => {
|
|
433
|
-
handleCartPurchase(onPurchase);
|
|
434
|
-
return true;
|
|
435
|
-
}}
|
|
318
|
+
onPurchase={async () => { handleCartPurchase(onPurchase); return true; }}
|
|
436
319
|
atlasJSON={atlasJSON}
|
|
437
320
|
atlasIMG={atlasIMG}
|
|
438
321
|
onCheckoutStart={onCheckoutStart}
|
|
@@ -454,7 +337,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
454
337
|
currencySymbol={currencySymbol}
|
|
455
338
|
/>
|
|
456
339
|
) : (
|
|
457
|
-
<Container>
|
|
340
|
+
<Container className="store-scroll-area">
|
|
458
341
|
{featuredItems && featuredItems.length > 0 && (
|
|
459
342
|
<FeaturedBanner
|
|
460
343
|
items={featuredItems}
|
|
@@ -466,62 +349,23 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
466
349
|
}}
|
|
467
350
|
onQuickBuy={onQuickBuy ? item => {
|
|
468
351
|
const blueprint = items.find(bp => bp.key === item.key);
|
|
469
|
-
if (blueprint)
|
|
470
|
-
|
|
352
|
+
if (blueprint) {
|
|
353
|
+
onQuickBuy(blueprint, 1);
|
|
354
|
+
} else {
|
|
471
355
|
const pack = packs.find(p => p.key === item.key);
|
|
472
|
-
if (pack)
|
|
473
|
-
const packBlueprint: IProductBlueprint = {
|
|
474
|
-
key: pack.key,
|
|
475
|
-
name: pack.title,
|
|
476
|
-
description: pack.description || '',
|
|
477
|
-
price: pack.priceUSD,
|
|
478
|
-
currency: PaymentCurrency.USD,
|
|
479
|
-
texturePath: pack.image.default || pack.image.src,
|
|
480
|
-
type: PurchaseType.Pack,
|
|
481
|
-
onPurchase: async () => {},
|
|
482
|
-
itemType: ItemType.Consumable,
|
|
483
|
-
itemSubType: ItemSubType.Other,
|
|
484
|
-
rarity: ItemRarities.Common,
|
|
485
|
-
weight: 0,
|
|
486
|
-
isStackable: false,
|
|
487
|
-
maxStackSize: 1,
|
|
488
|
-
isUsable: false,
|
|
489
|
-
};
|
|
490
|
-
onQuickBuy(packBlueprint, 1);
|
|
491
|
-
}
|
|
356
|
+
if (pack) onQuickBuy(packToBlueprint(pack), 1);
|
|
492
357
|
}
|
|
493
358
|
} : undefined}
|
|
494
359
|
/>
|
|
495
360
|
)}
|
|
496
361
|
<MainContent>
|
|
497
|
-
<
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
setActiveTab(nextTab);
|
|
505
|
-
if (onTabChange) {
|
|
506
|
-
const itemCount = nextTab === 'items' ? filteredItems.items.length
|
|
507
|
-
: nextTab === 'premium' ? filteredItems.premium.length
|
|
508
|
-
: nextTab === 'packs' ? packs.length
|
|
509
|
-
: 0;
|
|
510
|
-
onTabChange(nextTab, itemCount);
|
|
511
|
-
}
|
|
512
|
-
}}
|
|
513
|
-
/>
|
|
514
|
-
</TabsFlexWrapper>
|
|
515
|
-
<CartButtonWrapper>
|
|
516
|
-
<CTAButton
|
|
517
|
-
icon={<FaShoppingCart />}
|
|
518
|
-
onClick={handleOpenCart}
|
|
519
|
-
/>
|
|
520
|
-
{getTotalItems() > 0 && (
|
|
521
|
-
<CartBadge>{getTotalItems()}</CartBadge>
|
|
522
|
-
)}
|
|
523
|
-
</CartButtonWrapper>
|
|
524
|
-
</HeaderRow>
|
|
362
|
+
<StoreHeader
|
|
363
|
+
tabs={availableTabIds.map(id => ({ id, label: tabsMap[id]?.title, icon: tabsMap[id]?.icon }))}
|
|
364
|
+
activeTabId={activeTab}
|
|
365
|
+
onTabChange={handleTabChange}
|
|
366
|
+
cartItemCount={getTotalItems()}
|
|
367
|
+
onOpenCart={handleOpenCart}
|
|
368
|
+
/>
|
|
525
369
|
<TabContent>
|
|
526
370
|
{tabsMap[activeTab]?.content}
|
|
527
371
|
</TabContent>
|
|
@@ -530,7 +374,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
530
374
|
<Footer>
|
|
531
375
|
<CTAButton
|
|
532
376
|
icon={<FaShoppingCart />}
|
|
533
|
-
label={`
|
|
377
|
+
label={`Proceed to Checkout (${currencySymbol}${getTotalPrice().toFixed(2)})`}
|
|
534
378
|
onClick={handleOpenCart}
|
|
535
379
|
fullWidth
|
|
536
380
|
/>
|
|
@@ -546,50 +390,12 @@ const Container = styled.div`
|
|
|
546
390
|
display: flex;
|
|
547
391
|
flex-direction: column;
|
|
548
392
|
width: 100%;
|
|
549
|
-
height:
|
|
393
|
+
height: 100%;
|
|
550
394
|
gap: 0.5rem;
|
|
551
395
|
position: relative;
|
|
552
396
|
overflow: hidden;
|
|
553
397
|
`;
|
|
554
398
|
|
|
555
|
-
const HeaderRow = styled.div`
|
|
556
|
-
display: flex;
|
|
557
|
-
align-items: flex-end;
|
|
558
|
-
justify-content: space-between;
|
|
559
|
-
margin-bottom: 0.25rem;
|
|
560
|
-
padding-top: 10px;
|
|
561
|
-
padding-right: 12px;
|
|
562
|
-
`;
|
|
563
|
-
|
|
564
|
-
const TabsFlexWrapper = styled.div`
|
|
565
|
-
flex: 1;
|
|
566
|
-
min-width: 0;
|
|
567
|
-
`;
|
|
568
|
-
|
|
569
|
-
const CartButtonWrapper = styled.div`
|
|
570
|
-
position: relative;
|
|
571
|
-
flex-shrink: 0;
|
|
572
|
-
`;
|
|
573
|
-
|
|
574
|
-
const CartBadge = styled.div`
|
|
575
|
-
position: absolute;
|
|
576
|
-
top: -8px;
|
|
577
|
-
right: -8px;
|
|
578
|
-
background: #ef4444; /* red */
|
|
579
|
-
color: white;
|
|
580
|
-
font-family: 'Press Start 2P', cursive;
|
|
581
|
-
font-size: 0.5rem;
|
|
582
|
-
padding: 4px;
|
|
583
|
-
border-radius: 50%;
|
|
584
|
-
border: 2px solid #000;
|
|
585
|
-
display: flex;
|
|
586
|
-
align-items: center;
|
|
587
|
-
justify-content: center;
|
|
588
|
-
min-width: 16px;
|
|
589
|
-
min-height: 16px;
|
|
590
|
-
box-sizing: content-box;
|
|
591
|
-
`;
|
|
592
|
-
|
|
593
399
|
const MainContent = styled.div`
|
|
594
400
|
flex: 1;
|
|
595
401
|
display: flex;
|
|
@@ -604,7 +410,7 @@ const TabContent = styled.div`
|
|
|
604
410
|
`;
|
|
605
411
|
|
|
606
412
|
const Footer = styled.div`
|
|
607
|
-
padding:
|
|
413
|
+
padding: 1rem;
|
|
608
414
|
border-top: 2px solid #f59e0b;
|
|
609
415
|
background: rgba(0, 0, 0, 0.2);
|
|
610
416
|
flex-shrink: 0;
|
|
@@ -616,7 +422,6 @@ const TabLabelWithBadge = styled.span`
|
|
|
616
422
|
gap: 5px;
|
|
617
423
|
`;
|
|
618
424
|
|
|
619
|
-
|
|
620
425
|
const LoadingMessage = styled.div`
|
|
621
426
|
text-align: center;
|
|
622
427
|
color: ${uiColors.white};
|