@rpg-engine/long-bow 0.8.182 → 0.8.184
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/Marketplace/CharacterListingForm.d.ts +4 -2
- package/dist/components/Marketplace/CharacterListingModal.d.ts +17 -0
- package/dist/components/Marketplace/CharacterMarketplacePanel.d.ts +4 -0
- package/dist/components/Marketplace/CharacterMarketplaceRows.d.ts +6 -0
- package/dist/components/Marketplace/Marketplace.d.ts +3 -2
- package/dist/components/Marketplace/MyCharacterListingsPanel.d.ts +4 -0
- package/dist/components/shared/DCRateStrip.d.ts +2 -0
- package/dist/components/shared/RadioOption.d.ts +22 -0
- package/dist/long-bow.cjs.development.js +476 -371
- 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 +477 -372
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Features/marketplace/CharacterListingModal.stories.d.ts +8 -0
- package/dist/stories/shared/RadioOption.stories.d.ts +8 -0
- package/package.json +1 -1
- package/src/components/DCWallet/DCWalletContent.tsx +5 -47
- package/src/components/Marketplace/CharacterListingForm.tsx +56 -351
- package/src/components/Marketplace/CharacterListingModal.tsx +404 -0
- package/src/components/Marketplace/CharacterMarketplacePanel.tsx +112 -67
- package/src/components/Marketplace/CharacterMarketplaceRows.tsx +21 -9
- package/src/components/Marketplace/Marketplace.tsx +11 -6
- package/src/components/Marketplace/MyCharacterListingsPanel.tsx +15 -14
- package/src/components/shared/DCRateStrip.tsx +67 -0
- package/src/components/shared/RadioOption.tsx +93 -0
- package/src/stories/Features/marketplace/CharacterListingModal.stories.tsx +131 -0
- package/src/stories/Features/marketplace/CharacterMarketplace.stories.tsx +39 -23
- package/src/stories/shared/RadioOption.stories.tsx +93 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ICharacterListing } from '@rpg-engine/shared';
|
|
1
|
+
import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
|
|
2
2
|
import { Coins } from 'pixelarticons/react/Coins';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import styled from 'styled-components';
|
|
@@ -9,8 +9,12 @@ import { Ellipsis } from '../shared/Ellipsis';
|
|
|
9
9
|
|
|
10
10
|
export interface ICharacterMarketplaceRowsProps {
|
|
11
11
|
listing: ICharacterListing;
|
|
12
|
+
/** Items atlas — for UI sprites like the DC coin */
|
|
12
13
|
atlasJSON: any;
|
|
13
14
|
atlasIMG: any;
|
|
15
|
+
/** Entities atlas — for character sprites */
|
|
16
|
+
characterAtlasJSON: any;
|
|
17
|
+
characterAtlasIMG: any;
|
|
14
18
|
onCharacterBuy?: () => void;
|
|
15
19
|
onCharacterDelist?: () => void;
|
|
16
20
|
disabled?: boolean;
|
|
@@ -20,6 +24,8 @@ export const CharacterMarketplaceRows: React.FC<ICharacterMarketplaceRowsProps>
|
|
|
20
24
|
listing,
|
|
21
25
|
atlasJSON,
|
|
22
26
|
atlasIMG,
|
|
27
|
+
characterAtlasJSON,
|
|
28
|
+
characterAtlasIMG,
|
|
23
29
|
onCharacterBuy,
|
|
24
30
|
onCharacterDelist,
|
|
25
31
|
disabled,
|
|
@@ -32,8 +38,8 @@ export const CharacterMarketplaceRows: React.FC<ICharacterMarketplaceRowsProps>
|
|
|
32
38
|
<SpriteContainer>
|
|
33
39
|
<CharacterSprite>
|
|
34
40
|
<SpriteFromAtlas
|
|
35
|
-
atlasIMG={
|
|
36
|
-
atlasJSON={
|
|
41
|
+
atlasIMG={characterAtlasIMG}
|
|
42
|
+
atlasJSON={characterAtlasJSON}
|
|
37
43
|
spriteKey={`${characterSnapshot.textureKey}/down/standing/0.png`}
|
|
38
44
|
imgScale={2.5}
|
|
39
45
|
height={56}
|
|
@@ -53,15 +59,15 @@ export const CharacterMarketplaceRows: React.FC<ICharacterMarketplaceRowsProps>
|
|
|
53
59
|
<CharacterClass>{characterSnapshot.class}</CharacterClass>
|
|
54
60
|
)}
|
|
55
61
|
<PriceRow>
|
|
56
|
-
<
|
|
62
|
+
<DCCoinWrapper>
|
|
57
63
|
<SpriteFromAtlas
|
|
58
64
|
atlasIMG={atlasIMG}
|
|
59
65
|
atlasJSON={atlasJSON}
|
|
60
|
-
spriteKey="others/
|
|
66
|
+
spriteKey="others/definya-coin.png"
|
|
61
67
|
imgScale={1}
|
|
62
68
|
/>
|
|
63
|
-
</
|
|
64
|
-
<
|
|
69
|
+
</DCCoinWrapper>
|
|
70
|
+
<DCPrice>{formatDCAmount(price)}</DCPrice>
|
|
65
71
|
</PriceRow>
|
|
66
72
|
{isBeingBought && (
|
|
67
73
|
<PendingBadge>Sale Pending</PendingBadge>
|
|
@@ -142,7 +148,7 @@ const PriceRow = styled.div`
|
|
|
142
148
|
margin-top: 0.2rem;
|
|
143
149
|
`;
|
|
144
150
|
|
|
145
|
-
const
|
|
151
|
+
const DCCoinWrapper = styled.span`
|
|
146
152
|
display: flex;
|
|
147
153
|
align-items: center;
|
|
148
154
|
justify-content: center;
|
|
@@ -151,7 +157,7 @@ const GoldIcon = styled.span`
|
|
|
151
157
|
left: -0.4rem;
|
|
152
158
|
`;
|
|
153
159
|
|
|
154
|
-
const
|
|
160
|
+
const DCPrice = styled.span`
|
|
155
161
|
font-family: 'Press Start 2P', cursive;
|
|
156
162
|
font-size: 0.55rem !important;
|
|
157
163
|
color: #fef08a;
|
|
@@ -180,6 +186,8 @@ export interface IGroupedCharacterMarketplaceRowProps {
|
|
|
180
186
|
otherListings: ICharacterListing[];
|
|
181
187
|
atlasJSON: any;
|
|
182
188
|
atlasIMG: any;
|
|
189
|
+
characterAtlasJSON: any;
|
|
190
|
+
characterAtlasIMG: any;
|
|
183
191
|
onBuy: (id: string) => void;
|
|
184
192
|
currentCharacterId?: string;
|
|
185
193
|
}
|
|
@@ -189,6 +197,8 @@ export const GroupedCharacterMarketplaceRow: React.FC<IGroupedCharacterMarketpla
|
|
|
189
197
|
otherListings,
|
|
190
198
|
atlasJSON,
|
|
191
199
|
atlasIMG,
|
|
200
|
+
characterAtlasJSON,
|
|
201
|
+
characterAtlasIMG,
|
|
192
202
|
onBuy,
|
|
193
203
|
currentCharacterId,
|
|
194
204
|
}) => {
|
|
@@ -201,6 +211,8 @@ export const GroupedCharacterMarketplaceRow: React.FC<IGroupedCharacterMarketpla
|
|
|
201
211
|
listing={listing}
|
|
202
212
|
atlasJSON={atlasJSON}
|
|
203
213
|
atlasIMG={atlasIMG}
|
|
214
|
+
characterAtlasJSON={characterAtlasJSON}
|
|
215
|
+
characterAtlasIMG={characterAtlasIMG}
|
|
204
216
|
onCharacterBuy={isOwnListing ? undefined : () => onBuy(listing._id)}
|
|
205
217
|
disabled={isOwnListing}
|
|
206
218
|
/>
|
|
@@ -117,6 +117,9 @@ export interface IMarketPlaceProps {
|
|
|
117
117
|
walletProps?: IDCWalletContentProps;
|
|
118
118
|
showWalletTab?: boolean;
|
|
119
119
|
|
|
120
|
+
/** Entities atlas for character sprites (separate from the items atlasIMG/atlasJSON) */
|
|
121
|
+
characterAtlasIMG?: any;
|
|
122
|
+
characterAtlasJSON?: any;
|
|
120
123
|
// Character marketplace props
|
|
121
124
|
characterListings?: ICharacterListing[];
|
|
122
125
|
characterListingsTotal?: number;
|
|
@@ -131,8 +134,6 @@ export interface IMarketPlaceProps {
|
|
|
131
134
|
onCharacterList?: (characterId: string, price: number) => void;
|
|
132
135
|
onCharacterDelist?: (listingId: string) => void;
|
|
133
136
|
accountCharacters?: ICharacter[];
|
|
134
|
-
onCharacterSelectToList?: (character: ICharacter) => void;
|
|
135
|
-
selectedCharacterToList?: ICharacter | null;
|
|
136
137
|
characterListingsLoading?: boolean;
|
|
137
138
|
characterNameFilter?: string;
|
|
138
139
|
onCharacterNameFilterChange?: (name: string) => void;
|
|
@@ -195,11 +196,11 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
|
|
|
195
196
|
onCharacterDelist,
|
|
196
197
|
onCharacterList,
|
|
197
198
|
accountCharacters = [],
|
|
198
|
-
onCharacterSelectToList,
|
|
199
|
-
selectedCharacterToList,
|
|
200
199
|
characterListingsLoading = false,
|
|
201
200
|
characterNameFilter = '',
|
|
202
201
|
onCharacterNameFilterChange,
|
|
202
|
+
characterAtlasIMG,
|
|
203
|
+
characterAtlasJSON,
|
|
203
204
|
} = props;
|
|
204
205
|
|
|
205
206
|
const [activeTab, setActiveTab] = useState<ActiveTab>('marketplace');
|
|
@@ -323,6 +324,8 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
|
|
|
323
324
|
onCharacterBuy={onCharacterBuy ?? (() => {})}
|
|
324
325
|
atlasJSON={props.atlasJSON}
|
|
325
326
|
atlasIMG={props.atlasIMG}
|
|
327
|
+
characterAtlasJSON={characterAtlasJSON ?? props.atlasJSON}
|
|
328
|
+
characterAtlasIMG={characterAtlasIMG ?? props.atlasIMG}
|
|
326
329
|
enableHotkeys={props.enableHotkeys}
|
|
327
330
|
disableHotkeys={props.disableHotkeys}
|
|
328
331
|
nameFilter={characterNameFilter}
|
|
@@ -341,6 +344,8 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
|
|
|
341
344
|
onCharacterDelist={onCharacterDelist ?? (() => {})}
|
|
342
345
|
atlasJSON={props.atlasJSON}
|
|
343
346
|
atlasIMG={props.atlasIMG}
|
|
347
|
+
characterAtlasJSON={characterAtlasJSON ?? props.atlasJSON}
|
|
348
|
+
characterAtlasIMG={characterAtlasIMG ?? props.atlasIMG}
|
|
344
349
|
enableHotkeys={props.enableHotkeys}
|
|
345
350
|
disableHotkeys={props.disableHotkeys}
|
|
346
351
|
/>
|
|
@@ -349,11 +354,11 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
|
|
|
349
354
|
{characterSubTab === 'list' && (
|
|
350
355
|
<CharacterListingForm
|
|
351
356
|
accountCharacters={accountCharacters ?? []}
|
|
352
|
-
selectedCharacterToList={selectedCharacterToList ?? null}
|
|
353
|
-
onCharacterSelectToList={onCharacterSelectToList ?? (() => {})}
|
|
354
357
|
onCharacterList={onCharacterList ?? (() => {})}
|
|
355
358
|
atlasJSON={props.atlasJSON}
|
|
356
359
|
atlasIMG={props.atlasIMG}
|
|
360
|
+
characterAtlasJSON={characterAtlasJSON ?? props.atlasJSON}
|
|
361
|
+
characterAtlasIMG={characterAtlasIMG ?? props.atlasIMG}
|
|
357
362
|
enableHotkeys={props.enableHotkeys}
|
|
358
363
|
disableHotkeys={props.disableHotkeys}
|
|
359
364
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ICharacterListing } from '@rpg-engine/shared';
|
|
1
|
+
import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
|
|
2
2
|
import { Delete } from 'pixelarticons/react/Delete';
|
|
3
3
|
import { User } from 'pixelarticons/react/User';
|
|
4
4
|
import React, { useEffect, useRef, useState } from 'react';
|
|
@@ -14,8 +14,12 @@ export interface IMyCharacterListingsPanelProps {
|
|
|
14
14
|
itemsPerPage: number;
|
|
15
15
|
onPageChange: (page: number) => void;
|
|
16
16
|
onCharacterDelist: (listingId: string) => void;
|
|
17
|
+
/** Items atlas — for UI sprites like the DC coin */
|
|
17
18
|
atlasJSON: any;
|
|
18
19
|
atlasIMG: any;
|
|
20
|
+
/** Entities atlas — for character sprites */
|
|
21
|
+
characterAtlasJSON: any;
|
|
22
|
+
characterAtlasIMG: any;
|
|
19
23
|
enableHotkeys?: () => void;
|
|
20
24
|
disableHotkeys?: () => void;
|
|
21
25
|
}
|
|
@@ -29,6 +33,8 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
|
|
|
29
33
|
onCharacterDelist,
|
|
30
34
|
atlasJSON,
|
|
31
35
|
atlasIMG,
|
|
36
|
+
characterAtlasJSON,
|
|
37
|
+
characterAtlasIMG,
|
|
32
38
|
enableHotkeys,
|
|
33
39
|
}) => {
|
|
34
40
|
const [delistingListingId, setDelistingListingId] = useState<string | null>(null);
|
|
@@ -50,9 +56,6 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
|
|
|
50
56
|
}
|
|
51
57
|
};
|
|
52
58
|
|
|
53
|
-
const getListingPrice = (listing: ICharacterListing): string => {
|
|
54
|
-
return listing.price.toLocaleString();
|
|
55
|
-
};
|
|
56
59
|
|
|
57
60
|
const getFormattedDate = (date: Date) => {
|
|
58
61
|
return new Date(date).toLocaleDateString();
|
|
@@ -84,8 +87,8 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
|
|
|
84
87
|
<CharacterListingCard key={listing._id}>
|
|
85
88
|
<CharacterSprite>
|
|
86
89
|
<SpriteFromAtlas
|
|
87
|
-
atlasIMG={
|
|
88
|
-
atlasJSON={
|
|
90
|
+
atlasIMG={characterAtlasIMG}
|
|
91
|
+
atlasJSON={characterAtlasJSON}
|
|
89
92
|
spriteKey={`${listing.characterSnapshot.textureKey}/down/standing/0.png`}
|
|
90
93
|
imgScale={3}
|
|
91
94
|
height={64}
|
|
@@ -96,15 +99,15 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
|
|
|
96
99
|
<CharacterName>{listing.characterSnapshot.name || 'Unknown'}</CharacterName>
|
|
97
100
|
<CharacterMeta>Level {listing.characterSnapshot.level}</CharacterMeta>
|
|
98
101
|
<ListingPrice>
|
|
99
|
-
<
|
|
102
|
+
<DCCoinWrapper>
|
|
100
103
|
<SpriteFromAtlas
|
|
101
104
|
atlasIMG={atlasIMG}
|
|
102
105
|
atlasJSON={atlasJSON}
|
|
103
|
-
spriteKey="others/
|
|
106
|
+
spriteKey="others/definya-coin.png"
|
|
104
107
|
imgScale={1}
|
|
105
108
|
/>
|
|
106
|
-
</
|
|
107
|
-
{
|
|
109
|
+
</DCCoinWrapper>
|
|
110
|
+
{formatDCAmount(listing.price)} DC
|
|
108
111
|
</ListingPrice>
|
|
109
112
|
<ListingMeta>Listed {getFormattedDate(listing.createdAt)}</ListingMeta>
|
|
110
113
|
{listing.isBeingBought && (
|
|
@@ -216,13 +219,11 @@ const ListingPrice = styled.div`
|
|
|
216
219
|
color: #fef08a;
|
|
217
220
|
`;
|
|
218
221
|
|
|
219
|
-
const
|
|
222
|
+
const DCCoinWrapper = styled.span`
|
|
220
223
|
display: flex;
|
|
221
224
|
align-items: center;
|
|
222
225
|
justify-content: center;
|
|
223
|
-
|
|
224
|
-
top: -0.4rem;
|
|
225
|
-
left: -0.3rem;
|
|
226
|
+
flex-shrink: 0;
|
|
226
227
|
`;
|
|
227
228
|
|
|
228
229
|
const ListingMeta = styled.span`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DC_TO_GOLD_SWAP_RATE } from '@rpg-engine/shared';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
|
|
5
|
+
/** 100 DC = $1 USD */
|
|
6
|
+
const DC_TO_USD = 100;
|
|
7
|
+
|
|
8
|
+
export const DCRateStrip: React.FC = () => (
|
|
9
|
+
<RateStrip>
|
|
10
|
+
<RateItem>
|
|
11
|
+
<RateNum>1 DC</RateNum>
|
|
12
|
+
<RateSep>=</RateSep>
|
|
13
|
+
<RateVal>{DC_TO_GOLD_SWAP_RATE.toLocaleString()} Gold</RateVal>
|
|
14
|
+
</RateItem>
|
|
15
|
+
<RateDivider />
|
|
16
|
+
<RateItem>
|
|
17
|
+
<RateNum>{DC_TO_USD} DC</RateNum>
|
|
18
|
+
<RateSep>=</RateSep>
|
|
19
|
+
<RateVal>1 USD</RateVal>
|
|
20
|
+
</RateItem>
|
|
21
|
+
<RateDivider />
|
|
22
|
+
<RateItem>
|
|
23
|
+
<RateNum>1 USD</RateNum>
|
|
24
|
+
<RateSep>=</RateSep>
|
|
25
|
+
<RateVal>{(DC_TO_USD * DC_TO_GOLD_SWAP_RATE / 1000).toFixed(0)}K Gold</RateVal>
|
|
26
|
+
</RateItem>
|
|
27
|
+
</RateStrip>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const RateStrip = styled.div`
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
border-top: 1px solid rgba(245, 158, 11, 0.1);
|
|
34
|
+
padding-top: 8px;
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const RateItem = styled.div`
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 5px;
|
|
41
|
+
flex: 1;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const RateDivider = styled.div`
|
|
46
|
+
width: 1px;
|
|
47
|
+
height: 12px;
|
|
48
|
+
background: rgba(245, 158, 11, 0.2);
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const RateNum = styled.span`
|
|
52
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
53
|
+
font-size: 6px !important;
|
|
54
|
+
color: rgba(255, 255, 255, 0.5) !important;
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const RateSep = styled.span`
|
|
58
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
59
|
+
font-size: 6px !important;
|
|
60
|
+
color: rgba(255, 255, 255, 0.2) !important;
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const RateVal = styled.span`
|
|
64
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
65
|
+
font-size: 6px !important;
|
|
66
|
+
color: rgba(254, 240, 138, 0.6) !important;
|
|
67
|
+
`;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
export interface IRadioOptionProps {
|
|
5
|
+
selected: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onSelect: () => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A selectable row with an amber radio circle indicator.
|
|
13
|
+
* Used for single-select option lists throughout the Marketplace UI.
|
|
14
|
+
* Export `RadioCircle` separately so consumers can compose custom layouts.
|
|
15
|
+
*/
|
|
16
|
+
export const RadioOption: React.FC<IRadioOptionProps> = ({
|
|
17
|
+
selected,
|
|
18
|
+
disabled = false,
|
|
19
|
+
onSelect,
|
|
20
|
+
children,
|
|
21
|
+
}) => {
|
|
22
|
+
const handleClick = () => {
|
|
23
|
+
if (!disabled) {
|
|
24
|
+
onSelect();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<RadioOptionContainer
|
|
30
|
+
$selected={selected}
|
|
31
|
+
$disabled={disabled}
|
|
32
|
+
onClick={handleClick}
|
|
33
|
+
role="radio"
|
|
34
|
+
aria-checked={selected}
|
|
35
|
+
aria-disabled={disabled}
|
|
36
|
+
>
|
|
37
|
+
<RadioCircle $selected={selected} />
|
|
38
|
+
{children}
|
|
39
|
+
</RadioOptionContainer>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const RadioOptionContainer = styled.div<{ $selected: boolean; $disabled?: boolean }>`
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
padding: 10px 12px;
|
|
48
|
+
border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.15)')};
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'transparent')};
|
|
51
|
+
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
|
|
52
|
+
opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
|
|
53
|
+
transition: border-color 0.15s, background 0.15s;
|
|
54
|
+
|
|
55
|
+
&:hover {
|
|
56
|
+
border-color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.15)' : '#f59e0b')};
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
export const RadioCircle = styled.div<{ $selected: boolean }>`
|
|
61
|
+
width: 16px;
|
|
62
|
+
height: 16px;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
|
|
70
|
+
&::after {
|
|
71
|
+
content: '';
|
|
72
|
+
width: 8px;
|
|
73
|
+
height: 8px;
|
|
74
|
+
border-radius: 50%;
|
|
75
|
+
background: #f59e0b;
|
|
76
|
+
opacity: ${({ $selected }) => ($selected ? 1 : 0)};
|
|
77
|
+
transition: opacity 0.15s;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
/** Convenience wrapper for option label text with RPGUI font override. */
|
|
82
|
+
export const RadioOptionLabel = styled.span<{ $disabled?: boolean }>`
|
|
83
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
84
|
+
font-size: 0.65rem !important;
|
|
85
|
+
color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.4)' : '#ffffff')} !important;
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
/** Convenience wrapper for option sub-text with RPGUI font override. */
|
|
89
|
+
export const RadioOptionSub = styled.span`
|
|
90
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
91
|
+
font-size: 0.55rem !important;
|
|
92
|
+
color: rgba(255, 255, 255, 0.5) !important;
|
|
93
|
+
`;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ICharacter } from '@rpg-engine/shared';
|
|
2
|
+
import { Meta, Story } from '@storybook/react';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { CharacterListingModal, ICharacterListingModalProps } from '../../../components/Marketplace/CharacterListingModal';
|
|
5
|
+
import { RPGUIRoot } from '../../..';
|
|
6
|
+
import entitiesAtlasJSON from '../../../mocks/atlas/entities/entities.json';
|
|
7
|
+
import entitiesAtlasIMG from '../../../mocks/atlas/entities/entities.png';
|
|
8
|
+
import itemsAtlasJSON from '../../../mocks/atlas/items/items.json';
|
|
9
|
+
import itemsAtlasIMG from '../../../mocks/atlas/items/items.png';
|
|
10
|
+
|
|
11
|
+
const meta: Meta<ICharacterListingModalProps> = {
|
|
12
|
+
title: 'Features/Marketplace/CharacterListingModal',
|
|
13
|
+
component: CharacterListingModal,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
const mockCharacters: ICharacter[] = [
|
|
19
|
+
{
|
|
20
|
+
_id: 'char-1',
|
|
21
|
+
name: 'Sir Galahad',
|
|
22
|
+
textureKey: 'black-knight',
|
|
23
|
+
isListedForSale: false,
|
|
24
|
+
tradedAt: undefined,
|
|
25
|
+
skills: { level: 25 } as any,
|
|
26
|
+
} as ICharacter,
|
|
27
|
+
{
|
|
28
|
+
_id: 'char-2',
|
|
29
|
+
name: 'Merlin Jr.',
|
|
30
|
+
textureKey: 'pink-mage-1',
|
|
31
|
+
isListedForSale: false,
|
|
32
|
+
tradedAt: undefined,
|
|
33
|
+
skills: { level: 30 } as any,
|
|
34
|
+
} as ICharacter,
|
|
35
|
+
{
|
|
36
|
+
_id: 'char-3',
|
|
37
|
+
name: 'ShadowStep',
|
|
38
|
+
textureKey: 'redhair-girl-1',
|
|
39
|
+
isListedForSale: false,
|
|
40
|
+
tradedAt: undefined,
|
|
41
|
+
skills: { level: 18 } as any,
|
|
42
|
+
} as ICharacter,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const mockCharactersWithListed: ICharacter[] = [
|
|
46
|
+
...mockCharacters,
|
|
47
|
+
{
|
|
48
|
+
_id: 'char-4',
|
|
49
|
+
name: 'AlreadyListed',
|
|
50
|
+
textureKey: 'dragon-knight',
|
|
51
|
+
isListedForSale: true,
|
|
52
|
+
tradedAt: undefined,
|
|
53
|
+
skills: { level: 40 } as any,
|
|
54
|
+
} as ICharacter,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const ModalWrapper: React.FC<Omit<ICharacterListingModalProps, 'isOpen' | 'onClose' | 'onCharacterList'>> = (props) => {
|
|
58
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
59
|
+
const [lastListed, setLastListed] = useState<string | null>(null);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<RPGUIRoot>
|
|
63
|
+
<div style={{ padding: '20px', fontFamily: 'monospace', fontSize: '12px', color: '#fff' }}>
|
|
64
|
+
{lastListed ? (
|
|
65
|
+
<p>✅ Listed: {lastListed}</p>
|
|
66
|
+
) : (
|
|
67
|
+
<p>Click "List" to submit the form</p>
|
|
68
|
+
)}
|
|
69
|
+
<button
|
|
70
|
+
style={{ marginTop: 8, padding: '6px 12px', cursor: 'pointer' }}
|
|
71
|
+
onClick={() => setIsOpen(true)}
|
|
72
|
+
>
|
|
73
|
+
Reopen Modal
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<CharacterListingModal
|
|
77
|
+
{...props}
|
|
78
|
+
isOpen={isOpen}
|
|
79
|
+
onClose={() => setIsOpen(false)}
|
|
80
|
+
onCharacterList={(id, price) => {
|
|
81
|
+
setLastListed(`ID=${id}, Price=${price} DC`);
|
|
82
|
+
setIsOpen(false);
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</RPGUIRoot>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const WithMultipleCharacters: Story = () => (
|
|
90
|
+
<ModalWrapper
|
|
91
|
+
accountCharacters={mockCharacters}
|
|
92
|
+
atlasJSON={itemsAtlasJSON}
|
|
93
|
+
atlasIMG={itemsAtlasIMG}
|
|
94
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
95
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
WithMultipleCharacters.storyName = 'Multiple eligible characters';
|
|
99
|
+
|
|
100
|
+
export const WithAlreadyListedFiltered: Story = () => (
|
|
101
|
+
<ModalWrapper
|
|
102
|
+
accountCharacters={mockCharactersWithListed}
|
|
103
|
+
atlasJSON={itemsAtlasJSON}
|
|
104
|
+
atlasIMG={itemsAtlasIMG}
|
|
105
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
106
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
WithAlreadyListedFiltered.storyName = 'Some characters already listed (filtered out)';
|
|
110
|
+
|
|
111
|
+
export const NoEligibleCharacters: Story = () => (
|
|
112
|
+
<ModalWrapper
|
|
113
|
+
accountCharacters={[]}
|
|
114
|
+
atlasJSON={itemsAtlasJSON}
|
|
115
|
+
atlasIMG={itemsAtlasIMG}
|
|
116
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
117
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
NoEligibleCharacters.storyName = 'No eligible characters';
|
|
121
|
+
|
|
122
|
+
export const SingleCharacter: Story = () => (
|
|
123
|
+
<ModalWrapper
|
|
124
|
+
accountCharacters={[mockCharacters[0]]}
|
|
125
|
+
atlasJSON={itemsAtlasJSON}
|
|
126
|
+
atlasIMG={itemsAtlasIMG}
|
|
127
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
128
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
SingleCharacter.storyName = 'Single eligible character';
|