@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,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[] =
|
|
167
|
-
|
|
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={
|
|
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 ??
|
|
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={
|
|
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
|
-
|
|
269
|
-
<
|
|
270
|
-
{
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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={<
|
|
279
|
-
|
|
280
|
-
onClick={onShowWallet}
|
|
457
|
+
icon={<FaShoppingCart />}
|
|
458
|
+
onClick={handleOpenCart}
|
|
281
459
|
/>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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={
|
|
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:
|
|
500
|
+
gap: 0.5rem;
|
|
334
501
|
position: relative;
|
|
335
502
|
`;
|
|
336
503
|
|
|
337
|
-
const
|
|
504
|
+
const HeaderRow = styled.div`
|
|
338
505
|
display: flex;
|
|
339
506
|
align-items: center;
|
|
340
|
-
justify-content:
|
|
341
|
-
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
355
|
-
|
|
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
|
+
`;
|