@rpg-engine/long-bow 0.8.141 → 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 (62) 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 +3 -2
  8. package/dist/components/Marketplace/Marketplace.d.ts +35 -2
  9. package/dist/components/Marketplace/MarketplaceSettingsPanel.d.ts +2 -1
  10. package/dist/components/Store/PaymentMethodModal.d.ts +1 -0
  11. package/dist/components/shared/LabelPill/LabelPill.d.ts +9 -0
  12. package/dist/components/shared/LabelPill/index.d.ts +1 -0
  13. package/dist/components/shared/SegmentedToggle/SegmentedToggle.d.ts +12 -0
  14. package/dist/components/shared/SegmentedToggle/index.d.ts +1 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/long-bow.cjs.development.js +11529 -1288
  17. package/dist/long-bow.cjs.development.js.map +1 -1
  18. package/dist/long-bow.cjs.production.min.js +1 -1
  19. package/dist/long-bow.cjs.production.min.js.map +1 -1
  20. package/dist/long-bow.esm.js +11518 -1290
  21. package/dist/long-bow.esm.js.map +1 -1
  22. package/dist/stories/Features/marketplace/BlueprintSearchModal.stories.d.ts +1 -0
  23. package/dist/stories/Features/marketplace/BuyOrderPanel.stories.d.ts +1 -0
  24. package/dist/stories/Features/marketplace/BuyOrderRows.stories.d.ts +1 -0
  25. package/dist/stories/Features/marketplace/HistoryPanel.stories.d.ts +1 -0
  26. package/dist/stories/Features/trading/MarketplaceRows.stories.d.ts +2 -1
  27. package/dist/stories/UI/buttonsAndInputs/SegmentedToggle.stories.d.ts +6 -0
  28. package/dist/stories/UI/text/LabelPill.stories.d.ts +7 -0
  29. package/dist/utils/atlasUtils.d.ts +2 -0
  30. package/package.json +2 -2
  31. package/src/components/Marketplace/BlueprintSearchModal.tsx +418 -0
  32. package/src/components/Marketplace/BuyOrderDetailsModal.tsx +307 -0
  33. package/src/components/Marketplace/BuyOrderPanel.tsx +266 -0
  34. package/src/components/Marketplace/BuyOrderRows.tsx +287 -0
  35. package/src/components/Marketplace/BuyPanel.tsx +406 -166
  36. package/src/components/Marketplace/HistoryPanel.tsx +422 -0
  37. package/src/components/Marketplace/ManagmentPanel.tsx +13 -15
  38. package/src/components/Marketplace/Marketplace.tsx +181 -30
  39. package/src/components/Marketplace/MarketplaceBuyModal.tsx +1 -0
  40. package/src/components/Marketplace/MarketplaceRows.tsx +41 -10
  41. package/src/components/Marketplace/MarketplaceSettingsPanel.tsx +4 -3
  42. package/src/components/Store/CartView.tsx +11 -0
  43. package/src/components/Store/PaymentMethodModal.tsx +26 -9
  44. package/src/components/shared/LabelPill/LabelPill.tsx +45 -0
  45. package/src/components/shared/LabelPill/index.ts +1 -0
  46. package/src/components/shared/SegmentedToggle/SegmentedToggle.tsx +61 -0
  47. package/src/components/shared/SegmentedToggle/index.ts +1 -0
  48. package/src/components/shared/SpriteFromAtlas.tsx +7 -2
  49. package/src/index.tsx +4 -0
  50. package/src/mocks/atlas/items/items.json +33998 -25238
  51. package/src/mocks/atlas/items/items.png +0 -0
  52. package/src/mocks/itemContainer.mocks.ts +31 -0
  53. package/src/stories/Features/marketplace/BlueprintSearchModal.stories.tsx +145 -0
  54. package/src/stories/Features/marketplace/BuyOrderPanel.stories.tsx +207 -0
  55. package/src/stories/Features/marketplace/BuyOrderRows.stories.tsx +116 -0
  56. package/src/stories/Features/marketplace/HistoryPanel.stories.tsx +157 -0
  57. package/src/stories/Features/trading/Marketplace.stories.tsx +107 -0
  58. package/src/stories/Features/trading/MarketplaceRows.stories.tsx +11 -0
  59. package/src/stories/UI/buttonsAndInputs/SegmentedToggle.stories.tsx +54 -0
  60. package/src/stories/UI/text/LabelPill.stories.tsx +43 -0
  61. package/src/utils/__test__/atlasUtils.spec.ts +26 -0
  62. package/src/utils/atlasUtils.ts +80 -0
@@ -1,4 +1,10 @@
1
- import { goldToDC, IEquipmentSet, IMarketplaceItem } from '@rpg-engine/shared';
1
+ import {
2
+ goldToDC,
3
+ IEquipmentSet,
4
+ IMarketplaceBuyOrderItem,
5
+ IMarketplaceItem,
6
+ MarketplaceBuyOrderStatus,
7
+ } from '@rpg-engine/shared';
2
8
  import React, { useEffect, useMemo, useRef, useState } from 'react';
3
9
  import { AiFillCaretRight } from 'react-icons/ai';
4
10
  import { SortVertical } from 'pixelarticons/react/SortVertical';
@@ -6,10 +12,22 @@ import styled from 'styled-components';
6
12
  import { ConfirmModal } from '../ConfirmModal';
7
13
  import { Dropdown } from '../Dropdown';
8
14
  import { Input } from '../Input';
15
+ import { SegmentedToggle } from '../shared/SegmentedToggle';
16
+ import { Pager } from '../Pager';
17
+ import { BuyOrderRow } from './BuyOrderRows';
9
18
  import { MarketplaceBuyModal, MarketplacePaymentMethod } from './MarketplaceBuyModal';
10
19
  import { GroupedMarketplaceRow } from './MarketplaceRows';
11
20
  import { itemRarityOptions, itemTypeOptions, orderByOptions } from './filters';
12
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
+
13
31
  export interface IBuyPanelProps {
14
32
  items: IMarketplaceItem[];
15
33
  atlasJSON: any;
@@ -29,12 +47,20 @@ export interface IBuyPanelProps {
29
47
  scale?: number;
30
48
  equipmentSet?: IEquipmentSet | null;
31
49
  onMarketPlaceItemBuy?: (marketPlaceItemId: string, paymentMethod?: MarketplacePaymentMethod) => void;
50
+ onFulfillBuyOrder?: (buyOrderId: string) => void;
32
51
  characterId: string;
33
52
  enableHotkeys?: () => void;
34
53
  disableHotkeys?: () => void;
54
+ totalItems: number;
35
55
  currentPage: number;
56
+ itemsPerPage: number;
57
+ onPageChange: (page: number) => void;
36
58
  dcBalance?: number;
37
59
  dcToGoldSwapRate?: number;
60
+ openBuyOrders?: IMarketplaceBuyOrderItem[];
61
+ openBuyOrdersTotal?: number;
62
+ openBuyOrdersPage?: number;
63
+ onOpenBuyOrdersPageChange?: (page: number) => void;
38
64
  }
39
65
 
40
66
  export const BuyPanel: React.FC<IBuyPanelProps> = ({
@@ -50,15 +76,25 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
50
76
  onChangePriceInput,
51
77
  equipmentSet,
52
78
  onMarketPlaceItemBuy,
79
+ onFulfillBuyOrder,
53
80
  characterId,
54
81
  enableHotkeys,
55
82
  disableHotkeys,
83
+ totalItems,
56
84
  currentPage,
85
+ itemsPerPage,
86
+ onPageChange,
57
87
  dcBalance = 0,
58
88
  dcToGoldSwapRate = 0,
89
+ openBuyOrders = [],
90
+ openBuyOrdersTotal = 0,
91
+ openBuyOrdersPage = 1,
92
+ onOpenBuyOrdersPageChange,
59
93
  }) => {
60
94
  const [name, setName] = useState('');
61
95
  const [showFilters, setShowFilters] = useState(false);
96
+ const [browseMode, setBrowseMode] = useState<MarketplaceBrowseMode>('all');
97
+ const [selectedRarity, setSelectedRarity] = useState('');
62
98
  const [mainLevel, setMainLevel] = useState<
63
99
  [number | undefined, number | undefined]
64
100
  >([undefined, undefined]);
@@ -70,12 +106,13 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
70
106
  undefined,
71
107
  ]);
72
108
  const [buyingItemId, setBuyingItemId] = useState<string | null>(null);
109
+ const [fulfillingBuyOrderId, setFulfillingBuyOrderId] = useState<string | null>(null);
73
110
 
74
111
  const itemsContainer = useRef<HTMLDivElement>(null);
75
112
 
76
113
  useEffect(() => {
77
114
  itemsContainer.current?.scrollTo(0, 0);
78
- }, [currentPage]);
115
+ }, [browseMode, currentPage, openBuyOrdersPage]);
79
116
 
80
117
  const buyingItem = buyingItemId
81
118
  ? items.find(i => i._id === buyingItemId)
@@ -103,6 +140,40 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
103
140
  });
104
141
  }, [items]);
105
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
+
106
177
  return (
107
178
  <>
108
179
  {buyingItemId && buyingItem && hasDCBalance && (
@@ -129,202 +200,306 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
129
200
  message="Are you sure you want to buy this item?"
130
201
  />
131
202
  )}
132
- <InputWrapper>
133
- <p>SEARCH</p>
134
- <Input
135
- onChange={e => {
136
- setName(e.target.value);
137
- onChangeNameInput(e.target.value);
203
+ {fulfillingBuyOrderId && (
204
+ <ConfirmModal
205
+ onClose={setFulfillingBuyOrderId.bind(null, null)}
206
+ onConfirm={() => {
207
+ onFulfillBuyOrder?.(fulfillingBuyOrderId);
208
+ setFulfillingBuyOrderId(null);
209
+ enableHotkeys?.();
138
210
  }}
139
- value={name}
140
- placeholder="Enter name..."
141
- onBlur={enableHotkeys}
142
- onFocus={disableHotkeys}
143
- className="search-input"
211
+ message="Try to fulfill this buy request with a matching item from your inventory or depot?"
144
212
  />
145
- <button className="filter-btn" onClick={() => setShowFilters(!showFilters)}>
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
+ ]}
224
+ />
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"
238
+ />
239
+ </SearchField>
240
+
241
+ <FilterButton
242
+ type="button"
243
+ $active={showFilters}
244
+ onClick={() => setShowFilters(!showFilters)}
245
+ aria-label="Toggle marketplace filters"
246
+ >
146
247
  <SortVertical width={18} height={18} />
147
- </button>
148
- </InputWrapper>
248
+ </FilterButton>
249
+ </ToolbarRow>
149
250
 
150
- <OptionsWrapper showFilters={showFilters}>
151
- {showFilters && (
152
- <FilterInputsWrapper>
153
- <div>
154
- <p>Main level</p>
155
- <div className="input-group">
156
- <Input
157
- onChange={e => {
158
- setMainLevel([Number(e.target.value), mainLevel[1]]);
159
- onChangeMainLevelInput([Number(e.target.value), mainLevel[1]]);
160
- }}
161
- placeholder="Min"
162
- type="number"
163
- min={0}
164
- onBlur={enableHotkeys}
165
- onFocus={disableHotkeys}
166
- />
167
- <AiFillCaretRight className="separator-icon" />
168
- <Input
169
- onChange={e => {
170
- setMainLevel([mainLevel[0], Number(e.target.value)]);
171
- onChangeMainLevelInput([mainLevel[0], Number(e.target.value)]);
172
- }}
173
- placeholder="Max"
174
- type="number"
175
- min={0}
176
- onBlur={enableHotkeys}
177
- onFocus={disableHotkeys}
251
+ {showFilters && (
252
+ <OptionsWrapper showFilters={showFilters}>
253
+ <WrapperContainer>
254
+ <StyledDropdown
255
+ options={itemTypeOptions}
256
+ onChange={onChangeType}
257
+ width="100%"
178
258
  />
179
- </div>
180
- </div>
181
-
182
- <div>
183
- <p>Secondary level</p>
184
- <div className="input-group">
185
- <Input
186
- onChange={e => {
187
- setSecondaryLevel([Number(e.target.value), secondaryLevel[1]]);
188
- onChangeSecondaryLevelInput([
189
- Number(e.target.value),
190
- secondaryLevel[1],
191
- ]);
259
+ <StyledDropdown
260
+ options={itemRarityOptions}
261
+ onChange={value => {
262
+ setSelectedRarity(value);
263
+ onChangeRarity(value);
192
264
  }}
193
- placeholder="Min"
194
- type="number"
195
- min={0}
196
- onBlur={enableHotkeys}
197
- onFocus={disableHotkeys}
265
+ width="100%"
198
266
  />
199
- <AiFillCaretRight className="separator-icon" />
200
- <Input
201
- onChange={e => {
202
- setSecondaryLevel([secondaryLevel[0], Number(e.target.value)]);
203
- onChangeSecondaryLevelInput([
204
- secondaryLevel[0],
205
- Number(e.target.value),
206
- ]);
207
- }}
208
- placeholder="Max"
209
- type="number"
210
- min={0}
211
- onBlur={enableHotkeys}
212
- onFocus={disableHotkeys}
267
+ <StyledDropdown
268
+ options={orderByOptions}
269
+ onChange={onChangeOrder}
270
+ width="100%"
213
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>
214
302
  </div>
215
- </div>
216
-
217
- <div>
218
- <p>Price</p>
219
- <div className="input-group">
220
- <Input
221
- onChange={e => {
222
- setPrice([Number(e.target.value), price[1]]);
223
- onChangePriceInput([Number(e.target.value), price[1]]);
224
- }}
225
- placeholder="Min"
226
- type="number"
227
- min={0}
228
- onBlur={enableHotkeys}
229
- onFocus={disableHotkeys}
230
- />
231
- <AiFillCaretRight className="separator-icon" />
232
- <Input
233
- onChange={e => {
234
- setPrice([price[0], Number(e.target.value)]);
235
- onChangePriceInput([price[0], Number(e.target.value)]);
236
- }}
237
- placeholder="Max"
238
- type="number"
239
- min={0}
240
- onBlur={enableHotkeys}
241
- onFocus={disableHotkeys}
242
- />
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>
243
337
  </div>
244
- </div>
245
- </FilterInputsWrapper>
246
- )}
247
338
 
248
- <WrapperContainer>
249
- <StyledDropdown
250
- options={itemTypeOptions}
251
- onChange={onChangeType}
252
- width="100%"
253
- />
254
- <StyledDropdown
255
- options={itemRarityOptions}
256
- onChange={onChangeRarity}
257
- width="100%"
258
- />
259
- <StyledDropdown
260
- options={orderByOptions}
261
- onChange={onChangeOrder}
262
- width="100%"
263
- />
264
- </WrapperContainer>
265
- </OptionsWrapper>
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
+ )}
266
370
 
267
371
  <ItemComponentScrollWrapper id="MarketContainer" ref={itemsContainer}>
268
- {groupedItems.map(({ bestListing, otherListings }) => (
269
- <GroupedMarketplaceRow
270
- key={bestListing.item.key}
271
- bestListing={bestListing}
272
- otherListings={otherListings}
273
- atlasIMG={atlasIMG}
274
- atlasJSON={atlasJSON}
275
- equipmentSet={equipmentSet}
276
- dcToGoldSwapRate={dcToGoldSwapRate}
277
- getDCEquivalentPrice={getDCEquivalentPrice}
278
- characterId={characterId}
279
- onBuy={setBuyingItemId}
280
- />
281
- ))}
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
+ )}
282
443
  </ItemComponentScrollWrapper>
283
444
  </>
284
445
  );
285
446
  };
286
447
 
287
- const InputWrapper = styled.div`
448
+ const ToolbarRow = styled.div`
288
449
  width: 95%;
289
- display: flex !important;
290
- justify-content: flex-start;
450
+ display: grid;
451
+ grid-template-columns: auto minmax(220px, 1fr) auto;
452
+ gap: 10px;
291
453
  align-items: center;
292
454
  margin: 0 auto 10px auto;
293
455
  background: rgba(0, 0, 0, 0.2);
294
456
  padding: 8px 12px;
295
457
  border-radius: 4px;
296
- border: 1px solid rgba(255, 255, 255, 0.1);
458
+ border: 1px solid rgba(255, 255, 255, 0.05);
297
459
 
298
- p {
299
- width: auto;
300
- margin-right: 20px;
301
- font-size: 0.7rem;
302
- color: #ccc;
303
- text-transform: uppercase;
304
- letter-spacing: 1px;
305
- margin-bottom: 0px;
460
+ @media (max-width: 950px) {
461
+ grid-template-columns: 1fr auto;
462
+ grid-template-areas:
463
+ 'toggle toggle'
464
+ 'search filter';
306
465
  }
466
+ `;
467
+
468
+ const SearchField = styled.div`
469
+ min-width: 0;
307
470
 
308
471
  input.search-input {
309
472
  height: 10px;
310
- flex-grow: 1;
473
+ width: 100%;
474
+ }
475
+ `;
476
+
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);
311
493
  }
494
+ `;
312
495
 
313
- .filter-btn {
314
- background: transparent;
315
- border: none;
316
- color: #ccc;
317
- cursor: pointer;
318
- display: flex;
319
- align-items: center;
320
- justify-content: center;
321
- padding: 0 5px;
322
- margin-left: 10px;
323
- transition: color 0.15s;
496
+ const BrowseModeRow = styled.div`
497
+ display: flex;
498
+ align-items: center;
499
+ min-width: 0;
324
500
 
325
- &:hover {
326
- color: #f59e0b;
327
- }
501
+ @media (max-width: 950px) {
502
+ grid-area: toggle;
328
503
  }
329
504
  `;
330
505
 
@@ -401,6 +576,71 @@ const ItemComponentScrollWrapper = styled.div`
401
576
  }
402
577
  `;
403
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
+
404
644
  const StyledDropdown = styled(Dropdown)`
405
645
  margin: 0px !important;
406
646
  width: 100% !important;