@rpg-engine/long-bow 0.8.4 → 0.8.5

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 (34) hide show
  1. package/dist/components/InformationCenter/sections/bestiary/BestiarySection.d.ts +1 -0
  2. package/dist/components/InformationCenter/sections/faq/FaqSection.d.ts +1 -0
  3. package/dist/components/InformationCenter/sections/items/ItemsSection.d.ts +1 -0
  4. package/dist/components/InternalTabs/InternalTabs.d.ts +2 -0
  5. package/dist/components/Store/InternalStoreTab.d.ts +15 -0
  6. package/dist/components/Store/Store.d.ts +3 -0
  7. package/dist/components/Store/StoreItemRow.d.ts +13 -0
  8. package/dist/components/Store/StoreTabContent.d.ts +14 -0
  9. package/dist/components/Store/StoreTypes.d.ts +19 -0
  10. package/dist/components/shared/PaginatedContent/PaginatedContent.d.ts +24 -0
  11. package/dist/long-bow.cjs.development.js +19 -7
  12. package/dist/long-bow.cjs.development.js.map +1 -1
  13. package/dist/long-bow.cjs.production.min.js +1 -1
  14. package/dist/long-bow.cjs.production.min.js.map +1 -1
  15. package/dist/long-bow.esm.js +19 -7
  16. package/dist/long-bow.esm.js.map +1 -1
  17. package/dist/stories/Features/store/Store.stories.d.ts +1 -0
  18. package/dist/utils/itemUtils.d.ts +8 -0
  19. package/package.json +1 -1
  20. package/src/components/InformationCenter/InformationCenter.tsx +15 -1
  21. package/src/components/InformationCenter/InformationCenterCell.tsx +7 -0
  22. package/src/components/InformationCenter/sections/bestiary/BestiarySection.tsx +31 -42
  23. package/src/components/InformationCenter/sections/faq/FaqSection.tsx +14 -34
  24. package/src/components/InformationCenter/sections/items/ItemsSection.tsx +40 -40
  25. package/src/components/InternalTabs/InternalTabs.tsx +9 -5
  26. package/src/components/Item/Inventory/itemContainerHelper.ts +13 -0
  27. package/src/components/Store/InternalStoreTab.tsx +142 -0
  28. package/src/components/Store/Store.tsx +192 -0
  29. package/src/components/Store/StoreItemRow.tsx +198 -0
  30. package/src/components/Store/StoreTabContent.tsx +46 -0
  31. package/src/components/Store/StoreTypes.ts +21 -0
  32. package/src/components/shared/PaginatedContent/PaginatedContent.tsx +182 -0
  33. package/src/stories/Features/store/Store.stories.tsx +102 -0
  34. package/src/utils/itemUtils.ts +36 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ interface IItemSprite {
2
+ key: string;
3
+ stackQty?: number;
4
+ texturePath: string;
5
+ isStackable?: boolean;
6
+ }
7
+ export declare const getItemTextureKeyPath: (item: IItemSprite, atlasJSON: Record<string, any>) => string;
8
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -54,6 +54,7 @@ export const InformationCenter: React.FC<IInformationCenterProps> = ({
54
54
  error,
55
55
  initialSearchQuery = '',
56
56
  }) => {
57
+ const [activeTab, setActiveTab] = useState('bestiary');
57
58
  const [
58
59
  selectedItem,
59
60
  setSelectedItem,
@@ -79,6 +80,7 @@ export const InformationCenter: React.FC<IInformationCenterProps> = ({
79
80
  entitiesAtlasJSON={entitiesAtlasJSON}
80
81
  entitiesAtlasIMG={entitiesAtlasIMG}
81
82
  initialSearchQuery={initialSearchQuery}
83
+ tabId="bestiary"
82
84
  />
83
85
  ),
84
86
  },
@@ -92,6 +94,7 @@ export const InformationCenter: React.FC<IInformationCenterProps> = ({
92
94
  itemsAtlasJSON={itemsAtlasJSON}
93
95
  itemsAtlasIMG={itemsAtlasIMG}
94
96
  initialSearchQuery={initialSearchQuery}
97
+ tabId="items"
95
98
  />
96
99
  ),
97
100
  },
@@ -102,6 +105,7 @@ export const InformationCenter: React.FC<IInformationCenterProps> = ({
102
105
  <FaqSection
103
106
  faqItems={faqItems}
104
107
  initialSearchQuery={initialSearchQuery}
108
+ tabId="faq"
105
109
  />
106
110
  ),
107
111
  },
@@ -112,6 +116,7 @@ export const InformationCenter: React.FC<IInformationCenterProps> = ({
112
116
  <TutorialsSection
113
117
  videoGuides={videoGuides}
114
118
  initialSearchQuery={initialSearchQuery}
119
+ tabId="tutorials"
115
120
  />
116
121
  ),
117
122
  },
@@ -119,7 +124,16 @@ export const InformationCenter: React.FC<IInformationCenterProps> = ({
119
124
 
120
125
  return (
121
126
  <Container>
122
- <InternalTabs tabs={tabs} activeTextColor="#000000" />
127
+ <InternalTabs
128
+ tabs={tabs}
129
+ activeTextColor="#000000"
130
+ activeTab={activeTab}
131
+ onTabChange={setActiveTab}
132
+ activeColor="#fef08a"
133
+ inactiveColor="#6b7280"
134
+ borderColor="#f59e0b"
135
+ hoverColor="#fef3c7"
136
+ />
123
137
  {selectedItem && (
124
138
  <InformationCenterItemDetails
125
139
  item={selectedItem}
@@ -56,9 +56,14 @@ const CellContainer = styled.div`
56
56
  display: flex;
57
57
  flex-direction: column;
58
58
  align-items: center;
59
+ justify-content: center;
59
60
  cursor: pointer;
60
61
  transition: background-color 0.2s ease;
62
+ width: 100%;
63
+ height: 100%;
64
+ min-width: 120px;
61
65
  min-height: 120px;
66
+ box-sizing: border-box;
62
67
 
63
68
  &:hover {
64
69
  background: rgba(0, 0, 0, 0.3);
@@ -75,6 +80,7 @@ const SpriteContainer = styled.div`
75
80
  position: relative;
76
81
  background: rgba(0, 0, 0, 0.3);
77
82
  border-radius: 4px;
83
+ flex-shrink: 0;
78
84
 
79
85
  .sprite-from-atlas-img {
80
86
  position: absolute;
@@ -93,4 +99,5 @@ const CellName = styled.h3`
93
99
  font-family: 'Press Start 2P', cursive;
94
100
  line-height: 1.2;
95
101
  word-break: break-word;
102
+ max-width: 100%;
96
103
  `;
@@ -1,8 +1,8 @@
1
1
  import React, { useState } from 'react';
2
2
  import styled from 'styled-components';
3
+ import { PaginatedContent } from '../../../shared/PaginatedContent/PaginatedContent';
3
4
  import { Portal } from '../../../shared/Portal/Portal';
4
5
  import { InformationCenterCell } from '../../InformationCenterCell';
5
- import { InformationCenterTabView } from '../../InformationCenterTabView';
6
6
  import { IInformationCenterNPC } from '../../InformationCenterTypes';
7
7
  import { InformationCenterNPCDetails } from './InformationCenterNPCDetails';
8
8
  import { InformationCenterNPCTooltip } from './InformationCenterNPCTooltip';
@@ -14,6 +14,7 @@ interface IBestiarySectionProps {
14
14
  entitiesAtlasJSON: Record<string, any>;
15
15
  entitiesAtlasIMG: string;
16
16
  initialSearchQuery: string;
17
+ tabId: string;
17
18
  }
18
19
 
19
20
  export const BestiarySection: React.FC<IBestiarySectionProps> = ({
@@ -23,10 +24,9 @@ export const BestiarySection: React.FC<IBestiarySectionProps> = ({
23
24
  entitiesAtlasJSON,
24
25
  entitiesAtlasIMG,
25
26
  initialSearchQuery,
27
+ tabId,
26
28
  }) => {
27
- const [bestiarySearchQuery, setBestiarySearchQuery] = useState(
28
- initialSearchQuery
29
- );
29
+ const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
30
30
  const [tooltipData, setTooltipData] = useState<{
31
31
  npc: IInformationCenterNPC;
32
32
  position: { x: number; y: number };
@@ -37,12 +37,6 @@ export const BestiarySection: React.FC<IBestiarySectionProps> = ({
37
37
  ] = useState<IInformationCenterNPC | null>(null);
38
38
  const [isTouchDevice] = useState('ontouchstart' in window);
39
39
 
40
- const filterItems = (items: IInformationCenterNPC[]) => {
41
- return items.filter(item =>
42
- item.name.toLowerCase().includes(bestiarySearchQuery.toLowerCase())
43
- );
44
- };
45
-
46
40
  const handleMouseEnter = (
47
41
  monster: IInformationCenterNPC,
48
42
  event: React.MouseEvent
@@ -93,35 +87,38 @@ export const BestiarySection: React.FC<IBestiarySectionProps> = ({
93
87
  setTooltipData(null);
94
88
  };
95
89
 
96
- const renderContent = (items: IInformationCenterNPC[]) => (
97
- <BestiaryGrid>
98
- {items.map(item => (
99
- <InformationCenterCell
100
- key={item.id}
101
- name={item.name}
102
- spriteKey={item.key}
103
- atlasJSON={entitiesAtlasJSON}
104
- atlasIMG={entitiesAtlasIMG}
105
- onClick={() => handleMonsterClick(item)}
106
- onMouseEnter={e => handleMouseEnter(item, e)}
107
- onMouseLeave={handleMouseLeave}
108
- onMouseMove={handleMouseMove}
109
- onTouchStart={e => handleTouchStart(item, e)}
110
- />
111
- ))}
112
- </BestiaryGrid>
90
+ const renderItem = (item: IInformationCenterNPC) => (
91
+ <InformationCenterCell
92
+ key={item.id}
93
+ name={item.name}
94
+ spriteKey={item.key}
95
+ atlasJSON={entitiesAtlasJSON}
96
+ atlasIMG={entitiesAtlasIMG}
97
+ onClick={() => handleMonsterClick(item)}
98
+ onMouseEnter={e => handleMouseEnter(item, e)}
99
+ onMouseLeave={handleMouseLeave}
100
+ onMouseMove={handleMouseMove}
101
+ onTouchStart={e => handleTouchStart(item, e)}
102
+ />
103
+ );
104
+
105
+ const filteredItems = bestiaryItems.filter(item =>
106
+ item.name.toLowerCase().includes(searchQuery.toLowerCase())
113
107
  );
114
108
 
115
109
  return (
116
110
  <>
117
- <InformationCenterTabView
118
- items={bestiaryItems}
119
- searchQuery={bestiarySearchQuery}
120
- onSearchChange={setBestiarySearchQuery}
121
- filterItems={filterItems}
122
- renderContent={renderContent}
123
- searchPlaceholder="Search monsters..."
111
+ <PaginatedContent<IInformationCenterNPC>
112
+ items={filteredItems}
113
+ renderItem={renderItem}
124
114
  emptyMessage="No monsters found"
115
+ tabId={tabId}
116
+ layout="grid"
117
+ searchOptions={{
118
+ value: searchQuery,
119
+ onChange: setSearchQuery,
120
+ placeholder: 'Search monsters...',
121
+ }}
125
122
  />
126
123
  {tooltipData && (
127
124
  <Portal>
@@ -154,14 +151,6 @@ export const BestiarySection: React.FC<IBestiarySectionProps> = ({
154
151
  );
155
152
  };
156
153
 
157
- const BestiaryGrid = styled.div`
158
- display: grid;
159
- grid-template-columns: repeat(4, 1fr);
160
- gap: 1rem;
161
- padding: 0 1rem;
162
- min-height: 300px;
163
- `;
164
-
165
154
  const TooltipWrapper = styled.div`
166
155
  position: fixed;
167
156
  z-index: 1000;
@@ -1,58 +1,38 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import styled from 'styled-components';
3
+ import { PaginatedContent } from '../../../shared/PaginatedContent/PaginatedContent';
3
4
  import { IFaqItem } from '../../InformationCenter';
4
- import { InformationCenterTabView } from '../../InformationCenterTabView';
5
5
 
6
6
  interface IFaqSectionProps {
7
7
  faqItems: IFaqItem[];
8
8
  initialSearchQuery: string;
9
+ tabId: string;
9
10
  }
10
11
 
11
12
  export const FaqSection: React.FC<IFaqSectionProps> = ({
12
13
  faqItems,
13
14
  initialSearchQuery,
15
+ tabId,
14
16
  }) => {
15
- const [faqSearchQuery, setFaqSearchQuery] = useState(initialSearchQuery);
16
-
17
- const filterItems = (items: IFaqItem[]) => {
18
- return items.filter(
19
- item =>
20
- item.question.toLowerCase().includes(faqSearchQuery.toLowerCase()) ||
21
- item.answer.toLowerCase().includes(faqSearchQuery.toLowerCase())
22
- );
23
- };
24
-
25
- const renderContent = (items: IFaqItem[]) => (
26
- <FaqContainer>
27
- {items.map(item => (
28
- <FaqItem key={item.id}>
29
- <Question>{item.question}</Question>
30
- <Answer>{item.answer}</Answer>
31
- </FaqItem>
32
- ))}
33
- </FaqContainer>
17
+ const renderItem = (item: IFaqItem) => (
18
+ <FaqItem>
19
+ <Question>{item.question}</Question>
20
+ <Answer>{item.answer}</Answer>
21
+ </FaqItem>
34
22
  );
35
23
 
36
24
  return (
37
- <InformationCenterTabView
25
+ <PaginatedContent<IFaqItem>
38
26
  items={faqItems}
39
- searchQuery={faqSearchQuery}
40
- onSearchChange={setFaqSearchQuery}
41
- filterItems={filterItems}
42
- renderContent={renderContent}
43
- searchPlaceholder="Search FAQ..."
27
+ renderItem={renderItem}
44
28
  emptyMessage="No FAQ items found"
29
+ tabId={tabId}
30
+ layout="list"
31
+ itemsPerPage={10}
45
32
  />
46
33
  );
47
34
  };
48
35
 
49
- const FaqContainer = styled.div`
50
- display: flex;
51
- flex-direction: column;
52
- gap: 1rem;
53
- padding: 0 1rem;
54
- `;
55
-
56
36
  const FaqItem = styled.div`
57
37
  background: rgba(0, 0, 0, 0.2);
58
38
  padding: 1rem;
@@ -2,8 +2,8 @@ import { ItemType } from '@rpg-engine/shared';
2
2
  import React, { useState } from 'react';
3
3
  import styled from 'styled-components';
4
4
  import { IOptionsProps } from '../../../Dropdown';
5
+ import { PaginatedContent } from '../../../shared/PaginatedContent/PaginatedContent';
5
6
  import { InformationCenterCell } from '../../InformationCenterCell';
6
- import { InformationCenterTabView } from '../../InformationCenterTabView';
7
7
  import {
8
8
  IInformationCenterItem,
9
9
  IInformationCenterNPC,
@@ -19,6 +19,7 @@ interface IItemsSectionProps {
19
19
  itemsAtlasJSON: Record<string, any>;
20
20
  itemsAtlasIMG: string;
21
21
  initialSearchQuery: string;
22
+ tabId: string;
22
23
  }
23
24
 
24
25
  export const ItemsSection: React.FC<IItemsSectionProps> = ({
@@ -27,8 +28,9 @@ export const ItemsSection: React.FC<IItemsSectionProps> = ({
27
28
  itemsAtlasJSON,
28
29
  itemsAtlasIMG,
29
30
  initialSearchQuery,
31
+ tabId,
30
32
  }) => {
31
- const [itemsSearchQuery, setItemsSearchQuery] = useState(initialSearchQuery);
33
+ const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
32
34
  const [selectedItemCategory, setSelectedItemCategory] = useState<string>(
33
35
  'all'
34
36
  );
@@ -48,14 +50,11 @@ export const ItemsSection: React.FC<IItemsSectionProps> = ({
48
50
  { id: 3, value: ItemType.Armor, option: 'Armor' },
49
51
  ];
50
52
 
51
- const filterItems = (items: IInformationCenterItem[]) => {
52
- return items.filter(
53
- item =>
54
- (selectedItemCategory === 'all' ||
55
- item.type === selectedItemCategory) &&
56
- item.name.toLowerCase().includes(itemsSearchQuery.toLowerCase())
57
- );
58
- };
53
+ const filteredItems = items.filter(
54
+ item =>
55
+ (selectedItemCategory === 'all' || item.type === selectedItemCategory) &&
56
+ item.name.toLowerCase().includes(searchQuery.toLowerCase())
57
+ );
59
58
 
60
59
  const getDroppedByNPCs = (
61
60
  itemId: string,
@@ -109,41 +108,40 @@ export const ItemsSection: React.FC<IItemsSectionProps> = ({
109
108
  setHoveredItem(null);
110
109
  };
111
110
 
112
- const renderContent = (items: IInformationCenterItem[]) => (
113
- <ItemsGrid>
114
- {items.map(item => (
115
- <InformationCenterCell
116
- key={item.key}
117
- name={item.name}
118
- spriteKey={item.texturePath}
119
- atlasJSON={itemsAtlasJSON}
120
- atlasIMG={itemsAtlasIMG}
121
- onMouseEnter={e => handleMouseEnter(e, item)}
122
- onMouseMove={handleMouseMove}
123
- onMouseLeave={handleMouseLeave}
124
- onTouchStart={e => handleTouchStart(e, item)}
125
- onClick={() => handleItemClick(item)}
126
- />
127
- ))}
128
- </ItemsGrid>
111
+ const renderItem = (item: IInformationCenterItem) => (
112
+ <InformationCenterCell
113
+ key={item.key}
114
+ name={item.name}
115
+ spriteKey={item.texturePath}
116
+ atlasJSON={itemsAtlasJSON}
117
+ atlasIMG={itemsAtlasIMG}
118
+ onMouseEnter={e => handleMouseEnter(e, item)}
119
+ onMouseMove={handleMouseMove}
120
+ onMouseLeave={handleMouseLeave}
121
+ onTouchStart={e => handleTouchStart(e, item)}
122
+ onClick={() => handleItemClick(item)}
123
+ />
129
124
  );
130
125
 
131
126
  return (
132
127
  <>
133
- <InformationCenterTabView
134
- items={items}
135
- searchQuery={itemsSearchQuery}
136
- onSearchChange={setItemsSearchQuery}
137
- filterItems={filterItems}
138
- renderContent={renderContent}
139
- searchPlaceholder="Search items..."
128
+ <PaginatedContent<IInformationCenterItem>
129
+ items={filteredItems}
130
+ renderItem={renderItem}
131
+ emptyMessage="No items found"
140
132
  filterOptions={{
141
133
  options: itemCategoryOptions,
142
134
  selectedOption: selectedItemCategory,
143
135
  onOptionChange: setSelectedItemCategory,
144
136
  }}
145
- emptyMessage="No items found"
137
+ searchOptions={{
138
+ value: searchQuery,
139
+ onChange: setSearchQuery,
140
+ placeholder: 'Search items...',
141
+ }}
146
142
  dependencies={[selectedItemCategory]}
143
+ tabId={tabId}
144
+ layout="grid"
147
145
  />
148
146
  {hoveredItem && (
149
147
  <TooltipWrapper
@@ -165,11 +163,13 @@ export const ItemsSection: React.FC<IItemsSectionProps> = ({
165
163
  );
166
164
  };
167
165
 
168
- const ItemsGrid = styled.div`
169
- display: grid;
170
- grid-template-columns: repeat(4, 1fr);
171
- gap: 0.5rem;
172
- padding: 0 1rem;
166
+ const StyledPaginatedContent = styled(PaginatedContent)`
167
+ .PaginatedContent-content {
168
+ display: grid;
169
+ grid-template-columns: repeat(4, 1fr);
170
+ gap: 0.5rem;
171
+ padding: 0 1rem;
172
+ }
173
173
  `;
174
174
 
175
175
  const TooltipWrapper = styled.div`
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  interface TabItem {
@@ -14,6 +14,8 @@ export interface TableTabProps {
14
14
  inactiveColor?: string;
15
15
  borderColor?: string;
16
16
  hoverColor?: string;
17
+ onTabChange?: (tabId: string) => void;
18
+ activeTab?: string;
17
19
  }
18
20
 
19
21
  export const InternalTabs: React.FC<TableTabProps> = ({
@@ -23,8 +25,10 @@ export const InternalTabs: React.FC<TableTabProps> = ({
23
25
  inactiveColor = '#6b7280',
24
26
  borderColor = '#f59e0b',
25
27
  hoverColor = '#fef3c7',
28
+ onTabChange,
29
+ activeTab: externalActiveTab,
26
30
  }) => {
27
- const [activeTab, setActiveTab] = useState(tabs[0].id);
31
+ const activeTabId = externalActiveTab ?? tabs[0].id;
28
32
 
29
33
  return (
30
34
  <TableWrapper>
@@ -32,20 +36,20 @@ export const InternalTabs: React.FC<TableTabProps> = ({
32
36
  {tabs.map(tab => (
33
37
  <TabButton
34
38
  key={tab.id}
35
- active={activeTab === tab.id}
39
+ active={activeTabId === tab.id}
36
40
  activeColor={activeColor}
37
41
  activeTextColor={activeTextColor}
38
42
  inactiveColor={inactiveColor}
39
43
  borderColor={borderColor}
40
44
  hoverColor={hoverColor}
41
- onClick={() => setActiveTab(tab.id)}
45
+ onClick={() => onTabChange?.(tab.id)}
42
46
  >
43
47
  {tab.title}
44
48
  </TabButton>
45
49
  ))}
46
50
  </TabHeader>
47
51
  <ContentWrapper>
48
- {tabs.find(tab => tab.id === activeTab)?.content}
52
+ {tabs.find(tab => tab.id === activeTabId)?.content}
49
53
  </ContentWrapper>
50
54
  </TableWrapper>
51
55
  );
@@ -182,5 +182,18 @@ export const generateContextMenu = (
182
182
  ];
183
183
  }
184
184
 
185
+ console.log(item.type === ItemType.Container)
186
+ if (item.type === ItemType.Container) {
187
+ const existInContextAction = contextActionMenu.some(
188
+ item => item.text === DepotSocketEvents.OpenContainer
189
+ || item.id === ItemSocketEvents.ContainerOpen
190
+ || item.text === 'Open'
191
+ );
192
+
193
+ if (!existInContextAction) {
194
+ contextActionMenu.push({ id: DepotSocketEvents.OpenContainer, text: 'Open' });
195
+ }
196
+ }
197
+
185
198
  return contextActionMenu;
186
199
  };
@@ -0,0 +1,142 @@
1
+ import { ItemType, UserAccountTypes } from '@rpg-engine/shared';
2
+ import React, { useMemo, useState } from 'react';
3
+ import styled from 'styled-components';
4
+ import { usePagination } from '../CraftBook/hooks/usePagination';
5
+ import { Pagination } from '../shared/Pagination/Pagination';
6
+ import { SearchBar } from '../shared/SearchBar/SearchBar';
7
+ import { StoreItemRow } from './StoreItemRow';
8
+ import { IStoreItem } from './StoreTypes';
9
+
10
+ const ITEMS_PER_PAGE = 4;
11
+
12
+ interface IInternalStoreTabProps {
13
+ items: IStoreItem[];
14
+ atlasJSON: Record<string, any>;
15
+ atlasIMG: string;
16
+ onPurchase: (item: IStoreItem, quantity: number) => void;
17
+ userGold: number;
18
+ userAccountType: UserAccountTypes;
19
+ type?: ItemType | 'premium' | 'all';
20
+ initialSearchQuery?: string;
21
+ }
22
+
23
+ export const InternalStoreTab: React.FC<IInternalStoreTabProps> = ({
24
+ items,
25
+ atlasJSON,
26
+ atlasIMG,
27
+ onPurchase,
28
+ userGold,
29
+ userAccountType,
30
+ type = 'all',
31
+ initialSearchQuery = '',
32
+ }) => {
33
+ const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
34
+
35
+ const filteredItems = useMemo(() => {
36
+ return items.filter(item => {
37
+ const matchesSearch = item.name
38
+ .toLowerCase()
39
+ .includes(searchQuery.toLowerCase());
40
+
41
+ if (!matchesSearch) return false;
42
+
43
+ switch (type) {
44
+ case ItemType.Weapon:
45
+ return item.type === ItemType.Weapon;
46
+ case ItemType.Consumable:
47
+ return item.type === ItemType.Consumable;
48
+ case 'premium':
49
+ return (
50
+ item.requiredAccountType && item.requiredAccountType.length > 0
51
+ );
52
+ default:
53
+ return true;
54
+ }
55
+ });
56
+ }, [items, searchQuery, type]);
57
+
58
+ const {
59
+ currentPage,
60
+ setCurrentPage,
61
+ paginatedItems,
62
+ totalPages,
63
+ } = usePagination({
64
+ items: filteredItems,
65
+ itemsPerPage: ITEMS_PER_PAGE,
66
+ dependencies: [searchQuery, type],
67
+ });
68
+
69
+ return (
70
+ <Container>
71
+ <SearchContainer>
72
+ <SearchBar
73
+ value={searchQuery}
74
+ onChange={setSearchQuery}
75
+ placeholder="Search items..."
76
+ />
77
+ </SearchContainer>
78
+
79
+ <StoreContent>
80
+ {paginatedItems.length > 0 ? (
81
+ paginatedItems.map(item => (
82
+ <StoreItemRow
83
+ key={item._id}
84
+ item={item}
85
+ atlasJSON={atlasJSON}
86
+ atlasIMG={atlasIMG}
87
+ onPurchase={onPurchase}
88
+ userGold={userGold}
89
+ userAccountType={userAccountType}
90
+ />
91
+ ))
92
+ ) : (
93
+ <EmptyMessage>No items found</EmptyMessage>
94
+ )}
95
+ </StoreContent>
96
+
97
+ <PaginationContainer>
98
+ <Pagination
99
+ currentPage={currentPage}
100
+ totalPages={Math.max(1, totalPages)}
101
+ onPageChange={setCurrentPage}
102
+ />
103
+ </PaginationContainer>
104
+ </Container>
105
+ );
106
+ };
107
+
108
+ const Container = styled.div`
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 1rem;
112
+ padding: 1rem;
113
+ min-height: 400px;
114
+ `;
115
+
116
+ const SearchContainer = styled.div`
117
+ width: 100%;
118
+ `;
119
+
120
+ const StoreContent = styled.div`
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 0.5rem;
124
+ flex: 1;
125
+ `;
126
+
127
+ const PaginationContainer = styled.div`
128
+ display: flex;
129
+ justify-content: center;
130
+ padding-top: 1rem;
131
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
132
+ `;
133
+
134
+ const EmptyMessage = styled.div`
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ padding: 2rem;
139
+ color: rgba(255, 255, 255, 0.5);
140
+ font-family: 'Press Start 2P', cursive;
141
+ font-size: 0.8rem;
142
+ `;