@rpg-engine/long-bow 0.8.139 → 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.
@@ -3,15 +3,17 @@ import {
3
3
  getItemTextureKeyPath,
4
4
  IEquipmentSet,
5
5
  IItem,
6
+ IMarketplaceItem,
6
7
  } from '@rpg-engine/shared';
7
- import React from 'react';
8
+ import { Coins } from 'pixelarticons/react/Coins';
9
+ import { Delete } from 'pixelarticons/react/Delete';
10
+ import React, { useState } from 'react';
8
11
  import styled from 'styled-components';
9
- import { uiColors } from '../../constants/uiColors';
10
12
  import { uiFonts } from '../../constants/uiFonts';
11
- import { Button, ButtonTypes } from '../Button';
12
13
  import { ItemInfoWrapper } from '../Item/Cards/ItemInfoWrapper';
13
14
  import { onRenderGems } from '../Item/Inventory/ItemGem';
14
15
  import { rarityColor } from '../Item/Inventory/ItemSlotRarity';
16
+ import { CTAButton } from '../shared/CTAButton/CTAButton';
15
17
  import { Ellipsis } from '../shared/Ellipsis';
16
18
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
17
19
 
@@ -46,7 +48,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
46
48
 
47
49
  return (
48
50
  <MarketplaceWrapper>
49
- <ItemIconContainer>
51
+ <ItemSection>
50
52
  <SpriteContainer>
51
53
  <ItemInfoWrapper
52
54
  item={item}
@@ -73,7 +75,6 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
73
75
  imgClassname="sprite-from-atlas-img--item"
74
76
  />
75
77
  </RarityContainer>
76
-
77
78
  <QuantityContainer>
78
79
  {item.stackQty &&
79
80
  item.stackQty > 1 &&
@@ -81,120 +82,282 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
81
82
  </QuantityContainer>
82
83
  </ItemInfoWrapper>
83
84
  </SpriteContainer>
84
- <PriceValue>
85
- <p>
85
+
86
+ <ItemDetails>
87
+ <ItemName>
86
88
  <Ellipsis maxLines={1} maxWidth="200px" fontSize="10px">
87
89
  {item.name}
88
90
  </Ellipsis>
89
- </p>
90
- </PriceValue>
91
- </ItemIconContainer>
92
-
93
- <Flex>
94
- <ItemIconContainer>
95
- <GoldContainer>
96
- <SpriteFromAtlas
91
+ </ItemName>
92
+ <PriceRow>
93
+ <GoldPriceRow>
94
+ <GoldIcon>
95
+ <SpriteFromAtlas
96
+ atlasIMG={atlasIMG}
97
+ atlasJSON={atlasJSON}
98
+ spriteKey="others/gold-coin-qty-5.png"
99
+ imgScale={1}
100
+ />
101
+ </GoldIcon>
102
+ <GoldPrice>{itemPrice}</GoldPrice>
103
+ </GoldPriceRow>
104
+ {dcEquivalentPrice !== undefined && (
105
+ <DCPrice>({formatDCAmount(dcEquivalentPrice)} DC)</DCPrice>
106
+ )}
107
+ </PriceRow>
108
+ </ItemDetails>
109
+ </ItemSection>
110
+
111
+ <ActionSection>
112
+ <CTAButton
113
+ icon={onMarketPlaceItemBuy ? <Coins width={18} height={18} /> : <Delete width={18} height={18} />}
114
+ label={onMarketPlaceItemBuy ? 'Buy' : 'Remove'}
115
+ disabled={disabled}
116
+ onClick={() => {
117
+ if (disabled) return;
118
+ onMarketPlaceItemBuy?.();
119
+ onMarketPlaceItemRemove?.();
120
+ }}
121
+ iconColor={onMarketPlaceItemBuy ? '#f59e0b' : '#ef4444'}
122
+ />
123
+ </ActionSection>
124
+ </MarketplaceWrapper>
125
+ );
126
+ };
127
+
128
+ export interface IGroupedMarketplaceRowProps {
129
+ bestListing: IMarketplaceItem;
130
+ otherListings: IMarketplaceItem[];
131
+ atlasJSON: any;
132
+ atlasIMG: any;
133
+ equipmentSet?: IEquipmentSet | null;
134
+ dcToGoldSwapRate?: number;
135
+ getDCEquivalentPrice: (goldPrice: number) => number;
136
+ characterId: string;
137
+ onBuy: (id: string) => void;
138
+ }
139
+
140
+ export const GroupedMarketplaceRow: React.FC<IGroupedMarketplaceRowProps> = ({
141
+ bestListing,
142
+ otherListings,
143
+ atlasJSON,
144
+ atlasIMG,
145
+ equipmentSet,
146
+ dcToGoldSwapRate = 0,
147
+ getDCEquivalentPrice,
148
+ characterId,
149
+ onBuy,
150
+ }) => {
151
+ const [expanded, setExpanded] = useState(false);
152
+ const totalOffers = otherListings.length + 1;
153
+ const hasMultiple = otherListings.length > 0;
154
+
155
+ return (
156
+ <GroupWrapper>
157
+ <GroupHeader
158
+ onClick={hasMultiple ? () => setExpanded(!expanded) : undefined}
159
+ clickable={hasMultiple}
160
+ >
161
+ <MarketplaceRows
162
+ atlasIMG={atlasIMG}
163
+ atlasJSON={atlasJSON}
164
+ item={bestListing.item}
165
+ itemPrice={bestListing.price}
166
+ dcEquivalentPrice={
167
+ dcToGoldSwapRate > 0
168
+ ? getDCEquivalentPrice(bestListing.price)
169
+ : undefined
170
+ }
171
+ equipmentSet={equipmentSet}
172
+ onMarketPlaceItemBuy={() => onBuy(bestListing._id)}
173
+ disabled={bestListing.owner === characterId}
174
+ />
175
+ {hasMultiple && (
176
+ <GroupMeta>
177
+ <OfferBadge>{totalOffers} offers</OfferBadge>
178
+ <Chevron expanded={expanded}>&#9656;</Chevron>
179
+ </GroupMeta>
180
+ )}
181
+ </GroupHeader>
182
+
183
+ {expanded && (
184
+ <SubRows>
185
+ {otherListings.map((listing) => (
186
+ <MarketplaceRows
187
+ key={listing._id}
97
188
  atlasIMG={atlasIMG}
98
189
  atlasJSON={atlasJSON}
99
- spriteKey="others/gold-coin-qty-5.png"
100
- imgScale={2}
190
+ item={listing.item}
191
+ itemPrice={listing.price}
192
+ dcEquivalentPrice={
193
+ dcToGoldSwapRate > 0
194
+ ? getDCEquivalentPrice(listing.price)
195
+ : undefined
196
+ }
197
+ equipmentSet={equipmentSet}
198
+ onMarketPlaceItemBuy={() => onBuy(listing._id)}
199
+ disabled={listing.owner === characterId}
101
200
  />
102
- </GoldContainer>
103
- <PriceValue>
104
- <p>
105
- <Ellipsis maxLines={1} maxWidth="120px" fontSize="10px">
106
- ${itemPrice}
107
- </Ellipsis>
108
- </p>
109
- </PriceValue>
110
- {dcEquivalentPrice !== undefined && (
111
- <DCPriceLabel>{formatDCAmount(dcEquivalentPrice)} DC</DCPriceLabel>
112
- )}
113
- </ItemIconContainer>
114
- <ButtonContainer>
115
- <Button
116
- buttonType={ButtonTypes.RPGUIButton}
117
- disabled={disabled}
118
- onPointerDown={() => {
119
- if (disabled) return;
120
-
121
- onMarketPlaceItemBuy?.();
122
- onMarketPlaceItemRemove?.();
123
- }}
124
- >
125
- {onMarketPlaceItemBuy ? 'Buy' : 'Remove'}
126
- </Button>
127
- </ButtonContainer>
128
- </Flex>
129
- </MarketplaceWrapper>
201
+ ))}
202
+ </SubRows>
203
+ )}
204
+ </GroupWrapper>
130
205
  );
131
206
  };
132
207
 
208
+ const GroupWrapper = styled.div`
209
+ margin-bottom: 2px;
210
+ `;
211
+
212
+ const GroupHeader = styled.div<{ clickable: boolean }>`
213
+ position: relative;
214
+ cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')};
215
+ `;
216
+
217
+ const GroupMeta = styled.div`
218
+ position: absolute;
219
+ right: 180px;
220
+ top: 50%;
221
+ transform: translateY(-50%);
222
+ display: flex;
223
+ align-items: center;
224
+ gap: 6px;
225
+ pointer-events: none;
226
+ `;
227
+
228
+ const OfferBadge = styled.span`
229
+ font-family: 'Press Start 2P', cursive;
230
+ font-size: 0.5rem;
231
+ color: rgba(255, 255, 255, 0.5);
232
+ background: rgba(255, 255, 255, 0.08);
233
+ padding: 2px 6px;
234
+ border-radius: 8px;
235
+ white-space: nowrap;
236
+ `;
237
+
238
+ const Chevron = styled.span<{ expanded: boolean }>`
239
+ display: inline-block;
240
+ font-size: 0.7rem;
241
+ color: rgba(255, 255, 255, 0.4);
242
+ transition: transform 0.2s ease;
243
+ transform: rotate(${({ expanded }) => (expanded ? '90deg' : '0deg')});
244
+ `;
245
+
246
+ const SubRows = styled.div`
247
+ margin-left: 12px;
248
+ padding-left: 8px;
249
+ border-left: 2px solid rgba(245, 158, 11, 0.25);
250
+
251
+ > div > div {
252
+ background: rgba(0, 0, 0, 0.4);
253
+ }
254
+ `;
255
+
133
256
  const MarketplaceWrapper = styled.div`
134
- margin: auto;
135
257
  display: flex;
258
+ align-items: center;
136
259
  justify-content: space-between;
137
- padding: 0.5rem;
260
+ padding: 0.6rem 1rem;
261
+ margin-bottom: 4px;
262
+ background: rgba(0, 0, 0, 0.25);
263
+ border: 1px solid rgba(255, 255, 255, 0.05);
264
+ border-radius: 6px;
265
+ border-left: 4px solid transparent;
266
+ transition: all 0.2s ease-in-out;
138
267
 
139
268
  &:hover {
140
- background-color: ${uiColors.darkGray};
141
- }
142
-
143
- p {
144
- font-size: 0.8rem;
269
+ background: rgba(245, 158, 11, 0.08);
270
+ border-color: rgba(245, 158, 11, 0.2);
271
+ border-left-color: #f59e0b;
272
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
273
+ transform: translateY(-1px);
145
274
  }
146
275
  `;
147
276
 
148
- const QuantityContainer = styled.p`
149
- position: absolute;
150
- display: block;
151
- top: 15px;
152
- left: 25px;
153
- font-size: ${uiFonts.size.xsmall} !important;
277
+ const ItemSection = styled.div`
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 0.75rem;
281
+ flex: 1;
282
+ min-width: 0;
154
283
  `;
155
284
 
156
- const GemContainer = styled.p`
157
- position: absolute;
158
- display: block;
159
- top: -5px;
160
- left: -10px;
161
- font-size: ${uiFonts.size.xsmall} !important;
285
+ const SpriteContainer = styled.div`
286
+ position: relative;
287
+ flex-shrink: 0;
288
+ min-width: 44px; /* Ensure wide stack quantities don't overlap ItemDetails */
162
289
  `;
163
290
 
164
- const Flex = styled.div`
291
+ const ItemDetails = styled.div`
165
292
  display: flex;
166
- gap: 24px;
293
+ flex-direction: column;
294
+ gap: 0.2rem;
295
+ min-width: 0;
296
+ margin-left: 1rem;
167
297
  `;
168
298
 
169
- const ItemIconContainer = styled.div`
299
+ const ItemName = styled.div`
300
+ font-family: 'Press Start 2P', cursive;
301
+ font-size: 0.65rem;
302
+ color: #ffffff;
303
+ `;
304
+
305
+ const PriceRow = styled.div`
170
306
  display: flex;
171
- justify-content: flex-start;
307
+ flex-direction: row;
172
308
  align-items: center;
309
+ gap: 0.5rem;
310
+ margin-top: 0.2rem;
173
311
  `;
174
312
 
175
- const GoldContainer = styled.div`
176
- position: relative;
177
- top: -0.5rem;
178
- left: 0.5rem;
313
+ const GoldPriceRow = styled.div`
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 0.3rem;
179
317
  `;
180
- const SpriteContainer = styled.div`
318
+
319
+ const GoldIcon = styled.span`
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: center;
181
323
  position: relative;
182
- left: 0.5rem;
324
+ top: -0.6rem;
325
+ left: -0.5rem;
183
326
  `;
184
327
 
185
- const DCPriceLabel = styled.span`
186
- margin-left: 8px;
328
+ const GoldPrice = styled.span`
329
+ font-family: 'Press Start 2P', cursive;
330
+ font-size: 0.6rem;
187
331
  color: #fef08a;
188
- font-size: 0.65rem;
332
+ line-height: 1;
333
+ `;
334
+
335
+ const DCPrice = styled.span`
336
+ font-family: 'Press Start 2P', cursive;
337
+ font-size: 0.55rem;
338
+ color: rgba(254, 240, 138, 0.65);
189
339
  white-space: nowrap;
190
340
  `;
191
341
 
192
- const PriceValue = styled.div`
193
- margin-left: 40px;
342
+ const ActionSection = styled.div`
343
+ flex-shrink: 0;
344
+ margin-left: 0.75rem;
194
345
  `;
195
346
 
196
- const ButtonContainer = styled.div`
197
- margin: auto;
347
+ const QuantityContainer = styled.p`
348
+ position: absolute;
349
+ display: block;
350
+ top: 15px;
351
+ left: 25px;
352
+ font-size: ${uiFonts.size.xsmall} !important;
353
+ `;
354
+
355
+ const GemContainer = styled.p`
356
+ position: absolute;
357
+ display: block;
358
+ top: -5px;
359
+ left: -10px;
360
+ font-size: ${uiFonts.size.xsmall} !important;
198
361
  `;
199
362
 
200
363
  const RarityContainer = styled.div<{ item: IItem }>`
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ export type MarketplaceAcceptedCurrency = 'gold' | 'gold_or_dc';
5
+
6
+ export interface IMarketplaceSettingsPanelProps {
7
+ acceptedCurrency: MarketplaceAcceptedCurrency;
8
+ onAcceptedCurrencyChange: (value: MarketplaceAcceptedCurrency) => void;
9
+ }
10
+
11
+ const CURRENCY_OPTIONS: { value: MarketplaceAcceptedCurrency; label: string; description: string }[] = [
12
+ {
13
+ value: 'gold_or_dc',
14
+ label: 'Gold or DC',
15
+ description: 'Accept both Gold and Definya Coin as payment',
16
+ },
17
+ {
18
+ value: 'gold',
19
+ label: 'Gold only',
20
+ description: 'Only accept Gold as payment',
21
+ },
22
+ ];
23
+
24
+ export const MarketplaceSettingsPanel: React.FC<IMarketplaceSettingsPanelProps> = ({
25
+ acceptedCurrency,
26
+ onAcceptedCurrencyChange,
27
+ }) => {
28
+ return (
29
+ <Wrapper>
30
+ <Section>
31
+ <SectionLabel>Accepted Currency</SectionLabel>
32
+ <OptionsGrid>
33
+ {CURRENCY_OPTIONS.map(option => (
34
+ <OptionCard
35
+ key={option.value}
36
+ $active={acceptedCurrency === option.value}
37
+ onClick={() => onAcceptedCurrencyChange(option.value)}
38
+ >
39
+ <OptionLabel $active={acceptedCurrency === option.value}>{option.label}</OptionLabel>
40
+ <OptionDescription>{option.description}</OptionDescription>
41
+ {acceptedCurrency === option.value && <ActiveBadge>Active</ActiveBadge>}
42
+ </OptionCard>
43
+ ))}
44
+ </OptionsGrid>
45
+ <Hint>Buyers will only be able to pay using the currency you accept.</Hint>
46
+ </Section>
47
+ </Wrapper>
48
+ );
49
+ };
50
+
51
+ const Wrapper = styled.div`
52
+ width: 95%;
53
+ margin: 0 auto;
54
+ padding-top: 4px;
55
+ `;
56
+
57
+ const Section = styled.div`
58
+ background: rgba(0, 0, 0, 0.15);
59
+ border-radius: 4px;
60
+ border: 1px solid rgba(255, 255, 255, 0.05);
61
+ padding: 16px 18px;
62
+ `;
63
+
64
+ const SectionLabel = styled.p`
65
+ margin: 0 0 14px 0;
66
+ font-size: 0.65rem;
67
+ color: #aaa;
68
+ text-transform: uppercase;
69
+ letter-spacing: 1px;
70
+ `;
71
+
72
+ const OptionsGrid = styled.div`
73
+ display: grid;
74
+ grid-template-columns: 1fr 1fr;
75
+ gap: 12px;
76
+ `;
77
+
78
+ const OptionCard = styled.button<{ $active: boolean }>`
79
+ position: relative;
80
+ display: flex;
81
+ flex-direction: column;
82
+ align-items: flex-start;
83
+ gap: 6px;
84
+ padding: 14px 16px;
85
+ background: ${({ $active }) => ($active ? 'rgba(245, 158, 11, 0.12)' : 'rgba(0, 0, 0, 0.25)')};
86
+ border: 2px solid ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.08)')};
87
+ border-radius: 6px;
88
+ cursor: pointer;
89
+ text-align: left;
90
+ transition: border-color 0.15s, background 0.15s;
91
+
92
+ &:hover {
93
+ border-color: ${({ $active }) => ($active ? '#f59e0b' : 'rgba(255,255,255,0.25)')};
94
+ background: ${({ $active }) => ($active ? 'rgba(245, 158, 11, 0.15)' : 'rgba(255,255,255,0.04)')};
95
+ }
96
+ `;
97
+
98
+ const OptionLabel = styled.span<{ $active: boolean }>`
99
+ font-family: 'Press Start 2P', cursive;
100
+ font-size: 0.6rem;
101
+ color: ${({ $active }) => ($active ? '#f59e0b' : '#cccccc')};
102
+ letter-spacing: 0.5px;
103
+ `;
104
+
105
+ const OptionDescription = styled.span`
106
+ font-size: 0.5rem;
107
+ color: #888;
108
+ line-height: 1.5;
109
+ `;
110
+
111
+ const ActiveBadge = styled.span`
112
+ position: absolute;
113
+ top: 8px;
114
+ right: 10px;
115
+ font-size: 0.45rem;
116
+ color: #f59e0b;
117
+ text-transform: uppercase;
118
+ letter-spacing: 0.5px;
119
+ opacity: 0.8;
120
+ `;
121
+
122
+ const Hint = styled.p`
123
+ margin: 14px 0 0 0;
124
+ font-size: 0.48rem;
125
+ color: #666;
126
+ line-height: 1.6;
127
+ `;
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ export interface ITabOption {
5
+ id: string;
6
+ label: React.ReactNode;
7
+ icon?: React.ReactNode;
8
+ }
9
+
10
+ export interface ITabsProps {
11
+ options: ITabOption[];
12
+ activeTabId: string;
13
+ onTabChange: (tabId: string) => void;
14
+ className?: string;
15
+ }
16
+
17
+ export const Tabs: React.FC<ITabsProps> = ({ options, activeTabId, onTabChange, className }) => {
18
+ return (
19
+ <TabsContainer className={className}>
20
+ {options.map((option) => (
21
+ <TabButton
22
+ key={option.id}
23
+ $active={option.id === activeTabId}
24
+ onClick={() => onTabChange(option.id)}
25
+ >
26
+ {option.icon && option.icon} {option.label}
27
+ </TabButton>
28
+ ))}
29
+ </TabsContainer>
30
+ );
31
+ };
32
+
33
+ const TabsContainer = styled.div`
34
+ display: flex;
35
+ gap: 15px;
36
+ width: 95%;
37
+ margin: 0 auto 15px auto;
38
+ border-bottom: 2px solid rgba(255, 255, 255, 0.1);
39
+ padding-bottom: 10px;
40
+ `;
41
+
42
+ const TabButton = styled.button<{ $active: boolean }>`
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 8px;
46
+ background: transparent;
47
+ border: none;
48
+ border-bottom: ${({ $active }) => ($active ? '3px solid #f59e0b' : '3px solid transparent')};
49
+ color: ${({ $active }) => ($active ? '#ffffff' : '#888888')};
50
+ font-family: 'Press Start 2P', cursive;
51
+ font-size: 0.70rem;
52
+ letter-spacing: 1px;
53
+ cursor: pointer;
54
+ padding: 5px 10px 10px 10px;
55
+ transition: color 0.2s, border-bottom 0.2s;
56
+
57
+ &:hover {
58
+ color: #ffffff;
59
+ }
60
+ `;
@@ -0,0 +1 @@
1
+ export * from './Tabs';
package/src/index.tsx CHANGED
@@ -37,6 +37,7 @@ export * from './components/ListMenu';
37
37
  export * from './components/Marketplace/Marketplace';
38
38
  export * from './components/Marketplace/MarketplaceBuyModal';
39
39
  export * from './components/Marketplace/MarketplaceRows';
40
+ export * from './components/Marketplace/MarketplaceSettingsPanel';
40
41
  export * from './components/Multitab/TabBody';
41
42
  export * from './components/Multitab/TabsContainer';
42
43
  export * from './components/NPCDialog/NPCDialog';
@@ -45,6 +45,8 @@ const Template: Story = () => {
45
45
  onAddItemToMarketplace={() => console.log('add')}
46
46
  characterId="1"
47
47
  onMoneyWithdraw={() => console.log('withdraw')}
48
+ dcBalance={50}
49
+ dcToGoldSwapRate={5500}
48
50
  totalItems={100}
49
51
  currentPage={page}
50
52
  itemsPerPage={30}