@rpg-engine/long-bow 0.8.170 → 0.8.172

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 (53) hide show
  1. package/dist/components/Store/CartView.d.ts +21 -1
  2. package/dist/components/Store/CountdownTimer.d.ts +7 -0
  3. package/dist/components/Store/FeaturedBanner.d.ts +23 -0
  4. package/dist/components/Store/PurchaseSuccess.d.ts +18 -0
  5. package/dist/components/Store/Store.d.ts +50 -2
  6. package/dist/components/Store/StoreBadges.d.ts +13 -0
  7. package/dist/components/Store/StoreCharacterSkinRow.d.ts +1 -0
  8. package/dist/components/Store/StoreItemRow.d.ts +10 -0
  9. package/dist/components/Store/TrustBar.d.ts +9 -0
  10. package/dist/components/Store/sections/StoreItemsSection.d.ts +13 -0
  11. package/dist/components/Store/sections/StorePacksSection.d.ts +11 -0
  12. package/dist/components/shared/CTAButton/CTAButton.d.ts +1 -0
  13. package/dist/components/shared/CustomScrollbar.d.ts +9 -0
  14. package/dist/index.d.ts +6 -1
  15. package/dist/long-bow.cjs.development.js +1284 -302
  16. package/dist/long-bow.cjs.development.js.map +1 -1
  17. package/dist/long-bow.cjs.production.min.js +1 -1
  18. package/dist/long-bow.cjs.production.min.js.map +1 -1
  19. package/dist/long-bow.esm.js +1281 -304
  20. package/dist/long-bow.esm.js.map +1 -1
  21. package/dist/stories/Features/store/FeaturedBanner.stories.d.ts +1 -0
  22. package/dist/stories/Features/store/PurchaseSuccess.stories.d.ts +1 -0
  23. package/dist/stories/Features/store/StoreBadges.stories.d.ts +1 -0
  24. package/dist/stories/Features/store/TrustBar.stories.d.ts +1 -0
  25. package/package.json +2 -2
  26. package/src/components/Marketplace/BuyPanel.tsx +1 -1
  27. package/src/components/Store/CartView.tsx +143 -13
  28. package/src/components/Store/CountdownTimer.tsx +86 -0
  29. package/src/components/Store/FeaturedBanner.tsx +273 -0
  30. package/src/components/Store/PurchaseSuccess.tsx +258 -0
  31. package/src/components/Store/Store.tsx +236 -50
  32. package/src/components/Store/StoreBadges.tsx +94 -0
  33. package/src/components/Store/StoreCharacterSkinRow.tsx +113 -22
  34. package/src/components/Store/StoreItemRow.tsx +135 -17
  35. package/src/components/Store/TrustBar.tsx +69 -0
  36. package/src/components/Store/__test__/CountdownTimer.spec.tsx +100 -0
  37. package/src/components/Store/__test__/FeaturedBanner.spec.tsx +207 -0
  38. package/src/components/Store/__test__/PurchaseSuccess.spec.tsx +174 -0
  39. package/src/components/Store/__test__/StoreBadges.spec.tsx +133 -0
  40. package/src/components/Store/__test__/TrustBar.spec.tsx +85 -0
  41. package/src/components/Store/sections/StoreItemsSection.tsx +27 -1
  42. package/src/components/Store/sections/StorePacksSection.tsx +92 -28
  43. package/src/components/shared/CTAButton/CTAButton.tsx +25 -1
  44. package/src/components/shared/CustomScrollbar.ts +41 -0
  45. package/src/components/shared/ItemRowWrapper.tsx +26 -12
  46. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -0
  47. package/src/components/shared/SpriteFromAtlas.tsx +4 -1
  48. package/src/index.tsx +6 -1
  49. package/src/stories/Features/store/FeaturedBanner.stories.tsx +121 -0
  50. package/src/stories/Features/store/PurchaseSuccess.stories.tsx +74 -0
  51. package/src/stories/Features/store/Store.stories.tsx +39 -3
  52. package/src/stories/Features/store/StoreBadges.stories.tsx +83 -0
  53. package/src/stories/Features/store/TrustBar.stories.tsx +51 -0
@@ -0,0 +1,258 @@
1
+ import { MetadataType } from '@rpg-engine/shared';
2
+ import React from 'react';
3
+ import { FaShoppingBag, FaStar, FaTimes } from 'react-icons/fa';
4
+ import styled, { keyframes } from 'styled-components';
5
+ import characterAtlasJSON from '../../mocks/atlas/entities/entities.json';
6
+ import characterAtlasIMG from '../../mocks/atlas/entities/entities.png';
7
+ import { CTAButton } from '../shared/CTAButton/CTAButton';
8
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
9
+
10
+ export interface IPurchaseSuccessItem {
11
+ name: string;
12
+ texturePath: string;
13
+ quantity: number;
14
+ metadataType?: MetadataType;
15
+ metadata?: Record<string, any>;
16
+ }
17
+
18
+ export interface IPurchaseSuccessProps {
19
+ items: IPurchaseSuccessItem[];
20
+ totalPrice: number;
21
+ atlasJSON: Record<string, any>;
22
+ atlasIMG: string;
23
+ onContinueShopping: () => void;
24
+ onClose: () => void;
25
+ }
26
+
27
+ export const PurchaseSuccess: React.FC<IPurchaseSuccessProps> = ({
28
+ items,
29
+ totalPrice,
30
+ atlasJSON,
31
+ atlasIMG,
32
+ onContinueShopping,
33
+ onClose,
34
+ }) => {
35
+ return (
36
+ <Container>
37
+ <Sparkles aria-hidden>
38
+ {[...Array(8)].map((_, i) => <Spark key={i} $i={i} />)}
39
+ </Sparkles>
40
+
41
+ <Header>
42
+ <TrophyArea>
43
+ <TrophyIcon>
44
+ <FaStar />
45
+ </TrophyIcon>
46
+ </TrophyArea>
47
+ <Title>PURCHASE COMPLETE!</Title>
48
+ <Subtitle>Your items are ready</Subtitle>
49
+ </Header>
50
+
51
+ <ItemsList>
52
+ {items.map((item, i) => {
53
+ const isCharSkin = item.metadataType === MetadataType.CharacterSkin;
54
+ const spriteKey = isCharSkin && item.metadata?.selectedSkinTextureKey
55
+ ? `${item.metadata.selectedSkinTextureKey}/down/standing/0.png`
56
+ : item.texturePath;
57
+
58
+ return (
59
+ <SuccessItem key={i}>
60
+ <ItemIconWrap>
61
+ <SpriteFromAtlas
62
+ atlasJSON={isCharSkin ? characterAtlasJSON : atlasJSON}
63
+ atlasIMG={isCharSkin ? characterAtlasIMG : atlasIMG}
64
+ spriteKey={spriteKey}
65
+ width={32}
66
+ height={32}
67
+ imgScale={2}
68
+ centered
69
+ />
70
+ </ItemIconWrap>
71
+ <ItemName>{item.name}</ItemName>
72
+ {item.quantity > 1 && <ItemQty>×{item.quantity}</ItemQty>}
73
+ </SuccessItem>
74
+ );
75
+ })}
76
+ </ItemsList>
77
+
78
+ <TotalPaid>
79
+ Paid: <TotalAmount>${totalPrice.toFixed(2)}</TotalAmount>
80
+ </TotalPaid>
81
+
82
+ <Actions>
83
+ <CTAButton
84
+ icon={<FaShoppingBag />}
85
+ label="Continue Shopping"
86
+ onClick={onContinueShopping}
87
+ fullWidth
88
+ />
89
+ <CloseLink onPointerDown={onClose}>
90
+ <FaTimes /> Close Store
91
+ </CloseLink>
92
+ </Actions>
93
+ </Container>
94
+ );
95
+ };
96
+
97
+ const scaleIn = keyframes`
98
+ from { transform: scale(0.85); opacity: 0; }
99
+ to { transform: scale(1); opacity: 1; }
100
+ `;
101
+
102
+ const float = keyframes`
103
+ 0% { transform: translateY(0) rotate(0deg); opacity: 1; }
104
+ 100% { transform: translateY(-60px) rotate(360deg); opacity: 0; }
105
+ `;
106
+
107
+ const glowPulse = keyframes`
108
+ 0%, 100% { box-shadow: 0 0 12px rgba(245,158,11,0.5), 0 0 24px rgba(245,158,11,0.2); }
109
+ 50% { box-shadow: 0 0 24px rgba(245,158,11,0.8), 0 0 48px rgba(245,158,11,0.4); }
110
+ `;
111
+
112
+ const Container = styled.div`
113
+ position: relative;
114
+ display: flex;
115
+ flex-direction: column;
116
+ align-items: center;
117
+ gap: 1.25rem;
118
+ padding: 2rem 1.5rem;
119
+ overflow: hidden;
120
+ animation: ${scaleIn} 0.25s ease-out;
121
+ text-align: center;
122
+ `;
123
+
124
+ const Sparkles = styled.div`
125
+ position: absolute;
126
+ inset: 0;
127
+ pointer-events: none;
128
+ `;
129
+
130
+ const Spark = styled.div<{ $i: number }>`
131
+ position: absolute;
132
+ width: 6px;
133
+ height: 6px;
134
+ border-radius: 50%;
135
+ background: #f59e0b;
136
+ top: ${p => 30 + (p.$i % 4) * 15}%;
137
+ left: ${p => 10 + (p.$i * 11) % 80}%;
138
+ animation: ${float} ${p => 1.5 + (p.$i % 3) * 0.4}s ease-out ${p => p.$i * 0.2}s infinite;
139
+ opacity: 0;
140
+ `;
141
+
142
+ const Header = styled.div`
143
+ display: flex;
144
+ flex-direction: column;
145
+ align-items: center;
146
+ gap: 0.5rem;
147
+ `;
148
+
149
+ const TrophyArea = styled.div`
150
+ margin-bottom: 0.25rem;
151
+ `;
152
+
153
+ const TrophyIcon = styled.div`
154
+ width: 64px;
155
+ height: 64px;
156
+ border-radius: 50%;
157
+ background: rgba(245, 158, 11, 0.15);
158
+ border: 2px solid #f59e0b;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ animation: ${glowPulse} 2s ease-in-out infinite;
163
+
164
+ svg {
165
+ font-size: 1.75rem;
166
+ color: #f59e0b;
167
+ }
168
+ `;
169
+
170
+ const Title = styled.h2`
171
+ font-family: 'Press Start 2P', cursive;
172
+ font-size: 0.9rem;
173
+ color: #fef08a;
174
+ margin: 0;
175
+ text-shadow: 0 0 8px rgba(245, 158, 11, 0.6);
176
+ `;
177
+
178
+ const Subtitle = styled.p`
179
+ font-family: 'Press Start 2P', cursive;
180
+ font-size: 0.5rem;
181
+ color: rgba(255, 255, 255, 0.6);
182
+ margin: 0;
183
+ `;
184
+
185
+ const ItemsList = styled.div`
186
+ display: flex;
187
+ flex-direction: column;
188
+ gap: 0.5rem;
189
+ width: 100%;
190
+ max-width: 340px;
191
+ max-height: 200px;
192
+ overflow-y: auto;
193
+
194
+ &::-webkit-scrollbar { width: 4px; background: rgba(0,0,0,0.2); }
195
+ &::-webkit-scrollbar-thumb { background: rgba(245,158,11,0.3); border-radius: 2px; }
196
+ `;
197
+
198
+ const SuccessItem = styled.div`
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 0.75rem;
202
+ padding: 0.5rem 0.75rem;
203
+ background: rgba(0, 0, 0, 0.25);
204
+ border: 1px solid rgba(245, 158, 11, 0.2);
205
+ border-radius: 4px;
206
+ `;
207
+
208
+ const ItemIconWrap = styled.div`
209
+ width: 32px;
210
+ height: 32px;
211
+ flex-shrink: 0;
212
+ `;
213
+
214
+ const ItemName = styled.span`
215
+ font-family: 'Press Start 2P', cursive;
216
+ font-size: 0.55rem;
217
+ color: #fff;
218
+ flex: 1;
219
+ text-align: left;
220
+ `;
221
+
222
+ const ItemQty = styled.span`
223
+ font-family: 'Press Start 2P', cursive;
224
+ font-size: 0.5rem;
225
+ color: #fef08a;
226
+ `;
227
+
228
+ const TotalPaid = styled.div`
229
+ font-family: 'Press Start 2P', cursive;
230
+ font-size: 0.6rem;
231
+ color: rgba(255, 255, 255, 0.7);
232
+ `;
233
+
234
+ const TotalAmount = styled.span`
235
+ color: #4ade80;
236
+ `;
237
+
238
+ const Actions = styled.div`
239
+ display: flex;
240
+ flex-direction: column;
241
+ align-items: center;
242
+ gap: 0.75rem;
243
+ width: 100%;
244
+ max-width: 340px;
245
+ `;
246
+
247
+ const CloseLink = styled.div`
248
+ font-family: 'Press Start 2P', cursive;
249
+ font-size: 0.5rem;
250
+ color: rgba(255, 255, 255, 0.45);
251
+ cursor: pointer;
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 0.3rem;
255
+ transition: color 0.15s;
256
+
257
+ &:hover { color: rgba(255, 255, 255, 0.7); }
258
+ `;
@@ -13,6 +13,7 @@ import { Tabs } from '../shared/Tabs';
13
13
  import { LabelPill } from '../shared/LabelPill/LabelPill';
14
14
  import { CTAButton } from '../shared/CTAButton/CTAButton';
15
15
  import { CartView } from './CartView';
16
+ import { FeaturedBanner, IFeaturedItem } from './FeaturedBanner';
16
17
  import { useStoreCart } from './hooks/useStoreCart';
17
18
  import { MetadataCollector } from './MetadataCollector';
18
19
  import { StoreItemsSection } from './sections/StoreItemsSection';
@@ -20,7 +21,7 @@ import { StorePacksSection } from './sections/StorePacksSection';
20
21
  import { StoreItemDetails } from './StoreItemDetails';
21
22
 
22
23
  // Define TabId union type for tab identifiers
23
- type TabId = 'premium' | 'packs' | 'items' | 'wallet';
24
+ type TabId = 'premium' | 'packs' | 'items' | 'wallet' | 'history';
24
25
 
25
26
  // Define IStoreProps locally as a workaround
26
27
  export interface IStoreProps {
@@ -43,9 +44,38 @@ export interface IStoreProps {
43
44
  textInputItemKeys?: string[];
44
45
  customPacksContent?: React.ReactNode;
45
46
  customWalletContent?: React.ReactNode;
47
+ customHistoryContent?: React.ReactNode;
46
48
  packsBadge?: string;
49
+ featuredItems?: IFeaturedItem[];
50
+ onQuickBuy?: (item: IProductBlueprint, quantity?: number) => void;
51
+ itemBadges?: Record<string, { badges?: import('./StoreBadges').IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
52
+ packBadges?: Record<string, { badges?: import('./StoreBadges').IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
53
+ /** Fires when an item row becomes visible (on mount). Useful for store_item_viewed analytics. */
54
+ onItemView?: (item: IProductBlueprint, position: number) => void;
55
+ /** Fires when a pack row becomes visible (on mount). Useful for pack_viewed analytics. */
56
+ onPackView?: (pack: IItemPack, position: number) => void;
57
+ /** Fires when the active store tab changes (e.g. 'items', 'packs', 'premium'). */
58
+ onTabChange?: (tab: string, itemsShown: number) => void;
59
+ /** Fires when the category filter changes in the items tab. */
60
+ onCategoryChange?: (category: string, itemsShown: number) => void;
61
+ /** Fires when the cart is opened. */
62
+ onCartOpen?: () => void;
63
+ /** Fires when any item or pack is added to the cart. */
64
+ onAddToCart?: (item: IProductBlueprint, quantity: number) => void;
65
+ /** Fires when an item is removed from the cart. */
66
+ onRemoveFromCart?: (itemKey: string) => void;
67
+ /** Fires when the user taps "Pay" — before the purchase resolves. */
68
+ onCheckoutStart?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
69
+ /** Fires after a successful purchase. */
70
+ onPurchaseSuccess?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
71
+ /** Fires when a purchase fails. */
72
+ onPurchaseError?: (error: string) => void;
73
+ /** Called when the DC nudge in CartView is tapped — open the DC purchase flow. */
74
+ onBuyDC?: () => void;
47
75
  }
48
76
 
77
+ export type { IFeaturedItem };
78
+
49
79
  export const Store: React.FC<IStoreProps> = ({
50
80
  items,
51
81
  packs = [],
@@ -65,8 +95,24 @@ export const Store: React.FC<IStoreProps> = ({
65
95
  textInputItemKeys = [],
66
96
  customPacksContent,
67
97
  customWalletContent,
98
+ customHistoryContent,
68
99
  packsTabLabel = 'Packs',
69
100
  packsBadge,
101
+ featuredItems,
102
+ onQuickBuy,
103
+ itemBadges,
104
+ packBadges,
105
+ onItemView,
106
+ onPackView,
107
+ onTabChange,
108
+ onCategoryChange,
109
+ onCartOpen,
110
+ onAddToCart,
111
+ onRemoveFromCart,
112
+ onCheckoutStart,
113
+ onPurchaseSuccess,
114
+ onPurchaseError,
115
+ onBuyDC,
70
116
  }) => {
71
117
  const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
72
118
  const [activeTab, setActiveTab] = useState<TabId>(() => {
@@ -90,6 +136,21 @@ export const Store: React.FC<IStoreProps> = ({
90
136
  const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
91
137
  const [currentMetadataItem, setCurrentMetadataItem] = useState<IProductBlueprint | null>(null);
92
138
 
139
+ const handleOpenCart = () => {
140
+ openCart();
141
+ onCartOpen?.();
142
+ };
143
+
144
+ const handleAddToCartTracked = (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => {
145
+ handleAddToCart(item, quantity, metadata);
146
+ onAddToCart?.(item, quantity);
147
+ };
148
+
149
+ const handleRemoveFromCartTracked = (itemKey: string) => {
150
+ handleRemoveFromCart(itemKey);
151
+ onRemoveFromCart?.(itemKey);
152
+ };
153
+
93
154
  const handleAddPackToCart = (pack: IItemPack, quantity: number = 1) => {
94
155
  const packItem: IProductBlueprint = {
95
156
  key: pack.key,
@@ -109,6 +170,7 @@ export const Store: React.FC<IStoreProps> = ({
109
170
  isUsable: false,
110
171
  };
111
172
  handleAddToCart(packItem, quantity);
173
+ onAddToCart?.(packItem, quantity);
112
174
  };
113
175
 
114
176
  const filterItems = (
@@ -163,8 +225,12 @@ export const Store: React.FC<IStoreProps> = ({
163
225
  }
164
226
 
165
227
  // Build tabs dynamically based on props
166
- const tabIds: TabId[] = tabOrder ?? ['premium', 'packs', 'items'];
167
- const availableTabIds: TabId[] = tabIds.filter(id => !(hidePremiumTab && id === 'premium'));
228
+ const tabIds: TabId[] = [
229
+ ...(tabOrder ?? ['premium', 'packs', 'items']),
230
+ ...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
231
+ ...((onShowHistory || customHistoryContent) ? ['history' as TabId] : [])
232
+ ];
233
+ const availableTabIds: TabId[] = Array.from(new Set(tabIds.filter(id => !(hidePremiumTab && id === 'premium'))));
168
234
 
169
235
  const tabsMap: Record<string, { id: TabId; title: ReactNode; icon: ReactNode; content: ReactNode }> = {
170
236
  premium: {
@@ -175,9 +241,31 @@ export const Store: React.FC<IStoreProps> = ({
175
241
  <StorePacksSection
176
242
  packs={packs.filter(pack => pack.priceUSD >= 9.99)}
177
243
  onAddToCart={handleAddPackToCart}
244
+ onQuickBuy={onQuickBuy ? (pack, qty) => {
245
+ const bp: IProductBlueprint = {
246
+ key: pack.key,
247
+ name: pack.title,
248
+ description: pack.description || '',
249
+ price: pack.priceUSD,
250
+ currency: PaymentCurrency.USD,
251
+ texturePath: pack.image.default || pack.image.src,
252
+ type: PurchaseType.Pack,
253
+ onPurchase: async () => {},
254
+ itemType: ItemType.Consumable,
255
+ itemSubType: ItemSubType.Other,
256
+ rarity: ItemRarities.Common,
257
+ weight: 0,
258
+ isStackable: false,
259
+ maxStackSize: 1,
260
+ isUsable: false,
261
+ };
262
+ onQuickBuy(bp, qty);
263
+ } : undefined}
178
264
  onSelectPack={setSelectedPack}
179
265
  atlasJSON={atlasJSON}
180
266
  atlasIMG={atlasIMG}
267
+ packBadges={packBadges}
268
+ onPackView={onPackView}
181
269
  />
182
270
  ),
183
271
  },
@@ -194,9 +282,31 @@ export const Store: React.FC<IStoreProps> = ({
194
282
  <StorePacksSection
195
283
  packs={hidePremiumTab ? packs : packs.filter(pack => pack.priceUSD < 9.99)}
196
284
  onAddToCart={handleAddPackToCart}
285
+ onQuickBuy={onQuickBuy ? (pack, qty) => {
286
+ const bp: IProductBlueprint = {
287
+ key: pack.key,
288
+ name: pack.title,
289
+ description: pack.description || '',
290
+ price: pack.priceUSD,
291
+ currency: PaymentCurrency.USD,
292
+ texturePath: pack.image.default || pack.image.src,
293
+ type: PurchaseType.Pack,
294
+ onPurchase: async () => {},
295
+ itemType: ItemType.Consumable,
296
+ itemSubType: ItemSubType.Other,
297
+ rarity: ItemRarities.Common,
298
+ weight: 0,
299
+ isStackable: false,
300
+ maxStackSize: 1,
301
+ isUsable: false,
302
+ };
303
+ onQuickBuy(bp, qty);
304
+ } : undefined}
197
305
  onSelectPack={setSelectedPack}
198
306
  atlasJSON={atlasJSON}
199
307
  atlasIMG={atlasIMG}
308
+ packBadges={packBadges}
309
+ onPackView={onPackView}
200
310
  />
201
311
  ),
202
312
  },
@@ -207,19 +317,37 @@ export const Store: React.FC<IStoreProps> = ({
207
317
  content: (
208
318
  <StoreItemsSection
209
319
  items={filteredItems.items}
210
- onAddToCart={handleAddToCart}
320
+ onAddToCart={handleAddToCartTracked}
321
+ onQuickBuy={onQuickBuy ? (item, qty, _meta) => onQuickBuy(item, qty) : undefined}
211
322
  atlasJSON={atlasJSON}
212
323
  atlasIMG={atlasIMG}
213
324
  userAccountType={userAccountType}
214
325
  textInputItemKeys={textInputItemKeys}
326
+ itemBadges={itemBadges}
327
+ onItemView={onItemView}
328
+ onCategoryChange={onCategoryChange}
215
329
  />
216
330
  ),
217
331
  },
218
332
  wallet: {
219
333
  id: 'wallet',
220
- title: 'Wallet',
334
+ title: walletLabel ?? 'Wallet',
221
335
  icon: <Wallet width={18} height={18} />,
222
- content: customWalletContent ?? null,
336
+ content: customWalletContent ?? (
337
+ <CenteredContent>
338
+ <CTAButton icon={<FaWallet />} label={`Open ${walletLabel ?? 'Wallet'}`} onClick={onShowWallet} />
339
+ </CenteredContent>
340
+ ),
341
+ },
342
+ history: {
343
+ id: 'history',
344
+ title: 'History',
345
+ icon: <FaHistory width={18} height={18} />,
346
+ content: customHistoryContent ?? (
347
+ <CenteredContent>
348
+ <CTAButton icon={<FaHistory />} label="Open History" onClick={onShowHistory} />
349
+ </CenteredContent>
350
+ ),
223
351
  },
224
352
  };
225
353
 
@@ -243,7 +371,7 @@ export const Store: React.FC<IStoreProps> = ({
243
371
  ) : isCartOpen ? (
244
372
  <CartView
245
373
  cartItems={cartItems}
246
- onRemoveFromCart={handleRemoveFromCart}
374
+ onRemoveFromCart={handleRemoveFromCartTracked}
247
375
  onClose={closeCart}
248
376
  onPurchase={async () => {
249
377
  await handleCartPurchase(onPurchase);
@@ -251,6 +379,10 @@ export const Store: React.FC<IStoreProps> = ({
251
379
  }}
252
380
  atlasJSON={atlasJSON}
253
381
  atlasIMG={atlasIMG}
382
+ onCheckoutStart={onCheckoutStart}
383
+ onPurchaseSuccess={onPurchaseSuccess}
384
+ onPurchaseError={onPurchaseError}
385
+ onBuyDC={onBuyDC}
254
386
  />
255
387
  ) : selectedPack ? (
256
388
  <StoreItemDetails
@@ -265,36 +397,71 @@ export const Store: React.FC<IStoreProps> = ({
265
397
  />
266
398
  ) : (
267
399
  <Container>
268
- <TopBar>
269
- <LeftButtons>
270
- {onShowHistory && (
271
- <CTAButton
272
- icon={<FaHistory />}
273
- onClick={onShowHistory}
274
- />
275
- )}
276
- {onShowWallet && (
400
+ {featuredItems && featuredItems.length > 0 && (
401
+ <FeaturedBanner
402
+ items={featuredItems}
403
+ atlasJSON={atlasJSON}
404
+ atlasIMG={atlasIMG}
405
+ onSelectItem={item => {
406
+ const pack = packs.find(p => p.key === item.key);
407
+ if (pack) setSelectedPack(pack);
408
+ }}
409
+ onQuickBuy={onQuickBuy ? item => {
410
+ const blueprint = items.find(bp => bp.key === item.key);
411
+ if (blueprint) onQuickBuy(blueprint, 1);
412
+ else {
413
+ const pack = packs.find(p => p.key === item.key);
414
+ if (pack) {
415
+ const packBlueprint: IProductBlueprint = {
416
+ key: pack.key,
417
+ name: pack.title,
418
+ description: pack.description || '',
419
+ price: pack.priceUSD,
420
+ currency: PaymentCurrency.USD,
421
+ texturePath: pack.image.default || pack.image.src,
422
+ type: PurchaseType.Pack,
423
+ onPurchase: async () => {},
424
+ itemType: ItemType.Consumable,
425
+ itemSubType: ItemSubType.Other,
426
+ rarity: ItemRarities.Common,
427
+ weight: 0,
428
+ isStackable: false,
429
+ maxStackSize: 1,
430
+ isUsable: false,
431
+ };
432
+ onQuickBuy(packBlueprint, 1);
433
+ }
434
+ }
435
+ } : undefined}
436
+ />
437
+ )}
438
+ <MainContent>
439
+ <HeaderRow>
440
+ <Tabs
441
+ options={availableTabIds.map(id => ({ id, label: tabsMap[id]?.title, icon: tabsMap[id]?.icon }))}
442
+ activeTabId={activeTab}
443
+ onTabChange={(tabId) => {
444
+ const nextTab = tabId as TabId;
445
+ setActiveTab(nextTab);
446
+ if (onTabChange) {
447
+ const itemCount = nextTab === 'items' ? filteredItems.items.length
448
+ : nextTab === 'premium' ? filteredItems.premium.length
449
+ : nextTab === 'packs' ? packs.length
450
+ : 0;
451
+ onTabChange(nextTab, itemCount);
452
+ }
453
+ }}
454
+ />
455
+ <CartButtonWrapper>
277
456
  <CTAButton
278
- icon={<FaWallet />}
279
- label={walletLabel ?? 'DC Wallet'}
280
- onClick={onShowWallet}
457
+ icon={<FaShoppingCart />}
458
+ onClick={handleOpenCart}
281
459
  />
282
- )}
283
- </LeftButtons>
284
- <CartButton>
285
- <CTAButton
286
- icon={<FaShoppingCart />}
287
- label={`${getTotalItems()} items ($${getTotalPrice().toFixed(2)})`}
288
- onClick={openCart}
289
- />
290
- </CartButton>
291
- </TopBar>
292
- <MainContent>
293
- <Tabs
294
- options={availableTabIds.map(id => ({ id, label: tabsMap[id].title, icon: tabsMap[id].icon }))}
295
- activeTabId={activeTab}
296
- onTabChange={(tabId) => setActiveTab(tabId as TabId)}
297
- />
460
+ {getTotalItems() > 0 && (
461
+ <CartBadge>{getTotalItems()}</CartBadge>
462
+ )}
463
+ </CartButtonWrapper>
464
+ </HeaderRow>
298
465
  <TabContent>
299
466
  {tabsMap[activeTab]?.content}
300
467
  </TabContent>
@@ -314,7 +481,7 @@ export const Store: React.FC<IStoreProps> = ({
314
481
  <CTAButton
315
482
  icon={<FaShoppingCart />}
316
483
  label={`Proceed to Checkout ($${getTotalPrice().toFixed(2)})`}
317
- onClick={openCart}
484
+ onClick={handleOpenCart}
318
485
  fullWidth
319
486
  />
320
487
  </Footer>
@@ -330,29 +497,40 @@ const Container = styled.div`
330
497
  flex-direction: column;
331
498
  width: 100%;
332
499
  height: 100%;
333
- gap: 1rem;
500
+ gap: 0.5rem;
334
501
  position: relative;
335
502
  `;
336
503
 
337
- const TopBar = styled.div`
504
+ const HeaderRow = styled.div`
338
505
  display: flex;
339
506
  align-items: center;
340
- justify-content: flex-end;
341
- gap: 1rem;
342
- padding: 0 1rem;
343
- flex-shrink: 0;
344
- margin-top: 0.5rem;
507
+ justify-content: space-between;
508
+ margin-bottom: 0.25rem;
345
509
  `;
346
510
 
347
- const LeftButtons = styled.div`
348
- display: flex;
349
- gap: 0.5rem;
350
- min-width: fit-content;
351
- margin-right: auto;
511
+ const CartButtonWrapper = styled.div`
512
+ position: relative;
513
+ margin-right: 0.5rem;
514
+ margin-top: -16px;
352
515
  `;
353
516
 
354
- const CartButton = styled.div`
355
- min-width: fit-content;
517
+ const CartBadge = styled.div`
518
+ position: absolute;
519
+ top: -8px;
520
+ right: -8px;
521
+ background: #ef4444; /* red */
522
+ color: white;
523
+ font-family: 'Press Start 2P', cursive;
524
+ font-size: 0.5rem;
525
+ padding: 4px;
526
+ border-radius: 50%;
527
+ border: 2px solid #000;
528
+ display: flex;
529
+ align-items: center;
530
+ justify-content: center;
531
+ min-width: 16px;
532
+ min-height: 16px;
533
+ box-sizing: content-box;
356
534
  `;
357
535
 
358
536
  const MainContent = styled.div`
@@ -416,3 +594,11 @@ const ErrorMessage = styled.div`
416
594
  color: ${uiColors.red};
417
595
  padding: 2rem;
418
596
  `;
597
+
598
+ const CenteredContent = styled.div`
599
+ display: flex;
600
+ align-items: center;
601
+ justify-content: center;
602
+ height: 100%;
603
+ padding: 2rem;
604
+ `;