@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.
- package/dist/components/Store/CartView.d.ts +21 -1
- package/dist/components/Store/CountdownTimer.d.ts +7 -0
- package/dist/components/Store/FeaturedBanner.d.ts +23 -0
- package/dist/components/Store/PurchaseSuccess.d.ts +18 -0
- package/dist/components/Store/Store.d.ts +50 -2
- package/dist/components/Store/StoreBadges.d.ts +13 -0
- package/dist/components/Store/StoreCharacterSkinRow.d.ts +1 -0
- package/dist/components/Store/StoreItemRow.d.ts +10 -0
- package/dist/components/Store/TrustBar.d.ts +9 -0
- package/dist/components/Store/sections/StoreItemsSection.d.ts +13 -0
- package/dist/components/Store/sections/StorePacksSection.d.ts +11 -0
- package/dist/components/shared/CTAButton/CTAButton.d.ts +1 -0
- package/dist/components/shared/CustomScrollbar.d.ts +9 -0
- package/dist/index.d.ts +6 -1
- package/dist/long-bow.cjs.development.js +1284 -302
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +1281 -304
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Features/store/FeaturedBanner.stories.d.ts +1 -0
- package/dist/stories/Features/store/PurchaseSuccess.stories.d.ts +1 -0
- package/dist/stories/Features/store/StoreBadges.stories.d.ts +1 -0
- package/dist/stories/Features/store/TrustBar.stories.d.ts +1 -0
- package/package.json +2 -2
- package/src/components/Marketplace/BuyPanel.tsx +1 -1
- package/src/components/Store/CartView.tsx +143 -13
- package/src/components/Store/CountdownTimer.tsx +86 -0
- package/src/components/Store/FeaturedBanner.tsx +273 -0
- package/src/components/Store/PurchaseSuccess.tsx +258 -0
- package/src/components/Store/Store.tsx +236 -50
- package/src/components/Store/StoreBadges.tsx +94 -0
- package/src/components/Store/StoreCharacterSkinRow.tsx +113 -22
- package/src/components/Store/StoreItemRow.tsx +135 -17
- package/src/components/Store/TrustBar.tsx +69 -0
- package/src/components/Store/__test__/CountdownTimer.spec.tsx +100 -0
- package/src/components/Store/__test__/FeaturedBanner.spec.tsx +207 -0
- package/src/components/Store/__test__/PurchaseSuccess.spec.tsx +174 -0
- package/src/components/Store/__test__/StoreBadges.spec.tsx +133 -0
- package/src/components/Store/__test__/TrustBar.spec.tsx +85 -0
- package/src/components/Store/sections/StoreItemsSection.tsx +27 -1
- package/src/components/Store/sections/StorePacksSection.tsx +92 -28
- package/src/components/shared/CTAButton/CTAButton.tsx +25 -1
- package/src/components/shared/CustomScrollbar.ts +41 -0
- package/src/components/shared/ItemRowWrapper.tsx +26 -12
- package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -0
- package/src/components/shared/SpriteFromAtlas.tsx +4 -1
- package/src/index.tsx +6 -1
- package/src/stories/Features/store/FeaturedBanner.stories.tsx +121 -0
- package/src/stories/Features/store/PurchaseSuccess.stories.tsx +74 -0
- package/src/stories/Features/store/Store.stories.tsx +39 -3
- package/src/stories/Features/store/StoreBadges.stories.tsx +83 -0
- 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
|
-
<
|
|
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:
|
|
138
|
-
padding: 0.
|
|
139
|
-
|
|
140
|
-
background: ${props =>
|
|
141
|
-
|
|
142
|
-
border-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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:
|
|
152
|
-
height:
|
|
212
|
+
width: 40px;
|
|
213
|
+
height: 40px;
|
|
153
214
|
display: flex;
|
|
154
215
|
align-items: center;
|
|
155
216
|
justify-content: center;
|
|
156
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
176
|
-
color: #
|
|
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
|
|
260
|
+
const SelectedSkin = styled.div`
|
|
180
261
|
font-family: 'Press Start 2P', cursive;
|
|
181
|
-
font-size: 0.
|
|
182
|
-
color:
|
|
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
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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:
|
|
221
|
+
gap: 1rem;
|
|
135
222
|
flex: 1;
|
|
136
223
|
min-width: 0;
|
|
137
224
|
`;
|
|
138
225
|
|
|
139
226
|
const ItemIconContainer = styled.div`
|
|
140
|
-
width:
|
|
141
|
-
height:
|
|
227
|
+
width: 40px;
|
|
228
|
+
height: 40px;
|
|
142
229
|
display: flex;
|
|
143
230
|
align-items: center;
|
|
144
231
|
justify-content: center;
|
|
145
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
165
|
-
color: #
|
|
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.
|
|
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.
|
|
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
|
+
`;
|