@rpg-engine/long-bow 0.8.66 → 0.8.68
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/Character/SkinSelectionGrid.d.ts +11 -0
- package/dist/components/Store/CartView.d.ts +1 -0
- package/dist/components/Store/MetadataCollector.d.ts +9 -0
- package/dist/components/Store/Store.d.ts +13 -1
- package/dist/components/Store/StoreCharacterSkinRow.d.ts +11 -0
- package/dist/components/Store/StoreItemRow.d.ts +1 -1
- package/dist/components/Store/hooks/useStoreCart.d.ts +6 -2
- package/dist/components/Store/hooks/useStoreMetadata.d.ts +15 -0
- package/dist/components/Store/sections/StoreItemsSection.d.ts +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/long-bow.cjs.development.js +1581 -193
- 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 +1549 -165
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Character/SkinSelectionGrid.stories.d.ts +1 -0
- package/dist/stories/Character/character/CharacterSkinSelectionModal.stories.d.ts +1 -5
- package/dist/stories/Features/store/MetadataCollector.stories.d.ts +1 -0
- package/package.json +2 -2
- package/src/components/Character/CharacterSkinSelectionModal.tsx +18 -71
- package/src/components/Character/SkinSelectionGrid.tsx +179 -0
- package/src/components/Store/CartView.tsx +66 -7
- package/src/components/Store/MetadataCollector.tsx +48 -0
- package/src/components/Store/Store.tsx +69 -7
- package/src/components/Store/StoreCharacterSkinRow.tsx +286 -0
- package/src/components/Store/StoreItemRow.tsx +1 -1
- package/src/components/Store/__test__/MetadataCollector.spec.tsx +228 -0
- package/src/components/Store/__test__/useStoreMetadata.spec.tsx +181 -0
- package/src/components/Store/hooks/useStoreCart.ts +89 -44
- package/src/components/Store/hooks/useStoreMetadata.ts +55 -0
- package/src/components/Store/sections/StoreItemsSection.tsx +30 -11
- package/src/index.tsx +6 -3
- package/src/stories/Character/SkinSelectionGrid.stories.tsx +106 -0
- package/src/stories/Character/character/CharacterSkinSelectionModal.stories.tsx +86 -25
- package/src/stories/Features/store/MetadataCollector.stories.tsx +94 -0
- package/src/stories/Features/store/Store.stories.tsx +103 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ICharacterSkinSelectionModalProps } from '../../../components/Character/CharacterSkinSelectionModal';
|
|
3
|
-
declare const meta: Meta;
|
|
4
|
-
export default meta;
|
|
5
|
-
export declare const KnightSkins: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, ICharacterSkinSelectionModalProps>;
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpg-engine/long-bow",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.68",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"@capacitor/core": "^6.1.0",
|
|
86
86
|
"@rollup/plugin-image": "^2.1.1",
|
|
87
|
-
"@rpg-engine/shared": "^0.9.
|
|
87
|
+
"@rpg-engine/shared": "^0.9.123",
|
|
88
88
|
"dayjs": "^1.11.2",
|
|
89
89
|
"font-awesome": "^4.7.0",
|
|
90
90
|
"fs-extra": "^10.1.0",
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { Button, ButtonTypes } from '../Button';
|
|
4
|
-
import { ErrorBoundary } from '../Item/Inventory/ErrorBoundary';
|
|
5
|
-
import PropertySelect, {
|
|
6
|
-
IPropertiesProps,
|
|
7
|
-
} from '../PropertySelect/PropertySelect';
|
|
8
|
-
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
9
4
|
import { ICharacterProps } from './CharacterSelection';
|
|
5
|
+
import { SkinSelectionGrid } from './SkinSelectionGrid';
|
|
10
6
|
|
|
11
7
|
export interface ICharacterSkinSelectionModalProps {
|
|
12
8
|
isOpen: boolean;
|
|
@@ -27,51 +23,21 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
|
|
|
27
23
|
atlasIMG,
|
|
28
24
|
initialSelectedSkin = '',
|
|
29
25
|
}) => {
|
|
30
|
-
|
|
31
|
-
const propertySelectValues = availableCharacters.map(item => ({
|
|
32
|
-
id: item.textureKey,
|
|
33
|
-
name: item.name,
|
|
34
|
-
}));
|
|
26
|
+
const [selectedSkin, setSelectedSkin] = useState(initialSelectedSkin);
|
|
35
27
|
|
|
36
|
-
//
|
|
37
|
-
const [selectedValue, setSelectedValue] = useState<
|
|
38
|
-
IPropertiesProps | undefined
|
|
39
|
-
>();
|
|
40
|
-
const [selectedSpriteKey, setSelectedSpriteKey] = useState('');
|
|
41
|
-
|
|
42
|
-
// Update sprite when the selected value changes
|
|
43
|
-
const updateSelectedSpriteKey = () => {
|
|
44
|
-
const textureKey = selectedValue ? selectedValue.id : '';
|
|
45
|
-
const spriteKey = textureKey ? textureKey + '/down/standing/0.png' : '';
|
|
46
|
-
|
|
47
|
-
if (spriteKey === selectedSpriteKey) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
setSelectedSpriteKey(spriteKey);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Update sprite when selectedValue changes
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
updateSelectedSpriteKey();
|
|
57
|
-
}, [selectedValue]);
|
|
58
|
-
|
|
59
|
-
// Initialize selectedValue
|
|
28
|
+
// Initialize selected skin
|
|
60
29
|
useEffect(() => {
|
|
61
30
|
if (initialSelectedSkin) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
setSelectedValue(initialProperty || propertySelectValues[0]);
|
|
66
|
-
} else if (propertySelectValues.length > 0) {
|
|
67
|
-
setSelectedValue(propertySelectValues[0]);
|
|
31
|
+
setSelectedSkin(initialSelectedSkin);
|
|
32
|
+
} else if (availableCharacters.length > 0) {
|
|
33
|
+
setSelectedSkin(availableCharacters[0].textureKey);
|
|
68
34
|
}
|
|
69
35
|
}, [initialSelectedSkin, availableCharacters]);
|
|
70
36
|
|
|
71
37
|
// Functions to handle confirmation and cancellation
|
|
72
38
|
const handleConfirm = () => {
|
|
73
|
-
if (
|
|
74
|
-
onConfirm(
|
|
39
|
+
if (selectedSkin) {
|
|
40
|
+
onConfirm(selectedSkin);
|
|
75
41
|
onClose();
|
|
76
42
|
}
|
|
77
43
|
};
|
|
@@ -84,32 +50,12 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
|
|
|
84
50
|
|
|
85
51
|
return (
|
|
86
52
|
<Container>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
imgScale={4}
|
|
94
|
-
height={80}
|
|
95
|
-
width={64}
|
|
96
|
-
containerStyle={{
|
|
97
|
-
display: 'flex',
|
|
98
|
-
alignItems: 'center',
|
|
99
|
-
paddingBottom: '15px',
|
|
100
|
-
}}
|
|
101
|
-
imgStyle={{
|
|
102
|
-
left: '22px',
|
|
103
|
-
}}
|
|
104
|
-
/>
|
|
105
|
-
</ErrorBoundary>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
<PropertySelect
|
|
109
|
-
availableProperties={propertySelectValues}
|
|
110
|
-
onChange={value => {
|
|
111
|
-
setSelectedValue(value);
|
|
112
|
-
}}
|
|
53
|
+
<SkinSelectionGrid
|
|
54
|
+
availableCharacters={availableCharacters}
|
|
55
|
+
initialSelectedSkin={selectedSkin}
|
|
56
|
+
onChange={setSelectedSkin}
|
|
57
|
+
atlasJSON={atlasJSON}
|
|
58
|
+
atlasIMG={atlasIMG}
|
|
113
59
|
/>
|
|
114
60
|
|
|
115
61
|
<ButtonsContainer>
|
|
@@ -119,7 +65,7 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
|
|
|
119
65
|
<Button
|
|
120
66
|
buttonType={ButtonTypes.RPGUIButton}
|
|
121
67
|
onClick={handleConfirm}
|
|
122
|
-
disabled={!
|
|
68
|
+
disabled={!selectedSkin}
|
|
123
69
|
>
|
|
124
70
|
Confirm
|
|
125
71
|
</Button>
|
|
@@ -129,11 +75,12 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
|
|
|
129
75
|
};
|
|
130
76
|
|
|
131
77
|
// Styled components
|
|
132
|
-
|
|
133
78
|
const Container = styled.div`
|
|
134
79
|
display: flex;
|
|
135
80
|
flex-direction: column;
|
|
136
81
|
align-items: center;
|
|
82
|
+
width: 400px;
|
|
83
|
+
max-width: 100%;
|
|
137
84
|
image-rendering: pixelated;
|
|
138
85
|
`;
|
|
139
86
|
|
|
@@ -142,7 +89,7 @@ const ButtonsContainer = styled.div`
|
|
|
142
89
|
justify-content: center;
|
|
143
90
|
gap: 0.8rem;
|
|
144
91
|
width: 100%;
|
|
145
|
-
margin:
|
|
92
|
+
margin: 2rem 0 0.75rem;
|
|
146
93
|
|
|
147
94
|
button {
|
|
148
95
|
min-width: 95px;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import SelectArrow from '../Arrow/SelectArrow';
|
|
4
|
+
import { ErrorBoundary } from '../Item/Inventory/ErrorBoundary';
|
|
5
|
+
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
6
|
+
import { ICharacterProps } from './CharacterSelection';
|
|
7
|
+
|
|
8
|
+
export interface ISkinSelectionGridProps {
|
|
9
|
+
availableCharacters: ICharacterProps[];
|
|
10
|
+
initialSelectedSkin?: string;
|
|
11
|
+
onChange: (skinKey: string) => void;
|
|
12
|
+
atlasJSON: any;
|
|
13
|
+
atlasIMG: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SkinSelectionGrid: React.FC<ISkinSelectionGridProps> = ({
|
|
17
|
+
availableCharacters,
|
|
18
|
+
initialSelectedSkin = '',
|
|
19
|
+
onChange,
|
|
20
|
+
atlasJSON,
|
|
21
|
+
atlasIMG,
|
|
22
|
+
}) => {
|
|
23
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
24
|
+
|
|
25
|
+
// Find the initial index based on initialSelectedSkin
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (initialSelectedSkin && availableCharacters.length > 0) {
|
|
28
|
+
const initialIndex = availableCharacters.findIndex(
|
|
29
|
+
character => character.textureKey === initialSelectedSkin
|
|
30
|
+
);
|
|
31
|
+
if (initialIndex !== -1) {
|
|
32
|
+
setCurrentIndex(initialIndex);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}, [initialSelectedSkin, availableCharacters]);
|
|
36
|
+
|
|
37
|
+
// Update the selected skin when currentIndex changes
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (availableCharacters.length > 0) {
|
|
40
|
+
const selectedCharacter = availableCharacters[currentIndex];
|
|
41
|
+
onChange(selectedCharacter.textureKey);
|
|
42
|
+
}
|
|
43
|
+
}, [currentIndex, availableCharacters, onChange]);
|
|
44
|
+
|
|
45
|
+
const handlePrevious = () => {
|
|
46
|
+
setCurrentIndex((prevIndex) =>
|
|
47
|
+
prevIndex === 0 ? availableCharacters.length - 1 : prevIndex - 1
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleNext = () => {
|
|
52
|
+
setCurrentIndex((prevIndex) =>
|
|
53
|
+
prevIndex === availableCharacters.length - 1 ? 0 : prevIndex + 1
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getSpriteKey = (textureKey: string) => {
|
|
58
|
+
return textureKey + '/down/standing/0.png';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (availableCharacters.length === 0) {
|
|
62
|
+
return <Container>No skins available</Container>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentCharacter = availableCharacters[currentIndex];
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Container>
|
|
69
|
+
<Header>Select Your Character Skin</Header>
|
|
70
|
+
<CarouselContainer>
|
|
71
|
+
<NavButtonWrapper>
|
|
72
|
+
<SelectArrow
|
|
73
|
+
direction="left"
|
|
74
|
+
onPointerDown={handlePrevious}
|
|
75
|
+
/>
|
|
76
|
+
</NavButtonWrapper>
|
|
77
|
+
|
|
78
|
+
<SkinPreview>
|
|
79
|
+
{currentCharacter && (
|
|
80
|
+
<>
|
|
81
|
+
<ErrorBoundary>
|
|
82
|
+
<SpriteFromAtlas
|
|
83
|
+
spriteKey={getSpriteKey(currentCharacter.textureKey)}
|
|
84
|
+
atlasIMG={atlasIMG}
|
|
85
|
+
atlasJSON={atlasJSON}
|
|
86
|
+
imgScale={4}
|
|
87
|
+
height={80}
|
|
88
|
+
width={64}
|
|
89
|
+
containerStyle={{
|
|
90
|
+
display: 'flex',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
}}
|
|
94
|
+
imgStyle={{
|
|
95
|
+
position: 'relative',
|
|
96
|
+
left: 0,
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</ErrorBoundary>
|
|
100
|
+
<SkinName>{currentCharacter.name}</SkinName>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</SkinPreview>
|
|
104
|
+
|
|
105
|
+
<NavButtonWrapper>
|
|
106
|
+
<SelectArrow
|
|
107
|
+
direction="right"
|
|
108
|
+
onPointerDown={handleNext}
|
|
109
|
+
/>
|
|
110
|
+
</NavButtonWrapper>
|
|
111
|
+
</CarouselContainer>
|
|
112
|
+
|
|
113
|
+
<CounterIndicator>
|
|
114
|
+
{currentIndex + 1} / {availableCharacters.length}
|
|
115
|
+
</CounterIndicator>
|
|
116
|
+
</Container>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const Container = styled.div`
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
align-items: center;
|
|
124
|
+
width: 100%;
|
|
125
|
+
margin-top: 1rem;
|
|
126
|
+
image-rendering: pixelated;
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const Header = styled.h3`
|
|
130
|
+
margin: 0 0 1rem 0;
|
|
131
|
+
color: white;
|
|
132
|
+
text-align: center;
|
|
133
|
+
font-size: 1.1rem;
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
const CarouselContainer = styled.div`
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: space-between;
|
|
140
|
+
width: 100%;
|
|
141
|
+
margin-bottom: 0.5rem;
|
|
142
|
+
position: relative;
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const SkinPreview = styled.div`
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
padding: 1rem;
|
|
151
|
+
background-color: rgba(245, 158, 11, 0.1);
|
|
152
|
+
border: 2px solid #f59e0b;
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
width: 140px;
|
|
155
|
+
height: 150px;
|
|
156
|
+
margin: 0 1.5rem;
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
const SkinName = styled.span`
|
|
160
|
+
color: white;
|
|
161
|
+
font-size: 1rem;
|
|
162
|
+
margin-top: 0.8rem;
|
|
163
|
+
text-align: center;
|
|
164
|
+
font-weight: bold;
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const NavButtonWrapper = styled.div`
|
|
168
|
+
width: 40px;
|
|
169
|
+
height: 42px;
|
|
170
|
+
position: relative;
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
const CounterIndicator = styled.div`
|
|
174
|
+
color: white;
|
|
175
|
+
font-size: 0.8rem;
|
|
176
|
+
margin-top: 0.5rem;
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
export default SkinSelectionGrid;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { IStoreItem } from '@rpg-engine/shared';
|
|
1
|
+
import { IStoreItem, MetadataType } from '@rpg-engine/shared';
|
|
2
2
|
import React, { useState } from 'react';
|
|
3
|
-
import { FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
|
|
3
|
+
import { FaInfoCircle, FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
6
6
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
interface ICartViewProps {
|
|
10
|
-
cartItems: {
|
|
10
|
+
cartItems: {
|
|
11
|
+
item: IStoreItem;
|
|
12
|
+
quantity: number;
|
|
13
|
+
metadata?: Record<string, any>;
|
|
14
|
+
}[];
|
|
11
15
|
onRemoveFromCart: (itemKey: string) => void;
|
|
12
16
|
onClose: () => void;
|
|
13
17
|
onPurchase: () => Promise<boolean>;
|
|
@@ -15,6 +19,26 @@ interface ICartViewProps {
|
|
|
15
19
|
atlasIMG: string;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
const MetadataDisplay: React.FC<{
|
|
23
|
+
type: MetadataType;
|
|
24
|
+
metadata: Record<string, any>;
|
|
25
|
+
}> = ({ type, metadata }) => {
|
|
26
|
+
switch (type) {
|
|
27
|
+
case MetadataType.CharacterSkin:
|
|
28
|
+
return (
|
|
29
|
+
<MetadataInfo>
|
|
30
|
+
<MetadataLabel>
|
|
31
|
+
<FaInfoCircle />
|
|
32
|
+
<span>Skin:</span>
|
|
33
|
+
</MetadataLabel>
|
|
34
|
+
<MetadataValue>{metadata.selectedSkin?.name || 'Custom skin'}</MetadataValue>
|
|
35
|
+
</MetadataInfo>
|
|
36
|
+
);
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
18
42
|
export const CartView: React.FC<ICartViewProps> = ({
|
|
19
43
|
cartItems,
|
|
20
44
|
onRemoveFromCart,
|
|
@@ -87,6 +111,13 @@ export const CartView: React.FC<ICartViewProps> = ({
|
|
|
87
111
|
${formatPrice(cartItem.item.price * cartItem.quantity)}
|
|
88
112
|
</span>
|
|
89
113
|
</ItemInfo>
|
|
114
|
+
|
|
115
|
+
{cartItem.metadata && cartItem.item.metadataType && (
|
|
116
|
+
<MetadataDisplay
|
|
117
|
+
type={cartItem.item.metadataType}
|
|
118
|
+
metadata={cartItem.metadata}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
90
121
|
</ItemDetails>
|
|
91
122
|
|
|
92
123
|
<CTAButton
|
|
@@ -243,19 +274,22 @@ const Footer = styled.div`
|
|
|
243
274
|
gap: 1.5rem;
|
|
244
275
|
padding-top: 1.5rem;
|
|
245
276
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
277
|
+
flex-shrink: 0;
|
|
246
278
|
`;
|
|
247
279
|
|
|
248
280
|
const TotalInfo = styled.div`
|
|
249
281
|
display: flex;
|
|
250
282
|
flex-direction: column;
|
|
251
|
-
gap: 0.
|
|
283
|
+
gap: 0.5rem;
|
|
252
284
|
`;
|
|
253
285
|
|
|
254
286
|
const TotalRow = styled.div`
|
|
255
287
|
display: flex;
|
|
288
|
+
align-items: center;
|
|
256
289
|
justify-content: space-between;
|
|
290
|
+
gap: 1rem;
|
|
257
291
|
font-family: 'Press Start 2P', cursive;
|
|
258
|
-
font-size:
|
|
292
|
+
font-size: 1rem;
|
|
259
293
|
color: #ffffff;
|
|
260
294
|
|
|
261
295
|
span:last-child {
|
|
@@ -265,8 +299,33 @@ const TotalRow = styled.div`
|
|
|
265
299
|
|
|
266
300
|
const ErrorMessage = styled.div`
|
|
267
301
|
color: #ef4444;
|
|
302
|
+
font-size: 0.875rem;
|
|
268
303
|
font-family: 'Press Start 2P', cursive;
|
|
269
|
-
font-size: 0.75rem;
|
|
270
|
-
margin-top: 0.5rem;
|
|
271
304
|
text-align: center;
|
|
272
305
|
`;
|
|
306
|
+
|
|
307
|
+
const MetadataInfo = styled.div`
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
margin-top: 0.5rem;
|
|
311
|
+
gap: 0.5rem;
|
|
312
|
+
font-size: 0.75rem;
|
|
313
|
+
color: #a3e635;
|
|
314
|
+
background: rgba(163, 230, 53, 0.1);
|
|
315
|
+
padding: 0.25rem 0.5rem;
|
|
316
|
+
border-radius: 4px;
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
const MetadataLabel = styled.div`
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 0.25rem;
|
|
323
|
+
font-weight: bold;
|
|
324
|
+
color: #d9f99d;
|
|
325
|
+
`;
|
|
326
|
+
|
|
327
|
+
const MetadataValue = styled.div`
|
|
328
|
+
overflow: hidden;
|
|
329
|
+
text-overflow: ellipsis;
|
|
330
|
+
white-space: nowrap;
|
|
331
|
+
`;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MetadataType } from "@rpg-engine/shared";
|
|
2
|
+
import React, { useEffect } from "react";
|
|
3
|
+
import { CharacterSkinSelectionModal } from "../Character/CharacterSkinSelectionModal";
|
|
4
|
+
|
|
5
|
+
export interface IMetadataCollectorProps {
|
|
6
|
+
metadataType: MetadataType;
|
|
7
|
+
config: Record<string, any>;
|
|
8
|
+
onCollect: (metadata: Record<string, any>) => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MetadataCollector: React.FC<IMetadataCollectorProps> = ({
|
|
13
|
+
metadataType,
|
|
14
|
+
config,
|
|
15
|
+
onCollect,
|
|
16
|
+
onCancel,
|
|
17
|
+
}) => {
|
|
18
|
+
// Make sure we clean up if unmounted without collecting
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
return () => {
|
|
21
|
+
// If we're unmounting without explicitly collecting or canceling,
|
|
22
|
+
// make sure to call onCancel to prevent any hanging promises
|
|
23
|
+
if (window.__metadataResolvers) {
|
|
24
|
+
onCancel();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}, [onCancel]);
|
|
28
|
+
|
|
29
|
+
// Use string comparison instead of direct property access
|
|
30
|
+
if (metadataType === 'CharacterSkin') {
|
|
31
|
+
return (
|
|
32
|
+
<CharacterSkinSelectionModal
|
|
33
|
+
isOpen={true}
|
|
34
|
+
onClose={onCancel}
|
|
35
|
+
onConfirm={(selectedSkin: any) => onCollect({ selectedSkin })}
|
|
36
|
+
availableCharacters={config.availableCharacters || []}
|
|
37
|
+
atlasJSON={config.atlasJSON}
|
|
38
|
+
atlasIMG={config.atlasIMG}
|
|
39
|
+
initialSelectedSkin={config.initialSelectedSkin}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
console.warn(`No collector implemented for metadata type: ${metadataType}`);
|
|
44
|
+
// Auto-cancel for unhandled types to prevent hanging promises
|
|
45
|
+
setTimeout(onCancel, 0);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { IItemPack,
|
|
1
|
+
import { IItemPack, IPurchase, IStoreItem, ItemRarities, ItemSubType, ItemType, UserAccountTypes } from '@rpg-engine/shared';
|
|
2
2
|
import React, { useMemo, useState } from 'react';
|
|
3
|
-
import { FaShoppingCart } from 'react-icons/fa';
|
|
3
|
+
import { FaHistory, FaShoppingCart } from 'react-icons/fa';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { uiColors } from '../../constants/uiColors';
|
|
6
6
|
import { DraggableContainer } from '../DraggableContainer';
|
|
@@ -9,16 +9,32 @@ import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
|
|
|
9
9
|
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
10
10
|
import { CartView } from './CartView';
|
|
11
11
|
import { useStoreCart } from './hooks/useStoreCart';
|
|
12
|
+
import { MetadataCollector } from './MetadataCollector';
|
|
12
13
|
import { StoreItemsSection } from './sections/StoreItemsSection';
|
|
13
14
|
import { StorePacksSection } from './sections/StorePacksSection';
|
|
14
15
|
import { StoreItemDetails } from './StoreItemDetails';
|
|
15
16
|
|
|
17
|
+
// Define IStoreProps locally as a workaround
|
|
18
|
+
export interface IStoreProps {
|
|
19
|
+
items: IStoreItem[];
|
|
20
|
+
packs?: IItemPack[];
|
|
21
|
+
atlasJSON: any;
|
|
22
|
+
atlasIMG: string;
|
|
23
|
+
onPurchase: (purchase: Partial<IPurchase>) => Promise<boolean>;
|
|
24
|
+
onShowHistory?: () => void; // Add the new optional prop
|
|
25
|
+
userAccountType: UserAccountTypes;
|
|
26
|
+
loading?: boolean;
|
|
27
|
+
error?: string;
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export const Store: React.FC<IStoreProps> = ({
|
|
17
32
|
items,
|
|
18
33
|
packs = [],
|
|
19
34
|
atlasJSON,
|
|
20
35
|
atlasIMG,
|
|
21
36
|
onPurchase,
|
|
37
|
+
onShowHistory, // Destructure the new prop
|
|
22
38
|
userAccountType,
|
|
23
39
|
loading = false,
|
|
24
40
|
error,
|
|
@@ -37,6 +53,8 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
37
53
|
getTotalPrice,
|
|
38
54
|
isCartOpen,
|
|
39
55
|
} = useStoreCart();
|
|
56
|
+
const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
|
|
57
|
+
const [currentMetadataItem, setCurrentMetadataItem] = useState<IStoreItem | null>(null);
|
|
40
58
|
|
|
41
59
|
const handleAddPackToCart = (pack: IItemPack) => {
|
|
42
60
|
const packItem: IStoreItem = {
|
|
@@ -87,6 +105,29 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
87
105
|
[items]
|
|
88
106
|
);
|
|
89
107
|
|
|
108
|
+
const handleMetadataCollected = (metadata: Record<string, any>) => {
|
|
109
|
+
if (currentMetadataItem && window.__metadataResolvers) {
|
|
110
|
+
// Resolve the promise in the useStoreMetadata hook
|
|
111
|
+
window.__metadataResolvers.resolve(metadata);
|
|
112
|
+
|
|
113
|
+
// Reset the metadata collection state
|
|
114
|
+
setCurrentMetadataItem(null);
|
|
115
|
+
// Removed unused setPendingMetadataQuantity call
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleMetadataCancel = () => {
|
|
120
|
+
if (window.__metadataResolvers) {
|
|
121
|
+
// Resolve with null to indicate cancellation
|
|
122
|
+
window.__metadataResolvers.resolve(null);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Reset the metadata collection state
|
|
126
|
+
setCurrentMetadataItem(null);
|
|
127
|
+
// Removed unused setPendingMetadataQuantity call
|
|
128
|
+
setIsCollectingMetadata(false);
|
|
129
|
+
};
|
|
130
|
+
|
|
90
131
|
if (loading) {
|
|
91
132
|
return <LoadingMessage>Loading...</LoadingMessage>;
|
|
92
133
|
}
|
|
@@ -141,9 +182,16 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
141
182
|
minWidth="600px"
|
|
142
183
|
height="auto"
|
|
143
184
|
type={RPGUIContainerTypes.Framed}
|
|
144
|
-
cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails']"
|
|
185
|
+
cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails'], .close-button"
|
|
145
186
|
>
|
|
146
|
-
{
|
|
187
|
+
{isCollectingMetadata && currentMetadataItem && currentMetadataItem.metadataType ? (
|
|
188
|
+
<MetadataCollector
|
|
189
|
+
metadataType={currentMetadataItem.metadataType}
|
|
190
|
+
config={currentMetadataItem.metadataConfig || {}}
|
|
191
|
+
onCollect={handleMetadataCollected}
|
|
192
|
+
onCancel={handleMetadataCancel}
|
|
193
|
+
/>
|
|
194
|
+
) : isCartOpen ? (
|
|
147
195
|
<CartView
|
|
148
196
|
cartItems={cartItems}
|
|
149
197
|
onRemoveFromCart={handleRemoveFromCart}
|
|
@@ -169,12 +217,19 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
169
217
|
) : (
|
|
170
218
|
<Container>
|
|
171
219
|
<TopBar>
|
|
220
|
+
<HistoryButton>
|
|
221
|
+
{onShowHistory && (
|
|
222
|
+
<CTAButton
|
|
223
|
+
icon={<FaHistory />}
|
|
224
|
+
label="History"
|
|
225
|
+
onClick={onShowHistory}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
</HistoryButton>
|
|
172
229
|
<CartButton>
|
|
173
230
|
<CTAButton
|
|
174
231
|
icon={<FaShoppingCart />}
|
|
175
|
-
label={`${getTotalItems()} items ($${getTotalPrice().toFixed(
|
|
176
|
-
2
|
|
177
|
-
)})`}
|
|
232
|
+
label={`${getTotalItems()} items ($${getTotalPrice().toFixed(2)})`}
|
|
178
233
|
onClick={openCart}
|
|
179
234
|
/>
|
|
180
235
|
</CartButton>
|
|
@@ -223,6 +278,7 @@ const Container = styled.div`
|
|
|
223
278
|
width: 100%;
|
|
224
279
|
height: 100%;
|
|
225
280
|
gap: 1rem;
|
|
281
|
+
position: relative;
|
|
226
282
|
`;
|
|
227
283
|
|
|
228
284
|
const TopBar = styled.div`
|
|
@@ -232,6 +288,12 @@ const TopBar = styled.div`
|
|
|
232
288
|
gap: 1rem;
|
|
233
289
|
padding: 0 1rem;
|
|
234
290
|
flex-shrink: 0;
|
|
291
|
+
margin-top: 0.5rem;
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
const HistoryButton = styled.div`
|
|
295
|
+
min-width: fit-content;
|
|
296
|
+
margin-right: auto;
|
|
235
297
|
`;
|
|
236
298
|
|
|
237
299
|
const CartButton = styled.div`
|