@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.
- package/dist/components/InformationCenter/sections/bestiary/BestiarySection.d.ts +1 -0
- package/dist/components/InformationCenter/sections/faq/FaqSection.d.ts +1 -0
- package/dist/components/InformationCenter/sections/items/ItemsSection.d.ts +1 -0
- package/dist/components/InternalTabs/InternalTabs.d.ts +2 -0
- package/dist/components/Store/InternalStoreTab.d.ts +15 -0
- package/dist/components/Store/Store.d.ts +3 -0
- package/dist/components/Store/StoreItemRow.d.ts +13 -0
- package/dist/components/Store/StoreTabContent.d.ts +14 -0
- package/dist/components/Store/StoreTypes.d.ts +19 -0
- package/dist/components/shared/PaginatedContent/PaginatedContent.d.ts +24 -0
- package/dist/long-bow.cjs.development.js +19 -7
- 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 +19 -7
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Features/store/Store.stories.d.ts +1 -0
- package/dist/utils/itemUtils.d.ts +8 -0
- package/package.json +1 -1
- package/src/components/InformationCenter/InformationCenter.tsx +15 -1
- package/src/components/InformationCenter/InformationCenterCell.tsx +7 -0
- package/src/components/InformationCenter/sections/bestiary/BestiarySection.tsx +31 -42
- package/src/components/InformationCenter/sections/faq/FaqSection.tsx +14 -34
- package/src/components/InformationCenter/sections/items/ItemsSection.tsx +40 -40
- package/src/components/InternalTabs/InternalTabs.tsx +9 -5
- package/src/components/Item/Inventory/itemContainerHelper.ts +13 -0
- package/src/components/Store/InternalStoreTab.tsx +142 -0
- package/src/components/Store/Store.tsx +192 -0
- package/src/components/Store/StoreItemRow.tsx +198 -0
- package/src/components/Store/StoreTabContent.tsx +46 -0
- package/src/components/Store/StoreTypes.ts +21 -0
- package/src/components/shared/PaginatedContent/PaginatedContent.tsx +182 -0
- package/src/stories/Features/store/Store.stories.tsx +102 -0
- package/src/utils/itemUtils.ts +36 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { ItemType } from '@rpg-engine/shared';
|
|
2
|
+
import React, { useMemo, useState } from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { uiColors } from '../../constants/uiColors';
|
|
5
|
+
import { DraggableContainer } from '../DraggableContainer';
|
|
6
|
+
import { InternalTabs } from '../InternalTabs/InternalTabs';
|
|
7
|
+
import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
|
|
8
|
+
import { SearchBar } from '../shared/SearchBar/SearchBar';
|
|
9
|
+
|
|
10
|
+
import { StoreTabContent } from './StoreTabContent';
|
|
11
|
+
import { IStoreItem, IStoreProps } from './StoreTypes';
|
|
12
|
+
|
|
13
|
+
export const Store: React.FC<IStoreProps> = ({
|
|
14
|
+
items,
|
|
15
|
+
atlasJSON,
|
|
16
|
+
atlasIMG,
|
|
17
|
+
onPurchase,
|
|
18
|
+
userGold,
|
|
19
|
+
userAccountType,
|
|
20
|
+
loading = false,
|
|
21
|
+
error,
|
|
22
|
+
initialSearchQuery = '',
|
|
23
|
+
onClose,
|
|
24
|
+
}) => {
|
|
25
|
+
const [activeTab, setActiveTab] = useState('all');
|
|
26
|
+
const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
|
|
27
|
+
|
|
28
|
+
const filterItems = (
|
|
29
|
+
itemsToFilter: IStoreItem[],
|
|
30
|
+
type: ItemType | 'premium' | 'all'
|
|
31
|
+
): IStoreItem[] => {
|
|
32
|
+
return itemsToFilter.filter(item => {
|
|
33
|
+
const matchesSearch = item.name
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.includes(searchQuery.toLowerCase());
|
|
36
|
+
|
|
37
|
+
if (!matchesSearch) return false;
|
|
38
|
+
|
|
39
|
+
switch (type) {
|
|
40
|
+
case ItemType.Weapon:
|
|
41
|
+
return item.type === ItemType.Weapon;
|
|
42
|
+
case ItemType.Consumable:
|
|
43
|
+
return item.type === ItemType.Consumable;
|
|
44
|
+
case 'premium':
|
|
45
|
+
return item.requiredAccountType?.length ?? 0 > 0;
|
|
46
|
+
default:
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Memoize filtered items for each tab to prevent unnecessary recalculations
|
|
53
|
+
const filteredItems = useMemo(
|
|
54
|
+
() => ({
|
|
55
|
+
all: filterItems(items, 'all'),
|
|
56
|
+
weapons: filterItems(items, ItemType.Weapon),
|
|
57
|
+
consumables: filterItems(items, ItemType.Consumable),
|
|
58
|
+
premium: filterItems(items, 'premium'),
|
|
59
|
+
}),
|
|
60
|
+
[items, searchQuery]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (loading) {
|
|
64
|
+
return <LoadingMessage>Loading...</LoadingMessage>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error) {
|
|
68
|
+
return <ErrorMessage>{error}</ErrorMessage>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tabs = [
|
|
72
|
+
{
|
|
73
|
+
id: 'all',
|
|
74
|
+
title: 'All',
|
|
75
|
+
content: (
|
|
76
|
+
<StoreTabContent
|
|
77
|
+
items={filteredItems.all}
|
|
78
|
+
atlasJSON={atlasJSON}
|
|
79
|
+
atlasIMG={atlasIMG}
|
|
80
|
+
onPurchase={onPurchase}
|
|
81
|
+
userGold={userGold}
|
|
82
|
+
userAccountType={userAccountType}
|
|
83
|
+
tabId="all"
|
|
84
|
+
/>
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'weapons',
|
|
89
|
+
title: 'Weapons',
|
|
90
|
+
content: (
|
|
91
|
+
<StoreTabContent
|
|
92
|
+
items={filteredItems.weapons}
|
|
93
|
+
atlasJSON={atlasJSON}
|
|
94
|
+
atlasIMG={atlasIMG}
|
|
95
|
+
onPurchase={onPurchase}
|
|
96
|
+
userGold={userGold}
|
|
97
|
+
userAccountType={userAccountType}
|
|
98
|
+
tabId="weapons"
|
|
99
|
+
/>
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'consumables',
|
|
104
|
+
title: 'Consumables',
|
|
105
|
+
content: (
|
|
106
|
+
<StoreTabContent
|
|
107
|
+
items={filteredItems.consumables}
|
|
108
|
+
atlasJSON={atlasJSON}
|
|
109
|
+
atlasIMG={atlasIMG}
|
|
110
|
+
onPurchase={onPurchase}
|
|
111
|
+
userGold={userGold}
|
|
112
|
+
userAccountType={userAccountType}
|
|
113
|
+
tabId="consumables"
|
|
114
|
+
/>
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'premium',
|
|
119
|
+
title: 'Premium',
|
|
120
|
+
content: (
|
|
121
|
+
<StoreTabContent
|
|
122
|
+
items={filteredItems.premium}
|
|
123
|
+
atlasJSON={atlasJSON}
|
|
124
|
+
atlasIMG={atlasIMG}
|
|
125
|
+
onPurchase={onPurchase}
|
|
126
|
+
userGold={userGold}
|
|
127
|
+
userAccountType={userAccountType}
|
|
128
|
+
tabId="premium"
|
|
129
|
+
/>
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<DraggableContainer
|
|
136
|
+
title="Store"
|
|
137
|
+
onCloseButton={onClose}
|
|
138
|
+
width="800px"
|
|
139
|
+
minWidth="600px"
|
|
140
|
+
type={RPGUIContainerTypes.Framed}
|
|
141
|
+
>
|
|
142
|
+
<Container>
|
|
143
|
+
<SearchContainer>
|
|
144
|
+
<StyledSearchBar
|
|
145
|
+
value={searchQuery}
|
|
146
|
+
onChange={setSearchQuery}
|
|
147
|
+
placeholder="Search items..."
|
|
148
|
+
/>
|
|
149
|
+
</SearchContainer>
|
|
150
|
+
<InternalTabs
|
|
151
|
+
tabs={tabs}
|
|
152
|
+
activeTextColor="#000000"
|
|
153
|
+
activeColor="#fef08a"
|
|
154
|
+
inactiveColor="#6b7280"
|
|
155
|
+
borderColor="#f59e0b"
|
|
156
|
+
hoverColor="#fef3c7"
|
|
157
|
+
activeTab={activeTab}
|
|
158
|
+
onTabChange={setActiveTab}
|
|
159
|
+
/>
|
|
160
|
+
</Container>
|
|
161
|
+
</DraggableContainer>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const Container = styled.div`
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
width: 100%;
|
|
169
|
+
min-height: 400px;
|
|
170
|
+
gap: 1rem;
|
|
171
|
+
padding: 1rem;
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
const SearchContainer = styled.div`
|
|
175
|
+
padding: 0 1rem;
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const StyledSearchBar = styled(SearchBar)`
|
|
179
|
+
width: 100%;
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
const LoadingMessage = styled.div`
|
|
183
|
+
text-align: center;
|
|
184
|
+
color: ${uiColors.white};
|
|
185
|
+
padding: 2rem;
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
const ErrorMessage = styled.div`
|
|
189
|
+
text-align: center;
|
|
190
|
+
color: ${uiColors.red};
|
|
191
|
+
padding: 2rem;
|
|
192
|
+
`;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { UserAccountTypes } from '@rpg-engine/shared';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { SelectArrow } from '../Arrow/SelectArrow';
|
|
5
|
+
import { Button, ButtonTypes } from '../Button';
|
|
6
|
+
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
7
|
+
import { IStoreItem } from './StoreTypes';
|
|
8
|
+
|
|
9
|
+
interface IStoreItemRowProps {
|
|
10
|
+
item: IStoreItem;
|
|
11
|
+
atlasJSON: Record<string, any>;
|
|
12
|
+
atlasIMG: string;
|
|
13
|
+
onPurchase: (item: IStoreItem, quantity: number) => void;
|
|
14
|
+
userGold: number;
|
|
15
|
+
userAccountType: UserAccountTypes;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
|
|
19
|
+
item,
|
|
20
|
+
atlasJSON,
|
|
21
|
+
atlasIMG,
|
|
22
|
+
onPurchase,
|
|
23
|
+
userGold,
|
|
24
|
+
userAccountType,
|
|
25
|
+
}) => {
|
|
26
|
+
const [quantity, setQuantity] = useState(1);
|
|
27
|
+
|
|
28
|
+
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
29
|
+
const value = parseInt(e.target.value) || 1;
|
|
30
|
+
setQuantity(Math.min(Math.max(1, value), item.stock));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleBlur = () => {
|
|
34
|
+
if (quantity < 1) setQuantity(1);
|
|
35
|
+
if (quantity > item.stock) setQuantity(item.stock);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const incrementQuantity = (amount = 1) => {
|
|
39
|
+
setQuantity(prev => Math.min(prev + amount, item.stock));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const decrementQuantity = (amount = 1) => {
|
|
43
|
+
setQuantity(prev => Math.max(1, prev - amount));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const canAfford = userGold >= item.price * quantity;
|
|
47
|
+
const hasRequiredAccount =
|
|
48
|
+
!item.requiredAccountType?.length ||
|
|
49
|
+
item.requiredAccountType.includes(userAccountType);
|
|
50
|
+
|
|
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
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ItemWrapper>
|
|
70
|
+
<ItemIconContainer>
|
|
71
|
+
<SpriteFromAtlas
|
|
72
|
+
atlasJSON={atlasJSON}
|
|
73
|
+
atlasIMG={atlasIMG}
|
|
74
|
+
spriteKey={item.texturePath}
|
|
75
|
+
width={32}
|
|
76
|
+
height={32}
|
|
77
|
+
imgScale={2}
|
|
78
|
+
centered
|
|
79
|
+
/>
|
|
80
|
+
</ItemIconContainer>
|
|
81
|
+
|
|
82
|
+
<ItemDetails>
|
|
83
|
+
<ItemName>{item.name}</ItemName>
|
|
84
|
+
<ItemPrice>
|
|
85
|
+
Price: {item.price} gold {renderAccountTypeIndicator()}
|
|
86
|
+
</ItemPrice>
|
|
87
|
+
</ItemDetails>
|
|
88
|
+
|
|
89
|
+
<Controls>
|
|
90
|
+
<ArrowsContainer>
|
|
91
|
+
<SelectArrow
|
|
92
|
+
direction="left"
|
|
93
|
+
onPointerDown={() => decrementQuantity()}
|
|
94
|
+
size={24}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<QuantityInput
|
|
98
|
+
type="number"
|
|
99
|
+
value={quantity}
|
|
100
|
+
onChange={handleQuantityChange}
|
|
101
|
+
onBlur={handleBlur}
|
|
102
|
+
min={1}
|
|
103
|
+
max={item.stock}
|
|
104
|
+
className="rpgui-input"
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<SelectArrow
|
|
108
|
+
direction="right"
|
|
109
|
+
onPointerDown={() => incrementQuantity()}
|
|
110
|
+
size={24}
|
|
111
|
+
/>
|
|
112
|
+
</ArrowsContainer>
|
|
113
|
+
|
|
114
|
+
<Button
|
|
115
|
+
buttonType={ButtonTypes.RPGUIButton}
|
|
116
|
+
disabled={!canAfford || !hasRequiredAccount}
|
|
117
|
+
onClick={() => onPurchase(item, quantity)}
|
|
118
|
+
>
|
|
119
|
+
Purchase
|
|
120
|
+
</Button>
|
|
121
|
+
</Controls>
|
|
122
|
+
</ItemWrapper>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const ItemWrapper = styled.div`
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 1rem;
|
|
130
|
+
padding: 1rem;
|
|
131
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
132
|
+
|
|
133
|
+
&:last-child {
|
|
134
|
+
border-bottom: none;
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const ItemIconContainer = styled.div`
|
|
139
|
+
width: 32px;
|
|
140
|
+
height: 32px;
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
border-radius: 4px;
|
|
145
|
+
padding: 4px;
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
const ItemDetails = styled.div`
|
|
149
|
+
flex: 1;
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 0.5rem;
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const ItemName = styled.div`
|
|
156
|
+
font-family: 'Press Start 2P', cursive;
|
|
157
|
+
font-size: 0.875rem;
|
|
158
|
+
color: #ffffff;
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const ItemPrice = styled.div`
|
|
162
|
+
font-family: 'Press Start 2P', cursive;
|
|
163
|
+
font-size: 0.75rem;
|
|
164
|
+
color: #fef08a;
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const Controls = styled.div`
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 1rem;
|
|
171
|
+
min-width: fit-content;
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
const ArrowsContainer = styled.div`
|
|
175
|
+
position: relative;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
width: 120px;
|
|
179
|
+
height: 42px;
|
|
180
|
+
justify-content: space-between;
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
const QuantityInput = styled.input`
|
|
184
|
+
width: 40px;
|
|
185
|
+
text-align: center;
|
|
186
|
+
margin: 0 auto;
|
|
187
|
+
font-size: 0.875rem;
|
|
188
|
+
background: rgba(0, 0, 0, 0.2);
|
|
189
|
+
color: #ffffff;
|
|
190
|
+
border: none;
|
|
191
|
+
padding: 0.25rem;
|
|
192
|
+
|
|
193
|
+
&::-webkit-inner-spin-button,
|
|
194
|
+
&::-webkit-outer-spin-button {
|
|
195
|
+
-webkit-appearance: none;
|
|
196
|
+
margin: 0;
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { UserAccountTypes } from '@rpg-engine/shared';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { PaginatedContent } from '../shared/PaginatedContent/PaginatedContent';
|
|
4
|
+
import { StoreItemRow } from './StoreItemRow';
|
|
5
|
+
import { IStoreItem } from './StoreTypes';
|
|
6
|
+
|
|
7
|
+
interface IStoreTabContentProps {
|
|
8
|
+
items: IStoreItem[];
|
|
9
|
+
atlasJSON: Record<string, any>;
|
|
10
|
+
atlasIMG: string;
|
|
11
|
+
onPurchase: (item: IStoreItem, quantity: number) => void;
|
|
12
|
+
userGold: number;
|
|
13
|
+
userAccountType: UserAccountTypes;
|
|
14
|
+
tabId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const StoreTabContent: React.FC<IStoreTabContentProps> = ({
|
|
18
|
+
items,
|
|
19
|
+
atlasJSON,
|
|
20
|
+
atlasIMG,
|
|
21
|
+
onPurchase,
|
|
22
|
+
userGold,
|
|
23
|
+
userAccountType,
|
|
24
|
+
tabId,
|
|
25
|
+
}) => {
|
|
26
|
+
const renderItem = (item: IStoreItem) => (
|
|
27
|
+
<StoreItemRow
|
|
28
|
+
key={item._id}
|
|
29
|
+
item={item}
|
|
30
|
+
atlasJSON={atlasJSON}
|
|
31
|
+
atlasIMG={atlasIMG}
|
|
32
|
+
onPurchase={onPurchase}
|
|
33
|
+
userGold={userGold}
|
|
34
|
+
userAccountType={userAccountType}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<PaginatedContent
|
|
40
|
+
items={items}
|
|
41
|
+
renderItem={renderItem}
|
|
42
|
+
emptyMessage="No items found"
|
|
43
|
+
tabId={tabId}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IItem, UserAccountTypes } from '@rpg-engine/shared';
|
|
2
|
+
|
|
3
|
+
export interface IStoreItem extends Omit<IItem, 'canSell'> {
|
|
4
|
+
price: number;
|
|
5
|
+
stock: number;
|
|
6
|
+
requiredAccountType?: UserAccountTypes[];
|
|
7
|
+
canSell: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IStoreProps {
|
|
11
|
+
items: IStoreItem[];
|
|
12
|
+
atlasJSON: Record<string, any>;
|
|
13
|
+
atlasIMG: string;
|
|
14
|
+
onPurchase: (item: IStoreItem, quantity: number) => void;
|
|
15
|
+
userGold: number;
|
|
16
|
+
userAccountType: UserAccountTypes;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
initialSearchQuery?: string;
|
|
20
|
+
onClose?: () => void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { usePagination } from '../../CraftBook/hooks/usePagination';
|
|
4
|
+
import { Dropdown, IOptionsProps } from '../../Dropdown';
|
|
5
|
+
import { Pagination } from '../Pagination/Pagination';
|
|
6
|
+
import { SearchBar } from '../SearchBar/SearchBar';
|
|
7
|
+
|
|
8
|
+
interface IPaginatedContentProps<T> {
|
|
9
|
+
items: T[];
|
|
10
|
+
itemsPerPage?: number;
|
|
11
|
+
renderItem: (item: T) => React.ReactNode;
|
|
12
|
+
emptyMessage?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
filterOptions?: {
|
|
15
|
+
options: IOptionsProps[];
|
|
16
|
+
selectedOption: string;
|
|
17
|
+
onOptionChange: (value: string) => void;
|
|
18
|
+
};
|
|
19
|
+
searchOptions?: {
|
|
20
|
+
value: string;
|
|
21
|
+
onChange: (value: string) => void;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
};
|
|
24
|
+
dependencies?: any[];
|
|
25
|
+
tabId?: string;
|
|
26
|
+
layout?: 'grid' | 'list';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PaginatedContent = <T extends unknown>({
|
|
30
|
+
items,
|
|
31
|
+
itemsPerPage = 5,
|
|
32
|
+
renderItem,
|
|
33
|
+
emptyMessage = 'No items found',
|
|
34
|
+
className,
|
|
35
|
+
filterOptions,
|
|
36
|
+
searchOptions,
|
|
37
|
+
dependencies = [],
|
|
38
|
+
tabId,
|
|
39
|
+
layout = 'list',
|
|
40
|
+
}: IPaginatedContentProps<T>): React.ReactElement => {
|
|
41
|
+
const {
|
|
42
|
+
currentPage,
|
|
43
|
+
setCurrentPage,
|
|
44
|
+
paginatedItems,
|
|
45
|
+
totalPages,
|
|
46
|
+
} = usePagination({
|
|
47
|
+
items,
|
|
48
|
+
itemsPerPage,
|
|
49
|
+
dependencies: [...dependencies, tabId],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
return <EmptyMessage>{emptyMessage}</EmptyMessage>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Container className={className}>
|
|
58
|
+
{(searchOptions || filterOptions) && (
|
|
59
|
+
<HeaderContainer>
|
|
60
|
+
<HeaderContent>
|
|
61
|
+
{searchOptions && (
|
|
62
|
+
<SearchContainer>
|
|
63
|
+
<StyledSearchBar
|
|
64
|
+
value={searchOptions.value}
|
|
65
|
+
onChange={searchOptions.onChange}
|
|
66
|
+
placeholder={searchOptions.placeholder || 'Search...'}
|
|
67
|
+
/>
|
|
68
|
+
</SearchContainer>
|
|
69
|
+
)}
|
|
70
|
+
{filterOptions && (
|
|
71
|
+
<FilterContainer>
|
|
72
|
+
<StyledDropdown
|
|
73
|
+
options={filterOptions.options}
|
|
74
|
+
onChange={filterOptions.onOptionChange}
|
|
75
|
+
width="200px"
|
|
76
|
+
/>
|
|
77
|
+
</FilterContainer>
|
|
78
|
+
)}
|
|
79
|
+
</HeaderContent>
|
|
80
|
+
</HeaderContainer>
|
|
81
|
+
)}
|
|
82
|
+
<Content className={`PaginatedContent-content ${layout}`}>
|
|
83
|
+
{paginatedItems.map((item, index) => (
|
|
84
|
+
<div key={index} className="PaginatedContent-item">
|
|
85
|
+
{renderItem(item)}
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</Content>
|
|
89
|
+
<PaginationContainer className="PaginatedContent-pagination">
|
|
90
|
+
<Pagination
|
|
91
|
+
currentPage={currentPage}
|
|
92
|
+
totalPages={totalPages}
|
|
93
|
+
onPageChange={setCurrentPage}
|
|
94
|
+
/>
|
|
95
|
+
</PaginationContainer>
|
|
96
|
+
</Container>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const Container = styled.div`
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
gap: 1rem;
|
|
104
|
+
min-height: 400px;
|
|
105
|
+
width: 100%;
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
const HeaderContainer = styled.div`
|
|
109
|
+
padding: 0 1rem;
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const HeaderContent = styled.div`
|
|
113
|
+
display: flex;
|
|
114
|
+
justify-content: space-between;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 1rem;
|
|
117
|
+
background: rgba(0, 0, 0, 0.2);
|
|
118
|
+
padding: 1rem;
|
|
119
|
+
border-radius: 4px;
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const SearchContainer = styled.div`
|
|
123
|
+
flex: 1;
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
const FilterContainer = styled.div`
|
|
127
|
+
display: flex;
|
|
128
|
+
justify-content: flex-end;
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
const StyledSearchBar = styled(SearchBar)`
|
|
132
|
+
width: 100%;
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
const Content = styled.div`
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 0.5rem;
|
|
139
|
+
flex: 1;
|
|
140
|
+
padding: 1rem;
|
|
141
|
+
min-height: 200px;
|
|
142
|
+
overflow-y: auto;
|
|
143
|
+
|
|
144
|
+
&.grid {
|
|
145
|
+
display: grid;
|
|
146
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
147
|
+
gap: 1rem;
|
|
148
|
+
|
|
149
|
+
.PaginatedContent-item {
|
|
150
|
+
aspect-ratio: 1;
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&.list {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
gap: 0.5rem;
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
const PaginationContainer = styled.div`
|
|
165
|
+
display: flex;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
padding: 1rem;
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
const EmptyMessage = styled.div`
|
|
171
|
+
text-align: center;
|
|
172
|
+
color: #9ca3af;
|
|
173
|
+
padding: 2rem;
|
|
174
|
+
flex: 1;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const StyledDropdown = styled(Dropdown)`
|
|
181
|
+
min-width: 150px;
|
|
182
|
+
`;
|