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