@rpg-engine/long-bow 0.8.219 → 0.8.221

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