@rpg-engine/long-bow 0.8.185 → 0.8.187
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/CharacterDetailModal.d.ts +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +370 -170
- 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 +393 -194
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Features/marketplace/CharacterDetailModal.stories.d.ts +7 -0
- package/package.json +1 -1
- package/src/components/Marketplace/CharacterDetailModal.tsx +477 -0
- package/src/components/Marketplace/CharacterMarketplacePanel.tsx +19 -97
- package/src/index.tsx +1 -0
- package/src/stories/Features/marketplace/CharacterDetailModal.stories.tsx +129 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Meta, Story } from '@storybook/react';
|
|
2
|
+
import { ICharacterDetailModalProps } from '../../../components/Marketplace/CharacterDetailModal';
|
|
3
|
+
declare const meta: Meta<ICharacterDetailModalProps>;
|
|
4
|
+
export default meta;
|
|
5
|
+
export declare const WarriorListing: Story;
|
|
6
|
+
export declare const HardcoreMageListing: Story;
|
|
7
|
+
export declare const PendingPurchase: Story;
|
package/package.json
CHANGED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
|
|
2
|
+
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
3
|
+
import React, { useCallback, useState } from 'react';
|
|
4
|
+
import { FaTimes } from 'react-icons/fa';
|
|
5
|
+
import styled, { keyframes } from 'styled-components';
|
|
6
|
+
import ModalPortal from '../Abstractions/ModalPortal';
|
|
7
|
+
import { ConfirmModal } from '../ConfirmModal';
|
|
8
|
+
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
9
|
+
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
10
|
+
|
|
11
|
+
export interface ICharacterDetailModalProps {
|
|
12
|
+
listing: ICharacterListing | null;
|
|
13
|
+
isOpen: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
onBuy: (listingId: string) => void;
|
|
16
|
+
/** Items atlas — for the DC coin sprite */
|
|
17
|
+
atlasJSON: any;
|
|
18
|
+
atlasIMG: any;
|
|
19
|
+
/** Entities atlas — for the character sprite */
|
|
20
|
+
characterAtlasJSON: any;
|
|
21
|
+
characterAtlasIMG: any;
|
|
22
|
+
enableHotkeys?: () => void;
|
|
23
|
+
disableHotkeys?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const RARITY_COLORS: Record<string, string> = {
|
|
27
|
+
legendary: '#f59e0b',
|
|
28
|
+
epic: '#a855f7',
|
|
29
|
+
rare: '#3b82f6',
|
|
30
|
+
uncommon: '#22c55e',
|
|
31
|
+
common: '#6b7280',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const rarityColor = (rarity?: string) =>
|
|
35
|
+
RARITY_COLORS[(rarity ?? '').toLowerCase()] ?? RARITY_COLORS.common;
|
|
36
|
+
|
|
37
|
+
export const CharacterDetailModal: React.FC<ICharacterDetailModalProps> = ({
|
|
38
|
+
listing,
|
|
39
|
+
isOpen,
|
|
40
|
+
onClose,
|
|
41
|
+
onBuy,
|
|
42
|
+
atlasJSON,
|
|
43
|
+
atlasIMG,
|
|
44
|
+
characterAtlasJSON,
|
|
45
|
+
characterAtlasIMG,
|
|
46
|
+
enableHotkeys,
|
|
47
|
+
disableHotkeys,
|
|
48
|
+
}) => {
|
|
49
|
+
const [isConfirming, setIsConfirming] = useState(false);
|
|
50
|
+
|
|
51
|
+
const handleClose = useCallback(() => {
|
|
52
|
+
setIsConfirming(false);
|
|
53
|
+
enableHotkeys?.();
|
|
54
|
+
onClose();
|
|
55
|
+
}, [enableHotkeys, onClose]);
|
|
56
|
+
|
|
57
|
+
const handleBuyClick = useCallback(() => {
|
|
58
|
+
setIsConfirming(true);
|
|
59
|
+
disableHotkeys?.();
|
|
60
|
+
}, [disableHotkeys]);
|
|
61
|
+
|
|
62
|
+
const handleConfirm = useCallback(() => {
|
|
63
|
+
if (!listing) return;
|
|
64
|
+
onBuy(listing._id);
|
|
65
|
+
setIsConfirming(false);
|
|
66
|
+
enableHotkeys?.();
|
|
67
|
+
onClose();
|
|
68
|
+
}, [listing, onBuy, enableHotkeys, onClose]);
|
|
69
|
+
|
|
70
|
+
const stopPropagation = useCallback(
|
|
71
|
+
(e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
},
|
|
74
|
+
[]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (!isOpen || !listing) return null;
|
|
78
|
+
|
|
79
|
+
const snap = listing.characterSnapshot;
|
|
80
|
+
const topSkills = Object.entries(snap.skills ?? {})
|
|
81
|
+
.sort(([, a], [, b]) => b - a)
|
|
82
|
+
.slice(0, 8);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<ModalPortal>
|
|
86
|
+
{isConfirming && (
|
|
87
|
+
<ConfirmModal
|
|
88
|
+
onConfirm={handleConfirm}
|
|
89
|
+
onClose={() => {
|
|
90
|
+
setIsConfirming(false);
|
|
91
|
+
enableHotkeys?.();
|
|
92
|
+
}}
|
|
93
|
+
message={`Buy ${snap.name} for ${formatDCAmount(listing.price)} DC?`}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
<Overlay onPointerDown={handleClose} />
|
|
98
|
+
<ModalContainer>
|
|
99
|
+
<ModalContent
|
|
100
|
+
onClick={stopPropagation as React.MouseEventHandler}
|
|
101
|
+
onTouchStart={stopPropagation as React.TouchEventHandler}
|
|
102
|
+
onPointerDown={stopPropagation as React.PointerEventHandler}
|
|
103
|
+
>
|
|
104
|
+
<Header>
|
|
105
|
+
<Title>Character Details</Title>
|
|
106
|
+
<CloseButton onPointerDown={handleClose} aria-label="Close" type="button">
|
|
107
|
+
<FaTimes />
|
|
108
|
+
</CloseButton>
|
|
109
|
+
</Header>
|
|
110
|
+
|
|
111
|
+
<HeroSection>
|
|
112
|
+
<SpriteContainer>
|
|
113
|
+
<SpriteFromAtlas
|
|
114
|
+
atlasIMG={characterAtlasIMG}
|
|
115
|
+
atlasJSON={characterAtlasJSON}
|
|
116
|
+
spriteKey={`${snap.textureKey}/down/standing/0.png`}
|
|
117
|
+
imgScale={4}
|
|
118
|
+
height={96}
|
|
119
|
+
width={96}
|
|
120
|
+
/>
|
|
121
|
+
</SpriteContainer>
|
|
122
|
+
<HeroInfo>
|
|
123
|
+
<CharacterName>{snap.name || 'Unknown'}</CharacterName>
|
|
124
|
+
<CharacterClass>
|
|
125
|
+
Lv.{snap.level} · {snap.class}
|
|
126
|
+
</CharacterClass>
|
|
127
|
+
<CharacterOrigin>
|
|
128
|
+
{snap.race} · {snap.faction}
|
|
129
|
+
</CharacterOrigin>
|
|
130
|
+
<ModeBadge $hardcore={snap.mode?.toLowerCase() === 'hardcore'}>
|
|
131
|
+
{snap.mode || 'Standard'}
|
|
132
|
+
</ModeBadge>
|
|
133
|
+
</HeroInfo>
|
|
134
|
+
</HeroSection>
|
|
135
|
+
|
|
136
|
+
<Divider />
|
|
137
|
+
|
|
138
|
+
{topSkills.length > 0 && (
|
|
139
|
+
<Section>
|
|
140
|
+
<SectionTitle>Skills</SectionTitle>
|
|
141
|
+
<SkillsGrid>
|
|
142
|
+
{topSkills.map(([name, value]) => (
|
|
143
|
+
<SkillRow key={name}>
|
|
144
|
+
<SkillName>{name}</SkillName>
|
|
145
|
+
<SkillValue>{value}</SkillValue>
|
|
146
|
+
</SkillRow>
|
|
147
|
+
))}
|
|
148
|
+
</SkillsGrid>
|
|
149
|
+
</Section>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{snap.equipment?.length > 0 && (
|
|
153
|
+
<>
|
|
154
|
+
<Divider />
|
|
155
|
+
<Section>
|
|
156
|
+
<SectionTitle>Equipment</SectionTitle>
|
|
157
|
+
<EquipmentList>
|
|
158
|
+
{snap.equipment.map((eq, i) => (
|
|
159
|
+
<EquipmentRow key={i}>
|
|
160
|
+
<EquipSlot>{eq.slot}</EquipSlot>
|
|
161
|
+
<EquipName>{eq.itemName}</EquipName>
|
|
162
|
+
<RarityBadge $rarity={eq.rarity}>{eq.rarity || 'Common'}</RarityBadge>
|
|
163
|
+
</EquipmentRow>
|
|
164
|
+
))}
|
|
165
|
+
</EquipmentList>
|
|
166
|
+
</Section>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<Divider />
|
|
171
|
+
|
|
172
|
+
<Footer>
|
|
173
|
+
<SellerInfo>Listed by {listing.listedByCharacterName}</SellerInfo>
|
|
174
|
+
<FooterActions>
|
|
175
|
+
<PriceDisplay>
|
|
176
|
+
<DCCoinWrapper>
|
|
177
|
+
<SpriteFromAtlas
|
|
178
|
+
atlasIMG={atlasIMG}
|
|
179
|
+
atlasJSON={atlasJSON}
|
|
180
|
+
spriteKey="others/definya-coin.png"
|
|
181
|
+
imgScale={1}
|
|
182
|
+
/>
|
|
183
|
+
</DCCoinWrapper>
|
|
184
|
+
<PriceAmount>{formatDCAmount(listing.price)} DC</PriceAmount>
|
|
185
|
+
</PriceDisplay>
|
|
186
|
+
<BuyBtn
|
|
187
|
+
icon={<ShoppingBag width={18} height={18} />}
|
|
188
|
+
label="Buy Character"
|
|
189
|
+
disabled={listing.isBeingBought}
|
|
190
|
+
onClick={handleBuyClick}
|
|
191
|
+
/>
|
|
192
|
+
</FooterActions>
|
|
193
|
+
{listing.isBeingBought && (
|
|
194
|
+
<PendingNotice>Purchase already in progress</PendingNotice>
|
|
195
|
+
)}
|
|
196
|
+
</Footer>
|
|
197
|
+
</ModalContent>
|
|
198
|
+
</ModalContainer>
|
|
199
|
+
</ModalPortal>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const scaleIn = keyframes`
|
|
204
|
+
from { transform: scale(0.88); opacity: 0; }
|
|
205
|
+
to { transform: scale(1); opacity: 1; }
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const Overlay = styled.div`
|
|
209
|
+
position: fixed;
|
|
210
|
+
inset: 0;
|
|
211
|
+
background: rgba(0, 0, 0, 0.75);
|
|
212
|
+
z-index: 1000;
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const ModalContainer = styled.div`
|
|
216
|
+
position: fixed;
|
|
217
|
+
inset: 0;
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
justify-content: center;
|
|
221
|
+
z-index: 1001;
|
|
222
|
+
pointer-events: none;
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
const ModalContent = styled.div`
|
|
226
|
+
background: #1a1a2e;
|
|
227
|
+
border: 2px solid #f59e0b;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
padding: 20px 24px;
|
|
230
|
+
width: 480px;
|
|
231
|
+
max-width: 94%;
|
|
232
|
+
max-height: 85dvh;
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-direction: column;
|
|
235
|
+
gap: 14px;
|
|
236
|
+
overflow-y: auto;
|
|
237
|
+
overflow-x: hidden;
|
|
238
|
+
pointer-events: auto;
|
|
239
|
+
animation: ${scaleIn} 0.15s ease-out;
|
|
240
|
+
|
|
241
|
+
&::-webkit-scrollbar { width: 6px; }
|
|
242
|
+
&::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; }
|
|
243
|
+
&::-webkit-scrollbar-thumb { background: rgba(245,158,11,0.3); border-radius: 4px; }
|
|
244
|
+
`;
|
|
245
|
+
|
|
246
|
+
const Header = styled.div`
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
justify-content: space-between;
|
|
250
|
+
flex-shrink: 0;
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
const Title = styled.h3`
|
|
254
|
+
margin: 0;
|
|
255
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
256
|
+
font-size: 0.6rem !important;
|
|
257
|
+
color: #fef08a !important;
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
const CloseButton = styled.button`
|
|
261
|
+
background: none;
|
|
262
|
+
border: none;
|
|
263
|
+
color: rgba(255, 255, 255, 0.5);
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
font-size: 1rem;
|
|
266
|
+
padding: 4px;
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
|
|
270
|
+
&:hover { color: #fff; }
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
const HeroSection = styled.div`
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: 16px;
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
const SpriteContainer = styled.div`
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
image-rendering: pixelated;
|
|
284
|
+
width: 96px;
|
|
285
|
+
height: 96px;
|
|
286
|
+
flex-shrink: 0;
|
|
287
|
+
background: rgba(255,255,255,0.03);
|
|
288
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
289
|
+
border-radius: 6px;
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
const HeroInfo = styled.div`
|
|
293
|
+
display: flex;
|
|
294
|
+
flex-direction: column;
|
|
295
|
+
gap: 6px;
|
|
296
|
+
flex: 1;
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
const CharacterName = styled.span`
|
|
300
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
301
|
+
font-size: 0.65rem !important;
|
|
302
|
+
color: #f3f4f6 !important;
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
const CharacterClass = styled.span`
|
|
306
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
307
|
+
font-size: 0.5rem !important;
|
|
308
|
+
color: #9ca3af !important;
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
const CharacterOrigin = styled.span`
|
|
312
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
313
|
+
font-size: 0.42rem !important;
|
|
314
|
+
color: #6b7280 !important;
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
const ModeBadge = styled.span<{ $hardcore?: boolean }>`
|
|
318
|
+
display: inline-block;
|
|
319
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
320
|
+
font-size: 0.35rem !important;
|
|
321
|
+
color: ${({ $hardcore }) => ($hardcore ? '#ef4444' : '#6b7280')} !important;
|
|
322
|
+
border: 1px solid ${({ $hardcore }) => ($hardcore ? 'rgba(239,68,68,0.4)' : 'rgba(107,114,128,0.3)')};
|
|
323
|
+
border-radius: 3px;
|
|
324
|
+
padding: 2px 6px;
|
|
325
|
+
text-transform: uppercase;
|
|
326
|
+
letter-spacing: 0.5px;
|
|
327
|
+
width: fit-content;
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
const Divider = styled.hr`
|
|
331
|
+
border: none;
|
|
332
|
+
border-top: 1px solid rgba(255,255,255,0.06);
|
|
333
|
+
margin: 0;
|
|
334
|
+
flex-shrink: 0;
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
const Section = styled.div`
|
|
338
|
+
display: flex;
|
|
339
|
+
flex-direction: column;
|
|
340
|
+
gap: 8px;
|
|
341
|
+
`;
|
|
342
|
+
|
|
343
|
+
const SectionTitle = styled.span`
|
|
344
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
345
|
+
font-size: 0.45rem !important;
|
|
346
|
+
color: #f59e0b !important;
|
|
347
|
+
text-transform: uppercase;
|
|
348
|
+
letter-spacing: 1px;
|
|
349
|
+
`;
|
|
350
|
+
|
|
351
|
+
const SkillsGrid = styled.div`
|
|
352
|
+
display: grid;
|
|
353
|
+
grid-template-columns: repeat(2, 1fr);
|
|
354
|
+
gap: 4px 12px;
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
const SkillRow = styled.div`
|
|
358
|
+
display: flex;
|
|
359
|
+
justify-content: space-between;
|
|
360
|
+
align-items: center;
|
|
361
|
+
gap: 8px;
|
|
362
|
+
`;
|
|
363
|
+
|
|
364
|
+
const SkillName = styled.span`
|
|
365
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
366
|
+
font-size: 0.38rem !important;
|
|
367
|
+
color: #9ca3af !important;
|
|
368
|
+
text-transform: capitalize;
|
|
369
|
+
`;
|
|
370
|
+
|
|
371
|
+
const SkillValue = styled.span`
|
|
372
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
373
|
+
font-size: 0.38rem !important;
|
|
374
|
+
color: #fef08a !important;
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
const EquipmentList = styled.div`
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
gap: 5px;
|
|
381
|
+
`;
|
|
382
|
+
|
|
383
|
+
const EquipmentRow = styled.div`
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
gap: 8px;
|
|
387
|
+
padding: 5px 8px;
|
|
388
|
+
background: rgba(255,255,255,0.02);
|
|
389
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
390
|
+
border-radius: 4px;
|
|
391
|
+
`;
|
|
392
|
+
|
|
393
|
+
const EquipSlot = styled.span`
|
|
394
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
395
|
+
font-size: 0.34rem !important;
|
|
396
|
+
color: #6b7280 !important;
|
|
397
|
+
text-transform: capitalize;
|
|
398
|
+
min-width: 52px;
|
|
399
|
+
flex-shrink: 0;
|
|
400
|
+
`;
|
|
401
|
+
|
|
402
|
+
const EquipName = styled.span`
|
|
403
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
404
|
+
font-size: 0.38rem !important;
|
|
405
|
+
color: #d1d5db !important;
|
|
406
|
+
flex: 1;
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
const RarityBadge = styled.span<{ $rarity?: string }>`
|
|
410
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
411
|
+
font-size: 0.32rem !important;
|
|
412
|
+
color: ${({ $rarity }) => rarityColor($rarity)} !important;
|
|
413
|
+
border: 1px solid ${({ $rarity }) => rarityColor($rarity)}44;
|
|
414
|
+
border-radius: 2px;
|
|
415
|
+
padding: 1px 4px;
|
|
416
|
+
text-transform: uppercase;
|
|
417
|
+
flex-shrink: 0;
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
const Footer = styled.div`
|
|
421
|
+
display: flex;
|
|
422
|
+
flex-direction: column;
|
|
423
|
+
gap: 8px;
|
|
424
|
+
flex-shrink: 0;
|
|
425
|
+
`;
|
|
426
|
+
|
|
427
|
+
const SellerInfo = styled.span`
|
|
428
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
429
|
+
font-size: 0.38rem !important;
|
|
430
|
+
color: #6b7280 !important;
|
|
431
|
+
text-transform: uppercase;
|
|
432
|
+
letter-spacing: 0.5px;
|
|
433
|
+
`;
|
|
434
|
+
|
|
435
|
+
const FooterActions = styled.div`
|
|
436
|
+
display: flex;
|
|
437
|
+
align-items: center;
|
|
438
|
+
justify-content: space-between;
|
|
439
|
+
gap: 12px;
|
|
440
|
+
`;
|
|
441
|
+
|
|
442
|
+
const PriceDisplay = styled.div`
|
|
443
|
+
display: flex;
|
|
444
|
+
align-items: center;
|
|
445
|
+
gap: 6px;
|
|
446
|
+
line-height: 1;
|
|
447
|
+
`;
|
|
448
|
+
|
|
449
|
+
const DCCoinWrapper = styled.span`
|
|
450
|
+
display: flex;
|
|
451
|
+
align-items: center;
|
|
452
|
+
justify-content: center;
|
|
453
|
+
flex-shrink: 0;
|
|
454
|
+
line-height: 0;
|
|
455
|
+
`;
|
|
456
|
+
|
|
457
|
+
const PriceAmount = styled.span`
|
|
458
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
459
|
+
font-size: 0.7rem !important;
|
|
460
|
+
color: #fef08a !important;
|
|
461
|
+
`;
|
|
462
|
+
|
|
463
|
+
const BuyBtn = styled(CTAButton)`
|
|
464
|
+
flex-shrink: 0;
|
|
465
|
+
padding: 10px 18px;
|
|
466
|
+
height: 34px;
|
|
467
|
+
|
|
468
|
+
span { font-size: 0.6rem; }
|
|
469
|
+
`;
|
|
470
|
+
|
|
471
|
+
const PendingNotice = styled.span`
|
|
472
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
473
|
+
font-size: 0.35rem !important;
|
|
474
|
+
color: #ef4444 !important;
|
|
475
|
+
text-transform: uppercase;
|
|
476
|
+
letter-spacing: 0.5px;
|
|
477
|
+
`;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { formatDCAmount, ICharacterListing
|
|
1
|
+
import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
|
|
2
2
|
import { User } from 'pixelarticons/react/User';
|
|
3
3
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import styled, { keyframes } from 'styled-components';
|
|
5
5
|
import { Input } from '../Input';
|
|
6
|
-
import { ConfirmModal } from '../ConfirmModal';
|
|
7
6
|
import { Pagination } from '../shared/Pagination/Pagination';
|
|
8
7
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
8
|
+
import { CharacterDetailModal } from './CharacterDetailModal';
|
|
9
9
|
|
|
10
10
|
export interface ICharacterMarketplacePanelProps {
|
|
11
11
|
characterListings: ICharacterListing[];
|
|
@@ -44,7 +44,7 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
44
44
|
onNameFilterChange,
|
|
45
45
|
isLoading = false,
|
|
46
46
|
}) => {
|
|
47
|
-
const [
|
|
47
|
+
const [selectedListing, setSelectedListing] = useState<ICharacterListing | null>(null);
|
|
48
48
|
const [localNameFilter, setLocalNameFilter] = useState(nameFilter);
|
|
49
49
|
const itemsContainer = useRef<HTMLDivElement>(null);
|
|
50
50
|
|
|
@@ -71,20 +71,8 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
71
71
|
onNameFilterChange?.(value);
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const handleBuyConfirm = () => {
|
|
79
|
-
if (buyingListingId) {
|
|
80
|
-
onCharacterBuy(buyingListingId);
|
|
81
|
-
setBuyingListingId(null);
|
|
82
|
-
enableHotkeys?.();
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const renderCharacterSprite = (snapshot: ICharacterListingSnapshot) => {
|
|
74
|
+
const renderCharacterSprite = (listing: ICharacterListing) => {
|
|
75
|
+
const snapshot = listing.characterSnapshot;
|
|
88
76
|
return (
|
|
89
77
|
<SpriteFromAtlas
|
|
90
78
|
atlasIMG={characterAtlasIMG}
|
|
@@ -99,16 +87,18 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
99
87
|
|
|
100
88
|
return (
|
|
101
89
|
<>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
90
|
+
<CharacterDetailModal
|
|
91
|
+
listing={selectedListing}
|
|
92
|
+
isOpen={!!selectedListing}
|
|
93
|
+
onClose={() => setSelectedListing(null)}
|
|
94
|
+
onBuy={onCharacterBuy}
|
|
95
|
+
atlasJSON={atlasJSON}
|
|
96
|
+
atlasIMG={atlasIMG}
|
|
97
|
+
characterAtlasJSON={characterAtlasJSON}
|
|
98
|
+
characterAtlasIMG={characterAtlasIMG}
|
|
99
|
+
enableHotkeys={enableHotkeys}
|
|
100
|
+
disableHotkeys={disableHotkeys}
|
|
101
|
+
/>
|
|
112
102
|
|
|
113
103
|
<ToolbarRow>
|
|
114
104
|
<SearchField>
|
|
@@ -139,33 +129,17 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
139
129
|
{filteredListings.map((listing) => (
|
|
140
130
|
<CharacterListingCard
|
|
141
131
|
key={listing._id}
|
|
142
|
-
onClick={() =>
|
|
132
|
+
onClick={() => setSelectedListing(listing)}
|
|
143
133
|
$isBeingBought={listing.isBeingBought}
|
|
144
134
|
>
|
|
145
135
|
<CharacterSprite>
|
|
146
|
-
{renderCharacterSprite(listing
|
|
136
|
+
{renderCharacterSprite(listing)}
|
|
147
137
|
</CharacterSprite>
|
|
148
138
|
<CharacterInfo>
|
|
149
139
|
<CharacterName>{listing.characterSnapshot.name || 'Unknown'}</CharacterName>
|
|
150
140
|
<CharacterMeta>
|
|
151
141
|
Lv.{listing.characterSnapshot.level} · {listing.characterSnapshot.class}
|
|
152
142
|
</CharacterMeta>
|
|
153
|
-
<CharacterDetails>
|
|
154
|
-
{listing.characterSnapshot.race} · {listing.characterSnapshot.faction}
|
|
155
|
-
</CharacterDetails>
|
|
156
|
-
<ModeBadge $hardcore={listing.characterSnapshot.mode?.toLowerCase() === 'hardcore'}>
|
|
157
|
-
{listing.characterSnapshot.mode || 'Standard'}
|
|
158
|
-
</ModeBadge>
|
|
159
|
-
{listing.characterSnapshot.equipment?.length > 0 && (
|
|
160
|
-
<EquipmentRow>
|
|
161
|
-
{listing.characterSnapshot.equipment.slice(0, 3).map((eq, i) => (
|
|
162
|
-
<EquipBadge key={i} $rarity={eq.rarity}>
|
|
163
|
-
{eq.rarity || 'Common'}
|
|
164
|
-
</EquipBadge>
|
|
165
|
-
))}
|
|
166
|
-
</EquipmentRow>
|
|
167
|
-
)}
|
|
168
|
-
<SellerInfo>by {listing.listedByCharacterName}</SellerInfo>
|
|
169
143
|
<ListingPrice>
|
|
170
144
|
<DCCoinWrapper>
|
|
171
145
|
<SpriteFromAtlas
|
|
@@ -303,58 +277,6 @@ const CharacterMeta = styled.span`
|
|
|
303
277
|
letter-spacing: 0.5px;
|
|
304
278
|
`;
|
|
305
279
|
|
|
306
|
-
const CharacterDetails = styled.span`
|
|
307
|
-
font-family: 'Press Start 2P', cursive !important;
|
|
308
|
-
font-size: 0.38rem !important;
|
|
309
|
-
color: #9ca3af !important;
|
|
310
|
-
text-transform: uppercase;
|
|
311
|
-
letter-spacing: 0.5px;
|
|
312
|
-
`;
|
|
313
|
-
|
|
314
|
-
const RARITY_COLORS: Record<string, string> = {
|
|
315
|
-
legendary: '#f59e0b',
|
|
316
|
-
epic: '#a855f7',
|
|
317
|
-
rare: '#3b82f6',
|
|
318
|
-
uncommon: '#22c55e',
|
|
319
|
-
common: '#6b7280',
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const ModeBadge = styled.span<{ $hardcore?: boolean }>`
|
|
323
|
-
font-family: 'Press Start 2P', cursive !important;
|
|
324
|
-
font-size: 0.32rem !important;
|
|
325
|
-
color: ${({ $hardcore }) => ($hardcore ? '#ef4444' : '#6b7280')} !important;
|
|
326
|
-
border: 1px solid ${({ $hardcore }) => ($hardcore ? 'rgba(239,68,68,0.4)' : 'rgba(107,114,128,0.3)')};
|
|
327
|
-
border-radius: 3px;
|
|
328
|
-
padding: 1px 4px;
|
|
329
|
-
text-transform: uppercase;
|
|
330
|
-
letter-spacing: 0.5px;
|
|
331
|
-
`;
|
|
332
|
-
|
|
333
|
-
const EquipmentRow = styled.div`
|
|
334
|
-
display: flex;
|
|
335
|
-
flex-wrap: wrap;
|
|
336
|
-
gap: 3px;
|
|
337
|
-
justify-content: center;
|
|
338
|
-
`;
|
|
339
|
-
|
|
340
|
-
const EquipBadge = styled.span<{ $rarity?: string }>`
|
|
341
|
-
font-family: 'Press Start 2P', cursive !important;
|
|
342
|
-
font-size: 0.3rem !important;
|
|
343
|
-
color: ${({ $rarity }) => RARITY_COLORS[($rarity ?? '').toLowerCase()] ?? RARITY_COLORS.common} !important;
|
|
344
|
-
border: 1px solid ${({ $rarity }) => RARITY_COLORS[($rarity ?? '').toLowerCase()] ?? RARITY_COLORS.common}44;
|
|
345
|
-
border-radius: 2px;
|
|
346
|
-
padding: 1px 3px;
|
|
347
|
-
text-transform: uppercase;
|
|
348
|
-
letter-spacing: 0.3px;
|
|
349
|
-
`;
|
|
350
|
-
|
|
351
|
-
const SellerInfo = styled.span`
|
|
352
|
-
font-family: 'Press Start 2P', cursive !important;
|
|
353
|
-
font-size: 0.4rem !important;
|
|
354
|
-
color: #666 !important;
|
|
355
|
-
text-transform: uppercase;
|
|
356
|
-
letter-spacing: 0.5px;
|
|
357
|
-
`;
|
|
358
280
|
|
|
359
281
|
const ListingPrice = styled.div`
|
|
360
282
|
display: flex;
|
package/src/index.tsx
CHANGED
|
@@ -45,6 +45,7 @@ export * from './components/Marketplace/HistoryPanel';
|
|
|
45
45
|
export * from './components/Marketplace/BlueprintSearchModal';
|
|
46
46
|
export * from './components/Marketplace/CharacterMarketplacePanel';
|
|
47
47
|
export * from './components/Marketplace/CharacterMarketplaceRows';
|
|
48
|
+
export * from './components/Marketplace/CharacterDetailModal';
|
|
48
49
|
export * from './components/Marketplace/CharacterListingForm';
|
|
49
50
|
export * from './components/Marketplace/CharacterListingModal';
|
|
50
51
|
export * from './components/Marketplace/MyCharacterListingsPanel';
|