@rpg-engine/long-bow 0.8.171 → 0.8.173

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 (54) 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 +1279 -303
  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 +1276 -305
  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/RPGUI/RPGUIScrollbar.tsx +2 -2
  28. package/src/components/Store/CartView.tsx +143 -33
  29. package/src/components/Store/CountdownTimer.tsx +86 -0
  30. package/src/components/Store/FeaturedBanner.tsx +270 -0
  31. package/src/components/Store/PurchaseSuccess.tsx +255 -0
  32. package/src/components/Store/Store.tsx +236 -51
  33. package/src/components/Store/StoreBadges.tsx +94 -0
  34. package/src/components/Store/StoreCharacterSkinRow.tsx +113 -22
  35. package/src/components/Store/StoreItemRow.tsx +135 -17
  36. package/src/components/Store/TrustBar.tsx +69 -0
  37. package/src/components/Store/__test__/CountdownTimer.spec.tsx +100 -0
  38. package/src/components/Store/__test__/FeaturedBanner.spec.tsx +207 -0
  39. package/src/components/Store/__test__/PurchaseSuccess.spec.tsx +174 -0
  40. package/src/components/Store/__test__/StoreBadges.spec.tsx +133 -0
  41. package/src/components/Store/__test__/TrustBar.spec.tsx +85 -0
  42. package/src/components/Store/sections/StoreItemsSection.tsx +27 -1
  43. package/src/components/Store/sections/StorePacksSection.tsx +92 -28
  44. package/src/components/shared/CTAButton/CTAButton.tsx +25 -1
  45. package/src/components/shared/CustomScrollbar.ts +41 -0
  46. package/src/components/shared/ItemRowWrapper.tsx +26 -12
  47. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +1 -0
  48. package/src/components/shared/SpriteFromAtlas.tsx +4 -1
  49. package/src/index.tsx +6 -1
  50. package/src/stories/Features/store/FeaturedBanner.stories.tsx +121 -0
  51. package/src/stories/Features/store/PurchaseSuccess.stories.tsx +74 -0
  52. package/src/stories/Features/store/Store.stories.tsx +39 -3
  53. package/src/stories/Features/store/StoreBadges.stories.tsx +83 -0
  54. package/src/stories/Features/store/TrustBar.stories.tsx +51 -0
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.171",
3
+ "version": "0.8.173",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -84,7 +84,7 @@
84
84
  "dependencies": {
85
85
  "@capacitor/core": "^6.1.0",
86
86
  "@rollup/plugin-image": "^2.1.1",
87
- "@rpg-engine/shared": "^0.10.103",
87
+ "@rpg-engine/shared": "^0.10.109",
88
88
  "dayjs": "^1.11.2",
89
89
  "font-awesome": "^4.7.0",
90
90
  "fs-extra": "^10.1.0",
@@ -192,7 +192,7 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
192
192
  const groupedBuyOrders = useMemo(() => {
193
193
  const groups = new Map<string, IMarketplaceBuyOrderItem[]>();
194
194
  for (const order of visibleBuyOrders) {
195
- const key = order.itemBlueprintKey;
195
+ const key = `${order.itemBlueprintKey}|${order.itemRarity ?? ''}`;
196
196
  if (!groups.has(key)) groups.set(key, []);
197
197
  groups.get(key)!.push(order);
198
198
  }
@@ -14,11 +14,11 @@ const Container = styled.div`
14
14
 
15
15
  .rpgui-content ::-webkit-scrollbar,
16
16
  .rpgui-content::-webkit-scrollbar {
17
- width: 25px !important;
17
+ width: 12px !important;
18
18
  }
19
19
 
20
20
  .rpgui-content ::-webkit-scrollbar-track,
21
21
  .rpgui-content::-webkit-scrollbar-track {
22
- background-size: 25px 60px !important;
22
+ background-size: 12px 60px !important;
23
23
  }
24
24
  `;
@@ -1,13 +1,13 @@
1
1
  import { IProductBlueprint, MetadataType } from '@rpg-engine/shared';
2
2
  import React, { useState } from 'react';
3
- import { FaInfoCircle, FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
3
+ import { FaCoins, FaInfoCircle, FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import characterAtlasJSON from '../../mocks/atlas/entities/entities.json';
6
6
  import characterAtlasIMG from '../../mocks/atlas/entities/entities.png';
7
7
  import { CTAButton } from '../shared/CTAButton/CTAButton';
8
8
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
9
-
10
-
9
+ import { ITrustSignal, TrustBar } from './TrustBar';
10
+ import { PurchaseSuccess } from './PurchaseSuccess';
11
11
 
12
12
  // Local cart item interface
13
13
  interface ICartItem {
@@ -16,13 +16,24 @@ interface ICartItem {
16
16
  metadata?: Record<string, any>;
17
17
  }
18
18
 
19
- interface ICartViewProps {
19
+ export interface ICartViewProps {
20
20
  cartItems: ICartItem[];
21
21
  onRemoveFromCart: (itemKey: string) => void;
22
22
  onClose: () => void;
23
23
  onPurchase: () => Promise<boolean>;
24
24
  atlasJSON: Record<string, any>;
25
25
  atlasIMG: string;
26
+ paymentMethodLabel?: string;
27
+ trustSignals?: ITrustSignal[];
28
+ onCloseStore?: () => void;
29
+ /** Called when user taps the "Buy DC" nudge — open wallet/DC purchase flow */
30
+ onBuyDC?: () => void;
31
+ /** Fires when user taps the pay button — before the purchase resolves */
32
+ onCheckoutStart?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
33
+ /** Fires after a successful purchase */
34
+ onPurchaseSuccess?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
35
+ /** Fires when a purchase fails */
36
+ onPurchaseError?: (error: string) => void;
26
37
  }
27
38
 
28
39
  const MetadataDisplay: React.FC<{
@@ -52,9 +63,17 @@ export const CartView: React.FC<ICartViewProps> = ({
52
63
  onPurchase,
53
64
  atlasJSON,
54
65
  atlasIMG,
66
+ paymentMethodLabel,
67
+ trustSignals,
68
+ onCloseStore,
69
+ onBuyDC,
70
+ onCheckoutStart,
71
+ onPurchaseSuccess,
72
+ onPurchaseError,
55
73
  }) => {
56
74
  const [isLoading, setIsLoading] = useState(false);
57
75
  const [error, setError] = useState<string | null>(null);
76
+ const [purchasedItems, setPurchasedItems] = useState<ICartItem[] | null>(null);
58
77
 
59
78
  const total = cartItems.reduce(
60
79
  (sum, cartItem) => sum + cartItem.item.price * cartItem.quantity,
@@ -72,23 +91,56 @@ export const CartView: React.FC<ICartViewProps> = ({
72
91
  try {
73
92
  setIsLoading(true);
74
93
  setError(null);
94
+ const snapshot = [...cartItems];
95
+ const trackItems = snapshot.map(ci => ({ key: ci.item.key, name: ci.item.name, quantity: ci.quantity }));
96
+ onCheckoutStart?.(trackItems, total);
75
97
  const success = await onPurchase();
76
98
 
77
- if (!success) {
78
- setError('Purchase failed. Please try again.');
99
+ if (success) {
100
+ onPurchaseSuccess?.(trackItems, total);
101
+ setPurchasedItems(snapshot);
102
+ } else {
103
+ const errMsg = 'Purchase failed. Please try again.';
104
+ onPurchaseError?.(errMsg);
105
+ setError(errMsg);
79
106
  }
80
107
  } catch (err) {
81
- setError('An error occurred during purchase. Please try again.');
108
+ const errMsg = 'An error occurred during purchase. Please try again.';
109
+ onPurchaseError?.(errMsg);
110
+ setError(errMsg);
82
111
  console.error('Purchase error:', err);
83
112
  } finally {
84
113
  setIsLoading(false);
85
114
  }
86
115
  };
87
116
 
117
+ // Show DC discount nudge when items have DC pricing and user might benefit
118
+ const hasDCItems = cartItems.some(ci => (ci.item as any).dcPrice);
119
+ const showDCNudge = hasDCItems && onBuyDC;
120
+
121
+ if (purchasedItems) {
122
+ return (
123
+ <PurchaseSuccess
124
+ items={purchasedItems.map(ci => ({
125
+ name: ci.item.name,
126
+ texturePath: ci.item.texturePath,
127
+ quantity: ci.quantity,
128
+ metadataType: ci.item.metadataType,
129
+ metadata: ci.metadata,
130
+ }))}
131
+ totalPrice={purchasedItems.reduce((s, ci) => s + ci.item.price * ci.quantity, 0)}
132
+ atlasJSON={atlasJSON}
133
+ atlasIMG={atlasIMG}
134
+ onContinueShopping={onClose}
135
+ onClose={onCloseStore ?? onClose}
136
+ />
137
+ );
138
+ }
139
+
88
140
  return (
89
141
  <Container>
90
142
  <Header>
91
- <Title>Shopping Cart</Title>
143
+ <Title>Shopping Cart ({cartItems.reduce((s, ci) => s + ci.quantity, 0)})</Title>
92
144
  <CloseButton onPointerDown={onClose}>
93
145
  <FaTimes />
94
146
  </CloseButton>
@@ -154,9 +206,20 @@ export const CartView: React.FC<ICartViewProps> = ({
154
206
  </CartItems>
155
207
 
156
208
  <Footer>
209
+ {showDCNudge && (
210
+ <DCNudge onPointerDown={onBuyDC}>
211
+ <FaCoins />
212
+ <span>Save more with DC — volume discounts available</span>
213
+ <DCNudgeLink>Buy DC →</DCNudgeLink>
214
+ </DCNudge>
215
+ )}
216
+
217
+ <TrustBar signals={trustSignals} />
218
+
157
219
  <TotalInfo>
220
+ <OrderSummaryLabel>Order Summary</OrderSummaryLabel>
158
221
  <TotalRow>
159
- <span>Total:</span>
222
+ <span>Subtotal:</span>
160
223
  <span>${formatPrice(total)}</span>
161
224
  </TotalRow>
162
225
  {dcTotal > 0 && (
@@ -165,11 +228,21 @@ export const CartView: React.FC<ICartViewProps> = ({
165
228
  <span>{dcTotal.toLocaleString()} DC</span>
166
229
  </TotalRow>
167
230
  )}
231
+ <TotalRow $isTotal>
232
+ <span>Total:</span>
233
+ <span>${formatPrice(total)}</span>
234
+ </TotalRow>
235
+ {paymentMethodLabel && (
236
+ <PaymentMethodRow>
237
+ <span>Paying with:</span>
238
+ <span>{paymentMethodLabel}</span>
239
+ </PaymentMethodRow>
240
+ )}
168
241
  {error && <ErrorMessage>{error}</ErrorMessage>}
169
242
  </TotalInfo>
170
243
  <CTAButton
171
244
  icon={<FaShoppingBag />}
172
- label={isLoading ? 'Processing...' : 'Complete Purchase'}
245
+ label={isLoading ? 'Processing...' : `Pay $${formatPrice(total)}`}
173
246
  onClick={handlePurchase}
174
247
  fullWidth
175
248
  disabled={cartItems.length === 0 || isLoading}
@@ -221,26 +294,6 @@ const CartItems = styled.div`
221
294
  flex: 1;
222
295
  min-height: 200px;
223
296
  padding-right: 0.5rem;
224
-
225
- /* Custom scrollbar styling */
226
- &::-webkit-scrollbar {
227
- width: 8px;
228
- background-color: rgba(0, 0, 0, 0.2);
229
- border-radius: 4px;
230
- }
231
-
232
- &::-webkit-scrollbar-thumb {
233
- background-color: rgba(255, 255, 255, 0.2);
234
- border-radius: 4px;
235
-
236
- &:hover {
237
- background-color: rgba(255, 255, 255, 0.3);
238
- }
239
- }
240
-
241
- /* Firefox scrollbar styling */
242
- scrollbar-width: thin;
243
- scrollbar-color: rgba(255, 255, 255, 0.2) rgba(0, 0, 0, 0.2);
244
297
  `;
245
298
 
246
299
  const EmptyCart = styled.div`
@@ -310,20 +363,77 @@ const TotalInfo = styled.div`
310
363
  gap: 0.5rem;
311
364
  `;
312
365
 
313
- const TotalRow = styled.div`
366
+ const OrderSummaryLabel = styled.div`
367
+ font-family: 'Press Start 2P', cursive;
368
+ font-size: 0.55rem;
369
+ color: rgba(255, 255, 255, 0.5);
370
+ text-transform: uppercase;
371
+ letter-spacing: 0.05em;
372
+ margin-bottom: 0.25rem;
373
+ `;
374
+
375
+ const TotalRow = styled.div<{ $isTotal?: boolean }>`
314
376
  display: flex;
315
377
  align-items: center;
316
378
  justify-content: space-between;
317
379
  gap: 1rem;
318
380
  font-family: 'Press Start 2P', cursive;
319
- font-size: 1rem;
320
- color: #ffffff;
381
+ font-size: ${p => p.$isTotal ? '1rem' : '0.75rem'};
382
+ color: ${p => p.$isTotal ? '#ffffff' : 'rgba(255,255,255,0.7)'};
383
+ ${p => p.$isTotal && `
384
+ padding-top: 0.5rem;
385
+ border-top: 1px solid rgba(255,255,255,0.15);
386
+ margin-top: 0.25rem;
387
+ `}
321
388
 
322
389
  span:last-child {
323
390
  color: #fef08a;
324
391
  }
325
392
  `;
326
393
 
394
+ const PaymentMethodRow = styled.div`
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: space-between;
398
+ gap: 1rem;
399
+ font-family: 'Press Start 2P', cursive;
400
+ font-size: 0.5rem;
401
+ color: rgba(255, 255, 255, 0.5);
402
+ margin-top: 0.25rem;
403
+
404
+ span:last-child {
405
+ color: rgba(255, 255, 255, 0.8);
406
+ }
407
+ `;
408
+
409
+ const DCNudge = styled.div`
410
+ display: flex;
411
+ align-items: center;
412
+ gap: 0.5rem;
413
+ padding: 0.5rem 0.75rem;
414
+ background: rgba(245, 158, 11, 0.1);
415
+ border: 1px solid rgba(245, 158, 11, 0.3);
416
+ border-radius: 4px;
417
+ cursor: pointer;
418
+ font-family: 'Press Start 2P', cursive;
419
+ font-size: 0.45rem;
420
+ color: #fbbf24;
421
+ transition: background 0.15s;
422
+
423
+ &:hover {
424
+ background: rgba(245, 158, 11, 0.18);
425
+ }
426
+
427
+ svg { flex-shrink: 0; font-size: 0.7rem; }
428
+ span { flex: 1; }
429
+ `;
430
+
431
+ const DCNudgeLink = styled.span`
432
+ color: #f59e0b;
433
+ white-space: nowrap;
434
+ text-decoration: underline;
435
+ `;
436
+
327
437
  const ErrorMessage = styled.div`
328
438
  color: #ef4444;
329
439
  font-size: 0.875rem;
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import styled, { keyframes } from 'styled-components';
3
+
4
+ export interface ICountdownTimerProps {
5
+ endsAt: string;
6
+ onExpired?: () => void;
7
+ size?: 'small' | 'default';
8
+ }
9
+
10
+ interface ITimeLeft {
11
+ days: number;
12
+ hours: number;
13
+ minutes: number;
14
+ seconds: number;
15
+ expired: boolean;
16
+ }
17
+
18
+ function calcTimeLeft(endsAt: string): ITimeLeft {
19
+ const diff = new Date(endsAt).getTime() - Date.now();
20
+ if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, expired: true };
21
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
22
+ const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
23
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
24
+ const seconds = Math.floor((diff % (1000 * 60)) / 1000);
25
+ return { days, hours, minutes, seconds, expired: false };
26
+ }
27
+
28
+ export const CountdownTimer: React.FC<ICountdownTimerProps> = ({ endsAt, onExpired, size = 'default' }) => {
29
+ const [timeLeft, setTimeLeft] = useState<ITimeLeft>(() => calcTimeLeft(endsAt));
30
+
31
+ useEffect(() => {
32
+ if (timeLeft.expired) {
33
+ onExpired?.();
34
+ return;
35
+ }
36
+ const id = setInterval(() => {
37
+ const next = calcTimeLeft(endsAt);
38
+ setTimeLeft(next);
39
+ if (next.expired) {
40
+ clearInterval(id);
41
+ onExpired?.();
42
+ }
43
+ }, 1000);
44
+ return () => clearInterval(id);
45
+ }, [endsAt, onExpired]);
46
+
47
+ if (timeLeft.expired) {
48
+ return <ExpiredLabel $size={size}>EXPIRED</ExpiredLabel>;
49
+ }
50
+
51
+ const parts: string[] = [];
52
+ if (timeLeft.days > 0) parts.push(`${timeLeft.days}d`);
53
+ parts.push(`${String(timeLeft.hours).padStart(2, '0')}h`);
54
+ parts.push(`${String(timeLeft.minutes).padStart(2, '0')}m`);
55
+ if (timeLeft.days === 0) parts.push(`${String(timeLeft.seconds).padStart(2, '0')}s`);
56
+
57
+ return (
58
+ <TimerLabel $size={size}>
59
+ ⏱ {parts.join(' ')}
60
+ </TimerLabel>
61
+ );
62
+ };
63
+
64
+ const pulse = keyframes`
65
+ 0%, 100% { opacity: 1; }
66
+ 50% { opacity: 0.6; }
67
+ `;
68
+
69
+ const baseLabel = `
70
+ font-family: 'Press Start 2P', cursive;
71
+ display: inline-flex;
72
+ align-items: center;
73
+ `;
74
+
75
+ const TimerLabel = styled.span<{ $size: 'small' | 'default' }>`
76
+ ${baseLabel}
77
+ font-size: ${p => p.$size === 'small' ? '0.45rem' : '0.6rem'};
78
+ color: #fbbf24;
79
+ animation: ${pulse} 2s ease-in-out infinite;
80
+ `;
81
+
82
+ const ExpiredLabel = styled.span<{ $size: 'small' | 'default' }>`
83
+ ${baseLabel}
84
+ font-size: ${p => p.$size === 'small' ? '0.45rem' : '0.6rem'};
85
+ color: #6b7280;
86
+ `;
@@ -0,0 +1,270 @@
1
+ import React from 'react';
2
+ import { FaBolt } from 'react-icons/fa';
3
+ import styled from 'styled-components';
4
+ import { uiColors } from '../../constants/uiColors';
5
+ import { CTAButton } from '../shared/CTAButton/CTAButton';
6
+ import { LabelPill } from '../shared/LabelPill/LabelPill';
7
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
8
+ import { CountdownTimer } from './CountdownTimer';
9
+
10
+ export interface IFeaturedItem {
11
+ key: string;
12
+ name: string;
13
+ description?: string;
14
+ imageUrl?: string | { src: string; default?: string };
15
+ texturePath?: string;
16
+ price: number;
17
+ originalPrice?: number;
18
+ endsAt?: string;
19
+ badge?: string;
20
+ }
21
+
22
+ export interface IFeaturedBannerProps {
23
+ items: IFeaturedItem[];
24
+ atlasJSON?: any;
25
+ atlasIMG?: string;
26
+ onSelectItem: (item: IFeaturedItem) => void;
27
+ onQuickBuy?: (item: IFeaturedItem) => void;
28
+ }
29
+
30
+ const BADGE_COLORS: Record<string, { bg: string; border: string; color: string }> = {
31
+ SALE: { bg: uiColors.orange, border: uiColors.orange, color: '#fff' },
32
+ NEW: { bg: uiColors.green, border: uiColors.green, color: '#fff' },
33
+ LIMITED: { bg: uiColors.cardinal, border: uiColors.cardinal, color: '#fff' },
34
+ POPULAR: { bg: uiColors.navyBlue, border: uiColors.navyBlue, color: '#fff' },
35
+ EVENT: { bg: uiColors.purple, border: uiColors.purple, color: '#fff' },
36
+ };
37
+
38
+ function getImageSrc(imageUrl?: string | { src: string; default?: string }): string | undefined {
39
+ if (!imageUrl) return undefined;
40
+ if (typeof imageUrl === 'string') return imageUrl;
41
+ return imageUrl.default || imageUrl.src;
42
+ }
43
+
44
+ export const FeaturedBanner: React.FC<IFeaturedBannerProps> = ({
45
+ items,
46
+ atlasJSON,
47
+ atlasIMG,
48
+ onSelectItem,
49
+ onQuickBuy,
50
+ }) => {
51
+ if (!items.length) return null;
52
+
53
+ return (
54
+ <BannerWrapper>
55
+ <BannerHeader>
56
+ <FaBolt />
57
+ <span>FEATURED OFFERS</span>
58
+ </BannerHeader>
59
+ <CardsRow>
60
+ {items.map(item => {
61
+ const badgeStyle = item.badge ? (BADGE_COLORS[item.badge.toUpperCase()] ?? BADGE_COLORS.SALE) : null;
62
+ const imgSrc = getImageSrc(item.imageUrl);
63
+ const canUseAtlas = atlasJSON && atlasIMG && item.texturePath && atlasJSON?.frames?.[item.texturePath];
64
+
65
+ return (
66
+ <FeaturedCard key={item.key} onClick={() => onSelectItem(item)}>
67
+ <CardImageArea>
68
+ {canUseAtlas ? (
69
+ <SpriteFromAtlas
70
+ atlasJSON={atlasJSON}
71
+ atlasIMG={atlasIMG}
72
+ spriteKey={item.texturePath!}
73
+ width={72}
74
+ height={72}
75
+ imgScale={2}
76
+ centered
77
+ />
78
+ ) : imgSrc ? (
79
+ <img src={imgSrc} alt={item.name} />
80
+ ) : (
81
+ <PlaceholderIcon>?</PlaceholderIcon>
82
+ )}
83
+ {badgeStyle && (
84
+ <BadgeOverlay>
85
+ <LabelPill
86
+ background={badgeStyle.bg}
87
+ borderColor={badgeStyle.border}
88
+ color={badgeStyle.color}
89
+ >
90
+ {item.badge}
91
+ </LabelPill>
92
+ </BadgeOverlay>
93
+ )}
94
+ </CardImageArea>
95
+
96
+ <CardBody>
97
+ <CardName>{item.name}</CardName>
98
+ {item.description && <CardDesc>{item.description}</CardDesc>}
99
+
100
+ <PriceRow>
101
+ {item.originalPrice != null && (
102
+ <OriginalPrice>${item.originalPrice.toFixed(2)}</OriginalPrice>
103
+ )}
104
+ <CurrentPrice $onSale={item.originalPrice != null}>
105
+ ${item.price.toFixed(2)}
106
+ </CurrentPrice>
107
+ </PriceRow>
108
+
109
+ {item.endsAt && (
110
+ <CountdownTimer endsAt={item.endsAt} size="small" />
111
+ )}
112
+ </CardBody>
113
+
114
+ <CardActions onClick={e => e.stopPropagation()}>
115
+ {onQuickBuy && (
116
+ <CTAButton
117
+ icon={<FaBolt />}
118
+ label="Buy Now"
119
+ onClick={() => onQuickBuy(item)}
120
+ />
121
+ )}
122
+ </CardActions>
123
+ </FeaturedCard>
124
+ );
125
+ })}
126
+ </CardsRow>
127
+ </BannerWrapper>
128
+ );
129
+ };
130
+
131
+ const BannerWrapper = styled.div`
132
+ padding: 0.5rem 1rem 0.75rem;
133
+ border-bottom: 1px solid rgba(245, 158, 11, 0.3);
134
+ background: linear-gradient(180deg, rgba(245, 158, 11, 0.06) 0%, transparent 100%);
135
+ `;
136
+
137
+ const BannerHeader = styled.div`
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 0.4rem;
141
+ font-family: 'Press Start 2P', cursive;
142
+ font-size: 0.55rem;
143
+ color: #f59e0b;
144
+ margin-bottom: 0.6rem;
145
+
146
+ svg { font-size: 0.7rem; }
147
+ `;
148
+
149
+ const CardsRow = styled.div`
150
+ display: flex;
151
+ gap: 0.75rem;
152
+ overflow-x: auto;
153
+ padding-bottom: 0.25rem;
154
+
155
+ @media (max-width: 950px) {
156
+ flex-direction: column;
157
+ overflow-x: unset;
158
+ }
159
+ `;
160
+
161
+ const FeaturedCard = styled.div`
162
+ flex: 0 0 auto;
163
+ width: 200px;
164
+ display: flex;
165
+ flex-direction: column;
166
+ background: rgba(0, 0, 0, 0.35);
167
+ border: 1px solid rgba(245, 158, 11, 0.35);
168
+ border-radius: 6px;
169
+ overflow: hidden;
170
+ cursor: pointer;
171
+ transition: border-color 0.2s, transform 0.15s;
172
+
173
+ &:hover {
174
+ border-color: #f59e0b;
175
+ transform: translateY(-2px);
176
+ }
177
+
178
+ @media (max-width: 950px) {
179
+ width: 100%;
180
+ flex-direction: row;
181
+ }
182
+ `;
183
+
184
+ const CardImageArea = styled.div`
185
+ position: relative;
186
+ width: 100%;
187
+ height: 88px;
188
+ background: rgba(0, 0, 0, 0.3);
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ flex-shrink: 0;
193
+
194
+ img {
195
+ width: 100%;
196
+ height: 100%;
197
+ object-fit: cover;
198
+ }
199
+
200
+ @media (max-width: 950px) {
201
+ width: 88px;
202
+ height: 88px;
203
+ }
204
+ `;
205
+
206
+ const BadgeOverlay = styled.div`
207
+ position: absolute;
208
+ top: 4px;
209
+ left: 4px;
210
+ `;
211
+
212
+ const PlaceholderIcon = styled.div`
213
+ font-family: 'Press Start 2P', cursive;
214
+ font-size: 1.5rem;
215
+ color: rgba(255,255,255,0.2);
216
+ `;
217
+
218
+ const CardBody = styled.div`
219
+ padding: 0.5rem 0.6rem 0.25rem;
220
+ display: flex;
221
+ flex-direction: column;
222
+ gap: 0.3rem;
223
+ flex: 1;
224
+ `;
225
+
226
+ const CardName = styled.div`
227
+ font-family: 'Press Start 2P', cursive;
228
+ font-size: 0.55rem;
229
+ color: #fff;
230
+ white-space: nowrap;
231
+ overflow: hidden;
232
+ text-overflow: ellipsis;
233
+ `;
234
+
235
+ const CardDesc = styled.div`
236
+ font-family: 'Press Start 2P', cursive;
237
+ font-size: 0.45rem;
238
+ color: rgba(255,255,255,0.6);
239
+ line-height: 1.4;
240
+ overflow: hidden;
241
+ display: -webkit-box;
242
+ -webkit-line-clamp: 2;
243
+ -webkit-box-orient: vertical;
244
+ `;
245
+
246
+ const PriceRow = styled.div`
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 0.4rem;
250
+ `;
251
+
252
+ const OriginalPrice = styled.span`
253
+ font-family: 'Press Start 2P', cursive;
254
+ font-size: 0.45rem;
255
+ color: rgba(255,255,255,0.4);
256
+ text-decoration: line-through;
257
+ `;
258
+
259
+ const CurrentPrice = styled.span<{ $onSale: boolean }>`
260
+ font-family: 'Press Start 2P', cursive;
261
+ font-size: 0.6rem;
262
+ color: ${p => p.$onSale ? '#4ade80' : '#fef08a'};
263
+ `;
264
+
265
+ const CardActions = styled.div`
266
+ padding: 0.4rem 0.6rem 0.6rem;
267
+ display: flex;
268
+ gap: 0.4rem;
269
+ justify-content: flex-end;
270
+ `;