@rpg-engine/long-bow 0.8.171 → 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,94 @@
1
+ import React from 'react';
2
+ import styled, { keyframes } from 'styled-components';
3
+ import { uiColors } from '../../constants/uiColors';
4
+ import { LabelPill } from '../shared/LabelPill/LabelPill';
5
+ import { CountdownTimer } from './CountdownTimer';
6
+
7
+ export type StoreBadgeType = 'popular' | 'bestSeller' | 'limited' | 'new' | 'sale' | 'event';
8
+
9
+ export interface IStoreBadge {
10
+ type: StoreBadgeType;
11
+ label?: string;
12
+ }
13
+
14
+ export interface IStoreBadgesProps {
15
+ badges?: IStoreBadge[];
16
+ buyCount?: number;
17
+ viewersCount?: number;
18
+ saleEndsAt?: string;
19
+ }
20
+
21
+ const BADGE_CONFIG: Record<StoreBadgeType, { bg: string; border: string; color: string; defaultLabel: string }> = {
22
+ popular: { bg: uiColors.navyBlue, border: uiColors.navyBlue, color: '#fff', defaultLabel: 'Popular' },
23
+ bestSeller: { bg: uiColors.darkYellow, border: uiColors.darkYellow, color: '#000', defaultLabel: 'Best Seller' },
24
+ limited: { bg: uiColors.cardinal, border: uiColors.cardinal, color: '#fff', defaultLabel: 'Limited' },
25
+ new: { bg: uiColors.green, border: uiColors.green, color: '#fff', defaultLabel: 'New' },
26
+ sale: { bg: uiColors.orange, border: uiColors.orange, color: '#fff', defaultLabel: 'Sale' },
27
+ event: { bg: uiColors.purple, border: uiColors.purple, color: '#fff', defaultLabel: 'Event' },
28
+ };
29
+
30
+ export const StoreBadges: React.FC<IStoreBadgesProps> = ({ badges, buyCount, viewersCount, saleEndsAt }) => {
31
+ const hasContent =
32
+ (badges && badges.length > 0) ||
33
+ (buyCount && buyCount > 0) ||
34
+ (viewersCount && viewersCount > 1) ||
35
+ saleEndsAt;
36
+
37
+ if (!hasContent) return null;
38
+
39
+ return (
40
+ <BadgesRow>
41
+ {badges?.map((badge, i) => {
42
+ const cfg = BADGE_CONFIG[badge.type];
43
+ return (
44
+ <LabelPill key={i} background={cfg.bg} borderColor={cfg.border} color={cfg.color}>
45
+ {badge.label ?? cfg.defaultLabel}
46
+ </LabelPill>
47
+ );
48
+ })}
49
+
50
+ {buyCount && buyCount > 0 && (
51
+ <LabelPill
52
+ background="rgba(99,102,241,0.25)"
53
+ borderColor="rgba(99,102,241,0.5)"
54
+ color="#a5b4fc"
55
+ >
56
+ {buyCount} bought
57
+ </LabelPill>
58
+ )}
59
+
60
+ {viewersCount && viewersCount > 1 && (
61
+ <ViewersPill>
62
+ <LabelPill
63
+ background="rgba(14,121,178,0.25)"
64
+ borderColor="rgba(14,121,178,0.5)"
65
+ color="#7dd3fc"
66
+ >
67
+ {viewersCount} viewing
68
+ </LabelPill>
69
+ </ViewersPill>
70
+ )}
71
+
72
+ {saleEndsAt && (
73
+ <CountdownTimer endsAt={saleEndsAt} size="small" />
74
+ )}
75
+ </BadgesRow>
76
+ );
77
+ };
78
+
79
+ const pulse = keyframes`
80
+ 0%, 100% { opacity: 1; }
81
+ 50% { opacity: 0.5; }
82
+ `;
83
+
84
+ const BadgesRow = styled.div`
85
+ display: flex;
86
+ flex-wrap: wrap;
87
+ align-items: center;
88
+ gap: 0.3rem;
89
+ margin-top: 0.2rem;
90
+ `;
91
+
92
+ const ViewersPill = styled.span`
93
+ animation: ${pulse} 2s ease-in-out infinite;
94
+ `;
@@ -10,6 +10,7 @@ import { SelectArrow } from '../Arrow/SelectArrow';
10
10
  import { ICharacterProps } from '../Character/CharacterSelection';
11
11
  import { CTAButton } from '../shared/CTAButton/CTAButton';
12
12
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
13
+ import { SimpleTooltip } from '../shared/SimpleTooltip';
13
14
  import { useCharacterSkinNavigation } from '../../hooks/useCharacterSkinNavigation';
14
15
 
15
16
  interface IStoreCharacterSkinRowProps {
@@ -22,6 +23,7 @@ interface IStoreCharacterSkinRowProps {
22
23
  metadata?: Record<string, any>
23
24
  ) => void;
24
25
  userAccountType: UserAccountTypes;
26
+ originalPrice?: number;
25
27
  }
26
28
 
27
29
  export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
@@ -30,6 +32,7 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
30
32
  atlasIMG,
31
33
  onAddToCart,
32
34
  userAccountType,
35
+ originalPrice,
33
36
  }) => {
34
37
  // Get available characters from metadata
35
38
  const availableCharacters: ICharacterProps[] =
@@ -117,7 +120,45 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
117
120
  />
118
121
  </SelectedSkinNav>
119
122
  )}
120
- <ItemPrice>${item.price}</ItemPrice>
123
+ <PriceRow>
124
+ {originalPrice != null && (
125
+ <OriginalPrice style={{ display: 'flex', alignItems: 'center' }}>
126
+ {(item.currency as string) === 'DC' ? (
127
+ <>
128
+ <DCCoinWrapper $scale={0.8}>
129
+ <SpriteFromAtlas atlasIMG={atlasIMG} atlasJSON={atlasJSON} spriteKey="others/definya-coin.png" imgScale={0.8} />
130
+ </DCCoinWrapper>
131
+ {originalPrice.toLocaleString()}
132
+ </>
133
+ ) : `$${originalPrice.toFixed(2)}`}
134
+ </OriginalPrice>
135
+ )}
136
+ {(item.currency as string) === 'DC' ? (
137
+ <ItemPrice $onSale={originalPrice != null} style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
138
+ <DCCoinWrapper>
139
+ <SimpleTooltip content="Definya Coin" direction="top">
140
+ <SpriteFromAtlas atlasIMG={atlasIMG} atlasJSON={atlasJSON} spriteKey="others/definya-coin.png" imgScale={1} />
141
+ </SimpleTooltip>
142
+ </DCCoinWrapper>
143
+ {item.price.toLocaleString()}
144
+ </ItemPrice>
145
+ ) : (
146
+ <ItemPrice $onSale={originalPrice != null} style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
147
+ ${item.price.toFixed(2)}
148
+ {(item as any).dcPrice ? (
149
+ <>
150
+ <span style={{ margin: '0 4px' }}>·</span>
151
+ <DCCoinWrapper $scale={0.9}>
152
+ <SimpleTooltip content="Definya Coin" direction="top">
153
+ <SpriteFromAtlas atlasIMG={atlasIMG} atlasJSON={atlasJSON} spriteKey="others/definya-coin.png" imgScale={0.9} />
154
+ </SimpleTooltip>
155
+ </DCCoinWrapper>
156
+ {((item as any).dcPrice as number).toLocaleString()}
157
+ </>
158
+ ) : ''}
159
+ </ItemPrice>
160
+ )}
161
+ </PriceRow>
121
162
  </ItemDetails>
122
163
  <Controls>
123
164
  <CTAButton
@@ -125,6 +166,7 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
125
166
  label="Add"
126
167
  onClick={handleAddToCart}
127
168
  disabled={!hasRequiredAccount}
169
+ pulse
128
170
  />
129
171
  </Controls>
130
172
  </ItemWrapper>
@@ -134,52 +176,91 @@ export const StoreCharacterSkinRow: React.FC<IStoreCharacterSkinRowProps> = ({
134
176
  const ItemWrapper = styled.div<{ $isHighlighted: boolean }>`
135
177
  display: flex;
136
178
  align-items: center;
137
- gap: 0.75rem;
138
- padding: 0.5rem 1rem;
139
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
140
- background: ${props =>
141
- props.$isHighlighted ? 'rgba(255, 215, 0, 0.1)' : 'transparent'};
142
- border-left: ${props =>
143
- props.$isHighlighted ? '3px solid #ffd700' : '3px solid transparent'};
144
-
145
- &:last-child {
146
- border-bottom: none;
179
+ gap: 1rem;
180
+ padding: 0.6rem 0.8rem;
181
+ margin-bottom: 6px;
182
+ background: ${props => props.$isHighlighted ? 'linear-gradient(to right, rgba(255, 215, 0, 0.15), rgba(0, 0, 0, 0.4))' : 'linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.25))'};
183
+ border: 1px solid ${props => props.$isHighlighted ? 'rgba(255, 215, 0, 0.4)' : 'rgba(255, 255, 255, 0.1)'};
184
+ border-radius: 8px;
185
+ border-left: 4px solid ${props => props.$isHighlighted ? '#fbbf24' : 'rgba(255, 255, 255, 0.2)'};
186
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 2px 4px rgba(0,0,0,0.2);
187
+ transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
188
+ position: relative;
189
+ overflow: hidden;
190
+
191
+ /* Subtle inner glow for premium feel */
192
+ &::before {
193
+ content: '';
194
+ position: absolute;
195
+ top: 0; left: 0; right: 0; bottom: 0;
196
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
197
+ border-radius: 8px;
198
+ pointer-events: none;
199
+ }
200
+
201
+ &:hover {
202
+ background: ${p => p.$isHighlighted ? 'linear-gradient(to right, rgba(255, 215, 0, 0.2), rgba(0, 0, 0, 0.5))' : 'linear-gradient(to right, rgba(245, 158, 11, 0.15), rgba(0, 0, 0, 0.4))'};
203
+ border-color: ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.6)' : 'rgba(245, 158, 11, 0.3)'};
204
+ border-left-color: ${p => p.$isHighlighted ? '#fcd34d' : '#f59e0b'};
205
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 4px 16px rgba(0, 0, 0, 0.4);
206
+ transform: scale(1.01) translateY(-1px);
207
+ z-index: 10;
147
208
  }
148
209
  `;
149
210
 
150
211
  const ItemIconContainer = styled.div`
151
- width: 32px;
152
- height: 32px;
212
+ width: 40px;
213
+ height: 40px;
153
214
  display: flex;
154
215
  align-items: center;
155
216
  justify-content: center;
156
- border-radius: 4px;
217
+ background: rgba(0, 0, 0, 0.6);
218
+ border: 1px solid rgba(255, 255, 255, 0.1);
219
+ border-radius: 6px;
157
220
  padding: 4px;
221
+ box-shadow: inset 0 0 8px rgba(0,0,0,0.8);
158
222
  `;
159
223
 
160
224
  const ItemDetails = styled.div`
161
225
  flex: 1;
162
226
  display: flex;
163
227
  flex-direction: column;
164
- gap: 0.25rem;
228
+ gap: 0.35rem;
165
229
  `;
166
230
 
167
231
  const ItemName = styled.div`
168
232
  font-family: 'Press Start 2P', cursive;
169
- font-size: 0.75rem;
233
+ font-size: 0.8125rem;
170
234
  color: #ffffff;
235
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
171
236
  `;
172
237
 
173
- const SelectedSkin = styled.div`
238
+ const PriceRow = styled.div`
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 0.5rem;
242
+ margin: 0.15rem 0;
243
+ `;
244
+
245
+ const OriginalPrice = styled.span`
174
246
  font-family: 'Press Start 2P', cursive;
175
- font-size: 0.5rem;
176
- color: #fef08a;
247
+ font-size: 0.5625rem;
248
+ color: #9ca3af !important;
249
+ text-decoration: line-through;
250
+ text-shadow: none !important;
251
+ `;
252
+
253
+ const ItemPrice = styled.div<{ $onSale?: boolean }>`
254
+ font-family: 'Press Start 2P', cursive;
255
+ font-size: 0.6875rem;
256
+ color: ${p => p.$onSale ? '#4ade80' : '#fbbf24'};
257
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
177
258
  `;
178
259
 
179
- const ItemPrice = styled.div`
260
+ const SelectedSkin = styled.div`
180
261
  font-family: 'Press Start 2P', cursive;
181
- font-size: 0.625rem;
182
- color: #fef08a;
262
+ font-size: 0.5rem;
263
+ color: rgba(255, 255, 255, 0.7);
183
264
  `;
184
265
 
185
266
  const Controls = styled.div`
@@ -189,6 +270,16 @@ const Controls = styled.div`
189
270
  min-width: fit-content;
190
271
  `;
191
272
 
273
+ const DCCoinWrapper = styled.span<{ $scale?: number }>`
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ position: relative;
278
+ top: ${p => p.$scale ? `${-0.6 * p.$scale}rem` : '-0.6rem'};
279
+ left: ${p => p.$scale ? `${-0.5 * p.$scale}rem` : '-0.5rem'};
280
+ margin-right: 0.3rem;
281
+ `;
282
+
192
283
  // Styled arrow override for inline nav arrows
193
284
  const SkinNavArrow = styled(SelectArrow)`
194
285
  position: static;
@@ -1,12 +1,14 @@
1
1
  import { IProductBlueprint, UserAccountTypes } from '@rpg-engine/shared';
2
- import React, { useState } from 'react';
3
- import { FaCartPlus } from 'react-icons/fa';
2
+ import React, { useEffect, useState } from 'react';
3
+ import { FaBolt, FaCartPlus } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { SelectArrow } from '../Arrow/SelectArrow';
6
6
  import { CTAButton } from '../shared/CTAButton/CTAButton';
7
7
  import { ItemRowWrapper } from '../shared/ItemRowWrapper';
8
8
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
9
+ import { SimpleTooltip } from '../shared/SimpleTooltip';
9
10
  import { useQuantityControl } from '../../hooks/useQuantityControl';
11
+ import { IStoreBadge, StoreBadges } from './StoreBadges';
10
12
 
11
13
  interface IStoreItemRowProps {
12
14
  item: IProductBlueprint;
@@ -17,9 +19,18 @@ interface IStoreItemRowProps {
17
19
  quantity: number,
18
20
  metadata?: Record<string, any>
19
21
  ) => void;
22
+ onQuickBuy?: (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => void;
20
23
  userAccountType: UserAccountTypes;
21
24
  showTextInput?: boolean;
22
25
  textInputPlaceholder?: string;
26
+ badges?: IStoreBadge[];
27
+ buyCount?: number;
28
+ viewersCount?: number;
29
+ saleEndsAt?: string;
30
+ originalPrice?: number;
31
+ /** Fires once on mount — use for store_item_viewed analytics. */
32
+ onView?: (item: IProductBlueprint, position: number) => void;
33
+ positionInList?: number;
23
34
  }
24
35
 
25
36
  export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
@@ -27,11 +38,23 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
27
38
  atlasJSON,
28
39
  atlasIMG,
29
40
  onAddToCart,
41
+ onQuickBuy,
30
42
  userAccountType,
31
43
  showTextInput = false,
32
44
  textInputPlaceholder = item.inputPlaceholder,
45
+ badges,
46
+ buyCount,
47
+ viewersCount,
48
+ saleEndsAt,
49
+ originalPrice,
50
+ onView,
51
+ positionInList = 0,
33
52
  }) => {
34
53
  const [textInputValue, setTextInputValue] = useState('');
54
+
55
+ useEffect(() => {
56
+ onView?.(item, positionInList);
57
+ }, []);
35
58
  const {
36
59
  quantity,
37
60
  handleQuantityChange,
@@ -56,6 +79,17 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
56
79
  }
57
80
  };
58
81
 
82
+ const handleQuickBuy = () => {
83
+ if (!hasRequiredAccount || !onQuickBuy) return;
84
+ if (showTextInput) {
85
+ onQuickBuy(item, 1, { inputValue: textInputValue });
86
+ setTextInputValue('');
87
+ } else {
88
+ onQuickBuy(item, quantity);
89
+ resetQuantity();
90
+ }
91
+ };
92
+
59
93
  return (
60
94
  <ItemRowWrapper $isHighlighted={item.store?.isHighlighted || false}>
61
95
  <LeftSection>
@@ -73,11 +107,52 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
73
107
 
74
108
  <ItemDetails>
75
109
  <ItemName>{item.name}</ItemName>
76
- <ItemPrice>
77
- ${item.price}
78
- {(item as any).dcPrice ? ` · ${((item as any).dcPrice as number).toLocaleString()} DC` : ''}
79
- </ItemPrice>
110
+ <PriceRow>
111
+ {originalPrice != null && (
112
+ <OriginalPrice style={{ display: 'flex', alignItems: 'center' }}>
113
+ {(item.currency as string) === 'DC' ? (
114
+ <>
115
+ <DCCoinWrapper $scale={0.8}>
116
+ <SpriteFromAtlas atlasIMG={atlasIMG} atlasJSON={atlasJSON} spriteKey="others/definya-coin.png" imgScale={0.8} />
117
+ </DCCoinWrapper>
118
+ {originalPrice.toLocaleString()}
119
+ </>
120
+ ) : `$${originalPrice.toFixed(2)}`}
121
+ </OriginalPrice>
122
+ )}
123
+ {(item.currency as string) === 'DC' ? (
124
+ <ItemPrice $onSale={originalPrice != null} style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
125
+ <DCCoinWrapper>
126
+ <SimpleTooltip content="Definya Coin" direction="top">
127
+ <SpriteFromAtlas atlasIMG={atlasIMG} atlasJSON={atlasJSON} spriteKey="others/definya-coin.png" imgScale={1} />
128
+ </SimpleTooltip>
129
+ </DCCoinWrapper>
130
+ {item.price.toLocaleString()}
131
+ </ItemPrice>
132
+ ) : (
133
+ <ItemPrice $onSale={originalPrice != null} style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
134
+ ${item.price.toFixed(2)}
135
+ {(item as any).dcPrice ? (
136
+ <>
137
+ <span style={{ margin: '0 4px' }}>·</span>
138
+ <DCCoinWrapper $scale={0.9}>
139
+ <SimpleTooltip content="Definya Coin" direction="top">
140
+ <SpriteFromAtlas atlasIMG={atlasIMG} atlasJSON={atlasJSON} spriteKey="others/definya-coin.png" imgScale={0.9} />
141
+ </SimpleTooltip>
142
+ </DCCoinWrapper>
143
+ {((item as any).dcPrice as number).toLocaleString()}
144
+ </>
145
+ ) : ''}
146
+ </ItemPrice>
147
+ )}
148
+ </PriceRow>
80
149
  <ItemDescription>{item.description}</ItemDescription>
150
+ <StoreBadges
151
+ badges={badges}
152
+ buyCount={buyCount}
153
+ viewersCount={viewersCount}
154
+ saleEndsAt={saleEndsAt}
155
+ />
81
156
  </ItemDetails>
82
157
  </LeftSection>
83
158
 
@@ -117,11 +192,23 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
117
192
  </ArrowsContainer>
118
193
  ) : null}
119
194
 
195
+ {onQuickBuy && (
196
+ <CTAButton
197
+ icon={<FaBolt />}
198
+ label="Buy"
199
+ onClick={handleQuickBuy}
200
+ disabled={!hasRequiredAccount}
201
+ iconColor="#fff"
202
+ textColor="#fff"
203
+ pulse
204
+ />
205
+ )}
120
206
  <CTAButton
121
207
  icon={<FaCartPlus />}
122
208
  label="Add"
123
209
  onClick={handleAddToCartInternal}
124
210
  disabled={!hasRequiredAccount}
211
+ pulse
125
212
  />
126
213
  </Controls>
127
214
  </ItemRowWrapper>
@@ -131,51 +218,82 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
131
218
  const LeftSection = styled.div`
132
219
  display: flex;
133
220
  align-items: center;
134
- gap: 0.75rem;
221
+ gap: 1rem;
135
222
  flex: 1;
136
223
  min-width: 0;
137
224
  `;
138
225
 
139
226
  const ItemIconContainer = styled.div`
140
- width: 32px;
141
- height: 32px;
227
+ width: 40px;
228
+ height: 40px;
142
229
  display: flex;
143
230
  align-items: center;
144
231
  justify-content: center;
145
- border-radius: 4px;
232
+ background: rgba(0, 0, 0, 0.6);
233
+ border: 1px solid rgba(255, 255, 255, 0.1);
234
+ border-radius: 6px;
146
235
  padding: 4px;
236
+ box-shadow: inset 0 0 8px rgba(0,0,0,0.8);
147
237
  `;
148
238
 
149
239
  const ItemDetails = styled.div`
150
240
  flex: 1;
151
241
  display: flex;
152
242
  flex-direction: column;
153
- gap: 0.25rem;
243
+ gap: 0.35rem;
154
244
  `;
155
245
 
156
246
  const ItemName = styled.div`
157
247
  font-family: 'Press Start 2P', cursive;
158
- font-size: 0.75rem;
248
+ font-size: 0.8125rem;
159
249
  color: #ffffff;
250
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
160
251
  `;
161
252
 
162
- const ItemPrice = styled.div`
253
+ const PriceRow = styled.div`
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 0.5rem;
257
+ margin: 0.15rem 0;
258
+ `;
259
+
260
+ const OriginalPrice = styled.span`
163
261
  font-family: 'Press Start 2P', cursive;
164
- font-size: 0.625rem;
165
- color: #fef08a;
262
+ font-size: 0.5625rem;
263
+ color: #9ca3af !important;
264
+ text-decoration: line-through;
265
+ text-shadow: none !important;
266
+ `;
267
+
268
+ const ItemPrice = styled.div<{ $onSale?: boolean }>`
269
+ font-family: 'Press Start 2P', cursive;
270
+ font-size: 0.6875rem;
271
+ color: ${p => p.$onSale ? '#4ade80' : '#fbbf24'};
272
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
166
273
  `;
167
274
 
168
275
  const ItemDescription = styled.div`
169
276
  font-family: 'Press Start 2P', cursive;
170
277
  font-size: 0.625rem;
171
- color: rgba(255, 255, 255, 0.7);
278
+ color: rgba(255, 255, 255, 0.85);
172
279
  line-height: 1.4;
280
+ margin-top: 2px;
281
+ `;
282
+
283
+ const DCCoinWrapper = styled.span<{ $scale?: number }>`
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ position: relative;
288
+ top: ${p => p.$scale ? `${-0.6 * p.$scale}rem` : '-0.6rem'};
289
+ left: ${p => p.$scale ? `${-0.5 * p.$scale}rem` : '-0.5rem'};
290
+ margin-right: 0.3rem;
173
291
  `;
174
292
 
175
293
  const Controls = styled.div`
176
294
  display: flex;
177
295
  align-items: center;
178
- gap: 0.5rem;
296
+ gap: 0.75rem;
179
297
  min-width: fit-content;
180
298
  `;
181
299
 
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { FaHeadset, FaLock, FaRocket } from 'react-icons/fa';
3
+ import styled from 'styled-components';
4
+
5
+ export interface ITrustSignal {
6
+ icon?: React.ReactNode;
7
+ label: string;
8
+ }
9
+
10
+ export interface ITrustBarProps {
11
+ signals?: ITrustSignal[];
12
+ }
13
+
14
+ const DefaultSignals: React.FC = () => (
15
+ <>
16
+ <Signal><SignalIcon><FaLock /></SignalIcon><SignalLabel>Secure Payment</SignalLabel></Signal>
17
+ <Signal><SignalIcon><FaRocket /></SignalIcon><SignalLabel>Instant Delivery</SignalLabel></Signal>
18
+ <Signal><SignalIcon><FaHeadset /></SignalIcon><SignalLabel>24/7 Support</SignalLabel></Signal>
19
+ </>
20
+ );
21
+
22
+ export const TrustBar: React.FC<ITrustBarProps> = ({ signals }) => (
23
+ <Bar>
24
+ {signals
25
+ ? signals.map((s, i) => (
26
+ <Signal key={i}>
27
+ {s.icon && <SignalIcon>{s.icon}</SignalIcon>}
28
+ <SignalLabel>{s.label}</SignalLabel>
29
+ </Signal>
30
+ ))
31
+ : <DefaultSignals />
32
+ }
33
+ </Bar>
34
+ );
35
+
36
+ const Bar = styled.div`
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ gap: 1.25rem;
41
+ padding: 0.6rem 1rem;
42
+ background: rgba(74, 222, 128, 0.06);
43
+ border: 1px solid rgba(74, 222, 128, 0.2);
44
+ border-radius: 4px;
45
+
46
+ @media (max-width: 950px) {
47
+ flex-wrap: wrap;
48
+ gap: 0.6rem;
49
+ }
50
+ `;
51
+
52
+ const Signal = styled.div`
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 0.35rem;
56
+ `;
57
+
58
+ const SignalIcon = styled.span`
59
+ color: #4ade80;
60
+ font-size: 0.7rem;
61
+ display: flex;
62
+ `;
63
+
64
+ const SignalLabel = styled.span`
65
+ font-family: 'Press Start 2P', cursive;
66
+ font-size: 0.45rem;
67
+ color: rgba(74, 222, 128, 0.85);
68
+ white-space: nowrap;
69
+ `;