@rpg-engine/long-bow 0.8.181 → 0.8.184
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/dist/components/Marketplace/CharacterListingForm.d.ts +15 -0
- package/dist/components/Marketplace/CharacterListingModal.d.ts +17 -0
- package/dist/components/Marketplace/CharacterMarketplacePanel.d.ts +22 -0
- package/dist/components/Marketplace/CharacterMarketplaceRows.d.ts +26 -0
- package/dist/components/Marketplace/Marketplace.d.ts +20 -1
- package/dist/components/Marketplace/MyCharacterListingsPanel.d.ts +19 -0
- package/dist/components/shared/DCRateStrip.d.ts +2 -0
- package/dist/components/shared/RadioOption.d.ts +22 -0
- package/dist/index.d.ts +4 -0
- package/dist/long-bow.cjs.development.js +1114 -130
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +1133 -154
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Features/marketplace/CharacterListingModal.stories.d.ts +8 -0
- package/dist/stories/Features/marketplace/CharacterMarketplace.stories.d.ts +10 -0
- package/dist/stories/shared/RadioOption.stories.d.ts +8 -0
- package/package.json +1 -1
- package/src/components/DCWallet/DCWalletContent.tsx +5 -47
- package/src/components/Marketplace/BuyPanel.tsx +1 -0
- package/src/components/Marketplace/CharacterListingForm.tsx +102 -0
- package/src/components/Marketplace/CharacterListingModal.tsx +404 -0
- package/src/components/Marketplace/CharacterMarketplacePanel.tsx +450 -0
- package/src/components/Marketplace/CharacterMarketplaceRows.tsx +265 -0
- package/src/components/Marketplace/GroupedRowContainer.tsx +3 -1
- package/src/components/Marketplace/ManagmentPanel.tsx +1 -0
- package/src/components/Marketplace/Marketplace.tsx +163 -2
- package/src/components/Marketplace/MyCharacterListingsPanel.tsx +327 -0
- package/src/components/shared/DCRateStrip.tsx +67 -0
- package/src/components/shared/ItemRowWrapper.tsx +3 -1
- package/src/components/shared/RadioOption.tsx +93 -0
- package/src/index.tsx +4 -0
- package/src/stories/Features/marketplace/CharacterListingModal.stories.tsx +131 -0
- package/src/stories/Features/marketplace/CharacterMarketplace.stories.tsx +340 -0
- package/src/stories/shared/RadioOption.stories.tsx +93 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Meta, Story } from '@storybook/react';
|
|
2
|
+
import { ICharacterListingModalProps } from '../../../components/Marketplace/CharacterListingModal';
|
|
3
|
+
declare const meta: Meta<ICharacterListingModalProps>;
|
|
4
|
+
export default meta;
|
|
5
|
+
export declare const WithMultipleCharacters: Story;
|
|
6
|
+
export declare const WithAlreadyListedFiltered: Story;
|
|
7
|
+
export declare const NoEligibleCharacters: Story;
|
|
8
|
+
export declare const SingleCharacter: Story;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Meta, Story } from '@storybook/react';
|
|
2
|
+
declare const meta: Meta;
|
|
3
|
+
export default meta;
|
|
4
|
+
export declare const CharacterBrowse: Story;
|
|
5
|
+
export declare const CharacterBrowseLoading: Story;
|
|
6
|
+
export declare const CharacterBrowseEmpty: Story;
|
|
7
|
+
export declare const CharacterBrowseFiltered: Story;
|
|
8
|
+
export declare const MyCharacterListings: Story;
|
|
9
|
+
export declare const MyCharacterListingsEmpty: Story;
|
|
10
|
+
export declare const CharacterListingPending: Story;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Meta, Story } from '@storybook/react';
|
|
2
|
+
import { IRadioOptionProps } from '../../components/shared/RadioOption';
|
|
3
|
+
declare const meta: Meta;
|
|
4
|
+
export default meta;
|
|
5
|
+
export declare const Unselected: Story<IRadioOptionProps>;
|
|
6
|
+
export declare const Selected: Story<IRadioOptionProps>;
|
|
7
|
+
export declare const Disabled: Story<IRadioOptionProps>;
|
|
8
|
+
export declare const InteractiveGroup: Story;
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { FaShoppingCart } from 'react-icons/fa';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
import { InternalTabs } from '../InternalTabs/InternalTabs';
|
|
5
|
+
import { DCRateStrip } from '../shared/DCRateStrip';
|
|
5
6
|
import { DCHistoryPanel, IDCTransaction } from './DCHistoryPanel';
|
|
6
7
|
import { DCTransferPanel, IDCTransferCharacterResult } from './DCTransferPanel';
|
|
7
8
|
|
|
@@ -22,7 +23,8 @@ export interface IDCWalletContentProps {
|
|
|
22
23
|
searchResults?: IDCTransferCharacterResult[];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
import { DC_TO_GOLD_SWAP_RATE } from '@rpg-engine/shared';
|
|
27
|
+
|
|
26
28
|
const DC_TO_USD = 100;
|
|
27
29
|
|
|
28
30
|
export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
|
|
@@ -42,7 +44,7 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
|
|
|
42
44
|
searchResults,
|
|
43
45
|
}) => {
|
|
44
46
|
const usdValue = (dcBalance / DC_TO_USD).toFixed(2);
|
|
45
|
-
const goldValue = (dcBalance *
|
|
47
|
+
const goldValue = (dcBalance * DC_TO_GOLD_SWAP_RATE).toLocaleString();
|
|
46
48
|
|
|
47
49
|
const tabs = [
|
|
48
50
|
{
|
|
@@ -94,13 +96,7 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
|
|
|
94
96
|
</BuyButton>
|
|
95
97
|
)}
|
|
96
98
|
</BalanceTop>
|
|
97
|
-
<
|
|
98
|
-
<RateItem><RateNum>1 DC</RateNum><RateSep>=</RateSep><RateVal>5,500 Gold</RateVal></RateItem>
|
|
99
|
-
<RateDivider />
|
|
100
|
-
<RateItem><RateNum>100 DC</RateNum><RateSep>=</RateSep><RateVal>1 USD</RateVal></RateItem>
|
|
101
|
-
<RateDivider />
|
|
102
|
-
<RateItem><RateNum>1 USD</RateNum><RateSep>=</RateSep><RateVal>550K Gold</RateVal></RateItem>
|
|
103
|
-
</RateStrip>
|
|
99
|
+
<DCRateStrip />
|
|
104
100
|
</BalanceHeader>
|
|
105
101
|
|
|
106
102
|
<TabsWrapper>
|
|
@@ -207,44 +203,6 @@ const BuyButtonLabel = styled.span`
|
|
|
207
203
|
text-shadow: none !important;
|
|
208
204
|
`;
|
|
209
205
|
|
|
210
|
-
const RateStrip = styled.div`
|
|
211
|
-
display: flex;
|
|
212
|
-
align-items: center;
|
|
213
|
-
border-top: 1px solid rgba(245, 158, 11, 0.1);
|
|
214
|
-
padding-top: 8px;
|
|
215
|
-
`;
|
|
216
|
-
|
|
217
|
-
const RateItem = styled.div`
|
|
218
|
-
display: flex;
|
|
219
|
-
align-items: center;
|
|
220
|
-
gap: 5px;
|
|
221
|
-
flex: 1;
|
|
222
|
-
justify-content: center;
|
|
223
|
-
`;
|
|
224
|
-
|
|
225
|
-
const RateDivider = styled.div`
|
|
226
|
-
width: 1px;
|
|
227
|
-
height: 12px;
|
|
228
|
-
background: rgba(245, 158, 11, 0.2);
|
|
229
|
-
`;
|
|
230
|
-
|
|
231
|
-
const RateNum = styled.span`
|
|
232
|
-
font-family: 'Press Start 2P', cursive !important;
|
|
233
|
-
font-size: 6px !important;
|
|
234
|
-
color: rgba(255, 255, 255, 0.5) !important;
|
|
235
|
-
`;
|
|
236
|
-
|
|
237
|
-
const RateSep = styled.span`
|
|
238
|
-
font-family: 'Press Start 2P', cursive !important;
|
|
239
|
-
font-size: 6px !important;
|
|
240
|
-
color: rgba(255, 255, 255, 0.2) !important;
|
|
241
|
-
`;
|
|
242
|
-
|
|
243
|
-
const RateVal = styled.span`
|
|
244
|
-
font-family: 'Press Start 2P', cursive !important;
|
|
245
|
-
font-size: 6px !important;
|
|
246
|
-
color: rgba(254, 240, 138, 0.6) !important;
|
|
247
|
-
`;
|
|
248
206
|
|
|
249
207
|
const TabsWrapper = styled.div`
|
|
250
208
|
padding: 0 2.5%;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ICharacter } from '@rpg-engine/shared';
|
|
2
|
+
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
6
|
+
import { CharacterListingModal } from './CharacterListingModal';
|
|
7
|
+
|
|
8
|
+
export interface ICharacterListingFormProps {
|
|
9
|
+
accountCharacters: ICharacter[];
|
|
10
|
+
onCharacterList: (characterId: string, price: number) => void;
|
|
11
|
+
/** Items atlas — for UI sprites like the DC coin */
|
|
12
|
+
atlasJSON: any;
|
|
13
|
+
atlasIMG: any;
|
|
14
|
+
/** Entities atlas — for character sprites */
|
|
15
|
+
characterAtlasJSON: any;
|
|
16
|
+
characterAtlasIMG: any;
|
|
17
|
+
enableHotkeys?: () => void;
|
|
18
|
+
disableHotkeys?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CharacterListingForm: React.FC<ICharacterListingFormProps> = ({
|
|
22
|
+
accountCharacters,
|
|
23
|
+
onCharacterList,
|
|
24
|
+
atlasJSON,
|
|
25
|
+
atlasIMG,
|
|
26
|
+
characterAtlasJSON,
|
|
27
|
+
characterAtlasIMG,
|
|
28
|
+
enableHotkeys,
|
|
29
|
+
disableHotkeys,
|
|
30
|
+
}) => {
|
|
31
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
32
|
+
|
|
33
|
+
const eligibleCount = accountCharacters.filter(
|
|
34
|
+
c => !c.isListedForSale && !c.tradedAt
|
|
35
|
+
).length;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Wrapper>
|
|
39
|
+
<CharacterListingModal
|
|
40
|
+
isOpen={isModalOpen}
|
|
41
|
+
onClose={() => setIsModalOpen(false)}
|
|
42
|
+
accountCharacters={accountCharacters}
|
|
43
|
+
atlasJSON={atlasJSON}
|
|
44
|
+
atlasIMG={atlasIMG}
|
|
45
|
+
characterAtlasJSON={characterAtlasJSON}
|
|
46
|
+
characterAtlasIMG={characterAtlasIMG}
|
|
47
|
+
onCharacterList={onCharacterList}
|
|
48
|
+
enableHotkeys={enableHotkeys}
|
|
49
|
+
disableHotkeys={disableHotkeys}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<Description>
|
|
53
|
+
List one of your offline characters on the marketplace. Prices are set in DC.
|
|
54
|
+
</Description>
|
|
55
|
+
|
|
56
|
+
<OpenButton
|
|
57
|
+
icon={<ShoppingBag width={20} height={20} />}
|
|
58
|
+
label={`List a Character${eligibleCount > 0 ? ` (${eligibleCount} eligible)` : ''}`}
|
|
59
|
+
disabled={eligibleCount === 0}
|
|
60
|
+
onClick={() => setIsModalOpen(true)}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
{eligibleCount === 0 && (
|
|
64
|
+
<NoEligible>No eligible characters to list.</NoEligible>
|
|
65
|
+
)}
|
|
66
|
+
</Wrapper>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const Wrapper = styled.div`
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
gap: 20px;
|
|
76
|
+
padding: 40px 24px;
|
|
77
|
+
width: 95%;
|
|
78
|
+
margin: 0 auto;
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const Description = styled.p`
|
|
82
|
+
margin: 0;
|
|
83
|
+
font-size: 0.5rem;
|
|
84
|
+
color: #666;
|
|
85
|
+
text-align: center;
|
|
86
|
+
line-height: 1.6;
|
|
87
|
+
max-width: 320px;
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const OpenButton = styled(CTAButton)`
|
|
91
|
+
padding: 14px 24px;
|
|
92
|
+
|
|
93
|
+
span { font-size: 0.65rem; }
|
|
94
|
+
svg { font-size: 1.2rem; }
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const NoEligible = styled.span`
|
|
98
|
+
font-size: 0.45rem;
|
|
99
|
+
color: #52525b;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
letter-spacing: 1px;
|
|
102
|
+
`;
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { ICharacter } from '@rpg-engine/shared';
|
|
2
|
+
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
3
|
+
import React, { useCallback, useState } from 'react';
|
|
4
|
+
import { FaTimes } from 'react-icons/fa';
|
|
5
|
+
import styled, { keyframes } from 'styled-components';
|
|
6
|
+
import ModalPortal from '../Abstractions/ModalPortal';
|
|
7
|
+
import { ConfirmModal } from '../ConfirmModal';
|
|
8
|
+
import { Input } from '../Input';
|
|
9
|
+
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
10
|
+
import { DCRateStrip } from '../shared/DCRateStrip';
|
|
11
|
+
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
12
|
+
|
|
13
|
+
export interface ICharacterListingModalProps {
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
accountCharacters: ICharacter[];
|
|
17
|
+
/** Items atlas — for UI sprites like the DC coin */
|
|
18
|
+
atlasJSON: any;
|
|
19
|
+
atlasIMG: any;
|
|
20
|
+
/** Entities atlas — for character sprites */
|
|
21
|
+
characterAtlasJSON: any;
|
|
22
|
+
characterAtlasIMG: any;
|
|
23
|
+
onCharacterList: (characterId: string, price: number) => void;
|
|
24
|
+
enableHotkeys?: () => void;
|
|
25
|
+
disableHotkeys?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
|
|
29
|
+
isOpen,
|
|
30
|
+
onClose,
|
|
31
|
+
accountCharacters,
|
|
32
|
+
atlasJSON,
|
|
33
|
+
atlasIMG,
|
|
34
|
+
characterAtlasJSON,
|
|
35
|
+
characterAtlasIMG,
|
|
36
|
+
onCharacterList,
|
|
37
|
+
enableHotkeys,
|
|
38
|
+
disableHotkeys,
|
|
39
|
+
}) => {
|
|
40
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
41
|
+
const [price, setPrice] = useState('');
|
|
42
|
+
const [isConfirming, setIsConfirming] = useState(false);
|
|
43
|
+
|
|
44
|
+
if (!isOpen) return null;
|
|
45
|
+
|
|
46
|
+
const eligibleCharacters = accountCharacters.filter(
|
|
47
|
+
c => !c.isListedForSale && !c.tradedAt
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const canList = !!selectedId && Number(price) > 0;
|
|
51
|
+
|
|
52
|
+
const handleClose = () => {
|
|
53
|
+
setSelectedId(null);
|
|
54
|
+
setPrice('');
|
|
55
|
+
enableHotkeys?.();
|
|
56
|
+
onClose();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleListClick = () => {
|
|
60
|
+
if (canList) setIsConfirming(true);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleConfirm = () => {
|
|
64
|
+
if (!selectedId || !canList) return;
|
|
65
|
+
onCharacterList(selectedId, Number(price));
|
|
66
|
+
setSelectedId(null);
|
|
67
|
+
setPrice('');
|
|
68
|
+
setIsConfirming(false);
|
|
69
|
+
enableHotkeys?.();
|
|
70
|
+
onClose();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const stopPropagation = useCallback(
|
|
74
|
+
(e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
},
|
|
77
|
+
[]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const getLevel = (c: ICharacter) => c.skills?.level || 1;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ModalPortal>
|
|
84
|
+
{isConfirming && (
|
|
85
|
+
<ConfirmModal
|
|
86
|
+
onConfirm={handleConfirm}
|
|
87
|
+
onClose={() => { setIsConfirming(false); enableHotkeys?.(); }}
|
|
88
|
+
message="Are you sure you want to list this character for sale?"
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
<Overlay onPointerDown={handleClose} />
|
|
93
|
+
<ModalContainer>
|
|
94
|
+
<ModalContent
|
|
95
|
+
onClick={stopPropagation as React.MouseEventHandler}
|
|
96
|
+
onTouchStart={stopPropagation as React.TouchEventHandler}
|
|
97
|
+
onPointerDown={stopPropagation as React.PointerEventHandler}
|
|
98
|
+
>
|
|
99
|
+
<Header>
|
|
100
|
+
<Title>List Character for Sale</Title>
|
|
101
|
+
<CloseButton onPointerDown={handleClose} aria-label="Close" type="button">
|
|
102
|
+
<FaTimes />
|
|
103
|
+
</CloseButton>
|
|
104
|
+
</Header>
|
|
105
|
+
|
|
106
|
+
<CharacterList>
|
|
107
|
+
{eligibleCharacters.length === 0 ? (
|
|
108
|
+
<EmptyState>No eligible characters to list.</EmptyState>
|
|
109
|
+
) : (
|
|
110
|
+
eligibleCharacters.map(character => {
|
|
111
|
+
const isSelected = selectedId === character._id;
|
|
112
|
+
return (
|
|
113
|
+
<CharacterRow
|
|
114
|
+
key={character._id}
|
|
115
|
+
$selected={isSelected}
|
|
116
|
+
onPointerDown={() => setSelectedId(character._id!)}
|
|
117
|
+
role="radio"
|
|
118
|
+
aria-checked={isSelected}
|
|
119
|
+
tabIndex={0}
|
|
120
|
+
onKeyDown={e => {
|
|
121
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
setSelectedId(character._id!);
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<RadioCircle $selected={isSelected} />
|
|
128
|
+
<SpriteWrapper>
|
|
129
|
+
<SpriteFromAtlas
|
|
130
|
+
atlasIMG={characterAtlasIMG}
|
|
131
|
+
atlasJSON={characterAtlasJSON}
|
|
132
|
+
spriteKey={`${character.textureKey}/down/standing/0.png`}
|
|
133
|
+
imgScale={2}
|
|
134
|
+
height={40}
|
|
135
|
+
width={40}
|
|
136
|
+
/>
|
|
137
|
+
</SpriteWrapper>
|
|
138
|
+
<CharacterInfo>
|
|
139
|
+
<CharacterName>{character.name || 'Unknown'}</CharacterName>
|
|
140
|
+
<CharacterMeta>Level {getLevel(character)}</CharacterMeta>
|
|
141
|
+
</CharacterInfo>
|
|
142
|
+
</CharacterRow>
|
|
143
|
+
);
|
|
144
|
+
})
|
|
145
|
+
)}
|
|
146
|
+
</CharacterList>
|
|
147
|
+
|
|
148
|
+
<PriceSection>
|
|
149
|
+
<PriceLabel>Set Price (DC)</PriceLabel>
|
|
150
|
+
<PriceRow>
|
|
151
|
+
<Input
|
|
152
|
+
onChange={e => setPrice(e.target.value)}
|
|
153
|
+
value={price}
|
|
154
|
+
placeholder="Amount..."
|
|
155
|
+
type="number"
|
|
156
|
+
disabled={eligibleCharacters.length === 0}
|
|
157
|
+
onFocus={disableHotkeys}
|
|
158
|
+
onBlur={enableHotkeys}
|
|
159
|
+
className="price-input"
|
|
160
|
+
/>
|
|
161
|
+
<ListBtn
|
|
162
|
+
icon={<ShoppingBag width={18} height={18} />}
|
|
163
|
+
label="List"
|
|
164
|
+
disabled={!canList}
|
|
165
|
+
onClick={handleListClick}
|
|
166
|
+
/>
|
|
167
|
+
</PriceRow>
|
|
168
|
+
{price && Number(price) > 0 && (
|
|
169
|
+
<PricePreview>
|
|
170
|
+
<DCCoinWrapper>
|
|
171
|
+
<SpriteFromAtlas
|
|
172
|
+
atlasIMG={atlasIMG}
|
|
173
|
+
atlasJSON={atlasJSON}
|
|
174
|
+
spriteKey="others/definya-coin.png"
|
|
175
|
+
imgScale={1}
|
|
176
|
+
/>
|
|
177
|
+
</DCCoinWrapper>
|
|
178
|
+
<PricePreviewAmount>{Number(price).toLocaleString()} DC</PricePreviewAmount>
|
|
179
|
+
</PricePreview>
|
|
180
|
+
)}
|
|
181
|
+
<DCRateStrip />
|
|
182
|
+
</PriceSection>
|
|
183
|
+
</ModalContent>
|
|
184
|
+
</ModalContainer>
|
|
185
|
+
</ModalPortal>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const scaleIn = keyframes`
|
|
190
|
+
from { transform: scale(0.85); opacity: 0; }
|
|
191
|
+
to { transform: scale(1); opacity: 1; }
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const Overlay = styled.div`
|
|
195
|
+
position: fixed;
|
|
196
|
+
inset: 0;
|
|
197
|
+
background: rgba(0, 0, 0, 0.7);
|
|
198
|
+
z-index: 1000;
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const ModalContainer = styled.div`
|
|
202
|
+
position: fixed;
|
|
203
|
+
inset: 0;
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
z-index: 1001;
|
|
208
|
+
pointer-events: none;
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
const ModalContent = styled.div`
|
|
212
|
+
background: #1a1a2e;
|
|
213
|
+
border: 2px solid #f59e0b;
|
|
214
|
+
border-radius: 8px;
|
|
215
|
+
padding: 20px 24px;
|
|
216
|
+
width: 440px;
|
|
217
|
+
max-width: 92%;
|
|
218
|
+
max-height: 80dvh;
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
gap: 16px;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
pointer-events: auto;
|
|
224
|
+
animation: ${scaleIn} 0.15s ease-out;
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
const Header = styled.div`
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const Title = styled.h3`
|
|
234
|
+
margin: 0;
|
|
235
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
236
|
+
font-size: 0.65rem !important;
|
|
237
|
+
color: #fef08a !important;
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
const CloseButton = styled.button`
|
|
241
|
+
background: none;
|
|
242
|
+
border: none;
|
|
243
|
+
color: rgba(255, 255, 255, 0.6);
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
font-size: 1rem;
|
|
246
|
+
padding: 4px;
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
pointer-events: auto;
|
|
250
|
+
|
|
251
|
+
&:hover { color: #ffffff; }
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const CharacterList = styled.div`
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
gap: 6px;
|
|
258
|
+
overflow-y: auto;
|
|
259
|
+
max-height: 260px;
|
|
260
|
+
padding: 2px;
|
|
261
|
+
|
|
262
|
+
&::-webkit-scrollbar { width: 6px; }
|
|
263
|
+
&::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; }
|
|
264
|
+
&::-webkit-scrollbar-thumb { background: rgba(245,158,11,0.3); border-radius: 4px; }
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
const CharacterRow = styled.div<{ $selected: boolean }>`
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
gap: 12px;
|
|
271
|
+
padding: 10px 12px;
|
|
272
|
+
border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.08)')};
|
|
273
|
+
border-radius: 6px;
|
|
274
|
+
background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(255,255,255,0.02)')};
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
transition: border-color 0.15s, background 0.15s;
|
|
277
|
+
|
|
278
|
+
&:hover {
|
|
279
|
+
border-color: ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(245,158,11,0.4)')};
|
|
280
|
+
background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(245,158,11,0.05)')};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
&:focus-visible {
|
|
284
|
+
outline: 2px solid rgba(245,158,11,0.6);
|
|
285
|
+
outline-offset: 2px;
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
|
|
289
|
+
const RadioCircle = styled.div<{ $selected: boolean }>`
|
|
290
|
+
width: 14px;
|
|
291
|
+
height: 14px;
|
|
292
|
+
border-radius: 50%;
|
|
293
|
+
border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
|
|
294
|
+
flex-shrink: 0;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: center;
|
|
298
|
+
|
|
299
|
+
&::after {
|
|
300
|
+
content: '';
|
|
301
|
+
width: 6px;
|
|
302
|
+
height: 6px;
|
|
303
|
+
border-radius: 50%;
|
|
304
|
+
background: #f59e0b;
|
|
305
|
+
opacity: ${({ $selected }) => ($selected ? 1 : 0)};
|
|
306
|
+
transition: opacity 0.15s;
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
const SpriteWrapper = styled.div`
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: center;
|
|
314
|
+
image-rendering: pixelated;
|
|
315
|
+
flex-shrink: 0;
|
|
316
|
+
width: 40px;
|
|
317
|
+
height: 40px;
|
|
318
|
+
`;
|
|
319
|
+
|
|
320
|
+
const CharacterInfo = styled.div`
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
gap: 4px;
|
|
324
|
+
flex: 1;
|
|
325
|
+
`;
|
|
326
|
+
|
|
327
|
+
const CharacterName = styled.span`
|
|
328
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
329
|
+
font-size: 0.5rem !important;
|
|
330
|
+
color: #f3f4f6 !important;
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const CharacterMeta = styled.span`
|
|
334
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
335
|
+
font-size: 0.4rem !important;
|
|
336
|
+
color: #888 !important;
|
|
337
|
+
text-transform: uppercase;
|
|
338
|
+
letter-spacing: 0.5px;
|
|
339
|
+
`;
|
|
340
|
+
|
|
341
|
+
const PricePreview = styled.div`
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 6px;
|
|
345
|
+
`;
|
|
346
|
+
|
|
347
|
+
const DCCoinWrapper = styled.span`
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
flex-shrink: 0;
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
const PricePreviewAmount = styled.span`
|
|
355
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
356
|
+
font-size: 0.55rem !important;
|
|
357
|
+
color: #fef08a !important;
|
|
358
|
+
`;
|
|
359
|
+
|
|
360
|
+
const PriceSection = styled.div`
|
|
361
|
+
display: flex;
|
|
362
|
+
flex-direction: column;
|
|
363
|
+
gap: 8px;
|
|
364
|
+
border-top: 1px solid rgba(255,255,255,0.06);
|
|
365
|
+
padding-top: 12px;
|
|
366
|
+
`;
|
|
367
|
+
|
|
368
|
+
const PriceLabel = styled.p`
|
|
369
|
+
margin: 0;
|
|
370
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
371
|
+
font-size: 0.55rem !important;
|
|
372
|
+
color: #888 !important;
|
|
373
|
+
text-transform: uppercase;
|
|
374
|
+
letter-spacing: 0.5px;
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
const PriceRow = styled.div`
|
|
378
|
+
display: flex;
|
|
379
|
+
gap: 8px;
|
|
380
|
+
align-items: center;
|
|
381
|
+
|
|
382
|
+
.price-input { flex: 1; height: 12px; }
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
const ListBtn = styled(CTAButton)`
|
|
386
|
+
flex-shrink: 0;
|
|
387
|
+
padding: 10px 16px;
|
|
388
|
+
height: 32px;
|
|
389
|
+
|
|
390
|
+
span { font-size: 0.6rem; }
|
|
391
|
+
svg { font-size: 1.1rem; }
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
const EmptyState = styled.div`
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
justify-content: center;
|
|
398
|
+
padding: 32px 16px;
|
|
399
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
400
|
+
font-size: 0.5rem !important;
|
|
401
|
+
color: #666 !important;
|
|
402
|
+
text-transform: uppercase;
|
|
403
|
+
letter-spacing: 1px;
|
|
404
|
+
`;
|