@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.
Files changed (70) hide show
  1. package/dist/components/InformationCenter/sections/bestiary/{BestiarySection.d.ts → InformationCenterBestiarySection.d.ts} +1 -1
  2. package/dist/components/InformationCenter/sections/faq/{FaqSection.d.ts → InformationCenterFaqSection.d.ts} +1 -1
  3. package/dist/components/InformationCenter/sections/items/{ItemsSection.d.ts → InformationCenterItemsSection.d.ts} +1 -1
  4. package/dist/components/InformationCenter/sections/tutorials/{TutorialsSection.d.ts → InformationCenterTutorialsSection.d.ts} +2 -1
  5. package/dist/components/Item/Inventory/ItemPropertyColorSelector.d.ts +10 -0
  6. package/dist/components/Item/Inventory/ItemPropertySimpleHandler.d.ts +10 -0
  7. package/dist/components/Store/CartView.d.ts +15 -0
  8. package/dist/components/Store/StoreItemDetails.d.ts +16 -0
  9. package/dist/components/Store/StoreItemRow.d.ts +1 -2
  10. package/dist/components/Store/StoreTypes.d.ts +33 -4
  11. package/dist/components/Store/hooks/useStoreCart.d.ts +14 -0
  12. package/dist/components/Store/sections/StoreItemsSection.d.ts +12 -0
  13. package/dist/components/Store/sections/StorePacksSection.d.ts +9 -0
  14. package/dist/components/shared/CTAButton/CTAButton.d.ts +13 -0
  15. package/dist/components/shared/Card/Card.d.ts +14 -0
  16. package/dist/components/shared/Ellipsis.d.ts +1 -1
  17. package/dist/components/shared/PaginatedContent/PaginatedContent.d.ts +3 -1
  18. package/dist/components/shared/ScrollableContent/ScrollableContent.d.ts +23 -0
  19. package/dist/components/shared/SearchBar/SearchBar.d.ts +2 -3
  20. package/dist/components/shared/SearchHeader/SearchHeader.d.ts +17 -0
  21. package/dist/components/shared/ShoppingCart/CartCard.d.ts +14 -0
  22. package/dist/components/shared/ShoppingCart/CartCardHorizontal.d.ts +13 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/long-bow.cjs.development.js +105 -39
  25. package/dist/long-bow.cjs.development.js.map +1 -1
  26. package/dist/long-bow.cjs.production.min.js +1 -1
  27. package/dist/long-bow.cjs.production.min.js.map +1 -1
  28. package/dist/long-bow.esm.js +105 -40
  29. package/dist/long-bow.esm.js.map +1 -1
  30. package/dist/stories/UI/buttonsAndInputs/CTAButton.stories.d.ts +18 -0
  31. package/dist/stories/UI/dropdownsAndSelectors/ItemPropertyColorSelector.stories.d.ts +3 -0
  32. package/package.json +3 -2
  33. package/src/components/InformationCenter/InformationCenter.tsx +8 -8
  34. package/src/components/InformationCenter/InformationCenterTabView.tsx +0 -1
  35. package/src/components/InformationCenter/sections/bestiary/{BestiarySection.tsx → InformationCenterBestiarySection.tsx} +2 -1
  36. package/src/components/InformationCenter/sections/faq/InformationCenterFaqSection.tsx +81 -0
  37. package/src/components/InformationCenter/sections/items/{ItemsSection.tsx → InformationCenterItemsSection.tsx} +2 -10
  38. package/src/components/InformationCenter/sections/tutorials/InformationCenterTutorialsSection.tsx +135 -0
  39. package/src/components/Item/Inventory/ItemPropertyColorSelector.tsx +75 -0
  40. package/src/components/Item/Inventory/ItemPropertySimpleHandler.tsx +26 -0
  41. package/src/components/Item/Inventory/itemContainerHelper.ts +10 -1
  42. package/src/components/Store/CartView.tsx +271 -0
  43. package/src/components/Store/Store.tsx +199 -96
  44. package/src/components/Store/StoreItemDetails.tsx +161 -0
  45. package/src/components/Store/StoreItemRow.tsx +24 -40
  46. package/src/components/Store/StoreTypes.ts +38 -4
  47. package/src/components/Store/hooks/useStoreCart.ts +121 -0
  48. package/src/components/Store/sections/StoreItemsSection.tsx +52 -0
  49. package/src/components/Store/sections/StorePacksSection.tsx +89 -0
  50. package/src/components/Store/sections/images/custom-skin.png +0 -0
  51. package/src/components/shared/CTAButton/CTAButton.tsx +127 -0
  52. package/src/components/shared/Card/Card.tsx +107 -0
  53. package/src/components/shared/Ellipsis.tsx +20 -22
  54. package/src/components/shared/PaginatedContent/PaginatedContent.tsx +48 -79
  55. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +160 -0
  56. package/src/components/shared/SearchBar/SearchBar.tsx +43 -24
  57. package/src/components/shared/SearchHeader/SearchHeader.tsx +80 -0
  58. package/src/components/shared/ShoppingCart/CartCard.tsx +116 -0
  59. package/src/components/shared/ShoppingCart/CartCardHorizontal.tsx +120 -0
  60. package/src/components/shared/SpriteFromAtlas.tsx +2 -0
  61. package/src/index.tsx +1 -0
  62. package/src/stories/Features/store/Store.stories.tsx +54 -4
  63. package/src/stories/UI/buttonsAndInputs/CTAButton.stories.tsx +77 -0
  64. package/src/stories/UI/dropdownsAndSelectors/ItemPropertyColorSelector.stories.tsx +77 -0
  65. package/dist/components/Store/InternalStoreTab.d.ts +0 -15
  66. package/dist/components/Store/StoreTabContent.d.ts +0 -14
  67. package/src/components/InformationCenter/sections/faq/FaqSection.tsx +0 -51
  68. package/src/components/InformationCenter/sections/tutorials/TutorialsSection.tsx +0 -144
  69. package/src/components/Store/InternalStoreTab.tsx +0 -142
  70. 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 { Button, ButtonTypes } from '../Button';
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
- onPurchase: (item: IStoreItem, quantity: number) => void;
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
- onPurchase,
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), item.stock));
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 > item.stock) setQuantity(item.stock);
34
+ if (quantity > 99) setQuantity(99);
36
35
  };
37
36
 
38
- const incrementQuantity = (amount = 1) => {
39
- setQuantity(prev => Math.min(prev + amount, item.stock));
37
+ const incrementQuantity = () => {
38
+ setQuantity(prev => Math.min(prev + 1, 99));
40
39
  };
41
40
 
42
- const decrementQuantity = (amount = 1) => {
43
- setQuantity(prev => Math.max(1, prev - amount));
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 renderAccountTypeIndicator = () => {
52
- if (!item.requiredAccountType?.length) return null;
53
-
54
- return (
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={() => decrementQuantity()}
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={item.stock}
88
+ max={99}
104
89
  className="rpgui-input"
105
90
  />
106
91
 
107
92
  <SelectArrow
108
93
  direction="right"
109
- onPointerDown={() => incrementQuantity()}
94
+ onPointerDown={incrementQuantity}
110
95
  size={24}
111
96
  />
112
97
  </ArrowsContainer>
113
98
 
114
- <Button
115
- buttonType={ButtonTypes.RPGUIButton}
116
- disabled={!canAfford || !hasRequiredAccount}
117
- onClick={() => onPurchase(item, quantity)}
118
- >
119
- Purchase
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
- canSell: boolean;
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: (item: IStoreItem, quantity: number) => void;
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
+ `;