@rpg-engine/long-bow 0.8.140 → 0.8.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.140",
3
+ "version": "0.8.141",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -92,6 +92,7 @@
92
92
  "lodash-es": "^4.17.21",
93
93
  "mobx": "^6.6.0",
94
94
  "mobx-react": "^7.5.0",
95
+ "pixelarticons": "^2.0.2",
95
96
  "react-colorful": "^5.6.1",
96
97
  "react-draggable": "^4.4.5",
97
98
  "react-error-boundary": "^3.1.4",
@@ -1,4 +1,5 @@
1
1
  import React, { ReactNode, useCallback } from 'react';
2
+ import { FaTimes } from 'react-icons/fa';
2
3
  import styled, { createGlobalStyle } from 'styled-components';
3
4
  import ModalPortal from './Abstractions/ModalPortal';
4
5
  import { Button, ButtonTypes } from './Button';
@@ -66,6 +67,13 @@ export const ConfirmModal: React.FC<IConfirmModalProps> = ({
66
67
  onTouchMove={stopPropagationTouch}
67
68
  onPointerDown={stopPropagationPointer}
68
69
  >
70
+ <Header>
71
+ <Title>Confirm</Title>
72
+ <CloseButton onPointerDown={handleClose} aria-label="Close">
73
+ <FaTimes />
74
+ </CloseButton>
75
+ </Header>
76
+
69
77
  <MessageContainer>
70
78
  {typeof message === 'string' ? (
71
79
  <Message>{message}</Message>
@@ -134,61 +142,76 @@ const ModalContainer = styled.div`
134
142
  `;
135
143
 
136
144
  const ModalContent = styled.div`
137
- background-color: #2a2a2a;
138
- border: 2px solid #444;
145
+ background: #1a1a2e;
146
+ border: 2px solid #f59e0b;
139
147
  border-radius: 8px;
140
- padding: 20px;
148
+ padding: 20px 24px 24px;
141
149
  min-width: 300px;
142
150
  max-width: 90%;
143
- margin: 0 auto;
144
- animation: scaleIn 0.2s ease-out;
145
- transform-origin: center;
146
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(0, 0, 0, 0.3);
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 16px;
147
154
  pointer-events: auto;
155
+ animation: scaleIn 0.15s ease-out;
148
156
 
149
157
  @keyframes scaleIn {
150
- from {
151
- transform: scale(0.8);
152
- opacity: 0;
153
- }
154
- to {
155
- transform: scale(1);
156
- opacity: 1;
157
- }
158
+ from { transform: scale(0.85); opacity: 0; }
159
+ to { transform: scale(1); opacity: 1; }
158
160
  }
159
161
 
160
162
  @media (max-width: 768px) {
161
- padding: 25px;
162
163
  width: 85%;
163
164
  }
164
165
  `;
165
166
 
167
+ const Header = styled.div`
168
+ display: flex;
169
+ align-items: center;
170
+ justify-content: space-between;
171
+ `;
172
+
173
+ const Title = styled.h3`
174
+ margin: 0;
175
+ font-family: 'Press Start 2P', cursive;
176
+ font-size: 0.7rem;
177
+ color: #fef08a;
178
+ `;
179
+
180
+ const CloseButton = styled.button`
181
+ background: none;
182
+ border: none;
183
+ color: rgba(255, 255, 255, 0.6);
184
+ cursor: pointer;
185
+ font-size: 1rem;
186
+ padding: 4px;
187
+ display: flex;
188
+ align-items: center;
189
+
190
+ &:hover {
191
+ color: #ffffff;
192
+ }
193
+ `;
194
+
166
195
  const MessageContainer = styled.div`
167
- margin-bottom: 20px;
168
196
  text-align: center;
169
197
  `;
170
198
 
171
199
  const Message = styled.p`
172
200
  margin: 0;
173
- font-size: 16px;
174
- color: #fff;
175
- text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
176
-
177
- @media (max-width: 768px) {
178
- font-size: 18px;
179
- }
201
+ font-family: 'Press Start 2P', cursive;
202
+ font-size: 0.6rem;
203
+ color: rgba(255, 255, 255, 0.85);
204
+ line-height: 1.8;
180
205
  `;
181
206
 
182
207
  const ButtonsContainer = styled.div`
183
208
  display: flex;
184
209
  justify-content: center;
185
210
  gap: 20px;
211
+ margin-top: 4px;
186
212
 
187
213
  @media (max-width: 768px) {
188
214
  gap: 30px;
189
- /* Increase button size for better touch targets */
190
- transform: scale(1.1);
191
- margin-top: 5px;
192
215
  }
193
216
  `;
194
217
 
@@ -1,12 +1,13 @@
1
1
  import { goldToDC, IEquipmentSet, IMarketplaceItem } from '@rpg-engine/shared';
2
- import React, { useEffect, useRef, useState } from 'react';
2
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { AiFillCaretRight } from 'react-icons/ai';
4
+ import { SortVertical } from 'pixelarticons/react/SortVertical';
4
5
  import styled from 'styled-components';
5
6
  import { ConfirmModal } from '../ConfirmModal';
6
7
  import { Dropdown } from '../Dropdown';
7
8
  import { Input } from '../Input';
8
9
  import { MarketplaceBuyModal, MarketplacePaymentMethod } from './MarketplaceBuyModal';
9
- import { MarketplaceRows } from './MarketplaceRows';
10
+ import { GroupedMarketplaceRow } from './MarketplaceRows';
10
11
  import { itemRarityOptions, itemTypeOptions, orderByOptions } from './filters';
11
12
 
12
13
  export interface IBuyPanelProps {
@@ -57,6 +58,7 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
57
58
  dcToGoldSwapRate = 0,
58
59
  }) => {
59
60
  const [name, setName] = useState('');
61
+ const [showFilters, setShowFilters] = useState(false);
60
62
  const [mainLevel, setMainLevel] = useState<
61
63
  [number | undefined, number | undefined]
62
64
  >([undefined, undefined]);
@@ -83,6 +85,24 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
83
85
  const getDCEquivalentPrice = (goldPrice: number): number =>
84
86
  dcToGoldSwapRate > 0 ? goldToDC(goldPrice) : 0;
85
87
 
88
+ const groupedItems = useMemo(() => {
89
+ const groups = new Map<string, IMarketplaceItem[]>();
90
+ for (const entry of items) {
91
+ const key = entry.item.key;
92
+ if (!groups.has(key)) {
93
+ groups.set(key, []);
94
+ }
95
+ groups.get(key)!.push(entry);
96
+ }
97
+ return Array.from(groups.values()).map(group => {
98
+ const sorted = [...group].sort((a, b) => a.price - b.price);
99
+ return {
100
+ bestListing: sorted[0],
101
+ otherListings: sorted.slice(1),
102
+ };
103
+ });
104
+ }, [items]);
105
+
86
106
  return (
87
107
  <>
88
108
  {buyingItemId && buyingItem && hasDCBalance && (
@@ -110,7 +130,7 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
110
130
  />
111
131
  )}
112
132
  <InputWrapper>
113
- <p>Search By Name</p>
133
+ <p>SEARCH</p>
114
134
  <Input
115
135
  onChange={e => {
116
136
  setName(e.target.value);
@@ -120,111 +140,121 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
120
140
  placeholder="Enter name..."
121
141
  onBlur={enableHotkeys}
122
142
  onFocus={disableHotkeys}
143
+ className="search-input"
123
144
  />
145
+ <button className="filter-btn" onClick={() => setShowFilters(!showFilters)}>
146
+ <SortVertical width={18} height={18} />
147
+ </button>
124
148
  </InputWrapper>
125
149
 
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
- />
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}
178
+ />
179
+ </div>
153
180
  </div>
154
181
 
155
182
  <div>
156
183
  <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
- />
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
+ ]);
192
+ }}
193
+ placeholder="Min"
194
+ type="number"
195
+ min={0}
196
+ onBlur={enableHotkeys}
197
+ onFocus={disableHotkeys}
198
+ />
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}
213
+ />
214
+ </div>
186
215
  </div>
187
216
 
188
217
  <div>
189
218
  <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
- />
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
+ />
243
+ </div>
215
244
  </div>
216
245
  </FilterInputsWrapper>
246
+ )}
217
247
 
218
248
  <WrapperContainer>
219
249
  <StyledDropdown
220
250
  options={itemTypeOptions}
221
251
  onChange={onChangeType}
222
- width="95%"
252
+ width="100%"
223
253
  />
224
254
  <StyledDropdown
225
255
  options={itemRarityOptions}
226
256
  onChange={onChangeRarity}
227
- width="95%"
257
+ width="100%"
228
258
  />
229
259
  <StyledDropdown
230
260
  options={orderByOptions}
@@ -235,17 +265,18 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
235
265
  </OptionsWrapper>
236
266
 
237
267
  <ItemComponentScrollWrapper id="MarketContainer" ref={itemsContainer}>
238
- {items?.map(({ item, price, _id, owner }, index) => (
239
- <MarketplaceRows
240
- key={`${item.key}_${index}`}
268
+ {groupedItems.map(({ bestListing, otherListings }) => (
269
+ <GroupedMarketplaceRow
270
+ key={bestListing.item.key}
271
+ bestListing={bestListing}
272
+ otherListings={otherListings}
241
273
  atlasIMG={atlasIMG}
242
274
  atlasJSON={atlasJSON}
243
- item={item}
244
- itemPrice={price}
245
- dcEquivalentPrice={dcToGoldSwapRate > 0 ? getDCEquivalentPrice(price) : undefined}
246
275
  equipmentSet={equipmentSet}
247
- onMarketPlaceItemBuy={setBuyingItemId.bind(null, _id)}
248
- disabled={owner === characterId}
276
+ dcToGoldSwapRate={dcToGoldSwapRate}
277
+ getDCEquivalentPrice={getDCEquivalentPrice}
278
+ characterId={characterId}
279
+ onBuy={setBuyingItemId}
249
280
  />
250
281
  ))}
251
282
  </ItemComponentScrollWrapper>
@@ -258,56 +289,98 @@ const InputWrapper = styled.div`
258
289
  display: flex !important;
259
290
  justify-content: flex-start;
260
291
  align-items: center;
261
- margin: auto;
292
+ margin: 0 auto 10px auto;
293
+ background: rgba(0, 0, 0, 0.2);
294
+ padding: 8px 12px;
295
+ border-radius: 4px;
296
+ border: 1px solid rgba(255, 255, 255, 0.1);
262
297
 
263
298
  p {
264
299
  width: auto;
265
300
  margin-right: 20px;
301
+ font-size: 0.7rem;
302
+ color: #ccc;
303
+ text-transform: uppercase;
304
+ letter-spacing: 1px;
305
+ margin-bottom: 0px;
266
306
  }
267
307
 
268
- input {
269
- width: 68%;
308
+ input.search-input {
270
309
  height: 10px;
310
+ flex-grow: 1;
271
311
  }
272
- `;
273
312
 
274
- const OptionsWrapper = styled.div`
275
- width: 100%;
276
- height: 100px;
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;
324
+
325
+ &:hover {
326
+ color: #f59e0b;
327
+ }
328
+ }
277
329
  `;
278
330
 
279
- const FilterInputsWrapper = styled.div`
331
+ const OptionsWrapper = styled.div<{ showFilters: boolean }>`
280
332
  width: 95%;
333
+ margin: 0 auto;
334
+ background: rgba(0, 0, 0, 0.15);
335
+ border-radius: 4px;
336
+ border: 1px solid rgba(255, 255, 255, 0.05);
337
+ padding: 10px;
281
338
  display: flex;
282
- justify-content: space-between;
283
- align-items: center;
284
- margin-bottom: 10px;
285
- margin-left: 10px;
286
- gap: 5px;
339
+ flex-direction: column;
340
+ gap: ${({ showFilters }) => (showFilters ? '15px' : '0')};
341
+ `;
342
+
343
+ const FilterInputsWrapper = styled.div`
344
+ display: grid;
345
+ grid-template-columns: repeat(3, 1fr);
346
+ gap: 15px;
287
347
  color: white;
288
- flex-wrap: wrap;
348
+
349
+ > div {
350
+ display: flex;
351
+ flex-direction: column;
352
+ gap: 5px;
353
+ }
289
354
 
290
355
  p {
291
- width: auto;
292
356
  margin: 0;
357
+ font-size: 0.65rem;
358
+ color: #aaa;
359
+ text-transform: uppercase;
360
+ letter-spacing: 1px;
293
361
  }
294
362
 
295
- input {
296
- width: 75px;
297
- height: 10px;
363
+ .input-group {
364
+ display: flex;
365
+ align-items: center;
366
+ gap: 5px;
367
+
368
+ input {
369
+ width: 100%;
370
+ height: 10px;
371
+ }
298
372
  }
299
373
 
300
- .big-input {
301
- width: 130px;
374
+ .separator-icon {
375
+ flex-shrink: 0;
376
+ color: rgba(255, 255, 255, 0.3);
302
377
  }
303
378
  `;
304
379
 
305
380
  const WrapperContainer = styled.div`
306
381
  display: grid;
307
- grid-template-columns: 40% 30% 30%;
308
- justify-content: space-between;
309
- width: calc(100% - 40px);
310
- margin-left: 10px;
382
+ grid-template-columns: 1fr 1fr 1fr;
383
+ gap: 15px;
311
384
 
312
385
  .rpgui-content .rpgui-dropdown-imp-header {
313
386
  padding: 0px 10px 0 !important;
@@ -317,8 +390,11 @@ const WrapperContainer = styled.div`
317
390
  const ItemComponentScrollWrapper = styled.div`
318
391
  overflow-y: scroll;
319
392
  height: 390px;
320
- width: 100%;
321
- margin-top: 1rem;
393
+ width: 95%;
394
+ margin: 1rem auto 0 auto;
395
+ background: rgba(0, 0, 0, 0.2);
396
+ border: 1px solid rgba(255, 255, 255, 0.05);
397
+ border-radius: 4px;
322
398
 
323
399
  @media (max-width: 950px) {
324
400
  height: 250px;
@@ -326,6 +402,6 @@ const ItemComponentScrollWrapper = styled.div`
326
402
  `;
327
403
 
328
404
  const StyledDropdown = styled(Dropdown)`
329
- margin: 3px !important;
330
- width: 170px !important;
405
+ margin: 0px !important;
406
+ width: 100% !important;
331
407
  `;