@rpg-engine/long-bow 0.8.65 → 0.8.67
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/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 +1572 -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 +1540 -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 +38 -5
- 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 +100 -2
|
@@ -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.67",
|
|
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
|
+
};
|
|
@@ -9,6 +9,7 @@ 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';
|
|
@@ -37,6 +38,8 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
37
38
|
getTotalPrice,
|
|
38
39
|
isCartOpen,
|
|
39
40
|
} = useStoreCart();
|
|
41
|
+
const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
|
|
42
|
+
const [currentMetadataItem, setCurrentMetadataItem] = useState<IStoreItem | null>(null);
|
|
40
43
|
|
|
41
44
|
const handleAddPackToCart = (pack: IItemPack) => {
|
|
42
45
|
const packItem: IStoreItem = {
|
|
@@ -87,6 +90,29 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
87
90
|
[items]
|
|
88
91
|
);
|
|
89
92
|
|
|
93
|
+
const handleMetadataCollected = (metadata: Record<string, any>) => {
|
|
94
|
+
if (currentMetadataItem && window.__metadataResolvers) {
|
|
95
|
+
// Resolve the promise in the useStoreMetadata hook
|
|
96
|
+
window.__metadataResolvers.resolve(metadata);
|
|
97
|
+
|
|
98
|
+
// Reset the metadata collection state
|
|
99
|
+
setCurrentMetadataItem(null);
|
|
100
|
+
// Removed unused setPendingMetadataQuantity call
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleMetadataCancel = () => {
|
|
105
|
+
if (window.__metadataResolvers) {
|
|
106
|
+
// Resolve with null to indicate cancellation
|
|
107
|
+
window.__metadataResolvers.resolve(null);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Reset the metadata collection state
|
|
111
|
+
setCurrentMetadataItem(null);
|
|
112
|
+
// Removed unused setPendingMetadataQuantity call
|
|
113
|
+
setIsCollectingMetadata(false);
|
|
114
|
+
};
|
|
115
|
+
|
|
90
116
|
if (loading) {
|
|
91
117
|
return <LoadingMessage>Loading...</LoadingMessage>;
|
|
92
118
|
}
|
|
@@ -141,9 +167,16 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
141
167
|
minWidth="600px"
|
|
142
168
|
height="auto"
|
|
143
169
|
type={RPGUIContainerTypes.Framed}
|
|
144
|
-
cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails']"
|
|
170
|
+
cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails'], .close-button"
|
|
145
171
|
>
|
|
146
|
-
{
|
|
172
|
+
{isCollectingMetadata && currentMetadataItem && currentMetadataItem.metadataType ? (
|
|
173
|
+
<MetadataCollector
|
|
174
|
+
metadataType={currentMetadataItem.metadataType}
|
|
175
|
+
config={currentMetadataItem.metadataConfig || {}}
|
|
176
|
+
onCollect={handleMetadataCollected}
|
|
177
|
+
onCancel={handleMetadataCancel}
|
|
178
|
+
/>
|
|
179
|
+
) : isCartOpen ? (
|
|
147
180
|
<CartView
|
|
148
181
|
cartItems={cartItems}
|
|
149
182
|
onRemoveFromCart={handleRemoveFromCart}
|
|
@@ -172,9 +205,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
172
205
|
<CartButton>
|
|
173
206
|
<CTAButton
|
|
174
207
|
icon={<FaShoppingCart />}
|
|
175
|
-
label={`${getTotalItems()} items ($${getTotalPrice().toFixed(
|
|
176
|
-
2
|
|
177
|
-
)})`}
|
|
208
|
+
label={`${getTotalItems()} items ($${getTotalPrice().toFixed(2)})`}
|
|
178
209
|
onClick={openCart}
|
|
179
210
|
/>
|
|
180
211
|
</CartButton>
|
|
@@ -223,6 +254,7 @@ const Container = styled.div`
|
|
|
223
254
|
width: 100%;
|
|
224
255
|
height: 100%;
|
|
225
256
|
gap: 1rem;
|
|
257
|
+
position: relative;
|
|
226
258
|
`;
|
|
227
259
|
|
|
228
260
|
const TopBar = styled.div`
|
|
@@ -232,6 +264,7 @@ const TopBar = styled.div`
|
|
|
232
264
|
gap: 1rem;
|
|
233
265
|
padding: 0 1rem;
|
|
234
266
|
flex-shrink: 0;
|
|
267
|
+
margin-top: 0.5rem;
|
|
235
268
|
`;
|
|
236
269
|
|
|
237
270
|
const CartButton = styled.div`
|