@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,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
export interface IRadioOptionProps {
|
|
5
|
+
selected: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onSelect: () => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A selectable row with an amber radio circle indicator.
|
|
13
|
+
* Used for single-select option lists throughout the Marketplace UI.
|
|
14
|
+
* Export `RadioCircle` separately so consumers can compose custom layouts.
|
|
15
|
+
*/
|
|
16
|
+
export const RadioOption: React.FC<IRadioOptionProps> = ({
|
|
17
|
+
selected,
|
|
18
|
+
disabled = false,
|
|
19
|
+
onSelect,
|
|
20
|
+
children,
|
|
21
|
+
}) => {
|
|
22
|
+
const handleClick = () => {
|
|
23
|
+
if (!disabled) {
|
|
24
|
+
onSelect();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<RadioOptionContainer
|
|
30
|
+
$selected={selected}
|
|
31
|
+
$disabled={disabled}
|
|
32
|
+
onClick={handleClick}
|
|
33
|
+
role="radio"
|
|
34
|
+
aria-checked={selected}
|
|
35
|
+
aria-disabled={disabled}
|
|
36
|
+
>
|
|
37
|
+
<RadioCircle $selected={selected} />
|
|
38
|
+
{children}
|
|
39
|
+
</RadioOptionContainer>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const RadioOptionContainer = styled.div<{ $selected: boolean; $disabled?: boolean }>`
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
padding: 10px 12px;
|
|
48
|
+
border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.15)')};
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'transparent')};
|
|
51
|
+
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
|
|
52
|
+
opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
|
|
53
|
+
transition: border-color 0.15s, background 0.15s;
|
|
54
|
+
|
|
55
|
+
&:hover {
|
|
56
|
+
border-color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.15)' : '#f59e0b')};
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
export const RadioCircle = styled.div<{ $selected: boolean }>`
|
|
61
|
+
width: 16px;
|
|
62
|
+
height: 16px;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
|
|
70
|
+
&::after {
|
|
71
|
+
content: '';
|
|
72
|
+
width: 8px;
|
|
73
|
+
height: 8px;
|
|
74
|
+
border-radius: 50%;
|
|
75
|
+
background: #f59e0b;
|
|
76
|
+
opacity: ${({ $selected }) => ($selected ? 1 : 0)};
|
|
77
|
+
transition: opacity 0.15s;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
/** Convenience wrapper for option label text with RPGUI font override. */
|
|
82
|
+
export const RadioOptionLabel = styled.span<{ $disabled?: boolean }>`
|
|
83
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
84
|
+
font-size: 0.65rem !important;
|
|
85
|
+
color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.4)' : '#ffffff')} !important;
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
/** Convenience wrapper for option sub-text with RPGUI font override. */
|
|
89
|
+
export const RadioOptionSub = styled.span`
|
|
90
|
+
font-family: 'Press Start 2P', cursive !important;
|
|
91
|
+
font-size: 0.55rem !important;
|
|
92
|
+
color: rgba(255, 255, 255, 0.5) !important;
|
|
93
|
+
`;
|
package/src/index.tsx
CHANGED
|
@@ -43,6 +43,10 @@ export * from './components/Marketplace/BuyOrderPanel';
|
|
|
43
43
|
export * from './components/Marketplace/BuyOrderRows';
|
|
44
44
|
export * from './components/Marketplace/HistoryPanel';
|
|
45
45
|
export * from './components/Marketplace/BlueprintSearchModal';
|
|
46
|
+
export * from './components/Marketplace/CharacterMarketplacePanel';
|
|
47
|
+
export * from './components/Marketplace/CharacterMarketplaceRows';
|
|
48
|
+
export * from './components/Marketplace/CharacterListingForm';
|
|
49
|
+
export * from './components/Marketplace/MyCharacterListingsPanel';
|
|
46
50
|
export * from './components/Multitab/TabBody';
|
|
47
51
|
export * from './components/Multitab/TabsContainer';
|
|
48
52
|
export * from './components/NPCDialog/NPCDialog';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ICharacter } from '@rpg-engine/shared';
|
|
2
|
+
import { Meta, Story } from '@storybook/react';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { CharacterListingModal, ICharacterListingModalProps } from '../../../components/Marketplace/CharacterListingModal';
|
|
5
|
+
import { RPGUIRoot } from '../../..';
|
|
6
|
+
import entitiesAtlasJSON from '../../../mocks/atlas/entities/entities.json';
|
|
7
|
+
import entitiesAtlasIMG from '../../../mocks/atlas/entities/entities.png';
|
|
8
|
+
import itemsAtlasJSON from '../../../mocks/atlas/items/items.json';
|
|
9
|
+
import itemsAtlasIMG from '../../../mocks/atlas/items/items.png';
|
|
10
|
+
|
|
11
|
+
const meta: Meta<ICharacterListingModalProps> = {
|
|
12
|
+
title: 'Features/Marketplace/CharacterListingModal',
|
|
13
|
+
component: CharacterListingModal,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
const mockCharacters: ICharacter[] = [
|
|
19
|
+
{
|
|
20
|
+
_id: 'char-1',
|
|
21
|
+
name: 'Sir Galahad',
|
|
22
|
+
textureKey: 'black-knight',
|
|
23
|
+
isListedForSale: false,
|
|
24
|
+
tradedAt: undefined,
|
|
25
|
+
skills: { level: 25 } as any,
|
|
26
|
+
} as ICharacter,
|
|
27
|
+
{
|
|
28
|
+
_id: 'char-2',
|
|
29
|
+
name: 'Merlin Jr.',
|
|
30
|
+
textureKey: 'pink-mage-1',
|
|
31
|
+
isListedForSale: false,
|
|
32
|
+
tradedAt: undefined,
|
|
33
|
+
skills: { level: 30 } as any,
|
|
34
|
+
} as ICharacter,
|
|
35
|
+
{
|
|
36
|
+
_id: 'char-3',
|
|
37
|
+
name: 'ShadowStep',
|
|
38
|
+
textureKey: 'redhair-girl-1',
|
|
39
|
+
isListedForSale: false,
|
|
40
|
+
tradedAt: undefined,
|
|
41
|
+
skills: { level: 18 } as any,
|
|
42
|
+
} as ICharacter,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const mockCharactersWithListed: ICharacter[] = [
|
|
46
|
+
...mockCharacters,
|
|
47
|
+
{
|
|
48
|
+
_id: 'char-4',
|
|
49
|
+
name: 'AlreadyListed',
|
|
50
|
+
textureKey: 'dragon-knight',
|
|
51
|
+
isListedForSale: true,
|
|
52
|
+
tradedAt: undefined,
|
|
53
|
+
skills: { level: 40 } as any,
|
|
54
|
+
} as ICharacter,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const ModalWrapper: React.FC<Omit<ICharacterListingModalProps, 'isOpen' | 'onClose' | 'onCharacterList'>> = (props) => {
|
|
58
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
59
|
+
const [lastListed, setLastListed] = useState<string | null>(null);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<RPGUIRoot>
|
|
63
|
+
<div style={{ padding: '20px', fontFamily: 'monospace', fontSize: '12px', color: '#fff' }}>
|
|
64
|
+
{lastListed ? (
|
|
65
|
+
<p>✅ Listed: {lastListed}</p>
|
|
66
|
+
) : (
|
|
67
|
+
<p>Click "List" to submit the form</p>
|
|
68
|
+
)}
|
|
69
|
+
<button
|
|
70
|
+
style={{ marginTop: 8, padding: '6px 12px', cursor: 'pointer' }}
|
|
71
|
+
onClick={() => setIsOpen(true)}
|
|
72
|
+
>
|
|
73
|
+
Reopen Modal
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<CharacterListingModal
|
|
77
|
+
{...props}
|
|
78
|
+
isOpen={isOpen}
|
|
79
|
+
onClose={() => setIsOpen(false)}
|
|
80
|
+
onCharacterList={(id, price) => {
|
|
81
|
+
setLastListed(`ID=${id}, Price=${price} DC`);
|
|
82
|
+
setIsOpen(false);
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</RPGUIRoot>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const WithMultipleCharacters: Story = () => (
|
|
90
|
+
<ModalWrapper
|
|
91
|
+
accountCharacters={mockCharacters}
|
|
92
|
+
atlasJSON={itemsAtlasJSON}
|
|
93
|
+
atlasIMG={itemsAtlasIMG}
|
|
94
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
95
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
WithMultipleCharacters.storyName = 'Multiple eligible characters';
|
|
99
|
+
|
|
100
|
+
export const WithAlreadyListedFiltered: Story = () => (
|
|
101
|
+
<ModalWrapper
|
|
102
|
+
accountCharacters={mockCharactersWithListed}
|
|
103
|
+
atlasJSON={itemsAtlasJSON}
|
|
104
|
+
atlasIMG={itemsAtlasIMG}
|
|
105
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
106
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
WithAlreadyListedFiltered.storyName = 'Some characters already listed (filtered out)';
|
|
110
|
+
|
|
111
|
+
export const NoEligibleCharacters: Story = () => (
|
|
112
|
+
<ModalWrapper
|
|
113
|
+
accountCharacters={[]}
|
|
114
|
+
atlasJSON={itemsAtlasJSON}
|
|
115
|
+
atlasIMG={itemsAtlasIMG}
|
|
116
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
117
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
NoEligibleCharacters.storyName = 'No eligible characters';
|
|
121
|
+
|
|
122
|
+
export const SingleCharacter: Story = () => (
|
|
123
|
+
<ModalWrapper
|
|
124
|
+
accountCharacters={[mockCharacters[0]]}
|
|
125
|
+
atlasJSON={itemsAtlasJSON}
|
|
126
|
+
atlasIMG={itemsAtlasIMG}
|
|
127
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
128
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
SingleCharacter.storyName = 'Single eligible character';
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { ICharacterListing } from '@rpg-engine/shared';
|
|
2
|
+
import { Meta, Story } from '@storybook/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { RPGUIRoot } from '../../..';
|
|
5
|
+
import { CharacterMarketplacePanel } from '../../../components/Marketplace/CharacterMarketplacePanel';
|
|
6
|
+
import { MyCharacterListingsPanel } from '../../../components/Marketplace/MyCharacterListingsPanel';
|
|
7
|
+
import entitiesAtlasJSON from '../../../mocks/atlas/entities/entities.json';
|
|
8
|
+
import entitiesAtlasIMG from '../../../mocks/atlas/entities/entities.png';
|
|
9
|
+
import itemsAtlasJSON from '../../../mocks/atlas/items/items.json';
|
|
10
|
+
import itemsAtlasIMG from '../../../mocks/atlas/items/items.png';
|
|
11
|
+
|
|
12
|
+
const meta: Meta = {
|
|
13
|
+
title: 'Features/Marketplace/CharacterMarketplace',
|
|
14
|
+
component: CharacterMarketplacePanel,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const daysAgo = (days: number): Date =>
|
|
21
|
+
new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
22
|
+
|
|
23
|
+
const mockCharacterListings: ICharacterListing[] = [
|
|
24
|
+
{
|
|
25
|
+
_id: 'cl-1',
|
|
26
|
+
character: 'char-1',
|
|
27
|
+
seller: 'player-2',
|
|
28
|
+
listedByCharacterName: 'WarriorKing',
|
|
29
|
+
price: 50000,
|
|
30
|
+
isBeingBought: false,
|
|
31
|
+
characterSnapshot: {
|
|
32
|
+
name: 'Sir Galahad',
|
|
33
|
+
level: 25,
|
|
34
|
+
class: 'Warrior',
|
|
35
|
+
race: 'Human',
|
|
36
|
+
faction: 'Alliance',
|
|
37
|
+
mode: 'Hardcore',
|
|
38
|
+
skills: { sword: 10, shield: 8 },
|
|
39
|
+
equipment: [
|
|
40
|
+
{ slot: 'weapon', itemName: 'Broad Sword', itemKey: 'items/broad-sword', rarity: 'Common' },
|
|
41
|
+
{ slot: 'armor', itemName: 'Steel Armor', itemKey: 'items/steel-armor', rarity: 'Rare' },
|
|
42
|
+
],
|
|
43
|
+
textureKey: 'black-knight',
|
|
44
|
+
},
|
|
45
|
+
createdAt: daysAgo(3),
|
|
46
|
+
updatedAt: daysAgo(3),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
_id: 'cl-2',
|
|
50
|
+
character: 'char-2',
|
|
51
|
+
seller: 'player-3',
|
|
52
|
+
listedByCharacterName: 'MageLord',
|
|
53
|
+
price: 75000,
|
|
54
|
+
isBeingBought: false,
|
|
55
|
+
characterSnapshot: {
|
|
56
|
+
name: 'Merlin Jr.',
|
|
57
|
+
level: 30,
|
|
58
|
+
class: 'Mage',
|
|
59
|
+
race: 'Elf',
|
|
60
|
+
faction: 'Horde',
|
|
61
|
+
mode: 'Standard',
|
|
62
|
+
skills: { fireball: 15, frostbolt: 12 },
|
|
63
|
+
equipment: [
|
|
64
|
+
{ slot: 'weapon', itemName: 'Fire Staff', itemKey: 'items/fire-staff', rarity: 'Epic' },
|
|
65
|
+
],
|
|
66
|
+
textureKey: 'pink-mage-1',
|
|
67
|
+
},
|
|
68
|
+
createdAt: daysAgo(1),
|
|
69
|
+
updatedAt: daysAgo(1),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
_id: 'cl-3',
|
|
73
|
+
character: 'char-3',
|
|
74
|
+
seller: 'player-4',
|
|
75
|
+
listedByCharacterName: 'RogueShadow',
|
|
76
|
+
price: 40000,
|
|
77
|
+
isBeingBought: true,
|
|
78
|
+
characterSnapshot: {
|
|
79
|
+
name: 'ShadowStep',
|
|
80
|
+
level: 20,
|
|
81
|
+
class: 'Rogue',
|
|
82
|
+
race: 'Dark Elf',
|
|
83
|
+
faction: 'Horde',
|
|
84
|
+
mode: 'Hardcore',
|
|
85
|
+
skills: { stealth: 10, backstab: 12 },
|
|
86
|
+
equipment: [
|
|
87
|
+
{ slot: 'weapon', itemName: 'Dagger', itemKey: 'items/dagger', rarity: 'Uncommon' },
|
|
88
|
+
],
|
|
89
|
+
textureKey: 'redhair-girl-1',
|
|
90
|
+
},
|
|
91
|
+
createdAt: daysAgo(7),
|
|
92
|
+
updatedAt: daysAgo(1),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
_id: 'cl-4',
|
|
96
|
+
character: 'char-4',
|
|
97
|
+
seller: 'player-5',
|
|
98
|
+
listedByCharacterName: 'PaladinLight',
|
|
99
|
+
price: 100000,
|
|
100
|
+
isBeingBought: false,
|
|
101
|
+
characterSnapshot: {
|
|
102
|
+
name: 'Lightbringer',
|
|
103
|
+
level: 40,
|
|
104
|
+
class: 'Paladin',
|
|
105
|
+
race: 'Human',
|
|
106
|
+
faction: 'Alliance',
|
|
107
|
+
mode: 'Standard',
|
|
108
|
+
skills: { holy: 20, shield: 15 },
|
|
109
|
+
equipment: [
|
|
110
|
+
{ slot: 'weapon', itemName: 'Holy Sword', itemKey: 'items/holy-sword', rarity: 'Legendary' },
|
|
111
|
+
{ slot: 'armor', itemName: 'Divine Armor', itemKey: 'items/divine-armor', rarity: 'Epic' },
|
|
112
|
+
],
|
|
113
|
+
textureKey: 'dragon-knight',
|
|
114
|
+
},
|
|
115
|
+
createdAt: daysAgo(14),
|
|
116
|
+
updatedAt: daysAgo(14),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
_id: 'cl-5',
|
|
120
|
+
character: 'char-5',
|
|
121
|
+
seller: 'player-6',
|
|
122
|
+
listedByCharacterName: 'ArcherQueen',
|
|
123
|
+
price: 35000,
|
|
124
|
+
isBeingBought: false,
|
|
125
|
+
characterSnapshot: {
|
|
126
|
+
name: 'ArrowStorm',
|
|
127
|
+
level: 18,
|
|
128
|
+
class: 'Archer',
|
|
129
|
+
race: 'Wood Elf',
|
|
130
|
+
faction: 'Alliance',
|
|
131
|
+
mode: 'Standard',
|
|
132
|
+
skills: { archery: 12, tracking: 8 },
|
|
133
|
+
equipment: [
|
|
134
|
+
{ slot: 'weapon', itemName: 'Longbow', itemKey: 'items/longbow', rarity: 'Rare' },
|
|
135
|
+
],
|
|
136
|
+
textureKey: 'pink-hair-girl-1',
|
|
137
|
+
},
|
|
138
|
+
createdAt: daysAgo(2),
|
|
139
|
+
updatedAt: daysAgo(2),
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const mockMyCharacterListings: ICharacterListing[] = [
|
|
144
|
+
{
|
|
145
|
+
_id: 'cl-10',
|
|
146
|
+
character: 'char-10',
|
|
147
|
+
seller: 'player-1',
|
|
148
|
+
listedByCharacterName: 'MyMainChar',
|
|
149
|
+
price: 80000,
|
|
150
|
+
isBeingBought: false,
|
|
151
|
+
characterSnapshot: {
|
|
152
|
+
name: 'MyWarrior',
|
|
153
|
+
level: 35,
|
|
154
|
+
class: 'Warrior',
|
|
155
|
+
race: 'Human',
|
|
156
|
+
faction: 'Alliance',
|
|
157
|
+
mode: 'Standard',
|
|
158
|
+
skills: { sword: 18, shield: 16 },
|
|
159
|
+
equipment: [
|
|
160
|
+
{ slot: 'weapon', itemName: 'Epic Sword', itemKey: 'items/epic-sword', rarity: 'Epic' },
|
|
161
|
+
],
|
|
162
|
+
textureKey: 'black-knight',
|
|
163
|
+
},
|
|
164
|
+
createdAt: daysAgo(10),
|
|
165
|
+
updatedAt: daysAgo(10),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
_id: 'cl-11',
|
|
169
|
+
character: 'char-11',
|
|
170
|
+
seller: 'player-1',
|
|
171
|
+
listedByCharacterName: 'MyMainChar',
|
|
172
|
+
price: 25000,
|
|
173
|
+
isBeingBought: true,
|
|
174
|
+
characterSnapshot: {
|
|
175
|
+
name: 'MyAltMage',
|
|
176
|
+
level: 15,
|
|
177
|
+
class: 'Mage',
|
|
178
|
+
race: 'Human',
|
|
179
|
+
faction: 'Alliance',
|
|
180
|
+
mode: 'Standard',
|
|
181
|
+
skills: { fireball: 8 },
|
|
182
|
+
equipment: [
|
|
183
|
+
{ slot: 'weapon', itemName: 'Basic Staff', itemKey: 'items/staff', rarity: 'Common' },
|
|
184
|
+
],
|
|
185
|
+
textureKey: 'red-mage-1',
|
|
186
|
+
},
|
|
187
|
+
createdAt: daysAgo(5),
|
|
188
|
+
updatedAt: daysAgo(1),
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
export const CharacterBrowse: Story = () => (
|
|
193
|
+
<RPGUIRoot>
|
|
194
|
+
<CharacterMarketplacePanel
|
|
195
|
+
characterListings={mockCharacterListings}
|
|
196
|
+
totalCount={50}
|
|
197
|
+
currentPage={1}
|
|
198
|
+
itemsPerPage={8}
|
|
199
|
+
onPageChange={() => {}}
|
|
200
|
+
onCharacterBuy={() => {}}
|
|
201
|
+
atlasJSON={itemsAtlasJSON}
|
|
202
|
+
atlasIMG={itemsAtlasIMG}
|
|
203
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
204
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
205
|
+
nameFilter=""
|
|
206
|
+
onNameFilterChange={() => {}}
|
|
207
|
+
isLoading={false}
|
|
208
|
+
/>
|
|
209
|
+
</RPGUIRoot>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
CharacterBrowse.storyName = 'Browse Character Listings';
|
|
213
|
+
|
|
214
|
+
export const CharacterBrowseLoading: Story = () => (
|
|
215
|
+
<RPGUIRoot>
|
|
216
|
+
<CharacterMarketplacePanel
|
|
217
|
+
characterListings={[]}
|
|
218
|
+
totalCount={0}
|
|
219
|
+
currentPage={1}
|
|
220
|
+
itemsPerPage={8}
|
|
221
|
+
onPageChange={() => {}}
|
|
222
|
+
onCharacterBuy={() => {}}
|
|
223
|
+
atlasJSON={itemsAtlasJSON}
|
|
224
|
+
atlasIMG={itemsAtlasIMG}
|
|
225
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
226
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
227
|
+
nameFilter=""
|
|
228
|
+
onNameFilterChange={() => {}}
|
|
229
|
+
isLoading={true}
|
|
230
|
+
/>
|
|
231
|
+
</RPGUIRoot>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
CharacterBrowseLoading.storyName = 'Loading State';
|
|
235
|
+
|
|
236
|
+
export const CharacterBrowseEmpty: Story = () => (
|
|
237
|
+
<RPGUIRoot>
|
|
238
|
+
<CharacterMarketplacePanel
|
|
239
|
+
characterListings={[]}
|
|
240
|
+
totalCount={0}
|
|
241
|
+
currentPage={1}
|
|
242
|
+
itemsPerPage={8}
|
|
243
|
+
onPageChange={() => {}}
|
|
244
|
+
onCharacterBuy={() => {}}
|
|
245
|
+
atlasJSON={itemsAtlasJSON}
|
|
246
|
+
atlasIMG={itemsAtlasIMG}
|
|
247
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
248
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
249
|
+
nameFilter=""
|
|
250
|
+
onNameFilterChange={() => {}}
|
|
251
|
+
isLoading={false}
|
|
252
|
+
/>
|
|
253
|
+
</RPGUIRoot>
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
CharacterBrowseEmpty.storyName = 'Empty State';
|
|
257
|
+
|
|
258
|
+
export const CharacterBrowseFiltered: Story = () => (
|
|
259
|
+
<RPGUIRoot>
|
|
260
|
+
<CharacterMarketplacePanel
|
|
261
|
+
characterListings={mockCharacterListings.filter(l =>
|
|
262
|
+
l.characterSnapshot.name.toLowerCase().includes('warrior')
|
|
263
|
+
)}
|
|
264
|
+
totalCount={2}
|
|
265
|
+
currentPage={1}
|
|
266
|
+
itemsPerPage={8}
|
|
267
|
+
onPageChange={() => {}}
|
|
268
|
+
onCharacterBuy={() => {}}
|
|
269
|
+
atlasJSON={itemsAtlasJSON}
|
|
270
|
+
atlasIMG={itemsAtlasIMG}
|
|
271
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
272
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
273
|
+
nameFilter="warrior"
|
|
274
|
+
onNameFilterChange={() => {}}
|
|
275
|
+
isLoading={false}
|
|
276
|
+
/>
|
|
277
|
+
</RPGUIRoot>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
CharacterBrowseFiltered.storyName = 'With Name Filter';
|
|
281
|
+
|
|
282
|
+
export const MyCharacterListings: Story = () => (
|
|
283
|
+
<RPGUIRoot>
|
|
284
|
+
<MyCharacterListingsPanel
|
|
285
|
+
myCharacterListings={mockMyCharacterListings}
|
|
286
|
+
totalCount={2}
|
|
287
|
+
currentPage={1}
|
|
288
|
+
itemsPerPage={8}
|
|
289
|
+
onPageChange={() => {}}
|
|
290
|
+
onCharacterDelist={() => {}}
|
|
291
|
+
atlasJSON={itemsAtlasJSON}
|
|
292
|
+
atlasIMG={itemsAtlasIMG}
|
|
293
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
294
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
295
|
+
/>
|
|
296
|
+
</RPGUIRoot>
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
MyCharacterListings.storyName = 'My Character Listings';
|
|
300
|
+
|
|
301
|
+
export const MyCharacterListingsEmpty: Story = () => (
|
|
302
|
+
<RPGUIRoot>
|
|
303
|
+
<MyCharacterListingsPanel
|
|
304
|
+
myCharacterListings={[]}
|
|
305
|
+
totalCount={0}
|
|
306
|
+
currentPage={1}
|
|
307
|
+
itemsPerPage={8}
|
|
308
|
+
onPageChange={() => {}}
|
|
309
|
+
onCharacterDelist={() => {}}
|
|
310
|
+
atlasJSON={itemsAtlasJSON}
|
|
311
|
+
atlasIMG={itemsAtlasIMG}
|
|
312
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
313
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
314
|
+
/>
|
|
315
|
+
</RPGUIRoot>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
MyCharacterListingsEmpty.storyName = 'My Listings - Empty State';
|
|
319
|
+
|
|
320
|
+
export const CharacterListingPending: Story = () => (
|
|
321
|
+
<RPGUIRoot>
|
|
322
|
+
<CharacterMarketplacePanel
|
|
323
|
+
characterListings={[mockCharacterListings[2]]} // The one with isBeingBought: true
|
|
324
|
+
totalCount={1}
|
|
325
|
+
currentPage={1}
|
|
326
|
+
itemsPerPage={8}
|
|
327
|
+
onPageChange={() => {}}
|
|
328
|
+
onCharacterBuy={() => {}}
|
|
329
|
+
atlasJSON={itemsAtlasJSON}
|
|
330
|
+
atlasIMG={itemsAtlasIMG}
|
|
331
|
+
characterAtlasJSON={entitiesAtlasJSON}
|
|
332
|
+
characterAtlasIMG={entitiesAtlasIMG}
|
|
333
|
+
nameFilter=""
|
|
334
|
+
onNameFilterChange={() => {}}
|
|
335
|
+
isLoading={false}
|
|
336
|
+
/>
|
|
337
|
+
</RPGUIRoot>
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
CharacterListingPending.storyName = 'Listing with Pending Sale';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Meta, Story } from '@storybook/react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { RPGUIRoot } from '../../components/RPGUI/RPGUIRoot';
|
|
4
|
+
import {
|
|
5
|
+
IRadioOptionProps,
|
|
6
|
+
RadioOption,
|
|
7
|
+
RadioOptionLabel,
|
|
8
|
+
RadioOptionSub,
|
|
9
|
+
} from '../../components/shared/RadioOption';
|
|
10
|
+
|
|
11
|
+
const meta: Meta = {
|
|
12
|
+
title: 'Shared/RadioOption',
|
|
13
|
+
component: RadioOption,
|
|
14
|
+
decorators: [
|
|
15
|
+
Story => (
|
|
16
|
+
<RPGUIRoot>
|
|
17
|
+
<div style={{ padding: '2rem', background: '#1a1a2e', minWidth: '320px' }}>
|
|
18
|
+
<Story />
|
|
19
|
+
</div>
|
|
20
|
+
</RPGUIRoot>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Single unselected option
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export const Unselected: Story<IRadioOptionProps> = () => (
|
|
31
|
+
<RadioOption selected={false} onSelect={() => {}}>
|
|
32
|
+
<div>
|
|
33
|
+
<RadioOptionLabel>Pay with Stripe</RadioOptionLabel>
|
|
34
|
+
</div>
|
|
35
|
+
</RadioOption>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Single selected option
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
export const Selected: Story<IRadioOptionProps> = () => (
|
|
42
|
+
<RadioOption selected={true} onSelect={() => {}}>
|
|
43
|
+
<div>
|
|
44
|
+
<RadioOptionLabel>Pay with Stripe</RadioOptionLabel>
|
|
45
|
+
</div>
|
|
46
|
+
</RadioOption>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Disabled option
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export const Disabled: Story<IRadioOptionProps> = () => (
|
|
53
|
+
<RadioOption selected={false} disabled={true} onSelect={() => {}}>
|
|
54
|
+
<div>
|
|
55
|
+
<RadioOptionLabel $disabled={true}>Pay with DC Wallet</RadioOptionLabel>
|
|
56
|
+
<RadioOptionSub>Insufficient balance</RadioOptionSub>
|
|
57
|
+
</div>
|
|
58
|
+
</RadioOption>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Interactive group of 3 options
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
const OPTIONS = [
|
|
65
|
+
{ id: 'stripe', label: 'Pay with Stripe', sub: 'Credit / debit card' },
|
|
66
|
+
{ id: 'dc', label: 'Pay with DC Wallet', sub: '250 DC available' },
|
|
67
|
+
{ id: 'paypal', label: 'Pay with PayPal', sub: 'Coming soon' },
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
type IOptionId = (typeof OPTIONS)[number]['id'];
|
|
71
|
+
|
|
72
|
+
export const InteractiveGroup: Story = () => {
|
|
73
|
+
const [selected, setSelected] = useState<IOptionId>('stripe');
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
77
|
+
{OPTIONS.map(opt => (
|
|
78
|
+
<RadioOption
|
|
79
|
+
key={opt.id}
|
|
80
|
+
selected={selected === opt.id}
|
|
81
|
+
onSelect={() => setSelected(opt.id)}
|
|
82
|
+
>
|
|
83
|
+
<div>
|
|
84
|
+
<RadioOptionLabel>{opt.label}</RadioOptionLabel>
|
|
85
|
+
<div style={{ marginTop: '3px' }}>
|
|
86
|
+
<RadioOptionSub>{opt.sub}</RadioOptionSub>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</RadioOption>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|