@rpg-engine/long-bow 0.8.219 → 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.
Files changed (35) hide show
  1. package/dist/components/DraggableContainer.d.ts +0 -6
  2. package/dist/components/Store/MetadataCollector.d.ts +2 -2
  3. package/dist/components/Store/Store.d.ts +10 -27
  4. package/dist/components/Store/StoreHeader.d.ts +14 -0
  5. package/dist/components/Store/hooks/useStoreCart.d.ts +2 -0
  6. package/dist/components/Store/hooks/useStoreMetadata.d.ts +4 -11
  7. package/dist/components/Store/hooks/useStoreTabs.d.ts +20 -0
  8. package/dist/components/Store/internal/packToBlueprint.d.ts +2 -0
  9. package/dist/components/Store/sections/StoreItemsSection.d.ts +5 -3
  10. package/dist/hooks/useStoreFiltering.d.ts +7 -4
  11. package/dist/long-bow.cjs.development.js +349 -396
  12. package/dist/long-bow.cjs.development.js.map +1 -1
  13. package/dist/long-bow.cjs.production.min.js +1 -1
  14. package/dist/long-bow.cjs.production.min.js.map +1 -1
  15. package/dist/long-bow.esm.js +351 -398
  16. package/dist/long-bow.esm.js.map +1 -1
  17. package/package.json +1 -1
  18. package/src/components/DraggableContainer.tsx +0 -24
  19. package/src/components/Store/CartView.tsx +7 -2
  20. package/src/components/Store/MetadataCollector.tsx +60 -40
  21. package/src/components/Store/Store.tsx +75 -282
  22. package/src/components/Store/StoreHeader.tsx +74 -0
  23. package/src/components/Store/__test__/MetadataCollector.spec.tsx +94 -164
  24. package/src/components/Store/__test__/Store.spec.tsx +4 -0
  25. package/src/components/Store/__test__/useStoreMetadata.spec.tsx +58 -156
  26. package/src/components/Store/__test__/useStoreTabs.spec.tsx +69 -0
  27. package/src/components/Store/hooks/useStoreCart.ts +5 -2
  28. package/src/components/Store/hooks/useStoreMetadata.ts +30 -48
  29. package/src/components/Store/hooks/useStoreTabs.ts +104 -0
  30. package/src/components/Store/internal/packToBlueprint.ts +21 -0
  31. package/src/components/Store/sections/StoreItemsSection.tsx +19 -60
  32. package/src/components/Store/sections/StorePacksSection.tsx +0 -1
  33. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -6
  34. package/src/hooks/useStoreFiltering.spec.tsx +79 -0
  35. 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,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 "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
64
  onBuyDC?: () => void;
89
- /** Currency symbol to display (e.g. "$" for USD, "R$" for BRL). Defaults to "$". */
90
65
  currencySymbol?: string;
91
- /** Callback to redeem a voucher code. When provided, the Redeem tab is shown. */
92
66
  onRedeem?: (code: string) => Promise<{ success: boolean; dcAmount?: number; error?: string }>;
93
- /** Called when the voucher code input gains focus. */
94
67
  onRedeemInputFocus?: () => void;
95
- /** Called when the voucher code input loses focus. */
96
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 }>;
97
75
  }
98
76
 
99
77
  export type { IFeaturedItem };
@@ -120,11 +98,6 @@ export const Store: React.FC<IStoreProps> = ({
120
98
  customWalletContent,
121
99
  customHistoryContent,
122
100
  fullScreen = false,
123
- containerWidth,
124
- containerHeight,
125
- mobileContainerWidth,
126
- mobileContainerHeight,
127
- mobileBreakpoint,
128
101
  packsTabLabel = 'Packs',
129
102
  packsBadge,
130
103
  featuredItems,
@@ -146,23 +119,12 @@ export const Store: React.FC<IStoreProps> = ({
146
119
  onRedeem,
147
120
  onRedeemInputFocus,
148
121
  onRedeemInputBlur,
122
+ width = '1000px',
123
+ height = 'min(85vh, 900px)',
124
+ itemCategoryOptions,
149
125
  }) => {
150
- const defaultTabOrder: TabId[] = ['premium', 'packs', 'items'];
151
126
  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
- });
127
+
166
128
  const {
167
129
  cartItems,
168
130
  handleAddToCart,
@@ -173,9 +135,32 @@ export const Store: React.FC<IStoreProps> = ({
173
135
  getTotalItems,
174
136
  getTotalPrice,
175
137
  isCartOpen,
138
+ isCollectingMetadata,
139
+ currentMetadataItem,
140
+ resolveMetadata,
176
141
  } = useStoreCart();
177
- const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
178
- const [currentMetadataItem, setCurrentMetadataItem] = useState<IProductBlueprint | null>(null);
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
+ });
179
164
 
180
165
  const handleOpenCart = () => {
181
166
  openCart();
@@ -193,87 +178,14 @@ export const Store: React.FC<IStoreProps> = ({
193
178
  };
194
179
 
195
180
  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
- }
181
+ const bp = packToBlueprint(pack);
182
+ handleAddToCart(bp, quantity);
183
+ onAddToCart?.(bp, quantity);
246
184
  };
247
185
 
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'))));
186
+ const makePackQuickBuy = onQuickBuy
187
+ ? (pack: IItemPack, qty?: number) => onQuickBuy(packToBlueprint(pack), qty)
188
+ : undefined;
277
189
 
278
190
  const tabsMap: Record<string, { id: TabId; title: ReactNode; icon: ReactNode; content: ReactNode }> = {
279
191
  premium: {
@@ -284,26 +196,7 @@ export const Store: React.FC<IStoreProps> = ({
284
196
  <StorePacksSection
285
197
  packs={packs.filter(pack => pack.priceUSD >= 9.99)}
286
198
  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}
199
+ onQuickBuy={makePackQuickBuy}
307
200
  onSelectPack={setSelectedPack}
308
201
  atlasJSON={atlasJSON}
309
202
  atlasIMG={atlasIMG}
@@ -326,26 +219,7 @@ export const Store: React.FC<IStoreProps> = ({
326
219
  <StorePacksSection
327
220
  packs={hidePremiumTab ? packs : packs.filter(pack => pack.priceUSD < 9.99)}
328
221
  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}
222
+ onQuickBuy={makePackQuickBuy}
349
223
  onSelectPack={setSelectedPack}
350
224
  atlasJSON={atlasJSON}
351
225
  atlasIMG={atlasIMG}
@@ -363,7 +237,7 @@ export const Store: React.FC<IStoreProps> = ({
363
237
  <StoreItemsSection
364
238
  items={filteredItems.items}
365
239
  onAddToCart={handleAddToCartTracked}
366
- onQuickBuy={onQuickBuy ? (item, qty, _meta) => onQuickBuy(item, qty) : undefined}
240
+ onQuickBuy={onQuickBuy ? (item, qty) => onQuickBuy(item, qty) : undefined}
367
241
  atlasJSON={atlasJSON}
368
242
  atlasIMG={atlasIMG}
369
243
  userAccountType={userAccountType}
@@ -372,6 +246,7 @@ export const Store: React.FC<IStoreProps> = ({
372
246
  onItemView={onItemView}
373
247
  onCategoryChange={onCategoryChange}
374
248
  currencySymbol={currencySymbol}
249
+ categoryOptions={itemCategoryOptions}
375
250
  />
376
251
  ),
377
252
  },
@@ -415,36 +290,32 @@ export const Store: React.FC<IStoreProps> = ({
415
290
  },
416
291
  };
417
292
 
293
+ if (loading) return <LoadingMessage>Loading...</LoadingMessage>;
294
+ if (error) return <ErrorMessage>{error}</ErrorMessage>;
295
+
418
296
  return (
419
297
  <DraggableContainer
420
- title="Store"
421
298
  onCloseButton={onClose}
422
- width={containerWidth ?? "1000px"}
299
+ width={width}
423
300
  minWidth="700px"
424
- height={containerHeight ?? "min(85vh, 900px)"}
425
- mobileWidth={mobileContainerWidth ?? "95vw"}
426
- mobileHeight={mobileContainerHeight ?? "95vh"}
427
- mobileBreakpoint={mobileBreakpoint}
301
+ height={height}
428
302
  type={RPGUIContainerTypes.Framed}
429
- cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails'], .close-button"
303
+ cancelDrag=".store-scroll-area, [class*='CartView'], [class*='StoreItemDetails'], .close-button"
430
304
  isFullScreen={fullScreen}
431
305
  >
432
- {isCollectingMetadata && currentMetadataItem && currentMetadataItem.metadataType ? (
306
+ {isCollectingMetadata && currentMetadataItem?.metadataType ? (
433
307
  <MetadataCollector
434
308
  metadataType={currentMetadataItem.metadataType}
435
309
  config={currentMetadataItem.metadataConfig || {}}
436
- onCollect={handleMetadataCollected}
437
- onCancel={handleMetadataCancel}
310
+ onCollect={resolveMetadata}
311
+ onCancel={() => resolveMetadata(null)}
438
312
  />
439
313
  ) : isCartOpen ? (
440
314
  <CartView
441
315
  cartItems={cartItems}
442
316
  onRemoveFromCart={handleRemoveFromCartTracked}
443
317
  onClose={closeCart}
444
- onPurchase={async () => {
445
- handleCartPurchase(onPurchase);
446
- return true;
447
- }}
318
+ onPurchase={async () => { handleCartPurchase(onPurchase); return true; }}
448
319
  atlasJSON={atlasJSON}
449
320
  atlasIMG={atlasIMG}
450
321
  onCheckoutStart={onCheckoutStart}
@@ -466,7 +337,7 @@ export const Store: React.FC<IStoreProps> = ({
466
337
  currencySymbol={currencySymbol}
467
338
  />
468
339
  ) : (
469
- <Container>
340
+ <Container className="store-scroll-area">
470
341
  {featuredItems && featuredItems.length > 0 && (
471
342
  <FeaturedBanner
472
343
  items={featuredItems}
@@ -478,62 +349,23 @@ export const Store: React.FC<IStoreProps> = ({
478
349
  }}
479
350
  onQuickBuy={onQuickBuy ? item => {
480
351
  const blueprint = items.find(bp => bp.key === item.key);
481
- if (blueprint) onQuickBuy(blueprint, 1);
482
- else {
352
+ if (blueprint) {
353
+ onQuickBuy(blueprint, 1);
354
+ } else {
483
355
  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
- }
356
+ if (pack) onQuickBuy(packToBlueprint(pack), 1);
504
357
  }
505
358
  } : undefined}
506
359
  />
507
360
  )}
508
361
  <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>
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
+ />
537
369
  <TabContent>
538
370
  {tabsMap[activeTab]?.content}
539
371
  </TabContent>
@@ -542,7 +374,7 @@ export const Store: React.FC<IStoreProps> = ({
542
374
  <Footer>
543
375
  <CTAButton
544
376
  icon={<FaShoppingCart />}
545
- label={`Checkout · ${getTotalItems()} item${getTotalItems() !== 1 ? 's' : ''} (${currencySymbol}${getTotalPrice().toFixed(2)})`}
377
+ label={`Proceed to Checkout (${currencySymbol}${getTotalPrice().toFixed(2)})`}
546
378
  onClick={handleOpenCart}
547
379
  fullWidth
548
380
  />
@@ -558,50 +390,12 @@ const Container = styled.div`
558
390
  display: flex;
559
391
  flex-direction: column;
560
392
  width: 100%;
561
- height: calc(100% - 48px);
393
+ height: 100%;
562
394
  gap: 0.5rem;
563
395
  position: relative;
564
396
  overflow: hidden;
565
397
  `;
566
398
 
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
399
  const MainContent = styled.div`
606
400
  flex: 1;
607
401
  display: flex;
@@ -616,7 +410,7 @@ const TabContent = styled.div`
616
410
  `;
617
411
 
618
412
  const Footer = styled.div`
619
- padding: 0.5rem;
413
+ padding: 1rem;
620
414
  border-top: 2px solid #f59e0b;
621
415
  background: rgba(0, 0, 0, 0.2);
622
416
  flex-shrink: 0;
@@ -628,7 +422,6 @@ const TabLabelWithBadge = styled.span`
628
422
  gap: 5px;
629
423
  `;
630
424
 
631
-
632
425
  const LoadingMessage = styled.div`
633
426
  text-align: center;
634
427
  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
+ `;