@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.
- 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 +1279 -303
- 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 +1276 -305
- 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/RPGUI/RPGUIScrollbar.tsx +2 -2
- package/src/components/Store/CartView.tsx +143 -33
- package/src/components/Store/CountdownTimer.tsx +86 -0
- package/src/components/Store/FeaturedBanner.tsx +270 -0
- package/src/components/Store/PurchaseSuccess.tsx +255 -0
- package/src/components/Store/Store.tsx +236 -51
- 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 +1 -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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
UserAccountTypes,
|
|
5
5
|
ItemType,
|
|
6
6
|
} from '@rpg-engine/shared';
|
|
7
|
-
import React from 'react';
|
|
7
|
+
import React, { useEffect } from 'react';
|
|
8
8
|
import styled from 'styled-components';
|
|
9
9
|
import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
|
|
10
10
|
import { StoreCharacterSkinRow } from '../StoreCharacterSkinRow';
|
|
@@ -12,6 +12,7 @@ import { StoreItemRow } from '../StoreItemRow';
|
|
|
12
12
|
import { SegmentedToggle } from '../../shared/SegmentedToggle';
|
|
13
13
|
import { SearchBar } from '../../shared/SearchBar/SearchBar';
|
|
14
14
|
import { useStoreFiltering } from '../../../hooks/useStoreFiltering';
|
|
15
|
+
import { IStoreBadge } from '../StoreBadges';
|
|
15
16
|
|
|
16
17
|
interface IStoreItemsSectionProps {
|
|
17
18
|
items: IProductBlueprint[];
|
|
@@ -20,19 +21,29 @@ interface IStoreItemsSectionProps {
|
|
|
20
21
|
quantity: number,
|
|
21
22
|
metadata?: Record<string, any>
|
|
22
23
|
) => void;
|
|
24
|
+
onQuickBuy?: (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => void;
|
|
23
25
|
atlasJSON: Record<string, any>;
|
|
24
26
|
atlasIMG: string;
|
|
25
27
|
userAccountType?: UserAccountTypes;
|
|
26
28
|
textInputItemKeys?: string[];
|
|
29
|
+
itemBadges?: Record<string, { badges?: IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
|
|
30
|
+
/** Fires when an item row becomes visible. Passes item and its 0-based position. */
|
|
31
|
+
onItemView?: (item: IProductBlueprint, position: number) => void;
|
|
32
|
+
/** Fires when the category filter changes. Passes new category and item count. */
|
|
33
|
+
onCategoryChange?: (category: string, itemsShown: number) => void;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
30
37
|
items,
|
|
31
38
|
onAddToCart,
|
|
39
|
+
onQuickBuy,
|
|
32
40
|
atlasJSON,
|
|
33
41
|
atlasIMG,
|
|
34
42
|
userAccountType,
|
|
35
43
|
textInputItemKeys = [],
|
|
44
|
+
itemBadges = {},
|
|
45
|
+
onItemView,
|
|
46
|
+
onCategoryChange,
|
|
36
47
|
}) => {
|
|
37
48
|
const {
|
|
38
49
|
searchQuery,
|
|
@@ -43,7 +54,14 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
|
43
54
|
filteredItems,
|
|
44
55
|
} = useStoreFiltering(items);
|
|
45
56
|
|
|
57
|
+
// Fire category change event when the filter changes
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
onCategoryChange?.(selectedCategory, filteredItems.length);
|
|
60
|
+
}, [selectedCategory, filteredItems.length]);
|
|
61
|
+
|
|
46
62
|
const renderStoreItem = (item: IProductBlueprint) => {
|
|
63
|
+
const meta = itemBadges[item.key];
|
|
64
|
+
const position = filteredItems.indexOf(item);
|
|
47
65
|
// Prefer a specialized character skin row when needed
|
|
48
66
|
if (item.metadataType === MetadataType.CharacterSkin) {
|
|
49
67
|
return (
|
|
@@ -66,8 +84,12 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
|
66
84
|
atlasJSON={atlasJSON}
|
|
67
85
|
atlasIMG={atlasIMG}
|
|
68
86
|
onAddToCart={onAddToCart}
|
|
87
|
+
onQuickBuy={onQuickBuy}
|
|
69
88
|
userAccountType={userAccountType || UserAccountTypes.Free}
|
|
70
89
|
showTextInput
|
|
90
|
+
onView={onItemView}
|
|
91
|
+
positionInList={position}
|
|
92
|
+
{...meta}
|
|
71
93
|
/>
|
|
72
94
|
);
|
|
73
95
|
}
|
|
@@ -79,7 +101,11 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
|
79
101
|
atlasJSON={atlasJSON}
|
|
80
102
|
atlasIMG={atlasIMG}
|
|
81
103
|
onAddToCart={onAddToCart}
|
|
104
|
+
onQuickBuy={onQuickBuy}
|
|
82
105
|
userAccountType={userAccountType || UserAccountTypes.Free}
|
|
106
|
+
onView={onItemView}
|
|
107
|
+
positionInList={position}
|
|
108
|
+
{...meta}
|
|
83
109
|
/>
|
|
84
110
|
);
|
|
85
111
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IItemPack } from '@rpg-engine/shared';
|
|
2
|
-
import React, { useCallback } from 'react';
|
|
3
|
-
import { FaCartPlus } from 'react-icons/fa';
|
|
2
|
+
import React, { useCallback, useEffect } from 'react';
|
|
3
|
+
import { FaBolt, FaCartPlus } from 'react-icons/fa';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { CTAButton } from '../../shared/CTAButton/CTAButton';
|
|
6
6
|
import { ItemRowWrapper } from '../../shared/ItemRowWrapper';
|
|
@@ -9,29 +9,55 @@ import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableCont
|
|
|
9
9
|
import { SelectArrow } from '../../Arrow/SelectArrow';
|
|
10
10
|
import { usePackFiltering } from '../../../hooks/usePackFiltering';
|
|
11
11
|
import { useQuantityControl } from '../../../hooks/useQuantityControl';
|
|
12
|
+
import { IStoreBadge, StoreBadges } from '../StoreBadges';
|
|
12
13
|
|
|
13
14
|
interface IStorePacksSectionProps {
|
|
14
15
|
packs: IItemPack[];
|
|
15
16
|
onAddToCart: (pack: IItemPack, quantity: number) => void;
|
|
17
|
+
onQuickBuy?: (pack: IItemPack, quantity: number) => void;
|
|
16
18
|
onSelectPack?: (pack: IItemPack) => void;
|
|
17
19
|
atlasJSON?: any;
|
|
18
20
|
atlasIMG?: string;
|
|
21
|
+
packBadges?: Record<string, { badges?: IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
|
|
22
|
+
/** Fires once on mount per pack row — use for pack_viewed analytics. */
|
|
23
|
+
onPackView?: (pack: IItemPack, position: number) => void;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface IPackRowItemProps {
|
|
22
27
|
pack: IItemPack;
|
|
23
28
|
onAddToCart: (pack: IItemPack, quantity: number) => void;
|
|
29
|
+
onQuickBuy?: (pack: IItemPack, quantity: number) => void;
|
|
24
30
|
renderPackIcon: (pack: IItemPack) => React.ReactNode;
|
|
31
|
+
badges?: IStoreBadge[];
|
|
32
|
+
buyCount?: number;
|
|
33
|
+
viewersCount?: number;
|
|
34
|
+
saleEndsAt?: string;
|
|
35
|
+
originalPrice?: number;
|
|
36
|
+
onPackView?: (pack: IItemPack, position: number) => void;
|
|
37
|
+
positionInList?: number;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
const PackRowItem: React.FC<IPackRowItemProps> = ({
|
|
40
|
+
const PackRowItem: React.FC<IPackRowItemProps> = ({
|
|
41
|
+
pack, onAddToCart, onQuickBuy, renderPackIcon,
|
|
42
|
+
badges, buyCount, viewersCount, saleEndsAt, originalPrice,
|
|
43
|
+
onPackView, positionInList = 0,
|
|
44
|
+
}) => {
|
|
28
45
|
const { quantity, handleQuantityChange, handleBlur, incrementQuantity, decrementQuantity, resetQuantity } = useQuantityControl();
|
|
29
46
|
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
onPackView?.(pack, positionInList);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
30
51
|
const handleAdd = () => {
|
|
31
52
|
onAddToCart(pack, quantity);
|
|
32
53
|
resetQuantity();
|
|
33
54
|
};
|
|
34
55
|
|
|
56
|
+
const handleQuickBuy = () => {
|
|
57
|
+
onQuickBuy?.(pack, quantity);
|
|
58
|
+
resetQuantity();
|
|
59
|
+
};
|
|
60
|
+
|
|
35
61
|
return (
|
|
36
62
|
<PackRow>
|
|
37
63
|
<LeftSection>
|
|
@@ -41,8 +67,14 @@ const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPac
|
|
|
41
67
|
|
|
42
68
|
<PackDetails>
|
|
43
69
|
<PackName>{pack.title}</PackName>
|
|
44
|
-
<
|
|
70
|
+
<PackPriceRow>
|
|
71
|
+
{originalPrice != null && (
|
|
72
|
+
<PackOriginalPrice>${originalPrice.toFixed(2)}</PackOriginalPrice>
|
|
73
|
+
)}
|
|
74
|
+
<PackPrice $onSale={originalPrice != null}>${pack.priceUSD}</PackPrice>
|
|
75
|
+
</PackPriceRow>
|
|
45
76
|
{pack.description && <PackDescription>{pack.description}</PackDescription>}
|
|
77
|
+
<StoreBadges badges={badges} buyCount={buyCount} viewersCount={viewersCount} saleEndsAt={saleEndsAt} />
|
|
46
78
|
</PackDetails>
|
|
47
79
|
</LeftSection>
|
|
48
80
|
|
|
@@ -60,11 +92,10 @@ const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPac
|
|
|
60
92
|
/>
|
|
61
93
|
<SelectArrow direction="right" onPointerDown={incrementQuantity} size={24} />
|
|
62
94
|
</ArrowsContainer>
|
|
63
|
-
|
|
64
|
-
icon={<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/>
|
|
95
|
+
{onQuickBuy && (
|
|
96
|
+
<CTAButton icon={<FaBolt />} label="Buy" onClick={handleQuickBuy} iconColor="#fff" textColor="#fff" />
|
|
97
|
+
)}
|
|
98
|
+
<CTAButton icon={<FaCartPlus />} label="Add" onClick={handleAdd} pulse />
|
|
68
99
|
</Controls>
|
|
69
100
|
</PackRow>
|
|
70
101
|
);
|
|
@@ -73,8 +104,11 @@ const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPac
|
|
|
73
104
|
export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
|
|
74
105
|
packs,
|
|
75
106
|
onAddToCart,
|
|
107
|
+
onQuickBuy,
|
|
76
108
|
atlasJSON,
|
|
77
109
|
atlasIMG,
|
|
110
|
+
packBadges = {},
|
|
111
|
+
onPackView,
|
|
78
112
|
}) => {
|
|
79
113
|
const { searchQuery, setSearchQuery, filteredPacks } = usePackFiltering(packs);
|
|
80
114
|
|
|
@@ -95,15 +129,23 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
|
|
|
95
129
|
);
|
|
96
130
|
|
|
97
131
|
const renderPack = useCallback(
|
|
98
|
-
(pack: IItemPack) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
(pack: IItemPack) => {
|
|
133
|
+
const meta = packBadges[pack.key];
|
|
134
|
+
const position = filteredPacks.indexOf(pack);
|
|
135
|
+
return (
|
|
136
|
+
<PackRowItem
|
|
137
|
+
key={pack.key}
|
|
138
|
+
pack={pack}
|
|
139
|
+
onAddToCart={onAddToCart}
|
|
140
|
+
onQuickBuy={onQuickBuy}
|
|
141
|
+
renderPackIcon={renderPackIcon}
|
|
142
|
+
onPackView={onPackView}
|
|
143
|
+
positionInList={position}
|
|
144
|
+
{...meta}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
[onAddToCart, onQuickBuy, renderPackIcon, packBadges, onPackView, filteredPacks]
|
|
107
149
|
);
|
|
108
150
|
|
|
109
151
|
return (
|
|
@@ -127,18 +169,23 @@ const PackRow = styled(ItemRowWrapper)``;
|
|
|
127
169
|
const LeftSection = styled.div`
|
|
128
170
|
display: flex;
|
|
129
171
|
align-items: center;
|
|
130
|
-
gap:
|
|
172
|
+
gap: 1.25rem;
|
|
131
173
|
flex: 1;
|
|
132
174
|
min-width: 0;
|
|
133
175
|
`;
|
|
134
176
|
|
|
135
177
|
const PackIconContainer = styled.div`
|
|
136
|
-
width:
|
|
137
|
-
height:
|
|
178
|
+
width: 48px;
|
|
179
|
+
height: 48px;
|
|
138
180
|
flex-shrink: 0;
|
|
139
181
|
display: flex;
|
|
140
182
|
align-items: center;
|
|
141
183
|
justify-content: center;
|
|
184
|
+
background: rgba(0, 0, 0, 0.6);
|
|
185
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
186
|
+
border-radius: 8px;
|
|
187
|
+
padding: 4px;
|
|
188
|
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
|
|
142
189
|
|
|
143
190
|
img {
|
|
144
191
|
width: 100%;
|
|
@@ -151,33 +198,50 @@ const PackDetails = styled.div`
|
|
|
151
198
|
flex: 1;
|
|
152
199
|
display: flex;
|
|
153
200
|
flex-direction: column;
|
|
154
|
-
gap: 0.
|
|
201
|
+
gap: 0.4rem;
|
|
155
202
|
min-width: 0;
|
|
156
203
|
`;
|
|
157
204
|
|
|
158
205
|
const PackName = styled.div`
|
|
159
206
|
font-family: 'Press Start 2P', cursive;
|
|
160
|
-
font-size: 0.
|
|
207
|
+
font-size: 0.875rem;
|
|
161
208
|
color: #ffffff;
|
|
209
|
+
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
|
210
|
+
letter-spacing: 0.5px;
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const PackPriceRow = styled.div`
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 0.5rem;
|
|
162
217
|
`;
|
|
163
218
|
|
|
164
|
-
const
|
|
219
|
+
const PackOriginalPrice = styled.span`
|
|
165
220
|
font-family: 'Press Start 2P', cursive;
|
|
166
221
|
font-size: 0.625rem;
|
|
167
|
-
color:
|
|
222
|
+
color: rgba(255, 255, 255, 0.5);
|
|
223
|
+
text-decoration: line-through;
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const PackPrice = styled.div<{ $onSale?: boolean }>`
|
|
227
|
+
font-family: 'Press Start 2P', cursive;
|
|
228
|
+
font-size: 0.75rem;
|
|
229
|
+
color: ${p => p.$onSale ? '#4ade80' : '#fbbf24'};
|
|
230
|
+
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
|
|
168
231
|
`;
|
|
169
232
|
|
|
170
233
|
const PackDescription = styled.div`
|
|
171
234
|
font-family: 'Press Start 2P', cursive;
|
|
172
235
|
font-size: 0.625rem;
|
|
173
|
-
color: rgba(255, 255, 255, 0.
|
|
174
|
-
line-height: 1.
|
|
236
|
+
color: rgba(255, 255, 255, 0.85);
|
|
237
|
+
line-height: 1.5;
|
|
238
|
+
margin-top: 2px;
|
|
175
239
|
`;
|
|
176
240
|
|
|
177
241
|
const Controls = styled.div`
|
|
178
242
|
display: flex;
|
|
179
243
|
align-items: center;
|
|
180
|
-
gap: 0.
|
|
244
|
+
gap: 0.75rem;
|
|
181
245
|
flex-shrink: 0;
|
|
182
246
|
`;
|
|
183
247
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import styled, { css } from 'styled-components';
|
|
2
|
+
import styled, { css, keyframes } from 'styled-components';
|
|
3
3
|
|
|
4
4
|
interface ICTAButtonProps {
|
|
5
5
|
icon: React.ReactNode;
|
|
@@ -10,6 +10,7 @@ interface ICTAButtonProps {
|
|
|
10
10
|
textColor?: string;
|
|
11
11
|
iconColor?: string;
|
|
12
12
|
disabled?: boolean;
|
|
13
|
+
pulse?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export const CTAButton: React.FC<ICTAButtonProps> = ({
|
|
@@ -21,6 +22,7 @@ export const CTAButton: React.FC<ICTAButtonProps> = ({
|
|
|
21
22
|
textColor = '#ffffff',
|
|
22
23
|
iconColor = '#f59e0b',
|
|
23
24
|
disabled = false,
|
|
25
|
+
pulse = false,
|
|
24
26
|
}) => {
|
|
25
27
|
return (
|
|
26
28
|
<ButtonContainer
|
|
@@ -29,6 +31,7 @@ export const CTAButton: React.FC<ICTAButtonProps> = ({
|
|
|
29
31
|
$fullWidth={fullWidth}
|
|
30
32
|
$disabled={disabled}
|
|
31
33
|
$color={textColor}
|
|
34
|
+
$pulse={pulse}
|
|
32
35
|
>
|
|
33
36
|
<ButtonContent>
|
|
34
37
|
<IconWrapper $color={iconColor} $disabled={disabled}>
|
|
@@ -44,10 +47,24 @@ export const CTAButton: React.FC<ICTAButtonProps> = ({
|
|
|
44
47
|
);
|
|
45
48
|
};
|
|
46
49
|
|
|
50
|
+
const pulseAnimation = keyframes`
|
|
51
|
+
0% {
|
|
52
|
+
box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
|
|
53
|
+
}
|
|
54
|
+
50% {
|
|
55
|
+
box-shadow: 0 0 20px rgba(245, 158, 11, 0.8), 0 0 10px rgba(245, 158, 11, 0.5) inset;
|
|
56
|
+
border-color: #fbbf24;
|
|
57
|
+
}
|
|
58
|
+
100% {
|
|
59
|
+
box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
47
63
|
const ButtonContainer = styled.div<{
|
|
48
64
|
$fullWidth: boolean;
|
|
49
65
|
$disabled: boolean;
|
|
50
66
|
$color: string;
|
|
67
|
+
$pulse: boolean;
|
|
51
68
|
}>`
|
|
52
69
|
display: inline-flex;
|
|
53
70
|
align-items: center;
|
|
@@ -70,6 +87,13 @@ const ButtonContainer = styled.div<{
|
|
|
70
87
|
justify-content: center;
|
|
71
88
|
`}
|
|
72
89
|
|
|
90
|
+
${props =>
|
|
91
|
+
props.$pulse &&
|
|
92
|
+
!props.$disabled &&
|
|
93
|
+
css`
|
|
94
|
+
animation: ${pulseAnimation} 2s infinite ease-in-out;
|
|
95
|
+
`}
|
|
96
|
+
|
|
73
97
|
&:hover {
|
|
74
98
|
background: ${props =>
|
|
75
99
|
props.$disabled ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.4)'};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A reusable CSS mixin for a sleek, thin, 4px webkit scrollbar.
|
|
5
|
+
* Drops the thick native OS scrollbar arrows and housing.
|
|
6
|
+
*/
|
|
7
|
+
export const customScrollbarCSS = css`
|
|
8
|
+
scrollbar-width: thin;
|
|
9
|
+
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
|
10
|
+
|
|
11
|
+
&::-webkit-scrollbar {
|
|
12
|
+
width: 4px;
|
|
13
|
+
height: 4px;
|
|
14
|
+
}
|
|
15
|
+
&::-webkit-scrollbar-track {
|
|
16
|
+
background: transparent;
|
|
17
|
+
}
|
|
18
|
+
&::-webkit-scrollbar-thumb {
|
|
19
|
+
background: rgba(255, 255, 255, 0.2);
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
|
|
22
|
+
&:hover {
|
|
23
|
+
background: rgba(255, 255, 255, 0.3);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
&::-webkit-scrollbar-corner {
|
|
27
|
+
background: transparent;
|
|
28
|
+
}
|
|
29
|
+
&::-webkit-scrollbar-button {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A basic div wrapper that applies the custom scrollbar style and guarantees overflow-y.
|
|
36
|
+
*/
|
|
37
|
+
export const CustomScrollbarContainer = styled.div`
|
|
38
|
+
${customScrollbarCSS}
|
|
39
|
+
overflow-y: auto;
|
|
40
|
+
overflow-x: hidden;
|
|
41
|
+
`;
|
|
@@ -4,19 +4,33 @@ export const ItemRowWrapper = styled.div<{ $isHighlighted?: boolean }>`
|
|
|
4
4
|
display: flex;
|
|
5
5
|
align-items: center;
|
|
6
6
|
justify-content: space-between;
|
|
7
|
-
padding: 0.6rem
|
|
8
|
-
margin-bottom:
|
|
9
|
-
background: ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.
|
|
10
|
-
border: 1px solid rgba(255, 255, 255, 0.
|
|
11
|
-
border-radius:
|
|
12
|
-
border-left: 4px solid ${p => p.$isHighlighted ? '#
|
|
13
|
-
|
|
7
|
+
padding: 0.6rem 0.8rem;
|
|
8
|
+
margin-bottom: 6px;
|
|
9
|
+
background: ${p => p.$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))'};
|
|
10
|
+
border: 1px solid ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.4)' : 'rgba(255, 255, 255, 0.1)'};
|
|
11
|
+
border-radius: 8px;
|
|
12
|
+
border-left: 4px solid ${p => p.$isHighlighted ? '#fbbf24' : 'rgba(255, 255, 255, 0.2)'};
|
|
13
|
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 2px 4px rgba(0,0,0,0.2);
|
|
14
|
+
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
15
|
+
position: relative;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
|
|
18
|
+
/* Subtle inner glow for premium feel */
|
|
19
|
+
&::before {
|
|
20
|
+
content: '';
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
23
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
14
27
|
|
|
15
28
|
&:hover {
|
|
16
|
-
background: rgba(245, 158, 11, 0.
|
|
17
|
-
border-color: rgba(245, 158, 11, 0.
|
|
18
|
-
border-left-color: #f59e0b;
|
|
19
|
-
box-shadow: 0 4px
|
|
20
|
-
transform: translateY(-1px);
|
|
29
|
+
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))'};
|
|
30
|
+
border-color: ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.6)' : 'rgba(245, 158, 11, 0.3)'};
|
|
31
|
+
border-left-color: ${p => p.$isHighlighted ? '#fcd34d' : '#f59e0b'};
|
|
32
|
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
33
|
+
transform: scale(1.01) translateY(-1px);
|
|
34
|
+
z-index: 10;
|
|
21
35
|
}
|
|
22
36
|
`;
|
|
@@ -68,7 +68,10 @@ export const SpriteFromAtlas: React.FC<IProps> = ({
|
|
|
68
68
|
height={height}
|
|
69
69
|
hasHover={grayScale}
|
|
70
70
|
onPointerDown={onPointerDown}
|
|
71
|
-
style={
|
|
71
|
+
style={{
|
|
72
|
+
...(centered ? { display: 'flex', justifyContent: 'center', alignItems: 'center' } : {}),
|
|
73
|
+
...containerStyle
|
|
74
|
+
}}
|
|
72
75
|
>
|
|
73
76
|
<ImgSprite
|
|
74
77
|
className={`sprite-from-atlas-img ${imgClassname || ''}`}
|
package/src/index.tsx
CHANGED
|
@@ -67,10 +67,15 @@ export * from './components/SocialModal/SocialModal';
|
|
|
67
67
|
export * from './components/Spellbook/Spellbook';
|
|
68
68
|
export * from './components/Stepper';
|
|
69
69
|
export * from './components/Store/CartView';
|
|
70
|
+
export * from './components/Store/CountdownTimer';
|
|
71
|
+
export * from './components/Store/FeaturedBanner';
|
|
70
72
|
export * from './components/Store/hooks/useStoreCart';
|
|
71
73
|
export * from './components/Store/MetadataCollector';
|
|
72
|
-
export * from './components/Store/Store';
|
|
73
74
|
export * from './components/Store/PaymentMethodModal';
|
|
75
|
+
export * from './components/Store/PurchaseSuccess';
|
|
76
|
+
export * from './components/Store/Store';
|
|
77
|
+
export * from './components/Store/StoreBadges';
|
|
78
|
+
export * from './components/Store/TrustBar';
|
|
74
79
|
export * from './components/Table/Table';
|
|
75
80
|
export * from './components/TextArea';
|
|
76
81
|
export * from './components/TimeWidget/TimeWidget';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { RPGUIRoot } from '../../../components/RPGUI/RPGUIRoot';
|
|
4
|
+
import { FeaturedBanner } from '../../../components/Store/FeaturedBanner';
|
|
5
|
+
import itemsAtlasJSON from '../../../mocks/atlas/items/items.json';
|
|
6
|
+
import itemsAtlasIMG from '../../../mocks/atlas/items/items.png';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Features/Store/FeaturedBanner',
|
|
10
|
+
component: FeaturedBanner,
|
|
11
|
+
parameters: { layout: 'centered' },
|
|
12
|
+
decorators: [
|
|
13
|
+
Story => (
|
|
14
|
+
<RPGUIRoot>
|
|
15
|
+
<div style={{ width: 800 }}>
|
|
16
|
+
<Story />
|
|
17
|
+
</div>
|
|
18
|
+
</RPGUIRoot>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
} satisfies Meta<typeof FeaturedBanner>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof FeaturedBanner>;
|
|
25
|
+
|
|
26
|
+
const future = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(); // 2 hours from now
|
|
27
|
+
const nearFuture = new Date(Date.now() + 4 * 60 * 1000).toISOString(); // 4 minutes from now
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<FeaturedBanner
|
|
32
|
+
items={[
|
|
33
|
+
{
|
|
34
|
+
key: 'starter-pack',
|
|
35
|
+
name: 'Starter Pack',
|
|
36
|
+
description: 'Everything you need to begin your adventure',
|
|
37
|
+
price: 4.99,
|
|
38
|
+
originalPrice: 9.99,
|
|
39
|
+
badge: 'SALE',
|
|
40
|
+
endsAt: future,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'dragon-skin',
|
|
44
|
+
name: 'Dragon Skin',
|
|
45
|
+
description: 'Rare dragon warrior appearance',
|
|
46
|
+
price: 14.99,
|
|
47
|
+
badge: 'LIMITED',
|
|
48
|
+
endsAt: nearFuture,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'gold-pack',
|
|
52
|
+
name: 'Gold Pack',
|
|
53
|
+
description: 'Premium content bundle',
|
|
54
|
+
price: 49.99,
|
|
55
|
+
badge: 'POPULAR',
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
atlasJSON={itemsAtlasJSON}
|
|
59
|
+
atlasIMG={itemsAtlasIMG}
|
|
60
|
+
onSelectItem={item => console.log('Selected:', item.key)}
|
|
61
|
+
onQuickBuy={item => console.log('Quick buy:', item.key)}
|
|
62
|
+
/>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithAtlasSprites: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<FeaturedBanner
|
|
69
|
+
items={[
|
|
70
|
+
{
|
|
71
|
+
key: 'atlas-item',
|
|
72
|
+
name: 'Atlas Item',
|
|
73
|
+
description: 'Rendered from sprite atlas',
|
|
74
|
+
texturePath: 'items/greater_life_potion.png',
|
|
75
|
+
price: 7.99,
|
|
76
|
+
originalPrice: 12.99,
|
|
77
|
+
badge: 'NEW',
|
|
78
|
+
},
|
|
79
|
+
]}
|
|
80
|
+
atlasJSON={itemsAtlasJSON}
|
|
81
|
+
atlasIMG={itemsAtlasIMG}
|
|
82
|
+
onSelectItem={item => console.log('Selected:', item.key)}
|
|
83
|
+
/>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const NoQuickBuy: Story = {
|
|
88
|
+
render: () => (
|
|
89
|
+
<FeaturedBanner
|
|
90
|
+
items={[
|
|
91
|
+
{ key: 'item-a', name: 'Item A', price: 5.99, badge: 'EVENT' },
|
|
92
|
+
{ key: 'item-b', name: 'Item B', price: 9.99, originalPrice: 14.99, badge: 'SALE' },
|
|
93
|
+
]}
|
|
94
|
+
atlasJSON={itemsAtlasJSON}
|
|
95
|
+
atlasIMG={itemsAtlasIMG}
|
|
96
|
+
onSelectItem={item => console.log('Selected:', item.key)}
|
|
97
|
+
/>
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const SingleItem: Story = {
|
|
102
|
+
render: () => (
|
|
103
|
+
<FeaturedBanner
|
|
104
|
+
items={[
|
|
105
|
+
{
|
|
106
|
+
key: 'solo',
|
|
107
|
+
name: 'Ultimate Bundle',
|
|
108
|
+
description: 'The most complete pack available — everything in one',
|
|
109
|
+
price: 99.99,
|
|
110
|
+
originalPrice: 149.99,
|
|
111
|
+
badge: 'LIMITED',
|
|
112
|
+
endsAt: future,
|
|
113
|
+
},
|
|
114
|
+
]}
|
|
115
|
+
atlasJSON={itemsAtlasJSON}
|
|
116
|
+
atlasIMG={itemsAtlasIMG}
|
|
117
|
+
onSelectItem={item => console.log('Selected:', item.key)}
|
|
118
|
+
onQuickBuy={item => console.log('Quick buy:', item.key)}
|
|
119
|
+
/>
|
|
120
|
+
),
|
|
121
|
+
};
|