@rpg-engine/long-bow 0.8.140 → 0.8.145

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 (68) hide show
  1. package/dist/components/Marketplace/BlueprintSearchModal.d.ts +17 -0
  2. package/dist/components/Marketplace/BuyOrderDetailsModal.d.ts +17 -0
  3. package/dist/components/Marketplace/BuyOrderPanel.d.ts +24 -0
  4. package/dist/components/Marketplace/BuyOrderRows.d.ts +13 -0
  5. package/dist/components/Marketplace/BuyPanel.d.ts +9 -1
  6. package/dist/components/Marketplace/HistoryPanel.d.ts +18 -0
  7. package/dist/components/Marketplace/ManagmentPanel.d.ts +4 -3
  8. package/dist/components/Marketplace/Marketplace.d.ts +38 -2
  9. package/dist/components/Marketplace/MarketplaceRows.d.ts +13 -1
  10. package/dist/components/Marketplace/MarketplaceSettingsPanel.d.ts +8 -0
  11. package/dist/components/Store/PaymentMethodModal.d.ts +1 -0
  12. package/dist/components/shared/LabelPill/LabelPill.d.ts +9 -0
  13. package/dist/components/shared/LabelPill/index.d.ts +1 -0
  14. package/dist/components/shared/SegmentedToggle/SegmentedToggle.d.ts +12 -0
  15. package/dist/components/shared/SegmentedToggle/index.d.ts +1 -0
  16. package/dist/components/shared/Tabs/Tabs.d.ts +13 -0
  17. package/dist/components/shared/Tabs/index.d.ts +1 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/long-bow.cjs.development.js +12074 -1449
  20. package/dist/long-bow.cjs.development.js.map +1 -1
  21. package/dist/long-bow.cjs.production.min.js +1 -1
  22. package/dist/long-bow.cjs.production.min.js.map +1 -1
  23. package/dist/long-bow.esm.js +12060 -1450
  24. package/dist/long-bow.esm.js.map +1 -1
  25. package/dist/stories/Features/marketplace/BlueprintSearchModal.stories.d.ts +1 -0
  26. package/dist/stories/Features/marketplace/BuyOrderPanel.stories.d.ts +1 -0
  27. package/dist/stories/Features/marketplace/BuyOrderRows.stories.d.ts +1 -0
  28. package/dist/stories/Features/marketplace/HistoryPanel.stories.d.ts +1 -0
  29. package/dist/stories/Features/trading/MarketplaceRows.stories.d.ts +2 -1
  30. package/dist/stories/UI/buttonsAndInputs/SegmentedToggle.stories.d.ts +6 -0
  31. package/dist/stories/UI/text/LabelPill.stories.d.ts +7 -0
  32. package/dist/utils/atlasUtils.d.ts +2 -0
  33. package/package.json +3 -2
  34. package/src/components/ConfirmModal.tsx +50 -27
  35. package/src/components/Marketplace/BlueprintSearchModal.tsx +418 -0
  36. package/src/components/Marketplace/BuyOrderDetailsModal.tsx +307 -0
  37. package/src/components/Marketplace/BuyOrderPanel.tsx +266 -0
  38. package/src/components/Marketplace/BuyOrderRows.tsx +287 -0
  39. package/src/components/Marketplace/BuyPanel.tsx +486 -170
  40. package/src/components/Marketplace/HistoryPanel.tsx +422 -0
  41. package/src/components/Marketplace/ManagmentPanel.tsx +176 -98
  42. package/src/components/Marketplace/Marketplace.tsx +227 -40
  43. package/src/components/Marketplace/MarketplaceBuyModal.tsx +1 -0
  44. package/src/components/Marketplace/MarketplaceRows.tsx +274 -80
  45. package/src/components/Marketplace/MarketplaceSettingsPanel.tsx +128 -0
  46. package/src/components/Store/CartView.tsx +11 -0
  47. package/src/components/Store/PaymentMethodModal.tsx +26 -9
  48. package/src/components/shared/LabelPill/LabelPill.tsx +45 -0
  49. package/src/components/shared/LabelPill/index.ts +1 -0
  50. package/src/components/shared/SegmentedToggle/SegmentedToggle.tsx +61 -0
  51. package/src/components/shared/SegmentedToggle/index.ts +1 -0
  52. package/src/components/shared/SpriteFromAtlas.tsx +7 -2
  53. package/src/components/shared/Tabs/Tabs.tsx +60 -0
  54. package/src/components/shared/Tabs/index.ts +1 -0
  55. package/src/index.tsx +5 -0
  56. package/src/mocks/atlas/items/items.json +33998 -25238
  57. package/src/mocks/atlas/items/items.png +0 -0
  58. package/src/mocks/itemContainer.mocks.ts +31 -0
  59. package/src/stories/Features/marketplace/BlueprintSearchModal.stories.tsx +145 -0
  60. package/src/stories/Features/marketplace/BuyOrderPanel.stories.tsx +207 -0
  61. package/src/stories/Features/marketplace/BuyOrderRows.stories.tsx +116 -0
  62. package/src/stories/Features/marketplace/HistoryPanel.stories.tsx +157 -0
  63. package/src/stories/Features/trading/Marketplace.stories.tsx +109 -0
  64. package/src/stories/Features/trading/MarketplaceRows.stories.tsx +11 -0
  65. package/src/stories/UI/buttonsAndInputs/SegmentedToggle.stories.tsx +54 -0
  66. package/src/stories/UI/text/LabelPill.stories.tsx +43 -0
  67. package/src/utils/__test__/atlasUtils.spec.ts +26 -0
  68. package/src/utils/atlasUtils.ts +80 -0
@@ -1,14 +1,33 @@
1
- import { goldToDC, IEquipmentSet, IMarketplaceItem } from '@rpg-engine/shared';
2
- import React, { useEffect, useRef, useState } from 'react';
1
+ import {
2
+ goldToDC,
3
+ IEquipmentSet,
4
+ IMarketplaceBuyOrderItem,
5
+ IMarketplaceItem,
6
+ MarketplaceBuyOrderStatus,
7
+ } from '@rpg-engine/shared';
8
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
3
9
  import { AiFillCaretRight } from 'react-icons/ai';
10
+ import { SortVertical } from 'pixelarticons/react/SortVertical';
4
11
  import styled from 'styled-components';
5
12
  import { ConfirmModal } from '../ConfirmModal';
6
13
  import { Dropdown } from '../Dropdown';
7
14
  import { Input } from '../Input';
15
+ import { SegmentedToggle } from '../shared/SegmentedToggle';
16
+ import { Pager } from '../Pager';
17
+ import { BuyOrderRow } from './BuyOrderRows';
8
18
  import { MarketplaceBuyModal, MarketplacePaymentMethod } from './MarketplaceBuyModal';
9
- import { MarketplaceRows } from './MarketplaceRows';
19
+ import { GroupedMarketplaceRow } from './MarketplaceRows';
10
20
  import { itemRarityOptions, itemTypeOptions, orderByOptions } from './filters';
11
21
 
22
+ type MarketplaceBrowseMode = 'all' | 'sell' | 'buy';
23
+
24
+ const BUY_REQUESTS_PER_PAGE = 5;
25
+
26
+ const formatBlueprintKey = (key: string): string => {
27
+ const name = key.includes('/') ? key.split('/').pop()! : key;
28
+ return name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
29
+ };
30
+
12
31
  export interface IBuyPanelProps {
13
32
  items: IMarketplaceItem[];
14
33
  atlasJSON: any;
@@ -28,12 +47,20 @@ export interface IBuyPanelProps {
28
47
  scale?: number;
29
48
  equipmentSet?: IEquipmentSet | null;
30
49
  onMarketPlaceItemBuy?: (marketPlaceItemId: string, paymentMethod?: MarketplacePaymentMethod) => void;
50
+ onFulfillBuyOrder?: (buyOrderId: string) => void;
31
51
  characterId: string;
32
52
  enableHotkeys?: () => void;
33
53
  disableHotkeys?: () => void;
54
+ totalItems: number;
34
55
  currentPage: number;
56
+ itemsPerPage: number;
57
+ onPageChange: (page: number) => void;
35
58
  dcBalance?: number;
36
59
  dcToGoldSwapRate?: number;
60
+ openBuyOrders?: IMarketplaceBuyOrderItem[];
61
+ openBuyOrdersTotal?: number;
62
+ openBuyOrdersPage?: number;
63
+ onOpenBuyOrdersPageChange?: (page: number) => void;
37
64
  }
38
65
 
39
66
  export const BuyPanel: React.FC<IBuyPanelProps> = ({
@@ -49,14 +76,25 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
49
76
  onChangePriceInput,
50
77
  equipmentSet,
51
78
  onMarketPlaceItemBuy,
79
+ onFulfillBuyOrder,
52
80
  characterId,
53
81
  enableHotkeys,
54
82
  disableHotkeys,
83
+ totalItems,
55
84
  currentPage,
85
+ itemsPerPage,
86
+ onPageChange,
56
87
  dcBalance = 0,
57
88
  dcToGoldSwapRate = 0,
89
+ openBuyOrders = [],
90
+ openBuyOrdersTotal = 0,
91
+ openBuyOrdersPage = 1,
92
+ onOpenBuyOrdersPageChange,
58
93
  }) => {
59
94
  const [name, setName] = useState('');
95
+ const [showFilters, setShowFilters] = useState(false);
96
+ const [browseMode, setBrowseMode] = useState<MarketplaceBrowseMode>('all');
97
+ const [selectedRarity, setSelectedRarity] = useState('');
60
98
  const [mainLevel, setMainLevel] = useState<
61
99
  [number | undefined, number | undefined]
62
100
  >([undefined, undefined]);
@@ -68,12 +106,13 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
68
106
  undefined,
69
107
  ]);
70
108
  const [buyingItemId, setBuyingItemId] = useState<string | null>(null);
109
+ const [fulfillingBuyOrderId, setFulfillingBuyOrderId] = useState<string | null>(null);
71
110
 
72
111
  const itemsContainer = useRef<HTMLDivElement>(null);
73
112
 
74
113
  useEffect(() => {
75
114
  itemsContainer.current?.scrollTo(0, 0);
76
- }, [currentPage]);
115
+ }, [browseMode, currentPage, openBuyOrdersPage]);
77
116
 
78
117
  const buyingItem = buyingItemId
79
118
  ? items.find(i => i._id === buyingItemId)
@@ -83,6 +122,58 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
83
122
  const getDCEquivalentPrice = (goldPrice: number): number =>
84
123
  dcToGoldSwapRate > 0 ? goldToDC(goldPrice) : 0;
85
124
 
125
+ const groupedItems = useMemo(() => {
126
+ const groups = new Map<string, IMarketplaceItem[]>();
127
+ for (const entry of items) {
128
+ const key = entry.item.key;
129
+ if (!groups.has(key)) {
130
+ groups.set(key, []);
131
+ }
132
+ groups.get(key)!.push(entry);
133
+ }
134
+ return Array.from(groups.values()).map(group => {
135
+ const sorted = [...group].sort((a, b) => a.price - b.price);
136
+ return {
137
+ bestListing: sorted[0],
138
+ otherListings: sorted.slice(1),
139
+ };
140
+ });
141
+ }, [items]);
142
+
143
+ const visibleBuyOrders = useMemo(() => {
144
+ const normalizedName = name.trim().toLowerCase();
145
+
146
+ return openBuyOrders.filter(order => {
147
+ if (order.status !== MarketplaceBuyOrderStatus.Active) {
148
+ return false;
149
+ }
150
+
151
+ if (normalizedName && !formatBlueprintKey(order.itemBlueprintKey).toLowerCase().includes(normalizedName)) {
152
+ return false;
153
+ }
154
+
155
+ if (selectedRarity && selectedRarity !== 'Rarity' && order.itemRarity !== selectedRarity) {
156
+ return false;
157
+ }
158
+
159
+ if (price[0] !== undefined && order.maxPrice < price[0]) {
160
+ return false;
161
+ }
162
+
163
+ if (price[1] !== undefined && order.maxPrice > price[1]) {
164
+ return false;
165
+ }
166
+
167
+ return true;
168
+ });
169
+ }, [name, openBuyOrders, price, selectedRarity]);
170
+
171
+ const showSellSection = browseMode === 'all' || browseMode === 'sell';
172
+ const showBuySection = browseMode === 'all' || browseMode === 'buy';
173
+ const hasVisibleContent =
174
+ (showSellSection && groupedItems.length > 0) ||
175
+ (showBuySection && visibleBuyOrders.length > 0);
176
+
86
177
  return (
87
178
  <>
88
179
  {buyingItemId && buyingItem && hasDCBalance && (
@@ -109,205 +200,362 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
109
200
  message="Are you sure you want to buy this item?"
110
201
  />
111
202
  )}
112
- <InputWrapper>
113
- <p>Search By Name</p>
114
- <Input
115
- onChange={e => {
116
- setName(e.target.value);
117
- onChangeNameInput(e.target.value);
203
+ {fulfillingBuyOrderId && (
204
+ <ConfirmModal
205
+ onClose={setFulfillingBuyOrderId.bind(null, null)}
206
+ onConfirm={() => {
207
+ onFulfillBuyOrder?.(fulfillingBuyOrderId);
208
+ setFulfillingBuyOrderId(null);
209
+ enableHotkeys?.();
118
210
  }}
119
- value={name}
120
- placeholder="Enter name..."
121
- onBlur={enableHotkeys}
122
- onFocus={disableHotkeys}
211
+ message="Try to fulfill this buy request with a matching item from your inventory or depot?"
123
212
  />
124
- </InputWrapper>
125
-
126
- <OptionsWrapper>
127
- <FilterInputsWrapper>
128
- <div>
129
- <p>Main level</p>
130
- <Input
131
- onChange={e => {
132
- setMainLevel([Number(e.target.value), mainLevel[1]]);
133
- onChangeMainLevelInput([Number(e.target.value), mainLevel[1]]);
134
- }}
135
- placeholder="Min"
136
- type="number"
137
- min={0}
138
- onBlur={enableHotkeys}
139
- onFocus={disableHotkeys}
140
- />
141
- <AiFillCaretRight />
142
- <Input
143
- onChange={e => {
144
- setMainLevel([mainLevel[0], Number(e.target.value)]);
145
- onChangeMainLevelInput([mainLevel[0], Number(e.target.value)]);
146
- }}
147
- placeholder="Max"
148
- type="number"
149
- min={0}
150
- onBlur={enableHotkeys}
151
- onFocus={disableHotkeys}
152
- />
153
- </div>
154
-
155
- <div>
156
- <p>Secondary level</p>
157
- <Input
158
- onChange={e => {
159
- setSecondaryLevel([Number(e.target.value), secondaryLevel[1]]);
160
- onChangeSecondaryLevelInput([
161
- Number(e.target.value),
162
- secondaryLevel[1],
163
- ]);
164
- }}
165
- placeholder="Min"
166
- type="number"
167
- min={0}
168
- onBlur={enableHotkeys}
169
- onFocus={disableHotkeys}
170
- />
171
- <AiFillCaretRight />
172
- <Input
173
- onChange={e => {
174
- setSecondaryLevel([secondaryLevel[0], Number(e.target.value)]);
175
- onChangeSecondaryLevelInput([
176
- secondaryLevel[0],
177
- Number(e.target.value),
178
- ]);
179
- }}
180
- placeholder="Max"
181
- type="number"
182
- min={0}
183
- onBlur={enableHotkeys}
184
- onFocus={disableHotkeys}
185
- />
186
- </div>
187
-
188
- <div>
189
- <p>Price</p>
190
- <Input
191
- onChange={e => {
192
- setPrice([Number(e.target.value), price[1]]);
193
- onChangePriceInput([Number(e.target.value), price[1]]);
194
- }}
195
- placeholder="Min"
196
- type="number"
197
- min={0}
198
- className="big-input"
199
- onBlur={enableHotkeys}
200
- onFocus={disableHotkeys}
201
- />
202
- <AiFillCaretRight />
203
- <Input
204
- onChange={e => {
205
- setPrice([price[0], Number(e.target.value)]);
206
- onChangePriceInput([price[0], Number(e.target.value)]);
207
- }}
208
- placeholder="Max"
209
- type="number"
210
- min={0}
211
- className="big-input"
212
- onBlur={enableHotkeys}
213
- onFocus={disableHotkeys}
214
- />
215
- </div>
216
- </FilterInputsWrapper>
217
-
218
- <WrapperContainer>
219
- <StyledDropdown
220
- options={itemTypeOptions}
221
- onChange={onChangeType}
222
- width="95%"
223
- />
224
- <StyledDropdown
225
- options={itemRarityOptions}
226
- onChange={onChangeRarity}
227
- width="95%"
213
+ )}
214
+ <ToolbarRow>
215
+ <BrowseModeRow>
216
+ <SegmentedToggle
217
+ activeId={browseMode}
218
+ onChange={value => setBrowseMode(value as MarketplaceBrowseMode)}
219
+ options={[
220
+ { id: 'all', label: 'All' },
221
+ { id: 'sell', label: 'Sell Offers' },
222
+ { id: 'buy', label: 'Buy Requests' },
223
+ ]}
228
224
  />
229
- <StyledDropdown
230
- options={orderByOptions}
231
- onChange={onChangeOrder}
232
- width="100%"
225
+ </BrowseModeRow>
226
+
227
+ <SearchField>
228
+ <Input
229
+ onChange={e => {
230
+ setName(e.target.value);
231
+ onChangeNameInput(e.target.value);
232
+ }}
233
+ value={name}
234
+ placeholder="Search items or requests..."
235
+ onBlur={enableHotkeys}
236
+ onFocus={disableHotkeys}
237
+ className="search-input"
233
238
  />
234
- </WrapperContainer>
235
- </OptionsWrapper>
239
+ </SearchField>
240
+
241
+ <FilterButton
242
+ type="button"
243
+ $active={showFilters}
244
+ onClick={() => setShowFilters(!showFilters)}
245
+ aria-label="Toggle marketplace filters"
246
+ >
247
+ <SortVertical width={18} height={18} />
248
+ </FilterButton>
249
+ </ToolbarRow>
250
+
251
+ {showFilters && (
252
+ <OptionsWrapper showFilters={showFilters}>
253
+ <WrapperContainer>
254
+ <StyledDropdown
255
+ options={itemTypeOptions}
256
+ onChange={onChangeType}
257
+ width="100%"
258
+ />
259
+ <StyledDropdown
260
+ options={itemRarityOptions}
261
+ onChange={value => {
262
+ setSelectedRarity(value);
263
+ onChangeRarity(value);
264
+ }}
265
+ width="100%"
266
+ />
267
+ <StyledDropdown
268
+ options={orderByOptions}
269
+ onChange={onChangeOrder}
270
+ width="100%"
271
+ />
272
+ </WrapperContainer>
273
+
274
+ <FilterInputsWrapper>
275
+ <div>
276
+ <p>Main level</p>
277
+ <div className="input-group">
278
+ <Input
279
+ onChange={e => {
280
+ setMainLevel([Number(e.target.value), mainLevel[1]]);
281
+ onChangeMainLevelInput([Number(e.target.value), mainLevel[1]]);
282
+ }}
283
+ placeholder="Min"
284
+ type="number"
285
+ min={0}
286
+ onBlur={enableHotkeys}
287
+ onFocus={disableHotkeys}
288
+ />
289
+ <AiFillCaretRight className="separator-icon" />
290
+ <Input
291
+ onChange={e => {
292
+ setMainLevel([mainLevel[0], Number(e.target.value)]);
293
+ onChangeMainLevelInput([mainLevel[0], Number(e.target.value)]);
294
+ }}
295
+ placeholder="Max"
296
+ type="number"
297
+ min={0}
298
+ onBlur={enableHotkeys}
299
+ onFocus={disableHotkeys}
300
+ />
301
+ </div>
302
+ </div>
303
+
304
+ <div>
305
+ <p>Secondary level</p>
306
+ <div className="input-group">
307
+ <Input
308
+ onChange={e => {
309
+ setSecondaryLevel([Number(e.target.value), secondaryLevel[1]]);
310
+ onChangeSecondaryLevelInput([
311
+ Number(e.target.value),
312
+ secondaryLevel[1],
313
+ ]);
314
+ }}
315
+ placeholder="Min"
316
+ type="number"
317
+ min={0}
318
+ onBlur={enableHotkeys}
319
+ onFocus={disableHotkeys}
320
+ />
321
+ <AiFillCaretRight className="separator-icon" />
322
+ <Input
323
+ onChange={e => {
324
+ setSecondaryLevel([secondaryLevel[0], Number(e.target.value)]);
325
+ onChangeSecondaryLevelInput([
326
+ secondaryLevel[0],
327
+ Number(e.target.value),
328
+ ]);
329
+ }}
330
+ placeholder="Max"
331
+ type="number"
332
+ min={0}
333
+ onBlur={enableHotkeys}
334
+ onFocus={disableHotkeys}
335
+ />
336
+ </div>
337
+ </div>
338
+
339
+ <div>
340
+ <p>Price</p>
341
+ <div className="input-group">
342
+ <Input
343
+ onChange={e => {
344
+ setPrice([Number(e.target.value), price[1]]);
345
+ onChangePriceInput([Number(e.target.value), price[1]]);
346
+ }}
347
+ placeholder="Min"
348
+ type="number"
349
+ min={0}
350
+ onBlur={enableHotkeys}
351
+ onFocus={disableHotkeys}
352
+ />
353
+ <AiFillCaretRight className="separator-icon" />
354
+ <Input
355
+ onChange={e => {
356
+ setPrice([price[0], Number(e.target.value)]);
357
+ onChangePriceInput([price[0], Number(e.target.value)]);
358
+ }}
359
+ placeholder="Max"
360
+ type="number"
361
+ min={0}
362
+ onBlur={enableHotkeys}
363
+ onFocus={disableHotkeys}
364
+ />
365
+ </div>
366
+ </div>
367
+ </FilterInputsWrapper>
368
+ </OptionsWrapper>
369
+ )}
236
370
 
237
371
  <ItemComponentScrollWrapper id="MarketContainer" ref={itemsContainer}>
238
- {items?.map(({ item, price, _id, owner }, index) => (
239
- <MarketplaceRows
240
- key={`${item.key}_${index}`}
241
- atlasIMG={atlasIMG}
242
- atlasJSON={atlasJSON}
243
- item={item}
244
- itemPrice={price}
245
- dcEquivalentPrice={dcToGoldSwapRate > 0 ? getDCEquivalentPrice(price) : undefined}
246
- equipmentSet={equipmentSet}
247
- onMarketPlaceItemBuy={setBuyingItemId.bind(null, _id)}
248
- disabled={owner === characterId}
249
- />
250
- ))}
372
+ {!hasVisibleContent && <EmptyState>No offers match the current filters.</EmptyState>}
373
+
374
+ {showSellSection && (
375
+ <MarketSection>
376
+ <SectionHeader>
377
+ <SectionTitle>Sell Offers</SectionTitle>
378
+ <SectionMeta>{groupedItems.length} groups</SectionMeta>
379
+ </SectionHeader>
380
+ {groupedItems.length === 0 ? (
381
+ <SectionEmpty>No sell offers found.</SectionEmpty>
382
+ ) : (
383
+ groupedItems.map(({ bestListing, otherListings }) => (
384
+ <GroupedMarketplaceRow
385
+ key={bestListing.item.key}
386
+ bestListing={bestListing}
387
+ otherListings={otherListings}
388
+ atlasIMG={atlasIMG}
389
+ atlasJSON={atlasJSON}
390
+ equipmentSet={equipmentSet}
391
+ dcToGoldSwapRate={dcToGoldSwapRate}
392
+ getDCEquivalentPrice={getDCEquivalentPrice}
393
+ characterId={characterId}
394
+ onBuy={setBuyingItemId}
395
+ />
396
+ ))
397
+ )}
398
+ {totalItems > itemsPerPage && (
399
+ <SectionPager>
400
+ <Pager
401
+ totalItems={totalItems}
402
+ currentPage={currentPage}
403
+ itemsPerPage={itemsPerPage}
404
+ onPageChange={onPageChange}
405
+ />
406
+ </SectionPager>
407
+ )}
408
+ </MarketSection>
409
+ )}
410
+
411
+ {showBuySection && (
412
+ <MarketSection>
413
+ <SectionHeader>
414
+ <SectionTitle>Buy Requests</SectionTitle>
415
+ <SectionMeta>{visibleBuyOrders.length} visible</SectionMeta>
416
+ </SectionHeader>
417
+ {visibleBuyOrders.length === 0 ? (
418
+ <SectionEmpty>No public buy requests found.</SectionEmpty>
419
+ ) : (
420
+ visibleBuyOrders.map(order => (
421
+ <BuyOrderRow
422
+ key={order._id}
423
+ buyOrder={order}
424
+ atlasJSON={atlasJSON}
425
+ atlasIMG={atlasIMG}
426
+ onFulfill={setFulfillingBuyOrderId}
427
+ showRequestTag
428
+ />
429
+ ))
430
+ )}
431
+ {openBuyOrdersTotal > BUY_REQUESTS_PER_PAGE && (
432
+ <SectionPager>
433
+ <Pager
434
+ totalItems={openBuyOrdersTotal}
435
+ currentPage={openBuyOrdersPage}
436
+ itemsPerPage={BUY_REQUESTS_PER_PAGE}
437
+ onPageChange={onOpenBuyOrdersPageChange ?? (() => {})}
438
+ />
439
+ </SectionPager>
440
+ )}
441
+ </MarketSection>
442
+ )}
251
443
  </ItemComponentScrollWrapper>
252
444
  </>
253
445
  );
254
446
  };
255
447
 
256
- const InputWrapper = styled.div`
448
+ const ToolbarRow = styled.div`
257
449
  width: 95%;
258
- display: flex !important;
259
- justify-content: flex-start;
450
+ display: grid;
451
+ grid-template-columns: auto minmax(220px, 1fr) auto;
452
+ gap: 10px;
260
453
  align-items: center;
261
- margin: auto;
454
+ margin: 0 auto 10px auto;
455
+ background: rgba(0, 0, 0, 0.2);
456
+ padding: 8px 12px;
457
+ border-radius: 4px;
458
+ border: 1px solid rgba(255, 255, 255, 0.05);
262
459
 
263
- p {
264
- width: auto;
265
- margin-right: 20px;
460
+ @media (max-width: 950px) {
461
+ grid-template-columns: 1fr auto;
462
+ grid-template-areas:
463
+ 'toggle toggle'
464
+ 'search filter';
266
465
  }
466
+ `;
467
+
468
+ const SearchField = styled.div`
469
+ min-width: 0;
267
470
 
268
- input {
269
- width: 68%;
471
+ input.search-input {
270
472
  height: 10px;
473
+ width: 100%;
271
474
  }
272
475
  `;
273
476
 
274
- const OptionsWrapper = styled.div`
275
- width: 100%;
276
- height: 100px;
477
+ const FilterButton = styled.button<{ $active: boolean }>`
478
+ width: 36px;
479
+ height: 36px;
480
+ border-radius: 8px;
481
+ border: 1px solid ${({ $active }) => ($active ? 'rgba(245, 158, 11, 0.55)' : 'rgba(255, 255, 255, 0.08)')};
482
+ background: ${({ $active }) => ($active ? 'rgba(245, 158, 11, 0.14)' : 'rgba(255, 255, 255, 0.03)')};
483
+ color: ${({ $active }) => ($active ? '#f59e0b' : '#ccc')};
484
+ cursor: pointer;
485
+ display: flex;
486
+ align-items: center;
487
+ justify-content: center;
488
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
489
+
490
+ &:hover {
491
+ color: #f59e0b;
492
+ border-color: rgba(245, 158, 11, 0.45);
493
+ }
277
494
  `;
278
495
 
279
- const FilterInputsWrapper = styled.div`
280
- width: 95%;
496
+ const BrowseModeRow = styled.div`
281
497
  display: flex;
282
- justify-content: space-between;
283
498
  align-items: center;
284
- margin-bottom: 10px;
285
- margin-left: 10px;
286
- gap: 5px;
499
+ min-width: 0;
500
+
501
+ @media (max-width: 950px) {
502
+ grid-area: toggle;
503
+ }
504
+ `;
505
+
506
+ const OptionsWrapper = styled.div<{ showFilters: boolean }>`
507
+ width: 95%;
508
+ margin: 0 auto;
509
+ background: rgba(0, 0, 0, 0.15);
510
+ border-radius: 4px;
511
+ border: 1px solid rgba(255, 255, 255, 0.05);
512
+ padding: 10px;
513
+ display: flex;
514
+ flex-direction: column;
515
+ gap: ${({ showFilters }) => (showFilters ? '15px' : '0')};
516
+ `;
517
+
518
+ const FilterInputsWrapper = styled.div`
519
+ display: grid;
520
+ grid-template-columns: repeat(3, 1fr);
521
+ gap: 15px;
287
522
  color: white;
288
- flex-wrap: wrap;
523
+
524
+ > div {
525
+ display: flex;
526
+ flex-direction: column;
527
+ gap: 5px;
528
+ }
289
529
 
290
530
  p {
291
- width: auto;
292
531
  margin: 0;
532
+ font-size: 0.65rem;
533
+ color: #aaa;
534
+ text-transform: uppercase;
535
+ letter-spacing: 1px;
293
536
  }
294
537
 
295
- input {
296
- width: 75px;
297
- height: 10px;
538
+ .input-group {
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 5px;
542
+
543
+ input {
544
+ width: 100%;
545
+ height: 10px;
546
+ }
298
547
  }
299
548
 
300
- .big-input {
301
- width: 130px;
549
+ .separator-icon {
550
+ flex-shrink: 0;
551
+ color: rgba(255, 255, 255, 0.3);
302
552
  }
303
553
  `;
304
554
 
305
555
  const WrapperContainer = styled.div`
306
556
  display: grid;
307
- grid-template-columns: 40% 30% 30%;
308
- justify-content: space-between;
309
- width: calc(100% - 40px);
310
- margin-left: 10px;
557
+ grid-template-columns: 1fr 1fr 1fr;
558
+ gap: 15px;
311
559
 
312
560
  .rpgui-content .rpgui-dropdown-imp-header {
313
561
  padding: 0px 10px 0 !important;
@@ -317,15 +565,83 @@ const WrapperContainer = styled.div`
317
565
  const ItemComponentScrollWrapper = styled.div`
318
566
  overflow-y: scroll;
319
567
  height: 390px;
320
- width: 100%;
321
- margin-top: 1rem;
568
+ width: 95%;
569
+ margin: 1rem auto 0 auto;
570
+ background: rgba(0, 0, 0, 0.2);
571
+ border: 1px solid rgba(255, 255, 255, 0.05);
572
+ border-radius: 4px;
322
573
 
323
574
  @media (max-width: 950px) {
324
575
  height: 250px;
325
576
  }
326
577
  `;
327
578
 
579
+ const MarketSection = styled.div`
580
+ padding: 12px;
581
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
582
+
583
+ &:last-child {
584
+ border-bottom: none;
585
+ }
586
+ `;
587
+
588
+ const SectionHeader = styled.div`
589
+ display: flex;
590
+ justify-content: space-between;
591
+ align-items: center;
592
+ gap: 10px;
593
+ margin-bottom: 10px;
594
+ `;
595
+
596
+ const SectionTitle = styled.p`
597
+ margin: 0;
598
+ font-size: 0.58rem;
599
+ color: #f3f4f6;
600
+ text-transform: uppercase;
601
+ letter-spacing: 1px;
602
+ `;
603
+
604
+ const SectionMeta = styled.span`
605
+ font-size: 0.44rem;
606
+ color: #8a8a8a;
607
+ text-transform: uppercase;
608
+ letter-spacing: 1px;
609
+ `;
610
+
611
+ const SectionEmpty = styled.div`
612
+ min-height: 56px;
613
+ display: flex;
614
+ align-items: center;
615
+ justify-content: center;
616
+ color: #71717a;
617
+ font-size: 0.48rem;
618
+ text-transform: uppercase;
619
+ letter-spacing: 1px;
620
+ background: rgba(255, 255, 255, 0.03);
621
+ border: 1px dashed rgba(255, 255, 255, 0.08);
622
+ border-radius: 6px;
623
+ `;
624
+
625
+ const EmptyState = styled.div`
626
+ min-height: 96px;
627
+ display: flex;
628
+ align-items: center;
629
+ justify-content: center;
630
+ color: #71717a;
631
+ font-size: 0.52rem;
632
+ text-transform: uppercase;
633
+ letter-spacing: 1px;
634
+ padding: 0 16px;
635
+ text-align: center;
636
+ `;
637
+
638
+ const SectionPager = styled.div`
639
+ display: flex;
640
+ justify-content: center;
641
+ margin-top: 10px;
642
+ `;
643
+
328
644
  const StyledDropdown = styled(Dropdown)`
329
- margin: 3px !important;
330
- width: 170px !important;
645
+ margin: 0px !important;
646
+ width: 100% !important;
331
647
  `;