@rpg-engine/long-bow 0.8.181 → 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.
Files changed (36) hide show
  1. package/dist/components/Marketplace/CharacterListingForm.d.ts +15 -0
  2. package/dist/components/Marketplace/CharacterListingModal.d.ts +17 -0
  3. package/dist/components/Marketplace/CharacterMarketplacePanel.d.ts +22 -0
  4. package/dist/components/Marketplace/CharacterMarketplaceRows.d.ts +26 -0
  5. package/dist/components/Marketplace/Marketplace.d.ts +20 -1
  6. package/dist/components/Marketplace/MyCharacterListingsPanel.d.ts +19 -0
  7. package/dist/components/shared/DCRateStrip.d.ts +2 -0
  8. package/dist/components/shared/RadioOption.d.ts +22 -0
  9. package/dist/index.d.ts +4 -0
  10. package/dist/long-bow.cjs.development.js +1114 -130
  11. package/dist/long-bow.cjs.development.js.map +1 -1
  12. package/dist/long-bow.cjs.production.min.js +1 -1
  13. package/dist/long-bow.cjs.production.min.js.map +1 -1
  14. package/dist/long-bow.esm.js +1133 -154
  15. package/dist/long-bow.esm.js.map +1 -1
  16. package/dist/stories/Features/marketplace/CharacterListingModal.stories.d.ts +8 -0
  17. package/dist/stories/Features/marketplace/CharacterMarketplace.stories.d.ts +10 -0
  18. package/dist/stories/shared/RadioOption.stories.d.ts +8 -0
  19. package/package.json +1 -1
  20. package/src/components/DCWallet/DCWalletContent.tsx +5 -47
  21. package/src/components/Marketplace/BuyPanel.tsx +1 -0
  22. package/src/components/Marketplace/CharacterListingForm.tsx +102 -0
  23. package/src/components/Marketplace/CharacterListingModal.tsx +404 -0
  24. package/src/components/Marketplace/CharacterMarketplacePanel.tsx +450 -0
  25. package/src/components/Marketplace/CharacterMarketplaceRows.tsx +265 -0
  26. package/src/components/Marketplace/GroupedRowContainer.tsx +3 -1
  27. package/src/components/Marketplace/ManagmentPanel.tsx +1 -0
  28. package/src/components/Marketplace/Marketplace.tsx +163 -2
  29. package/src/components/Marketplace/MyCharacterListingsPanel.tsx +327 -0
  30. package/src/components/shared/DCRateStrip.tsx +67 -0
  31. package/src/components/shared/ItemRowWrapper.tsx +3 -1
  32. package/src/components/shared/RadioOption.tsx +93 -0
  33. package/src/index.tsx +4 -0
  34. package/src/stories/Features/marketplace/CharacterListingModal.stories.tsx +131 -0
  35. package/src/stories/Features/marketplace/CharacterMarketplace.stories.tsx +340 -0
  36. package/src/stories/shared/RadioOption.stories.tsx +93 -0
@@ -0,0 +1,450 @@
1
+ import { formatDCAmount, ICharacterListing, ICharacterListingSnapshot } from '@rpg-engine/shared';
2
+ import { User } from 'pixelarticons/react/User';
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import styled, { keyframes } from 'styled-components';
5
+ import { Input } from '../Input';
6
+ import { ConfirmModal } from '../ConfirmModal';
7
+ import { Pagination } from '../shared/Pagination/Pagination';
8
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
9
+
10
+ export interface ICharacterMarketplacePanelProps {
11
+ characterListings: ICharacterListing[];
12
+ totalCount: number;
13
+ currentPage: number;
14
+ itemsPerPage: number;
15
+ onPageChange: (page: number) => void;
16
+ onCharacterBuy: (listingId: string) => void;
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
+ enableHotkeys?: () => void;
24
+ disableHotkeys?: () => void;
25
+ nameFilter?: string;
26
+ onNameFilterChange?: (name: string) => void;
27
+ isLoading?: boolean;
28
+ }
29
+
30
+ export const CharacterMarketplacePanel: React.FC<ICharacterMarketplacePanelProps> = ({
31
+ characterListings,
32
+ totalCount,
33
+ currentPage,
34
+ itemsPerPage,
35
+ onPageChange,
36
+ onCharacterBuy,
37
+ atlasJSON,
38
+ atlasIMG,
39
+ characterAtlasJSON,
40
+ characterAtlasIMG,
41
+ enableHotkeys,
42
+ disableHotkeys,
43
+ nameFilter = '',
44
+ onNameFilterChange,
45
+ isLoading = false,
46
+ }) => {
47
+ const [buyingListingId, setBuyingListingId] = useState<string | null>(null);
48
+ const [localNameFilter, setLocalNameFilter] = useState(nameFilter);
49
+ const itemsContainer = useRef<HTMLDivElement>(null);
50
+
51
+ useEffect(() => {
52
+ itemsContainer.current?.scrollTo(0, 0);
53
+ }, [currentPage]);
54
+
55
+ useEffect(() => {
56
+ setLocalNameFilter(nameFilter);
57
+ }, [nameFilter]);
58
+
59
+ const filteredListings = useMemo(() => {
60
+ if (!localNameFilter.trim()) {
61
+ return characterListings;
62
+ }
63
+ const filter = localNameFilter.trim().toLowerCase();
64
+ return characterListings.filter(listing =>
65
+ listing.characterSnapshot.name?.toLowerCase().includes(filter)
66
+ );
67
+ }, [characterListings, localNameFilter]);
68
+
69
+ const handleNameFilterChange = (value: string) => {
70
+ setLocalNameFilter(value);
71
+ onNameFilterChange?.(value);
72
+ };
73
+
74
+ const handleBuyClick = (listingId: string) => {
75
+ setBuyingListingId(listingId);
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) => {
88
+ return (
89
+ <SpriteFromAtlas
90
+ atlasIMG={characterAtlasIMG}
91
+ atlasJSON={characterAtlasJSON}
92
+ spriteKey={`${snapshot.textureKey}/down/standing/0.png`}
93
+ imgScale={3}
94
+ height={64}
95
+ width={64}
96
+ />
97
+ );
98
+ };
99
+
100
+ return (
101
+ <>
102
+ {buyingListingId && (
103
+ <ConfirmModal
104
+ onClose={() => {
105
+ setBuyingListingId(null);
106
+ enableHotkeys?.();
107
+ }}
108
+ onConfirm={handleBuyConfirm}
109
+ message="Are you sure you want to buy this character?"
110
+ />
111
+ )}
112
+
113
+ <ToolbarRow>
114
+ <SearchField>
115
+ <Input
116
+ onChange={e => handleNameFilterChange(e.target.value)}
117
+ value={localNameFilter}
118
+ placeholder="Search characters..."
119
+ onBlur={enableHotkeys}
120
+ onFocus={disableHotkeys}
121
+ className="search-input"
122
+ />
123
+ </SearchField>
124
+ </ToolbarRow>
125
+
126
+ <ListingsContainer id="MarketContainer" ref={itemsContainer}>
127
+ {isLoading ? (
128
+ <LoadingState>
129
+ <Spinner />
130
+ <LoadingText>Loading character listings...</LoadingText>
131
+ </LoadingState>
132
+ ) : filteredListings.length === 0 ? (
133
+ <EmptyState>
134
+ <User width={32} height={32} />
135
+ <EmptyText>No character listings found.</EmptyText>
136
+ </EmptyState>
137
+ ) : (
138
+ <ListingsGrid>
139
+ {filteredListings.map((listing) => (
140
+ <CharacterListingCard
141
+ key={listing._id}
142
+ onClick={() => handleBuyClick(listing._id)}
143
+ $isBeingBought={listing.isBeingBought}
144
+ >
145
+ <CharacterSprite>
146
+ {renderCharacterSprite(listing.characterSnapshot)}
147
+ </CharacterSprite>
148
+ <CharacterInfo>
149
+ <CharacterName>{listing.characterSnapshot.name || 'Unknown'}</CharacterName>
150
+ <CharacterMeta>
151
+ Lv.{listing.characterSnapshot.level} · {listing.characterSnapshot.class}
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
+ )}
168
+ <SellerInfo>by {listing.listedByCharacterName}</SellerInfo>
169
+ <ListingPrice>
170
+ <DCCoinWrapper>
171
+ <SpriteFromAtlas
172
+ atlasIMG={atlasIMG}
173
+ atlasJSON={atlasJSON}
174
+ spriteKey="others/definya-coin.png"
175
+ imgScale={1}
176
+ />
177
+ </DCCoinWrapper>
178
+ {formatDCAmount(listing.price)} DC
179
+ </ListingPrice>
180
+ </CharacterInfo>
181
+ {listing.isBeingBought && (
182
+ <BeingBoughtBadge>Pending</BeingBoughtBadge>
183
+ )}
184
+ </CharacterListingCard>
185
+ ))}
186
+ </ListingsGrid>
187
+ )}
188
+ </ListingsContainer>
189
+
190
+ {totalCount > itemsPerPage && (
191
+ <PagerFooter>
192
+ <Pagination
193
+ currentPage={currentPage}
194
+ totalPages={Math.ceil(totalCount / itemsPerPage)}
195
+ onPageChange={onPageChange}
196
+ />
197
+ </PagerFooter>
198
+ )}
199
+ </>
200
+ );
201
+ };
202
+
203
+ const ToolbarRow = styled.div`
204
+ width: 95%;
205
+ display: flex;
206
+ gap: 10px;
207
+ align-items: center;
208
+ margin: 0 auto 10px auto;
209
+ background: rgba(0, 0, 0, 0.2);
210
+ padding: 8px 12px;
211
+ border-radius: 4px;
212
+ border: 1px solid rgba(255, 255, 255, 0.05);
213
+ `;
214
+
215
+ const SearchField = styled.div`
216
+ flex: 1;
217
+ min-width: 0;
218
+
219
+ input.search-input {
220
+ height: 10px;
221
+ width: 100%;
222
+ }
223
+ `;
224
+
225
+ const ListingsContainer = styled.div`
226
+ overflow-y: scroll;
227
+ overflow-x: hidden;
228
+ height: 390px;
229
+ width: 95%;
230
+ margin: 1rem auto 0 auto;
231
+ background: rgba(0, 0, 0, 0.2);
232
+ border: 1px solid rgba(255, 255, 255, 0.05);
233
+ border-radius: 4px;
234
+
235
+ @media (max-width: 950px) {
236
+ height: 250px;
237
+ }
238
+ `;
239
+
240
+ const ListingsGrid = styled.div`
241
+ display: grid;
242
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
243
+ gap: 12px;
244
+ padding: 12px;
245
+ `;
246
+
247
+ const CharacterListingCard = styled.button<{ $isBeingBought?: boolean }>`
248
+ display: flex;
249
+ flex-direction: column;
250
+ align-items: center;
251
+ gap: 8px;
252
+ background: rgba(255, 255, 255, 0.03);
253
+ border: 1px solid ${({ $isBeingBought }) => $isBeingBought ? 'rgba(239, 68, 68, 0.3)' : 'rgba(255, 255, 255, 0.08)'};
254
+ border-radius: 8px;
255
+ padding: 12px;
256
+ cursor: pointer;
257
+ transition: border-color 0.15s, background 0.15s;
258
+ position: relative;
259
+ opacity: ${({ $isBeingBought }) => $isBeingBought ? 0.6 : 1};
260
+
261
+ &:hover {
262
+ border-color: ${({ $isBeingBought }) => $isBeingBought ? 'rgba(239, 68, 68, 0.5)' : 'rgba(245, 158, 11, 0.4)'};
263
+ background: ${({ $isBeingBought }) => $isBeingBought ? 'rgba(239, 68, 68, 0.05)' : 'rgba(245, 158, 11, 0.05)'};
264
+ }
265
+
266
+ ${({ $isBeingBought }) => $isBeingBought && `
267
+ pointer-events: none;
268
+ `}
269
+ `;
270
+
271
+ const CharacterSprite = styled.div`
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ image-rendering: pixelated;
276
+ width: 64px;
277
+ height: 64px;
278
+ flex-shrink: 0;
279
+ `;
280
+
281
+ const CharacterInfo = styled.div`
282
+ display: flex;
283
+ flex-direction: column;
284
+ align-items: center;
285
+ gap: 4px;
286
+ text-align: center;
287
+ width: 100%;
288
+ `;
289
+
290
+ const CharacterName = styled.span`
291
+ font-family: 'Press Start 2P', cursive !important;
292
+ font-size: 0.55rem !important;
293
+ color: #f3f4f6 !important;
294
+ text-transform: uppercase;
295
+ letter-spacing: 0.5px;
296
+ `;
297
+
298
+ const CharacterMeta = styled.span`
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;
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
+
359
+ const ListingPrice = styled.div`
360
+ display: flex;
361
+ align-items: center;
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;
368
+ `;
369
+
370
+ const DCCoinWrapper = styled.span`
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ flex-shrink: 0;
375
+ line-height: 0;
376
+ `;
377
+
378
+ const BeingBoughtBadge = styled.span`
379
+ position: absolute;
380
+ top: 6px;
381
+ right: 6px;
382
+ background: rgba(239, 68, 68, 0.2);
383
+ border: 1px solid rgba(239, 68, 68, 0.4);
384
+ border-radius: 4px;
385
+ padding: 2px 6px;
386
+ font-family: 'Press Start 2P', cursive !important;
387
+ font-size: 0.35rem !important;
388
+ color: #ef4444 !important;
389
+ text-transform: uppercase;
390
+ letter-spacing: 0.5px;
391
+ `;
392
+
393
+ const PagerFooter = styled.div`
394
+ display: flex;
395
+ justify-content: center;
396
+ align-items: center;
397
+ padding: 8px 0 4px;
398
+ min-height: 36px;
399
+ width: 95%;
400
+ margin: 0 auto;
401
+ `;
402
+
403
+ const LoadingState = styled.div`
404
+ display: flex;
405
+ flex-direction: column;
406
+ align-items: center;
407
+ justify-content: center;
408
+ gap: 14px;
409
+ height: 100%;
410
+ min-height: 160px;
411
+ `;
412
+
413
+ const spin = keyframes`
414
+ to { transform: rotate(360deg); }
415
+ `;
416
+
417
+ const Spinner = styled.div`
418
+ width: 28px;
419
+ height: 28px;
420
+ border: 3px solid rgba(245, 158, 11, 0.2);
421
+ border-top-color: #f59e0b;
422
+ border-radius: 50%;
423
+ animation: ${spin} 0.7s linear infinite;
424
+ `;
425
+
426
+ const LoadingText = styled.span`
427
+ font-family: 'Press Start 2P', cursive !important;
428
+ font-size: 0.48rem !important;
429
+ color: #8a8a8a !important;
430
+ text-transform: uppercase;
431
+ letter-spacing: 1px;
432
+ `;
433
+
434
+ const EmptyState = styled.div`
435
+ display: flex;
436
+ flex-direction: column;
437
+ align-items: center;
438
+ justify-content: center;
439
+ gap: 12px;
440
+ height: 100%;
441
+ min-height: 200px;
442
+ `;
443
+
444
+ const EmptyText = styled.span`
445
+ font-family: 'Press Start 2P', cursive !important;
446
+ font-size: 0.48rem !important;
447
+ color: #71717a !important;
448
+ text-transform: uppercase;
449
+ letter-spacing: 1px;
450
+ `;
@@ -0,0 +1,265 @@
1
+ import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
2
+ import { Coins } from 'pixelarticons/react/Coins';
3
+ import React from 'react';
4
+ import styled from 'styled-components';
5
+ import { ItemRowWrapper } from '../shared/ItemRowWrapper';
6
+ import { CTAButton } from '../shared/CTAButton/CTAButton';
7
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
8
+ import { Ellipsis } from '../shared/Ellipsis';
9
+
10
+ export interface ICharacterMarketplaceRowsProps {
11
+ listing: ICharacterListing;
12
+ /** Items atlas — for UI sprites like the DC coin */
13
+ atlasJSON: any;
14
+ atlasIMG: any;
15
+ /** Entities atlas — for character sprites */
16
+ characterAtlasJSON: any;
17
+ characterAtlasIMG: any;
18
+ onCharacterBuy?: () => void;
19
+ onCharacterDelist?: () => void;
20
+ disabled?: boolean;
21
+ }
22
+
23
+ export const CharacterMarketplaceRows: React.FC<ICharacterMarketplaceRowsProps> = ({
24
+ listing,
25
+ atlasJSON,
26
+ atlasIMG,
27
+ characterAtlasJSON,
28
+ characterAtlasIMG,
29
+ onCharacterBuy,
30
+ onCharacterDelist,
31
+ disabled,
32
+ }) => {
33
+ const { characterSnapshot, price, isBeingBought } = listing;
34
+
35
+ return (
36
+ <ItemRowWrapper $isHighlighted={disabled || isBeingBought}>
37
+ <ItemSection>
38
+ <SpriteContainer>
39
+ <CharacterSprite>
40
+ <SpriteFromAtlas
41
+ atlasIMG={characterAtlasIMG}
42
+ atlasJSON={characterAtlasJSON}
43
+ spriteKey={`${characterSnapshot.textureKey}/down/standing/0.png`}
44
+ imgScale={2.5}
45
+ height={56}
46
+ width={56}
47
+ />
48
+ </CharacterSprite>
49
+ </SpriteContainer>
50
+
51
+ <ItemDetails>
52
+ <ItemName>
53
+ <Ellipsis maxLines={1} maxWidth="180px" fontSize="10px">
54
+ {characterSnapshot.name || 'Unknown Character'}
55
+ </Ellipsis>
56
+ </ItemName>
57
+ <CharacterMeta>Level {characterSnapshot.level}</CharacterMeta>
58
+ {characterSnapshot.class && (
59
+ <CharacterClass>{characterSnapshot.class}</CharacterClass>
60
+ )}
61
+ <PriceRow>
62
+ <DCCoinWrapper>
63
+ <SpriteFromAtlas
64
+ atlasIMG={atlasIMG}
65
+ atlasJSON={atlasJSON}
66
+ spriteKey="others/definya-coin.png"
67
+ imgScale={1}
68
+ />
69
+ </DCCoinWrapper>
70
+ <DCPrice>{formatDCAmount(price)}</DCPrice>
71
+ </PriceRow>
72
+ {isBeingBought && (
73
+ <PendingBadge>Sale Pending</PendingBadge>
74
+ )}
75
+ </ItemDetails>
76
+ </ItemSection>
77
+
78
+ <ActionSection>
79
+ <CTAButton
80
+ icon={onCharacterBuy ? <Coins width={18} height={18} /> : undefined}
81
+ label={onCharacterBuy ? 'Buy' : 'Delist'}
82
+ disabled={disabled || isBeingBought}
83
+ onClick={() => {
84
+ if (disabled || isBeingBought) return;
85
+ onCharacterBuy?.();
86
+ onCharacterDelist?.();
87
+ }}
88
+ iconColor={onCharacterBuy ? '#f59e0b' : '#ef4444'}
89
+ />
90
+ </ActionSection>
91
+ </ItemRowWrapper>
92
+ );
93
+ };
94
+
95
+ const ItemSection = styled.div`
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 0.75rem;
99
+ flex: 1;
100
+ min-width: 0;
101
+ `;
102
+
103
+ const SpriteContainer = styled.div`
104
+ position: relative;
105
+ flex-shrink: 0;
106
+ min-width: 44px;
107
+ `;
108
+
109
+ const CharacterSprite = styled.div`
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ image-rendering: pixelated;
114
+ `;
115
+
116
+ const ItemDetails = styled.div`
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 0.2rem;
120
+ min-width: 0;
121
+ margin-left: 0.5rem;
122
+ `;
123
+
124
+ const ItemName = styled.div`
125
+ font-family: 'Press Start 2P', cursive;
126
+ font-size: 0.6rem;
127
+ color: #ffffff;
128
+ `;
129
+
130
+ const CharacterMeta = styled.span`
131
+ font-size: 0.45rem;
132
+ color: #888;
133
+ text-transform: uppercase;
134
+ letter-spacing: 0.5px;
135
+ `;
136
+
137
+ const CharacterClass = styled.span`
138
+ font-size: 0.4rem;
139
+ color: #666;
140
+ text-transform: uppercase;
141
+ letter-spacing: 0.5px;
142
+ `;
143
+
144
+ const PriceRow = styled.div`
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 0.4rem;
148
+ margin-top: 0.2rem;
149
+ `;
150
+
151
+ const DCCoinWrapper = styled.span`
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ position: relative;
156
+ top: -0.5rem;
157
+ left: -0.4rem;
158
+ `;
159
+
160
+ const DCPrice = styled.span`
161
+ font-family: 'Press Start 2P', cursive;
162
+ font-size: 0.55rem !important;
163
+ color: #fef08a;
164
+ line-height: 1;
165
+ `;
166
+
167
+ const ActionSection = styled.div`
168
+ flex-shrink: 0;
169
+ margin-left: 0.75rem;
170
+ `;
171
+
172
+ const PendingBadge = styled.span`
173
+ font-size: 0.38rem;
174
+ padding: 2px 6px;
175
+ border-radius: 4px;
176
+ text-transform: uppercase;
177
+ letter-spacing: 0.5px;
178
+ background: rgba(239, 68, 68, 0.2);
179
+ border: 1px solid rgba(239, 68, 68, 0.4);
180
+ color: #ef4444;
181
+ margin-top: 4px;
182
+ `;
183
+
184
+ export interface IGroupedCharacterMarketplaceRowProps {
185
+ bestListing: ICharacterListing;
186
+ otherListings: ICharacterListing[];
187
+ atlasJSON: any;
188
+ atlasIMG: any;
189
+ characterAtlasJSON: any;
190
+ characterAtlasIMG: any;
191
+ onBuy: (id: string) => void;
192
+ currentCharacterId?: string;
193
+ }
194
+
195
+ export const GroupedCharacterMarketplaceRow: React.FC<IGroupedCharacterMarketplaceRowProps> = ({
196
+ bestListing,
197
+ otherListings,
198
+ atlasJSON,
199
+ atlasIMG,
200
+ characterAtlasJSON,
201
+ characterAtlasIMG,
202
+ onBuy,
203
+ currentCharacterId,
204
+ }) => {
205
+ const makeRow = (listing: ICharacterListing) => {
206
+ const isOwnListing = listing.seller === currentCharacterId;
207
+
208
+ return (
209
+ <CharacterMarketplaceRows
210
+ key={listing._id}
211
+ listing={listing}
212
+ atlasJSON={atlasJSON}
213
+ atlasIMG={atlasIMG}
214
+ characterAtlasJSON={characterAtlasJSON}
215
+ characterAtlasIMG={characterAtlasIMG}
216
+ onCharacterBuy={isOwnListing ? undefined : () => onBuy(listing._id)}
217
+ disabled={isOwnListing}
218
+ />
219
+ );
220
+ };
221
+
222
+ if (otherListings.length === 0) {
223
+ return makeRow(bestListing);
224
+ }
225
+
226
+ return (
227
+ <GroupedRowContainer>
228
+ <MainRow>{makeRow(bestListing)}</MainRow>
229
+ <OtherListings>
230
+ {otherListings.map((listing) => (
231
+ <OtherListingRow key={listing._id}>
232
+ {makeRow(listing)}
233
+ </OtherListingRow>
234
+ ))}
235
+ </OtherListings>
236
+ </GroupedRowContainer>
237
+ );
238
+ };
239
+
240
+ const GroupedRowContainer = styled.div`
241
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
242
+
243
+ &:last-child {
244
+ border-bottom: none;
245
+ }
246
+ `;
247
+
248
+ const MainRow = styled.div``;
249
+
250
+ const OtherListings = styled.div`
251
+ display: flex;
252
+ flex-direction: column;
253
+ padding-left: 12px;
254
+ border-left: 2px solid rgba(245, 158, 11, 0.15);
255
+ margin-top: 4px;
256
+ margin-bottom: 4px;
257
+ `;
258
+
259
+ const OtherListingRow = styled.div`
260
+ opacity: 0.85;
261
+
262
+ &:hover {
263
+ opacity: 1;
264
+ }
265
+ `;