@rpg-engine/long-bow 0.8.7 → 0.8.9
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/InformationCenter/sections/bestiary/{BestiarySection.d.ts → InformationCenterBestiarySection.d.ts} +1 -1
- package/dist/components/InformationCenter/sections/faq/{FaqSection.d.ts → InformationCenterFaqSection.d.ts} +1 -1
- package/dist/components/InformationCenter/sections/items/{ItemsSection.d.ts → InformationCenterItemsSection.d.ts} +1 -1
- package/dist/components/InformationCenter/sections/tutorials/{TutorialsSection.d.ts → InformationCenterTutorialsSection.d.ts} +2 -1
- package/dist/components/Item/Inventory/ItemPropertyColorSelector.d.ts +10 -0
- package/dist/components/Item/Inventory/ItemPropertySimpleHandler.d.ts +10 -0
- package/dist/components/Store/CartView.d.ts +15 -0
- package/dist/components/Store/StoreItemDetails.d.ts +16 -0
- package/dist/components/Store/StoreItemRow.d.ts +1 -2
- package/dist/components/Store/StoreTypes.d.ts +33 -4
- package/dist/components/Store/hooks/useStoreCart.d.ts +14 -0
- package/dist/components/Store/sections/StoreItemsSection.d.ts +12 -0
- package/dist/components/Store/sections/StorePacksSection.d.ts +9 -0
- package/dist/components/shared/CTAButton/CTAButton.d.ts +13 -0
- package/dist/components/shared/Card/Card.d.ts +14 -0
- package/dist/components/shared/Ellipsis.d.ts +1 -1
- package/dist/components/shared/PaginatedContent/PaginatedContent.d.ts +3 -1
- package/dist/components/shared/ScrollableContent/ScrollableContent.d.ts +23 -0
- package/dist/components/shared/SearchBar/SearchBar.d.ts +2 -3
- package/dist/components/shared/SearchHeader/SearchHeader.d.ts +17 -0
- package/dist/components/shared/ShoppingCart/CartCard.d.ts +14 -0
- package/dist/components/shared/ShoppingCart/CartCardHorizontal.d.ts +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +105 -39
- 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 +105 -40
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/UI/buttonsAndInputs/CTAButton.stories.d.ts +18 -0
- package/dist/stories/UI/dropdownsAndSelectors/ItemPropertyColorSelector.stories.d.ts +3 -0
- package/package.json +3 -2
- package/src/components/InformationCenter/InformationCenter.tsx +8 -8
- package/src/components/InformationCenter/InformationCenterTabView.tsx +0 -1
- package/src/components/InformationCenter/sections/bestiary/{BestiarySection.tsx → InformationCenterBestiarySection.tsx} +2 -1
- package/src/components/InformationCenter/sections/faq/InformationCenterFaqSection.tsx +81 -0
- package/src/components/InformationCenter/sections/items/{ItemsSection.tsx → InformationCenterItemsSection.tsx} +2 -10
- package/src/components/InformationCenter/sections/tutorials/InformationCenterTutorialsSection.tsx +135 -0
- package/src/components/Item/Inventory/ItemPropertyColorSelector.tsx +75 -0
- package/src/components/Item/Inventory/ItemPropertySimpleHandler.tsx +26 -0
- package/src/components/Item/Inventory/itemContainerHelper.ts +10 -1
- package/src/components/Store/CartView.tsx +271 -0
- package/src/components/Store/Store.tsx +199 -96
- package/src/components/Store/StoreItemDetails.tsx +161 -0
- package/src/components/Store/StoreItemRow.tsx +24 -40
- package/src/components/Store/StoreTypes.ts +38 -4
- package/src/components/Store/hooks/useStoreCart.ts +121 -0
- package/src/components/Store/sections/StoreItemsSection.tsx +52 -0
- package/src/components/Store/sections/StorePacksSection.tsx +89 -0
- package/src/components/Store/sections/images/custom-skin.png +0 -0
- package/src/components/shared/CTAButton/CTAButton.tsx +127 -0
- package/src/components/shared/Card/Card.tsx +107 -0
- package/src/components/shared/Ellipsis.tsx +20 -22
- package/src/components/shared/PaginatedContent/PaginatedContent.tsx +48 -79
- package/src/components/shared/ScrollableContent/ScrollableContent.tsx +160 -0
- package/src/components/shared/SearchBar/SearchBar.tsx +43 -24
- package/src/components/shared/SearchHeader/SearchHeader.tsx +80 -0
- package/src/components/shared/ShoppingCart/CartCard.tsx +116 -0
- package/src/components/shared/ShoppingCart/CartCardHorizontal.tsx +120 -0
- package/src/components/shared/SpriteFromAtlas.tsx +2 -0
- package/src/index.tsx +1 -0
- package/src/stories/Features/store/Store.stories.tsx +54 -4
- package/src/stories/UI/buttonsAndInputs/CTAButton.stories.tsx +77 -0
- package/src/stories/UI/dropdownsAndSelectors/ItemPropertyColorSelector.stories.tsx +77 -0
- package/dist/components/Store/InternalStoreTab.d.ts +0 -15
- package/dist/components/Store/StoreTabContent.d.ts +0 -14
- package/src/components/InformationCenter/sections/faq/FaqSection.tsx +0 -51
- package/src/components/InformationCenter/sections/tutorials/TutorialsSection.tsx +0 -144
- package/src/components/Store/InternalStoreTab.tsx +0 -142
- package/src/components/Store/StoreTabContent.tsx +0 -46
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FaArrowLeft, FaCartPlus } from 'react-icons/fa';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
5
|
+
import { IItemPack, IStoreItem } from './StoreTypes';
|
|
6
|
+
|
|
7
|
+
interface IStoreItemDetailsProps {
|
|
8
|
+
item: IStoreItem | (IItemPack & { name: string; texturePath: string });
|
|
9
|
+
imageUrl: string | { src: string; default?: string };
|
|
10
|
+
onBack: () => void;
|
|
11
|
+
onAddToCart: (item: IStoreItem) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const StoreItemDetails: React.FC<IStoreItemDetailsProps> = ({
|
|
15
|
+
item,
|
|
16
|
+
onBack,
|
|
17
|
+
onAddToCart,
|
|
18
|
+
imageUrl,
|
|
19
|
+
}) => {
|
|
20
|
+
const getImageSrc = () => {
|
|
21
|
+
if (!imageUrl) return '/placeholder-thumbnail.png';
|
|
22
|
+
if (typeof imageUrl === 'string') return imageUrl;
|
|
23
|
+
return (
|
|
24
|
+
(imageUrl as { default?: string; src: string }).default ||
|
|
25
|
+
(imageUrl as { src: string }).src
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Container>
|
|
31
|
+
<Header>
|
|
32
|
+
<BackButton onClick={onBack}>
|
|
33
|
+
<FaArrowLeft />
|
|
34
|
+
<span>Back</span>
|
|
35
|
+
</BackButton>
|
|
36
|
+
</Header>
|
|
37
|
+
|
|
38
|
+
<Content>
|
|
39
|
+
<DetailsGrid>
|
|
40
|
+
<ItemIcon>
|
|
41
|
+
<img src={getImageSrc()} alt={item.name} />
|
|
42
|
+
</ItemIcon>
|
|
43
|
+
<ItemInfo>
|
|
44
|
+
<ItemName>{item.name}</ItemName>
|
|
45
|
+
<ItemPrice>
|
|
46
|
+
${'priceUSD' in item ? item.priceUSD : item.price}
|
|
47
|
+
</ItemPrice>
|
|
48
|
+
<Description>{item.description}</Description>
|
|
49
|
+
</ItemInfo>
|
|
50
|
+
</DetailsGrid>
|
|
51
|
+
|
|
52
|
+
<Actions>
|
|
53
|
+
<CTAButton
|
|
54
|
+
icon={<FaCartPlus />}
|
|
55
|
+
label="Add to Cart"
|
|
56
|
+
onClick={() => onAddToCart(item as IStoreItem)}
|
|
57
|
+
fullWidth
|
|
58
|
+
/>
|
|
59
|
+
</Actions>
|
|
60
|
+
</Content>
|
|
61
|
+
</Container>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const Container = styled.div`
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 1.5rem;
|
|
69
|
+
padding: 1.5rem;
|
|
70
|
+
height: 100%;
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
const Header = styled.div`
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 1rem;
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const BackButton = styled.button`
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 0.5rem;
|
|
83
|
+
background: none;
|
|
84
|
+
border: none;
|
|
85
|
+
color: #ffffff;
|
|
86
|
+
font-family: 'Press Start 2P', cursive;
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
padding: 0.5rem;
|
|
90
|
+
transition: opacity 0.2s;
|
|
91
|
+
|
|
92
|
+
&:hover {
|
|
93
|
+
opacity: 0.8;
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const Content = styled.div`
|
|
98
|
+
flex: 1;
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 2rem;
|
|
102
|
+
overflow-y: auto;
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const DetailsGrid = styled.div`
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: 280px 1fr;
|
|
108
|
+
gap: 2rem;
|
|
109
|
+
align-items: start;
|
|
110
|
+
|
|
111
|
+
@media (max-width: 768px) {
|
|
112
|
+
grid-template-columns: 1fr;
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const ItemIcon = styled.div`
|
|
117
|
+
width: 100%;
|
|
118
|
+
aspect-ratio: 1;
|
|
119
|
+
background: rgba(0, 0, 0, 0.2);
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
|
|
123
|
+
img {
|
|
124
|
+
width: 100%;
|
|
125
|
+
height: 100%;
|
|
126
|
+
object-fit: cover;
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
const ItemInfo = styled.div`
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
gap: 1rem;
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
const ItemName = styled.h2`
|
|
137
|
+
margin: 0;
|
|
138
|
+
font-family: 'Press Start 2P', cursive;
|
|
139
|
+
font-size: 1.25rem;
|
|
140
|
+
color: #fef08a;
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const ItemPrice = styled.div`
|
|
144
|
+
font-family: 'Press Start 2P', cursive;
|
|
145
|
+
font-size: 1rem;
|
|
146
|
+
color: #fef08a;
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
const Description = styled.p`
|
|
150
|
+
margin: 0;
|
|
151
|
+
font-family: 'Press Start 2P', cursive;
|
|
152
|
+
font-size: 0.875rem;
|
|
153
|
+
line-height: 1.6;
|
|
154
|
+
color: #ffffff;
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const Actions = styled.div`
|
|
158
|
+
margin-top: auto;
|
|
159
|
+
padding-top: 1rem;
|
|
160
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
161
|
+
`;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { UserAccountTypes } from '@rpg-engine/shared';
|
|
2
2
|
import React, { useState } from 'react';
|
|
3
|
+
import { FaCartPlus } from 'react-icons/fa';
|
|
3
4
|
import styled from 'styled-components';
|
|
4
5
|
import { SelectArrow } from '../Arrow/SelectArrow';
|
|
5
|
-
import {
|
|
6
|
+
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
6
7
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
7
8
|
import { IStoreItem } from './StoreTypes';
|
|
8
9
|
|
|
@@ -10,8 +11,7 @@ interface IStoreItemRowProps {
|
|
|
10
11
|
item: IStoreItem;
|
|
11
12
|
atlasJSON: Record<string, any>;
|
|
12
13
|
atlasIMG: string;
|
|
13
|
-
|
|
14
|
-
userGold: number;
|
|
14
|
+
onAddToCart: (item: IStoreItem, quantity: number) => void;
|
|
15
15
|
userAccountType: UserAccountTypes;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -19,50 +19,37 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
|
19
19
|
item,
|
|
20
20
|
atlasJSON,
|
|
21
21
|
atlasIMG,
|
|
22
|
-
|
|
23
|
-
userGold,
|
|
22
|
+
onAddToCart,
|
|
24
23
|
userAccountType,
|
|
25
24
|
}) => {
|
|
26
25
|
const [quantity, setQuantity] = useState(1);
|
|
27
26
|
|
|
28
27
|
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
29
28
|
const value = parseInt(e.target.value) || 1;
|
|
30
|
-
setQuantity(Math.min(Math.max(1, value),
|
|
29
|
+
setQuantity(Math.min(Math.max(1, value), 99));
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
const handleBlur = () => {
|
|
34
33
|
if (quantity < 1) setQuantity(1);
|
|
35
|
-
if (quantity >
|
|
34
|
+
if (quantity > 99) setQuantity(99);
|
|
36
35
|
};
|
|
37
36
|
|
|
38
|
-
const incrementQuantity = (
|
|
39
|
-
setQuantity(prev => Math.min(prev +
|
|
37
|
+
const incrementQuantity = () => {
|
|
38
|
+
setQuantity(prev => Math.min(prev + 1, 99));
|
|
40
39
|
};
|
|
41
40
|
|
|
42
|
-
const decrementQuantity = (
|
|
43
|
-
setQuantity(prev => Math.max(1, prev -
|
|
41
|
+
const decrementQuantity = () => {
|
|
42
|
+
setQuantity(prev => Math.max(1, prev - 1));
|
|
44
43
|
};
|
|
45
44
|
|
|
46
|
-
const canAfford = userGold >= item.price * quantity;
|
|
47
45
|
const hasRequiredAccount =
|
|
48
46
|
!item.requiredAccountType?.length ||
|
|
49
47
|
item.requiredAccountType.includes(userAccountType);
|
|
50
48
|
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<span>
|
|
56
|
-
{item.requiredAccountType
|
|
57
|
-
.map(type => {
|
|
58
|
-
const typeName = String(type).toLowerCase();
|
|
59
|
-
return typeName.includes('premium')
|
|
60
|
-
? typeName.replace('premium', '')
|
|
61
|
-
: typeName;
|
|
62
|
-
})
|
|
63
|
-
.join('/')}
|
|
64
|
-
</span>
|
|
65
|
-
);
|
|
49
|
+
const handleAddToCart = () => {
|
|
50
|
+
if (!hasRequiredAccount) return;
|
|
51
|
+
onAddToCart(item, quantity);
|
|
52
|
+
setQuantity(1); // Reset quantity after adding to cart
|
|
66
53
|
};
|
|
67
54
|
|
|
68
55
|
return (
|
|
@@ -81,16 +68,14 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
|
81
68
|
|
|
82
69
|
<ItemDetails>
|
|
83
70
|
<ItemName>{item.name}</ItemName>
|
|
84
|
-
<ItemPrice>
|
|
85
|
-
Price: {item.price} gold {renderAccountTypeIndicator()}
|
|
86
|
-
</ItemPrice>
|
|
71
|
+
<ItemPrice>${item.price}</ItemPrice>
|
|
87
72
|
</ItemDetails>
|
|
88
73
|
|
|
89
74
|
<Controls>
|
|
90
75
|
<ArrowsContainer>
|
|
91
76
|
<SelectArrow
|
|
92
77
|
direction="left"
|
|
93
|
-
onPointerDown={
|
|
78
|
+
onPointerDown={decrementQuantity}
|
|
94
79
|
size={24}
|
|
95
80
|
/>
|
|
96
81
|
|
|
@@ -100,24 +85,23 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
|
100
85
|
onChange={handleQuantityChange}
|
|
101
86
|
onBlur={handleBlur}
|
|
102
87
|
min={1}
|
|
103
|
-
max={
|
|
88
|
+
max={99}
|
|
104
89
|
className="rpgui-input"
|
|
105
90
|
/>
|
|
106
91
|
|
|
107
92
|
<SelectArrow
|
|
108
93
|
direction="right"
|
|
109
|
-
onPointerDown={
|
|
94
|
+
onPointerDown={incrementQuantity}
|
|
110
95
|
size={24}
|
|
111
96
|
/>
|
|
112
97
|
</ArrowsContainer>
|
|
113
98
|
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
onClick={
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
</Button>
|
|
99
|
+
<CTAButton
|
|
100
|
+
icon={<FaCartPlus />}
|
|
101
|
+
label="Add"
|
|
102
|
+
onClick={handleAddToCart}
|
|
103
|
+
disabled={!hasRequiredAccount}
|
|
104
|
+
/>
|
|
121
105
|
</Controls>
|
|
122
106
|
</ItemWrapper>
|
|
123
107
|
);
|
|
@@ -2,20 +2,54 @@ import { IItem, UserAccountTypes } from '@rpg-engine/shared';
|
|
|
2
2
|
|
|
3
3
|
export interface IStoreItem extends Omit<IItem, 'canSell'> {
|
|
4
4
|
price: number;
|
|
5
|
-
stock: number;
|
|
6
5
|
requiredAccountType?: UserAccountTypes[];
|
|
7
|
-
|
|
6
|
+
key: string;
|
|
7
|
+
name: string;
|
|
8
|
+
texturePath: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ICartItem {
|
|
12
|
+
item: IStoreItem;
|
|
13
|
+
quantity: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IItemPack {
|
|
17
|
+
key: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
priceUSD: number;
|
|
21
|
+
image: {
|
|
22
|
+
src: string;
|
|
23
|
+
default?: string;
|
|
24
|
+
};
|
|
8
25
|
}
|
|
9
26
|
|
|
10
27
|
export interface IStoreProps {
|
|
11
28
|
items: IStoreItem[];
|
|
29
|
+
packs?: IItemPack[];
|
|
12
30
|
atlasJSON: Record<string, any>;
|
|
13
31
|
atlasIMG: string;
|
|
14
|
-
onPurchase: (
|
|
15
|
-
userGold: number;
|
|
32
|
+
onPurchase: (purchase: IPurchase) => void;
|
|
16
33
|
userAccountType: UserAccountTypes;
|
|
17
34
|
loading?: boolean;
|
|
18
35
|
error?: string;
|
|
19
36
|
initialSearchQuery?: string;
|
|
20
37
|
onClose?: () => void;
|
|
21
38
|
}
|
|
39
|
+
|
|
40
|
+
export enum PurchaseType {
|
|
41
|
+
PremiumAccount = 'PremiumAccount',
|
|
42
|
+
Item = 'Item',
|
|
43
|
+
Pack = 'Pack',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface IPurchaseUnit {
|
|
47
|
+
purchaseKey: string;
|
|
48
|
+
qty: number;
|
|
49
|
+
type: PurchaseType;
|
|
50
|
+
name: string; // Adding name for better identification
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IPurchase {
|
|
54
|
+
purchases: IPurchaseUnit[];
|
|
55
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ICartItem,
|
|
4
|
+
IItemPack,
|
|
5
|
+
IPurchase,
|
|
6
|
+
IPurchaseUnit,
|
|
7
|
+
IStoreItem,
|
|
8
|
+
PurchaseType,
|
|
9
|
+
} from '../StoreTypes';
|
|
10
|
+
|
|
11
|
+
interface IUseStoreCart {
|
|
12
|
+
cartItems: ICartItem[];
|
|
13
|
+
isCartOpen: boolean;
|
|
14
|
+
handleAddToCart: (item: IStoreItem, quantity: number) => void;
|
|
15
|
+
handleRemoveFromCart: (itemKey: string) => void;
|
|
16
|
+
handlePurchase: (onPurchase: (purchase: IPurchase) => void) => void;
|
|
17
|
+
openCart: () => void;
|
|
18
|
+
closeCart: () => void;
|
|
19
|
+
getTotalItems: () => number;
|
|
20
|
+
getTotalPrice: () => number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useStoreCart = (): IUseStoreCart => {
|
|
24
|
+
const [cartItems, setCartItems] = useState<ICartItem[]>([]);
|
|
25
|
+
const [isCartOpen, setIsCartOpen] = useState(false);
|
|
26
|
+
const isMounted = useRef(true);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
return () => {
|
|
30
|
+
isMounted.current = false;
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const handleAddToCart = (item: IStoreItem, quantity: number) => {
|
|
35
|
+
setCartItems(prevItems => {
|
|
36
|
+
const existingItem = prevItems.find(
|
|
37
|
+
cartItem => cartItem.item.key === item.key
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (existingItem) {
|
|
41
|
+
return prevItems.map(cartItem =>
|
|
42
|
+
cartItem.item.key === item.key
|
|
43
|
+
? {
|
|
44
|
+
...cartItem,
|
|
45
|
+
quantity: Math.min(cartItem.quantity + quantity, 99),
|
|
46
|
+
}
|
|
47
|
+
: cartItem
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...prevItems, { item, quantity }];
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleRemoveFromCart = (itemKey: string) => {
|
|
56
|
+
setCartItems(prevItems =>
|
|
57
|
+
prevItems.filter(cartItem => cartItem.item.key !== itemKey)
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handlePurchase = (onPurchase: (purchase: IPurchase) => void) => {
|
|
62
|
+
const purchaseUnits: IPurchaseUnit[] = cartItems.map(cartItem => {
|
|
63
|
+
const isPack =
|
|
64
|
+
'priceUSD' in cartItem.item &&
|
|
65
|
+
typeof cartItem.item.priceUSD === 'number';
|
|
66
|
+
|
|
67
|
+
if (isPack) {
|
|
68
|
+
const packItem = cartItem.item as IStoreItem & IItemPack;
|
|
69
|
+
return {
|
|
70
|
+
purchaseKey: packItem.key,
|
|
71
|
+
qty: cartItem.quantity,
|
|
72
|
+
type: PurchaseType.Pack,
|
|
73
|
+
name: packItem.title,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
purchaseKey: cartItem.item.key,
|
|
79
|
+
qty: cartItem.quantity,
|
|
80
|
+
type: PurchaseType.Item,
|
|
81
|
+
name: cartItem.item.name,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const purchase: IPurchase = {
|
|
86
|
+
purchases: purchaseUnits,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
onPurchase(purchase);
|
|
90
|
+
|
|
91
|
+
if (isMounted.current) {
|
|
92
|
+
setCartItems([]);
|
|
93
|
+
setIsCartOpen(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const openCart = () => setIsCartOpen(true);
|
|
98
|
+
const closeCart = () => setIsCartOpen(false);
|
|
99
|
+
|
|
100
|
+
const getTotalItems = () =>
|
|
101
|
+
cartItems.reduce((sum, item) => sum + item.quantity, 0);
|
|
102
|
+
|
|
103
|
+
const getTotalPrice = () =>
|
|
104
|
+
Number(
|
|
105
|
+
cartItems
|
|
106
|
+
.reduce((sum, item) => sum + item.item.price * item.quantity, 0)
|
|
107
|
+
.toFixed(2)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
cartItems,
|
|
112
|
+
isCartOpen,
|
|
113
|
+
handleAddToCart,
|
|
114
|
+
handleRemoveFromCart,
|
|
115
|
+
handlePurchase,
|
|
116
|
+
openCart,
|
|
117
|
+
closeCart,
|
|
118
|
+
getTotalItems,
|
|
119
|
+
getTotalPrice,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { UserAccountTypes } from '@rpg-engine/shared';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
|
|
4
|
+
import { StoreItemRow } from '../StoreItemRow';
|
|
5
|
+
import { IStoreItem } from '../StoreTypes';
|
|
6
|
+
|
|
7
|
+
interface IStoreItemsSectionProps {
|
|
8
|
+
items: IStoreItem[];
|
|
9
|
+
onAddToCart: (item: IStoreItem, quantity: number) => void;
|
|
10
|
+
atlasJSON: Record<string, any>;
|
|
11
|
+
atlasIMG: string;
|
|
12
|
+
userAccountType?: UserAccountTypes;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
16
|
+
items,
|
|
17
|
+
onAddToCart,
|
|
18
|
+
atlasJSON,
|
|
19
|
+
atlasIMG,
|
|
20
|
+
userAccountType,
|
|
21
|
+
}) => {
|
|
22
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
23
|
+
|
|
24
|
+
const filteredItems = items.filter(item =>
|
|
25
|
+
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const renderStoreItem = (item: IStoreItem) => (
|
|
29
|
+
<StoreItemRow
|
|
30
|
+
item={item}
|
|
31
|
+
atlasJSON={atlasJSON}
|
|
32
|
+
atlasIMG={atlasIMG}
|
|
33
|
+
onAddToCart={onAddToCart}
|
|
34
|
+
userAccountType={userAccountType || UserAccountTypes.Free}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ScrollableContent
|
|
40
|
+
items={filteredItems}
|
|
41
|
+
renderItem={renderStoreItem}
|
|
42
|
+
emptyMessage="No items available."
|
|
43
|
+
searchOptions={{
|
|
44
|
+
value: searchQuery,
|
|
45
|
+
onChange: setSearchQuery,
|
|
46
|
+
placeholder: 'Search items...',
|
|
47
|
+
}}
|
|
48
|
+
layout="list"
|
|
49
|
+
maxHeight="400px"
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { FaCartPlus } from 'react-icons/fa';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { CTAButton } from '../../shared/CTAButton/CTAButton';
|
|
5
|
+
import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
|
|
6
|
+
import { ShoppingCardHorizontal } from '../../shared/ShoppingCart/CartCardHorizontal';
|
|
7
|
+
import { IItemPack } from '../StoreTypes';
|
|
8
|
+
|
|
9
|
+
interface IStorePacksSectionProps {
|
|
10
|
+
packs: IItemPack[];
|
|
11
|
+
onAddToCart: (pack: IItemPack) => void;
|
|
12
|
+
onSelectPack?: (pack: IItemPack) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
|
|
16
|
+
packs,
|
|
17
|
+
onAddToCart,
|
|
18
|
+
onSelectPack,
|
|
19
|
+
}) => {
|
|
20
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
21
|
+
|
|
22
|
+
const renderPackFooter = useCallback(
|
|
23
|
+
(pack: IItemPack) => (
|
|
24
|
+
<FooterContainer>
|
|
25
|
+
<Price>${pack.priceUSD}</Price>
|
|
26
|
+
<CTAButton
|
|
27
|
+
icon={<FaCartPlus />}
|
|
28
|
+
label="Add"
|
|
29
|
+
onClick={e => {
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
onAddToCart(pack);
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
</FooterContainer>
|
|
35
|
+
),
|
|
36
|
+
[onAddToCart]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const renderPack = useCallback(
|
|
40
|
+
(pack: IItemPack) => (
|
|
41
|
+
<ShoppingCardHorizontal
|
|
42
|
+
key={pack.key}
|
|
43
|
+
title={pack.title}
|
|
44
|
+
description={pack.description}
|
|
45
|
+
imageUrl={pack.image}
|
|
46
|
+
footer={renderPackFooter(pack)}
|
|
47
|
+
onClick={() => onSelectPack?.(pack)}
|
|
48
|
+
/>
|
|
49
|
+
),
|
|
50
|
+
[onSelectPack, renderPackFooter]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const filteredPacks = useMemo(
|
|
54
|
+
() =>
|
|
55
|
+
packs.filter(pack =>
|
|
56
|
+
pack.title.toLowerCase().includes(searchQuery.toLowerCase())
|
|
57
|
+
),
|
|
58
|
+
[packs, searchQuery]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ScrollableContent
|
|
63
|
+
items={filteredPacks}
|
|
64
|
+
renderItem={renderPack}
|
|
65
|
+
emptyMessage="No packs available."
|
|
66
|
+
searchOptions={{
|
|
67
|
+
value: searchQuery,
|
|
68
|
+
onChange: setSearchQuery,
|
|
69
|
+
placeholder: 'Search packs...',
|
|
70
|
+
}}
|
|
71
|
+
layout="grid"
|
|
72
|
+
gridColumns={2}
|
|
73
|
+
maxHeight="420px"
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const FooterContainer = styled.div`
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: space-between;
|
|
82
|
+
gap: 8px;
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const Price = styled.span`
|
|
86
|
+
font-family: 'Press Start 2P', cursive;
|
|
87
|
+
font-size: 0.6rem;
|
|
88
|
+
color: #fef08a;
|
|
89
|
+
`;
|
|
Binary file
|