@rpg-engine/long-bow 0.8.160 → 0.8.162

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.
@@ -14,7 +14,7 @@ import { Dropdown } from '../Dropdown';
14
14
  import { Input } from '../Input';
15
15
  import { SegmentedToggle } from '../shared/SegmentedToggle';
16
16
  import { Pager } from '../Pager';
17
- import { BuyOrderRow } from './BuyOrderRows';
17
+ import { GroupedBuyOrderRow } from './BuyOrderRows';
18
18
  import { MarketplaceBuyModal, MarketplacePaymentMethod } from './MarketplaceBuyModal';
19
19
  import { GroupedMarketplaceRow } from './MarketplaceRows';
20
20
  import { itemRarityOptions, itemTypeOptions, orderByOptions } from './filters';
@@ -173,11 +173,29 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
173
173
  });
174
174
  }, [name, openBuyOrders, price, selectedRarity]);
175
175
 
176
+ const groupedBuyOrders = useMemo(() => {
177
+ const groups = new Map<string, IMarketplaceBuyOrderItem[]>();
178
+ for (const order of visibleBuyOrders) {
179
+ const key = order.itemBlueprintKey;
180
+ if (!groups.has(key)) groups.set(key, []);
181
+ groups.get(key)!.push(order);
182
+ }
183
+ return Array.from(groups.values())
184
+ .map(group => {
185
+ const sorted = [...group].sort((a, b) => b.maxPrice - a.maxPrice);
186
+ return { bestOrder: sorted[0], otherOrders: sorted.slice(1) };
187
+ })
188
+ .sort((a, b) => {
189
+ const totalGold = (g: typeof a) => [g.bestOrder, ...g.otherOrders].reduce((sum, o) => sum + o.maxPrice, 0);
190
+ return totalGold(b) - totalGold(a);
191
+ });
192
+ }, [visibleBuyOrders]);
193
+
176
194
  const showSellSection = browseMode === 'sell';
177
195
  const showBuySection = browseMode === 'buy';
178
196
  const hasVisibleContent =
179
197
  (showSellSection && groupedItems.length > 0) ||
180
- (showBuySection && visibleBuyOrders.length > 0);
198
+ (showBuySection && groupedBuyOrders.length > 0);
181
199
 
182
200
  return (
183
201
  <>
@@ -254,12 +272,14 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
254
272
 
255
273
  {showFilters && (
256
274
  <OptionsWrapper showFilters={showFilters}>
257
- <WrapperContainer>
258
- <StyledDropdown
259
- options={itemTypeOptions}
260
- onChange={onChangeType}
261
- width="100%"
262
- />
275
+ <WrapperContainer $sell={showSellSection}>
276
+ {showSellSection && (
277
+ <StyledDropdown
278
+ options={itemTypeOptions}
279
+ onChange={onChangeType}
280
+ width="100%"
281
+ />
282
+ )}
263
283
  <StyledDropdown
264
284
  options={itemRarityOptions}
265
285
  onChange={value => {
@@ -268,107 +288,113 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
268
288
  }}
269
289
  width="100%"
270
290
  />
271
- <StyledDropdown
272
- options={orderByOptions}
273
- onChange={onChangeOrder}
274
- width="100%"
275
- />
291
+ {showSellSection && (
292
+ <StyledDropdown
293
+ options={orderByOptions}
294
+ onChange={onChangeOrder}
295
+ width="100%"
296
+ />
297
+ )}
276
298
  </WrapperContainer>
277
299
 
278
300
  <FilterInputsWrapper>
301
+ {showSellSection && (
302
+ <div>
303
+ <p>Main level</p>
304
+ <div className="input-group">
305
+ <Input
306
+ onChange={e => {
307
+ setMainLevel([Number(e.target.value), mainLevel[1]]);
308
+ onChangeMainLevelInput([Number(e.target.value), mainLevel[1]]);
309
+ }}
310
+ placeholder="Min"
311
+ type="number"
312
+ min={0}
313
+ onBlur={enableHotkeys}
314
+ onFocus={disableHotkeys}
315
+ />
316
+ <AiFillCaretRight className="separator-icon" />
317
+ <Input
318
+ onChange={e => {
319
+ setMainLevel([mainLevel[0], Number(e.target.value)]);
320
+ onChangeMainLevelInput([mainLevel[0], Number(e.target.value)]);
321
+ }}
322
+ placeholder="Max"
323
+ type="number"
324
+ min={0}
325
+ onBlur={enableHotkeys}
326
+ onFocus={disableHotkeys}
327
+ />
328
+ </div>
329
+ </div>
330
+ )}
331
+
332
+ {showSellSection && (
333
+ <div>
334
+ <p>Secondary level</p>
335
+ <div className="input-group">
336
+ <Input
337
+ onChange={e => {
338
+ setSecondaryLevel([Number(e.target.value), secondaryLevel[1]]);
339
+ onChangeSecondaryLevelInput([
340
+ Number(e.target.value),
341
+ secondaryLevel[1],
342
+ ]);
343
+ }}
344
+ placeholder="Min"
345
+ type="number"
346
+ min={0}
347
+ onBlur={enableHotkeys}
348
+ onFocus={disableHotkeys}
349
+ />
350
+ <AiFillCaretRight className="separator-icon" />
351
+ <Input
352
+ onChange={e => {
353
+ setSecondaryLevel([secondaryLevel[0], Number(e.target.value)]);
354
+ onChangeSecondaryLevelInput([
355
+ secondaryLevel[0],
356
+ Number(e.target.value),
357
+ ]);
358
+ }}
359
+ placeholder="Max"
360
+ type="number"
361
+ min={0}
362
+ onBlur={enableHotkeys}
363
+ onFocus={disableHotkeys}
364
+ />
365
+ </div>
366
+ </div>
367
+ )}
368
+
279
369
  <div>
280
- <p>Main level</p>
281
- <div className="input-group">
282
- <Input
283
- onChange={e => {
284
- setMainLevel([Number(e.target.value), mainLevel[1]]);
285
- onChangeMainLevelInput([Number(e.target.value), mainLevel[1]]);
286
- }}
287
- placeholder="Min"
288
- type="number"
289
- min={0}
290
- onBlur={enableHotkeys}
291
- onFocus={disableHotkeys}
292
- />
293
- <AiFillCaretRight className="separator-icon" />
294
- <Input
295
- onChange={e => {
296
- setMainLevel([mainLevel[0], Number(e.target.value)]);
297
- onChangeMainLevelInput([mainLevel[0], Number(e.target.value)]);
298
- }}
299
- placeholder="Max"
300
- type="number"
301
- min={0}
302
- onBlur={enableHotkeys}
303
- onFocus={disableHotkeys}
304
- />
305
- </div>
306
- </div>
307
-
308
- <div>
309
- <p>Secondary level</p>
310
- <div className="input-group">
311
- <Input
312
- onChange={e => {
313
- setSecondaryLevel([Number(e.target.value), secondaryLevel[1]]);
314
- onChangeSecondaryLevelInput([
315
- Number(e.target.value),
316
- secondaryLevel[1],
317
- ]);
318
- }}
319
- placeholder="Min"
320
- type="number"
321
- min={0}
322
- onBlur={enableHotkeys}
323
- onFocus={disableHotkeys}
324
- />
325
- <AiFillCaretRight className="separator-icon" />
326
- <Input
327
- onChange={e => {
328
- setSecondaryLevel([secondaryLevel[0], Number(e.target.value)]);
329
- onChangeSecondaryLevelInput([
330
- secondaryLevel[0],
331
- Number(e.target.value),
332
- ]);
333
- }}
334
- placeholder="Max"
335
- type="number"
336
- min={0}
337
- onBlur={enableHotkeys}
338
- onFocus={disableHotkeys}
339
- />
340
- </div>
341
- </div>
342
-
343
- <div>
344
- <p>Price</p>
345
- <div className="input-group">
346
- <Input
347
- onChange={e => {
348
- setPrice([Number(e.target.value), price[1]]);
349
- onChangePriceInput([Number(e.target.value), price[1]]);
350
- }}
351
- placeholder="Min"
352
- type="number"
353
- min={0}
354
- onBlur={enableHotkeys}
355
- onFocus={disableHotkeys}
356
- />
357
- <AiFillCaretRight className="separator-icon" />
358
- <Input
359
- onChange={e => {
360
- setPrice([price[0], Number(e.target.value)]);
361
- onChangePriceInput([price[0], Number(e.target.value)]);
362
- }}
363
- placeholder="Max"
364
- type="number"
365
- min={0}
366
- onBlur={enableHotkeys}
367
- onFocus={disableHotkeys}
368
- />
370
+ <p>Price</p>
371
+ <div className="input-group">
372
+ <Input
373
+ onChange={e => {
374
+ setPrice([Number(e.target.value), price[1]]);
375
+ onChangePriceInput([Number(e.target.value), price[1]]);
376
+ }}
377
+ placeholder="Min"
378
+ type="number"
379
+ min={0}
380
+ onBlur={enableHotkeys}
381
+ onFocus={disableHotkeys}
382
+ />
383
+ <AiFillCaretRight className="separator-icon" />
384
+ <Input
385
+ onChange={e => {
386
+ setPrice([price[0], Number(e.target.value)]);
387
+ onChangePriceInput([price[0], Number(e.target.value)]);
388
+ }}
389
+ placeholder="Max"
390
+ type="number"
391
+ min={0}
392
+ onBlur={enableHotkeys}
393
+ onFocus={disableHotkeys}
394
+ />
395
+ </div>
369
396
  </div>
370
- </div>
371
- </FilterInputsWrapper>
397
+ </FilterInputsWrapper>
372
398
  </OptionsWrapper>
373
399
  )}
374
400
 
@@ -416,19 +442,19 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
416
442
  <MarketSection>
417
443
  <SectionHeader>
418
444
  <SectionTitle>Buy Requests</SectionTitle>
419
- <SectionMeta>{visibleBuyOrders.length} visible</SectionMeta>
445
+ <SectionMeta>{groupedBuyOrders.length} groups</SectionMeta>
420
446
  </SectionHeader>
421
- {visibleBuyOrders.length === 0 ? (
447
+ {groupedBuyOrders.length === 0 ? (
422
448
  <SectionEmpty>No public buy requests found.</SectionEmpty>
423
449
  ) : (
424
- visibleBuyOrders.map(order => (
425
- <BuyOrderRow
426
- key={order._id}
427
- buyOrder={order}
450
+ groupedBuyOrders.map(({ bestOrder, otherOrders }) => (
451
+ <GroupedBuyOrderRow
452
+ key={bestOrder._id}
453
+ bestOrder={bestOrder}
454
+ otherOrders={otherOrders}
428
455
  atlasJSON={atlasJSON}
429
456
  atlasIMG={atlasIMG}
430
457
  onFulfill={setFulfillingBuyOrderId}
431
- showRequestTag
432
458
  />
433
459
  ))
434
460
  )}
@@ -532,7 +558,7 @@ const OptionsWrapper = styled.div<{ showFilters: boolean }>`
532
558
 
533
559
  const FilterInputsWrapper = styled.div`
534
560
  display: grid;
535
- grid-template-columns: repeat(3, 1fr);
561
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
536
562
  gap: 15px;
537
563
  color: white;
538
564
 
@@ -567,9 +593,9 @@ const FilterInputsWrapper = styled.div`
567
593
  }
568
594
  `;
569
595
 
570
- const WrapperContainer = styled.div`
596
+ const WrapperContainer = styled.div<{ $sell: boolean }>`
571
597
  display: grid;
572
- grid-template-columns: 1fr 1fr 1fr;
598
+ grid-template-columns: ${({ $sell }) => $sell ? 'repeat(3, 1fr)' : 'minmax(0, 200px)'};
573
599
  gap: 15px;
574
600
 
575
601
  .rpgui-content .rpgui-dropdown-imp-header {
@@ -0,0 +1,86 @@
1
+ import React, { useState } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ export interface IGroupedRowContainerProps {
5
+ mainRow: React.ReactNode;
6
+ subRows: React.ReactNode[];
7
+ badgeLabel?: string;
8
+ }
9
+
10
+ export const GroupedRowContainer: React.FC<IGroupedRowContainerProps> = ({
11
+ mainRow,
12
+ subRows,
13
+ badgeLabel = 'offers',
14
+ }) => {
15
+ const [expanded, setExpanded] = useState(false);
16
+ const hasMultiple = subRows.length > 0;
17
+ const totalCount = subRows.length + 1;
18
+
19
+ return (
20
+ <GroupWrapper>
21
+ <GroupHeader $clickable={hasMultiple} onClick={hasMultiple ? () => setExpanded(e => !e) : undefined}>
22
+ {mainRow}
23
+ {hasMultiple && (
24
+ <GroupMeta>
25
+ <OfferBadge>{totalCount} {badgeLabel}</OfferBadge>
26
+ <Chevron $expanded={expanded}>&#9656;</Chevron>
27
+ </GroupMeta>
28
+ )}
29
+ </GroupHeader>
30
+
31
+ {expanded && (
32
+ <SubRows>
33
+ {subRows}
34
+ </SubRows>
35
+ )}
36
+ </GroupWrapper>
37
+ );
38
+ };
39
+
40
+ const GroupWrapper = styled.div`
41
+ margin-bottom: 2px;
42
+ `;
43
+
44
+ const GroupHeader = styled.div<{ $clickable: boolean }>`
45
+ position: relative;
46
+ cursor: ${({ $clickable }) => ($clickable ? 'pointer' : 'default')};
47
+ `;
48
+
49
+ const GroupMeta = styled.div`
50
+ position: absolute;
51
+ right: 180px;
52
+ top: 50%;
53
+ transform: translateY(-50%);
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 6px;
57
+ pointer-events: none;
58
+ `;
59
+
60
+ const OfferBadge = styled.span`
61
+ font-family: 'Press Start 2P', cursive;
62
+ font-size: 0.5rem;
63
+ color: rgba(255, 255, 255, 0.5);
64
+ background: rgba(255, 255, 255, 0.08);
65
+ padding: 2px 6px;
66
+ border-radius: 8px;
67
+ white-space: nowrap;
68
+ `;
69
+
70
+ const Chevron = styled.span<{ $expanded: boolean }>`
71
+ display: inline-block;
72
+ font-size: 0.7rem;
73
+ color: rgba(255, 255, 255, 0.4);
74
+ transition: transform 0.2s ease;
75
+ transform: rotate(${({ $expanded }) => ($expanded ? '90deg' : '0deg')});
76
+ `;
77
+
78
+ const SubRows = styled.div`
79
+ margin-left: 12px;
80
+ padding-left: 8px;
81
+ border-left: 2px solid rgba(245, 158, 11, 0.25);
82
+
83
+ > div > div {
84
+ background: rgba(0, 0, 0, 0.4);
85
+ }
86
+ `;
@@ -7,8 +7,10 @@ import {
7
7
  } from '@rpg-engine/shared';
8
8
  import { Coins } from 'pixelarticons/react/Coins';
9
9
  import { Delete } from 'pixelarticons/react/Delete';
10
- import React, { useState } from 'react';
10
+ import React from 'react';
11
11
  import styled from 'styled-components';
12
+ import { GroupedRowContainer } from './GroupedRowContainer';
13
+ import { ItemRowWrapper } from '../shared/ItemRowWrapper';
12
14
  import { uiFonts } from '../../constants/uiFonts';
13
15
  import { ItemInfoWrapper } from '../Item/Cards/ItemInfoWrapper';
14
16
  import { onRenderGems } from '../Item/Inventory/ItemGem';
@@ -50,7 +52,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
50
52
  };
51
53
 
52
54
  return (
53
- <MarketplaceWrapper>
55
+ <ItemRowWrapper>
54
56
  <ItemSection>
55
57
  <SpriteContainer>
56
58
  <ItemInfoWrapper
@@ -138,7 +140,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
138
140
  iconColor={onMarketPlaceItemBuy ? '#f59e0b' : '#ef4444'}
139
141
  />
140
142
  </ActionSection>
141
- </MarketplaceWrapper>
143
+ </ItemRowWrapper>
142
144
  );
143
145
  };
144
146
 
@@ -167,133 +169,30 @@ export const GroupedMarketplaceRow: React.FC<IGroupedMarketplaceRowProps> = ({
167
169
  onBuy,
168
170
  onDCCoinClick,
169
171
  }) => {
170
- const [expanded, setExpanded] = useState(false);
171
- const totalOffers = otherListings.length + 1;
172
- const hasMultiple = otherListings.length > 0;
172
+ const makeRow = (listing: IMarketplaceItem) => (
173
+ <MarketplaceRows
174
+ key={listing._id}
175
+ atlasIMG={atlasIMG}
176
+ atlasJSON={atlasJSON}
177
+ item={listing.item}
178
+ itemPrice={listing.price}
179
+ dcEquivalentPrice={dcToGoldSwapRate > 0 ? getDCEquivalentPrice(listing.price) : undefined}
180
+ equipmentSet={equipmentSet}
181
+ onMarketPlaceItemBuy={() => onBuy(listing._id)}
182
+ onDCCoinClick={onDCCoinClick}
183
+ disabled={listing.owner === characterId}
184
+ />
185
+ );
173
186
 
174
187
  return (
175
- <GroupWrapper>
176
- <GroupHeader
177
- onClick={hasMultiple ? () => setExpanded(!expanded) : undefined}
178
- clickable={hasMultiple}
179
- >
180
- <MarketplaceRows
181
- atlasIMG={atlasIMG}
182
- atlasJSON={atlasJSON}
183
- item={bestListing.item}
184
- itemPrice={bestListing.price}
185
- dcEquivalentPrice={
186
- dcToGoldSwapRate > 0
187
- ? getDCEquivalentPrice(bestListing.price)
188
- : undefined
189
- }
190
- equipmentSet={equipmentSet}
191
- onMarketPlaceItemBuy={() => onBuy(bestListing._id)}
192
- onDCCoinClick={onDCCoinClick}
193
- disabled={bestListing.owner === characterId}
194
- />
195
- {hasMultiple && (
196
- <GroupMeta>
197
- <OfferBadge>{totalOffers} offers</OfferBadge>
198
- <Chevron expanded={expanded}>&#9656;</Chevron>
199
- </GroupMeta>
200
- )}
201
- </GroupHeader>
202
-
203
- {expanded && (
204
- <SubRows>
205
- {otherListings.map((listing) => (
206
- <MarketplaceRows
207
- key={listing._id}
208
- atlasIMG={atlasIMG}
209
- atlasJSON={atlasJSON}
210
- item={listing.item}
211
- itemPrice={listing.price}
212
- dcEquivalentPrice={
213
- dcToGoldSwapRate > 0
214
- ? getDCEquivalentPrice(listing.price)
215
- : undefined
216
- }
217
- equipmentSet={equipmentSet}
218
- onMarketPlaceItemBuy={() => onBuy(listing._id)}
219
- onDCCoinClick={onDCCoinClick}
220
- disabled={listing.owner === characterId}
221
- />
222
- ))}
223
- </SubRows>
224
- )}
225
- </GroupWrapper>
188
+ <GroupedRowContainer
189
+ mainRow={makeRow(bestListing)}
190
+ subRows={otherListings.map(makeRow)}
191
+ badgeLabel="offers"
192
+ />
226
193
  );
227
194
  };
228
195
 
229
- const GroupWrapper = styled.div`
230
- margin-bottom: 2px;
231
- `;
232
-
233
- const GroupHeader = styled.div<{ clickable: boolean }>`
234
- position: relative;
235
- cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')};
236
- `;
237
-
238
- const GroupMeta = styled.div`
239
- position: absolute;
240
- right: 180px;
241
- top: 50%;
242
- transform: translateY(-50%);
243
- display: flex;
244
- align-items: center;
245
- gap: 6px;
246
- pointer-events: none;
247
- `;
248
-
249
- const OfferBadge = styled.span`
250
- font-family: 'Press Start 2P', cursive;
251
- font-size: 0.5rem;
252
- color: rgba(255, 255, 255, 0.5);
253
- background: rgba(255, 255, 255, 0.08);
254
- padding: 2px 6px;
255
- border-radius: 8px;
256
- white-space: nowrap;
257
- `;
258
-
259
- const Chevron = styled.span<{ expanded: boolean }>`
260
- display: inline-block;
261
- font-size: 0.7rem;
262
- color: rgba(255, 255, 255, 0.4);
263
- transition: transform 0.2s ease;
264
- transform: rotate(${({ expanded }) => (expanded ? '90deg' : '0deg')});
265
- `;
266
-
267
- const SubRows = styled.div`
268
- margin-left: 12px;
269
- padding-left: 8px;
270
- border-left: 2px solid rgba(245, 158, 11, 0.25);
271
-
272
- > div > div {
273
- background: rgba(0, 0, 0, 0.4);
274
- }
275
- `;
276
-
277
- const MarketplaceWrapper = styled.div`
278
- display: flex;
279
- align-items: center;
280
- justify-content: space-between;
281
- padding: 0.6rem 1rem;
282
- margin-bottom: 4px;
283
- background: rgba(0, 0, 0, 0.25);
284
- border: 1px solid rgba(255, 255, 255, 0.05);
285
- border-radius: 6px;
286
- border-left: 4px solid transparent;
287
- transition: all 0.2s ease-in-out;
288
-
289
- &:hover {
290
- background: rgba(245, 158, 11, 0.08);
291
- border-color: rgba(245, 158, 11, 0.2);
292
- border-left-color: #f59e0b;
293
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
294
- transform: translateY(-1px);
295
- }
296
- `;
297
196
 
298
197
  const ItemSection = styled.div`
299
198
  display: flex;
@@ -8,6 +8,7 @@ interface PagerProps {
8
8
  currentPage: number;
9
9
  itemsPerPage: number;
10
10
  onPageChange: (page: number) => void;
11
+ compact?: boolean;
11
12
  }
12
13
 
13
14
  export const Pager: React.FC<PagerProps> = ({
@@ -15,13 +16,14 @@ export const Pager: React.FC<PagerProps> = ({
15
16
  currentPage,
16
17
  itemsPerPage,
17
18
  onPageChange,
19
+ compact = false,
18
20
  }) => {
19
21
  const totalPages = Math.ceil(totalItems / itemsPerPage);
20
22
 
21
23
  return (
22
24
  <Container>
23
- <p>Total items: {totalItems}</p>
24
- <PagerContainer>
25
+ {!compact && <p>Total items: {totalItems}</p>}
26
+ <PagerContainer $compact={compact}>
25
27
  <button
26
28
  disabled={currentPage === 1}
27
29
  onPointerDown={() => onPageChange(Math.max(currentPage - 1, 1))}
@@ -55,7 +57,7 @@ const Container = styled.div`
55
57
  }
56
58
  `;
57
59
 
58
- const PagerContainer = styled.div`
60
+ const PagerContainer = styled.div<{ $compact: boolean }>`
59
61
  display: flex;
60
62
  justify-content: center;
61
63
  align-items: center;
@@ -67,11 +69,17 @@ const PagerContainer = styled.div`
67
69
 
68
70
  div {
69
71
  color: white;
72
+ ${({ $compact }) => $compact && `
73
+ font-size: 0.55rem !important;
74
+ padding: 2px 6px !important;
75
+ min-width: unset !important;
76
+ `}
70
77
  }
71
78
 
72
79
  button {
73
- width: 40px;
74
- height: 40px;
80
+ width: ${({ $compact }) => ($compact ? '24px' : '40px')} !important;
81
+ height: ${({ $compact }) => ($compact ? '24px' : '40px')} !important;
82
+ font-size: ${({ $compact }) => ($compact ? '0.55rem' : 'inherit')} !important;
75
83
  background-color: ${uiColors.darkGray};
76
84
  border: none;
77
85
  border-radius: 5px;