@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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { ICharacter } 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 { Input } from '../Input';
|
|
9
|
+
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
10
|
+
import { DCRateStrip } from '../shared/DCRateStrip';
|
|
11
|
+
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
12
|
+
|
|
13
|
+
export interface ICharacterListingModalProps {
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
accountCharacters: ICharacter[];
|
|
17
|
+
/** Items atlas — for UI sprites like the DC coin */
|
|
18
|
+
atlasJSON: any;
|
|
19
|
+
atlasIMG: any;
|
|
20
|
+
/** Entities atlas — for character sprites */
|
|
21
|
+
characterAtlasJSON: any;
|
|
22
|
+
characterAtlasIMG: any;
|
|
23
|
+
onCharacterList: (characterId: string, price: number) => void;
|
|
24
|
+
enableHotkeys?: () => void;
|
|
25
|
+
disableHotkeys?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
|
|
29
|
+
isOpen,
|
|
30
|
+
onClose,
|
|
31
|
+
accountCharacters,
|
|
32
|
+
atlasJSON,
|
|
33
|
+
atlasIMG,
|
|
34
|
+
characterAtlasJSON,
|
|
35
|
+
characterAtlasIMG,
|
|
36
|
+
onCharacterList,
|
|
37
|
+
enableHotkeys,
|
|
38
|
+
disableHotkeys,
|
|
39
|
+
}) => {
|
|
40
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
41
|
+
const [price, setPrice] = useState('');
|
|
42
|
+
const [isConfirming, setIsConfirming] = useState(false);
|
|
43
|
+
|
|
44
|
+
if (!isOpen) return null;
|
|
45
|
+
|
|
46
|
+
const eligibleCharacters = accountCharacters.filter(
|
|
47
|
+
c => !c.isListedForSale && !c.tradedAt
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const canList = !!selectedId && Number(price) > 0;
|
|
51
|
+
|
|
52
|
+
const handleClose = () => {
|
|
53
|
+
setSelectedId(null);
|
|
54
|
+
setPrice('');
|
|
55
|
+
enableHotkeys?.();
|
|
56
|
+
onClose();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleListClick = () => {
|
|
60
|
+
if (canList) setIsConfirming(true);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleConfirm = () => {
|
|
64
|
+
if (!selectedId || !canList) return;
|
|
65
|
+
onCharacterList(selectedId, Number(price));
|
|
66
|
+
setSelectedId(null);
|
|
67
|
+
setPrice('');
|
|
68
|
+
setIsConfirming(false);
|
|
69
|
+
enableHotkeys?.();
|
|
70
|
+
onClose();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const stopPropagation = useCallback(
|
|
74
|
+
(e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
},
|
|
77
|
+
[]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const getLevel = (c: ICharacter) => c.skills?.level || 1;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ModalPortal>
|
|
84
|
+
{isConfirming && (
|
|
85
|
+
<ConfirmModal
|
|
86
|
+
onConfirm={handleConfirm}
|
|
87
|
+
onClose={() => { setIsConfirming(false); enableHotkeys?.(); }}
|
|
88
|
+
message="Are you sure you want to list this character for sale?"
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
<Overlay onPointerDown={handleClose} />
|
|
93
|
+
<ModalContainer>
|
|
94
|
+
<ModalContent
|
|
95
|
+
onClick={stopPropagation as React.MouseEventHandler}
|
|
96
|
+
onTouchStart={stopPropagation as React.TouchEventHandler}
|
|
97
|
+
onPointerDown={stopPropagation as React.PointerEventHandler}
|
|
98
|
+
>
|
|
99
|
+
<Header>
|
|
100
|
+
<Title>List Character for Sale</Title>
|
|
101
|
+
<CloseButton onPointerDown={handleClose} aria-label="Close" type="button">
|
|
102
|
+
<FaTimes />
|
|
103
|
+
</CloseButton>
|
|
104
|
+
</Header>
|
|
105
|
+
|
|
106
|
+
<CharacterList>
|
|
107
|
+
{eligibleCharacters.length === 0 ? (
|
|
108
|
+
<EmptyState>No eligible characters to list.</EmptyState>
|
|
109
|
+
) : (
|
|
110
|
+
eligibleCharacters.map(character => {
|
|
111
|
+
const isSelected = selectedId === character._id;
|
|
112
|
+
return (
|
|
113
|
+
<CharacterRow
|
|
114
|
+
key={character._id}
|
|
115
|
+
$selected={isSelected}
|
|
116
|
+
onPointerDown={() => setSelectedId(character._id!)}
|
|
117
|
+
role="radio"
|
|
118
|
+
aria-checked={isSelected}
|
|
119
|
+
tabIndex={0}
|
|
120
|
+
onKeyDown={e => {
|
|
121
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
setSelectedId(character._id!);
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<RadioCircle $selected={isSelected} />
|
|
128
|
+
<SpriteWrapper>
|
|
129
|
+
<SpriteFromAtlas
|
|
130
|
+
atlasIMG={characterAtlasIMG}
|
|
131
|
+
atlasJSON={characterAtlasJSON}
|
|
132
|
+
spriteKey={`${character.textureKey}/down/standing/0.png`}
|
|
133
|
+
imgScale={2}
|
|
134
|
+
height={40}
|
|
135
|
+
width={40}
|
|
136
|
+
/>
|
|
137
|
+
</SpriteWrapper>
|
|
138
|
+
<CharacterInfo>
|
|
139
|
+
<CharacterName>{character.name || 'Unknown'}</CharacterName>
|
|
140
|
+
<CharacterMeta>Level {getLevel(character)}</CharacterMeta>
|
|
141
|
+
</CharacterInfo>
|
|
142
|
+
</CharacterRow>
|
|
143
|
+
);
|
|
144
|
+
})
|
|
145
|
+
)}
|
|
146
|
+
</CharacterList>
|
|
147
|
+
|
|
148
|
+
<PriceSection>
|
|
149
|
+
<PriceLabel>Set Price (DC)</PriceLabel>
|
|
150
|
+
<PriceRow>
|
|
151
|
+
<Input
|
|
152
|
+
onChange={e => setPrice(e.target.value)}
|
|
153
|
+
value={price}
|
|
154
|
+
placeholder="Amount..."
|
|
155
|
+
type="number"
|
|
156
|
+
disabled={eligibleCharacters.length === 0}
|
|
157
|
+
onFocus={disableHotkeys}
|
|
158
|
+
onBlur={enableHotkeys}
|
|
159
|
+
className="price-input"
|
|
160
|
+
/>
|
|
161
|
+
<ListBtn
|
|
162
|
+
icon={<ShoppingBag width={18} height={18} />}
|
|
163
|
+
label="List"
|
|
164
|
+
disabled={!canList}
|
|
165
|
+
onClick={handleListClick}
|
|
166
|
+
/>
|
|
167
|
+
</PriceRow>
|
|
168
|
+
{price && Number(price) > 0 && (
|
|
169
|
+
<PricePreview>
|
|
170
|
+
<DCCoinWrapper>
|
|
171
|
+
<SpriteFromAtlas
|
|
172
|
+
atlasIMG={atlasIMG}
|
|
173
|
+
atlasJSON={atlasJSON}
|
|
174
|
+
spriteKey="others/definya-coin.png"
|
|
175
|
+
imgScale={1}
|
|
176
|
+
/>
|
|
177
|
+
</DCCoinWrapper>
|
|
178
|
+
<PricePreviewAmount>{Number(price).toLocaleString()} DC</PricePreviewAmount>
|
|
179
|
+
</PricePreview>
|
|
180
|
+
)}
|
|
181
|
+
<DCRateStrip />
|
|
182
|
+
</PriceSection>
|
|
183
|
+
</ModalContent>
|
|
184
|
+
</ModalContainer>
|
|
185
|
+
</ModalPortal>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const scaleIn = keyframes`
|
|
190
|
+
from { transform: scale(0.85); opacity: 0; }
|
|
191
|
+
to { transform: scale(1); opacity: 1; }
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const Overlay = styled.div`
|
|
195
|
+
position: fixed;
|
|
196
|
+
inset: 0;
|
|
197
|
+
background: rgba(0, 0, 0, 0.7);
|
|
198
|
+
z-index: 1000;
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const ModalContainer = styled.div`
|
|
202
|
+
position: fixed;
|
|
203
|
+
inset: 0;
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
z-index: 1001;
|
|
208
|
+
pointer-events: none;
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
const ModalContent = styled.div`
|
|
212
|
+
background: #1a1a2e;
|
|
213
|
+
border: 2px solid #f59e0b;
|
|
214
|
+
border-radius: 8px;
|
|
215
|
+
padding: 20px 24px;
|
|
216
|
+
width: 440px;
|
|
217
|
+
max-width: 92%;
|
|
218
|
+
max-height: 80dvh;
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
gap: 16px;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
pointer-events: auto;
|
|
224
|
+
animation: ${scaleIn} 0.15s ease-out;
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
const Header = styled.div`
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const Title = styled.h3`
|
|
234
|
+
margin: 0;
|
|
235
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
236
|
+
font-size: 0.65rem !important;
|
|
237
|
+
color: #fef08a !important;
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
const CloseButton = styled.button`
|
|
241
|
+
background: none;
|
|
242
|
+
border: none;
|
|
243
|
+
color: rgba(255, 255, 255, 0.6);
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
font-size: 1rem;
|
|
246
|
+
padding: 4px;
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
pointer-events: auto;
|
|
250
|
+
|
|
251
|
+
&:hover { color: #ffffff; }
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const CharacterList = styled.div`
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
gap: 6px;
|
|
258
|
+
overflow-y: auto;
|
|
259
|
+
max-height: 260px;
|
|
260
|
+
padding: 2px;
|
|
261
|
+
|
|
262
|
+
&::-webkit-scrollbar { width: 6px; }
|
|
263
|
+
&::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; }
|
|
264
|
+
&::-webkit-scrollbar-thumb { background: rgba(245,158,11,0.3); border-radius: 4px; }
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
const CharacterRow = styled.div<{ $selected: boolean }>`
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
gap: 12px;
|
|
271
|
+
padding: 10px 12px;
|
|
272
|
+
border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.08)')};
|
|
273
|
+
border-radius: 6px;
|
|
274
|
+
background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(255,255,255,0.02)')};
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
transition: border-color 0.15s, background 0.15s;
|
|
277
|
+
|
|
278
|
+
&:hover {
|
|
279
|
+
border-color: ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(245,158,11,0.4)')};
|
|
280
|
+
background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(245,158,11,0.05)')};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
&:focus-visible {
|
|
284
|
+
outline: 2px solid rgba(245,158,11,0.6);
|
|
285
|
+
outline-offset: 2px;
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
|
|
289
|
+
const RadioCircle = styled.div<{ $selected: boolean }>`
|
|
290
|
+
width: 14px;
|
|
291
|
+
height: 14px;
|
|
292
|
+
border-radius: 50%;
|
|
293
|
+
border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
|
|
294
|
+
flex-shrink: 0;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: center;
|
|
298
|
+
|
|
299
|
+
&::after {
|
|
300
|
+
content: '';
|
|
301
|
+
width: 6px;
|
|
302
|
+
height: 6px;
|
|
303
|
+
border-radius: 50%;
|
|
304
|
+
background: #f59e0b;
|
|
305
|
+
opacity: ${({ $selected }) => ($selected ? 1 : 0)};
|
|
306
|
+
transition: opacity 0.15s;
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
const SpriteWrapper = styled.div`
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: center;
|
|
314
|
+
image-rendering: pixelated;
|
|
315
|
+
flex-shrink: 0;
|
|
316
|
+
width: 40px;
|
|
317
|
+
height: 40px;
|
|
318
|
+
`;
|
|
319
|
+
|
|
320
|
+
const CharacterInfo = styled.div`
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
gap: 4px;
|
|
324
|
+
flex: 1;
|
|
325
|
+
`;
|
|
326
|
+
|
|
327
|
+
const CharacterName = styled.span`
|
|
328
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
329
|
+
font-size: 0.5rem !important;
|
|
330
|
+
color: #f3f4f6 !important;
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const CharacterMeta = styled.span`
|
|
334
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
335
|
+
font-size: 0.4rem !important;
|
|
336
|
+
color: #888 !important;
|
|
337
|
+
text-transform: uppercase;
|
|
338
|
+
letter-spacing: 0.5px;
|
|
339
|
+
`;
|
|
340
|
+
|
|
341
|
+
const PricePreview = styled.div`
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 6px;
|
|
345
|
+
`;
|
|
346
|
+
|
|
347
|
+
const DCCoinWrapper = styled.span`
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
flex-shrink: 0;
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
const PricePreviewAmount = styled.span`
|
|
355
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
356
|
+
font-size: 0.55rem !important;
|
|
357
|
+
color: #fef08a !important;
|
|
358
|
+
`;
|
|
359
|
+
|
|
360
|
+
const PriceSection = styled.div`
|
|
361
|
+
display: flex;
|
|
362
|
+
flex-direction: column;
|
|
363
|
+
gap: 8px;
|
|
364
|
+
border-top: 1px solid rgba(255,255,255,0.06);
|
|
365
|
+
padding-top: 12px;
|
|
366
|
+
`;
|
|
367
|
+
|
|
368
|
+
const PriceLabel = styled.p`
|
|
369
|
+
margin: 0;
|
|
370
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
371
|
+
font-size: 0.55rem !important;
|
|
372
|
+
color: #888 !important;
|
|
373
|
+
text-transform: uppercase;
|
|
374
|
+
letter-spacing: 0.5px;
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
const PriceRow = styled.div`
|
|
378
|
+
display: flex;
|
|
379
|
+
gap: 8px;
|
|
380
|
+
align-items: center;
|
|
381
|
+
|
|
382
|
+
.price-input { flex: 1; height: 12px; }
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
const ListBtn = styled(CTAButton)`
|
|
386
|
+
flex-shrink: 0;
|
|
387
|
+
padding: 10px 16px;
|
|
388
|
+
height: 32px;
|
|
389
|
+
|
|
390
|
+
span { font-size: 0.6rem; }
|
|
391
|
+
svg { font-size: 1.1rem; }
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
const EmptyState = styled.div`
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
justify-content: center;
|
|
398
|
+
padding: 32px 16px;
|
|
399
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
400
|
+
font-size: 0.5rem !important;
|
|
401
|
+
color: #666 !important;
|
|
402
|
+
text-transform: uppercase;
|
|
403
|
+
letter-spacing: 1px;
|
|
404
|
+
`;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { ICharacterListing, ICharacterListingSnapshot } from '@rpg-engine/shared';
|
|
1
|
+
import { formatDCAmount, ICharacterListing, ICharacterListingSnapshot } 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 { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
7
6
|
import { ConfirmModal } from '../ConfirmModal';
|
|
7
|
+
import { Pagination } from '../shared/Pagination/Pagination';
|
|
8
|
+
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
8
9
|
|
|
9
10
|
export interface ICharacterMarketplacePanelProps {
|
|
10
11
|
characterListings: ICharacterListing[];
|
|
@@ -13,8 +14,12 @@ export interface ICharacterMarketplacePanelProps {
|
|
|
13
14
|
itemsPerPage: number;
|
|
14
15
|
onPageChange: (page: number) => void;
|
|
15
16
|
onCharacterBuy: (listingId: string) => void;
|
|
17
|
+
/** Items atlas — for UI sprites like the DC coin */
|
|
16
18
|
atlasJSON: any;
|
|
17
19
|
atlasIMG: any;
|
|
20
|
+
/** Entities atlas — for character sprites */
|
|
21
|
+
characterAtlasJSON: any;
|
|
22
|
+
characterAtlasIMG: any;
|
|
18
23
|
enableHotkeys?: () => void;
|
|
19
24
|
disableHotkeys?: () => void;
|
|
20
25
|
nameFilter?: string;
|
|
@@ -31,6 +36,8 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
31
36
|
onCharacterBuy,
|
|
32
37
|
atlasJSON,
|
|
33
38
|
atlasIMG,
|
|
39
|
+
characterAtlasJSON,
|
|
40
|
+
characterAtlasIMG,
|
|
34
41
|
enableHotkeys,
|
|
35
42
|
disableHotkeys,
|
|
36
43
|
nameFilter = '',
|
|
@@ -76,15 +83,12 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
76
83
|
}
|
|
77
84
|
};
|
|
78
85
|
|
|
79
|
-
const getListingPrice = (listing: ICharacterListing): string => {
|
|
80
|
-
return listing.price.toLocaleString();
|
|
81
|
-
};
|
|
82
86
|
|
|
83
87
|
const renderCharacterSprite = (snapshot: ICharacterListingSnapshot) => {
|
|
84
88
|
return (
|
|
85
89
|
<SpriteFromAtlas
|
|
86
|
-
atlasIMG={
|
|
87
|
-
atlasJSON={
|
|
90
|
+
atlasIMG={characterAtlasIMG}
|
|
91
|
+
atlasJSON={characterAtlasJSON}
|
|
88
92
|
spriteKey={`${snapshot.textureKey}/down/standing/0.png`}
|
|
89
93
|
imgScale={3}
|
|
90
94
|
height={64}
|
|
@@ -144,19 +148,34 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
144
148
|
<CharacterInfo>
|
|
145
149
|
<CharacterName>{listing.characterSnapshot.name || 'Unknown'}</CharacterName>
|
|
146
150
|
<CharacterMeta>
|
|
147
|
-
|
|
151
|
+
Lv.{listing.characterSnapshot.level} · {listing.characterSnapshot.class}
|
|
148
152
|
</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
|
+
)}
|
|
149
168
|
<SellerInfo>by {listing.listedByCharacterName}</SellerInfo>
|
|
150
169
|
<ListingPrice>
|
|
151
|
-
<
|
|
170
|
+
<DCCoinWrapper>
|
|
152
171
|
<SpriteFromAtlas
|
|
153
172
|
atlasIMG={atlasIMG}
|
|
154
173
|
atlasJSON={atlasJSON}
|
|
155
|
-
spriteKey="others/
|
|
174
|
+
spriteKey="others/definya-coin.png"
|
|
156
175
|
imgScale={1}
|
|
157
176
|
/>
|
|
158
|
-
</
|
|
159
|
-
{
|
|
177
|
+
</DCCoinWrapper>
|
|
178
|
+
{formatDCAmount(listing.price)} DC
|
|
160
179
|
</ListingPrice>
|
|
161
180
|
</CharacterInfo>
|
|
162
181
|
{listing.isBeingBought && (
|
|
@@ -170,18 +189,11 @@ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps
|
|
|
170
189
|
|
|
171
190
|
{totalCount > itemsPerPage && (
|
|
172
191
|
<PagerFooter>
|
|
173
|
-
<Pagination
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
type="button"
|
|
179
|
-
onClick={() => onPageChange(page)}
|
|
180
|
-
>
|
|
181
|
-
{page}
|
|
182
|
-
</PageButton>
|
|
183
|
-
))}
|
|
184
|
-
</Pagination>
|
|
192
|
+
<Pagination
|
|
193
|
+
currentPage={currentPage}
|
|
194
|
+
totalPages={Math.ceil(totalCount / itemsPerPage)}
|
|
195
|
+
onPageChange={onPageChange}
|
|
196
|
+
/>
|
|
185
197
|
</PagerFooter>
|
|
186
198
|
)}
|
|
187
199
|
</>
|
|
@@ -261,6 +273,9 @@ const CharacterSprite = styled.div`
|
|
|
261
273
|
align-items: center;
|
|
262
274
|
justify-content: center;
|
|
263
275
|
image-rendering: pixelated;
|
|
276
|
+
width: 64px;
|
|
277
|
+
height: 64px;
|
|
278
|
+
flex-shrink: 0;
|
|
264
279
|
`;
|
|
265
280
|
|
|
266
281
|
const CharacterInfo = styled.div`
|
|
@@ -269,26 +284,74 @@ const CharacterInfo = styled.div`
|
|
|
269
284
|
align-items: center;
|
|
270
285
|
gap: 4px;
|
|
271
286
|
text-align: center;
|
|
287
|
+
width: 100%;
|
|
272
288
|
`;
|
|
273
289
|
|
|
274
290
|
const CharacterName = styled.span`
|
|
275
|
-
font-family: 'Press Start 2P', cursive;
|
|
276
|
-
font-size: 0.55rem;
|
|
277
|
-
color: #f3f4f6;
|
|
291
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
292
|
+
font-size: 0.55rem !important;
|
|
293
|
+
color: #f3f4f6 !important;
|
|
278
294
|
text-transform: uppercase;
|
|
279
295
|
letter-spacing: 0.5px;
|
|
280
296
|
`;
|
|
281
297
|
|
|
282
298
|
const CharacterMeta = styled.span`
|
|
283
|
-
font-
|
|
284
|
-
|
|
299
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
300
|
+
font-size: 0.45rem !important;
|
|
301
|
+
color: #888 !important;
|
|
302
|
+
text-transform: uppercase;
|
|
303
|
+
letter-spacing: 0.5px;
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
const CharacterDetails = styled.span`
|
|
307
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
308
|
+
font-size: 0.38rem !important;
|
|
309
|
+
color: #9ca3af !important;
|
|
285
310
|
text-transform: uppercase;
|
|
286
311
|
letter-spacing: 0.5px;
|
|
287
312
|
`;
|
|
288
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
|
+
|
|
289
351
|
const SellerInfo = styled.span`
|
|
290
|
-
font-
|
|
291
|
-
|
|
352
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
353
|
+
font-size: 0.4rem !important;
|
|
354
|
+
color: #666 !important;
|
|
292
355
|
text-transform: uppercase;
|
|
293
356
|
letter-spacing: 0.5px;
|
|
294
357
|
`;
|
|
@@ -296,19 +359,20 @@ const SellerInfo = styled.span`
|
|
|
296
359
|
const ListingPrice = styled.div`
|
|
297
360
|
display: flex;
|
|
298
361
|
align-items: center;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
font-
|
|
302
|
-
|
|
362
|
+
justify-content: center;
|
|
363
|
+
gap: 4px;
|
|
364
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
365
|
+
font-size: 0.5rem !important;
|
|
366
|
+
color: #fef08a !important;
|
|
367
|
+
line-height: 1;
|
|
303
368
|
`;
|
|
304
369
|
|
|
305
|
-
const
|
|
370
|
+
const DCCoinWrapper = styled.span`
|
|
306
371
|
display: flex;
|
|
307
372
|
align-items: center;
|
|
308
373
|
justify-content: center;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
left: -0.3rem;
|
|
374
|
+
flex-shrink: 0;
|
|
375
|
+
line-height: 0;
|
|
312
376
|
`;
|
|
313
377
|
|
|
314
378
|
const BeingBoughtBadge = styled.span`
|
|
@@ -319,8 +383,9 @@ const BeingBoughtBadge = styled.span`
|
|
|
319
383
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
320
384
|
border-radius: 4px;
|
|
321
385
|
padding: 2px 6px;
|
|
322
|
-
font-
|
|
323
|
-
|
|
386
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
387
|
+
font-size: 0.35rem !important;
|
|
388
|
+
color: #ef4444 !important;
|
|
324
389
|
text-transform: uppercase;
|
|
325
390
|
letter-spacing: 0.5px;
|
|
326
391
|
`;
|
|
@@ -335,28 +400,6 @@ const PagerFooter = styled.div`
|
|
|
335
400
|
margin: 0 auto;
|
|
336
401
|
`;
|
|
337
402
|
|
|
338
|
-
const Pagination = styled.div`
|
|
339
|
-
display: flex;
|
|
340
|
-
gap: 6px;
|
|
341
|
-
`;
|
|
342
|
-
|
|
343
|
-
const PageButton = styled.button<{ $active: boolean }>`
|
|
344
|
-
padding: 6px 10px;
|
|
345
|
-
font-family: 'Press Start 2P', cursive;
|
|
346
|
-
font-size: 0.5rem;
|
|
347
|
-
border-radius: 4px;
|
|
348
|
-
border: 1px solid ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.12)')};
|
|
349
|
-
background: ${({ $active }) => ($active ? 'rgba(245,158,11,0.15)' : 'rgba(0,0,0,0.3)')};
|
|
350
|
-
color: ${({ $active }) => ($active ? '#f59e0b' : '#777')};
|
|
351
|
-
cursor: pointer;
|
|
352
|
-
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
|
353
|
-
|
|
354
|
-
&:hover {
|
|
355
|
-
border-color: ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.3)')};
|
|
356
|
-
color: ${({ $active }) => ($active ? '#f59e0b' : '#bbb')};
|
|
357
|
-
}
|
|
358
|
-
`;
|
|
359
|
-
|
|
360
403
|
const LoadingState = styled.div`
|
|
361
404
|
display: flex;
|
|
362
405
|
flex-direction: column;
|
|
@@ -381,8 +424,9 @@ const Spinner = styled.div`
|
|
|
381
424
|
`;
|
|
382
425
|
|
|
383
426
|
const LoadingText = styled.span`
|
|
384
|
-
font-
|
|
385
|
-
|
|
427
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
428
|
+
font-size: 0.48rem !important;
|
|
429
|
+
color: #8a8a8a !important;
|
|
386
430
|
text-transform: uppercase;
|
|
387
431
|
letter-spacing: 1px;
|
|
388
432
|
`;
|
|
@@ -398,8 +442,9 @@ const EmptyState = styled.div`
|
|
|
398
442
|
`;
|
|
399
443
|
|
|
400
444
|
const EmptyText = styled.span`
|
|
401
|
-
font-
|
|
402
|
-
|
|
445
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
446
|
+
font-size: 0.48rem !important;
|
|
447
|
+
color: #71717a !important;
|
|
403
448
|
text-transform: uppercase;
|
|
404
449
|
letter-spacing: 1px;
|
|
405
450
|
`;
|