@rpg-engine/long-bow 0.7.90 → 0.7.92
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/CraftBook/CraftingTooltip.d.ts +13 -0
- package/dist/components/CraftBook/components/CraftBookHeader.d.ts +9 -0
- package/dist/components/CraftBook/components/CraftBookPagination.d.ts +0 -0
- package/dist/components/CraftBook/components/CraftBookSearch.d.ts +0 -0
- package/dist/components/CraftBook/hooks/useCraftBookFilters.d.ts +9 -0
- package/dist/components/CraftBook/hooks/useFilteredItems.d.ts +9 -0
- package/dist/components/CraftBook/hooks/usePagination.d.ts +13 -0
- package/dist/components/CraftBook/hooks/useResponsiveSize.d.ts +6 -0
- package/dist/components/CraftBook/utils/modifyString.d.ts +1 -0
- package/dist/components/shared/Pagination/Pagination.d.ts +9 -0
- package/dist/components/shared/SearchBar/SearchBar.d.ts +10 -0
- package/dist/hooks/useLocalStorage.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +464 -289
- 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 +464 -290
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Features/craftbook/CraftBook.stories.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/CraftBook/CraftBook.tsx +306 -121
- package/src/components/CraftBook/CraftingRecipe.tsx +97 -97
- package/src/components/CraftBook/CraftingTooltip.tsx +137 -0
- package/src/components/CraftBook/components/CraftBookHeader.tsx +81 -0
- package/src/components/CraftBook/components/CraftBookPagination.tsx +1 -0
- package/src/components/CraftBook/components/CraftBookSearch.tsx +1 -0
- package/src/components/CraftBook/hooks/useCraftBookFilters.ts +39 -0
- package/src/components/CraftBook/hooks/useFilteredItems.ts +39 -0
- package/src/components/CraftBook/hooks/usePagination.ts +39 -0
- package/src/components/CraftBook/hooks/useResponsiveSize.ts +50 -0
- package/src/components/CraftBook/utils/modifyString.ts +11 -0
- package/src/components/shared/Pagination/Pagination.tsx +69 -0
- package/src/components/shared/SearchBar/SearchBar.tsx +52 -0
- package/src/hooks/useLocalStorage.ts +44 -0
- package/src/stories/Features/craftbook/CraftBook.stories.tsx +41 -1
|
@@ -4,12 +4,13 @@ import {
|
|
|
4
4
|
IItemContainer,
|
|
5
5
|
ISkill,
|
|
6
6
|
} from '@rpg-engine/shared';
|
|
7
|
-
import React from 'react';
|
|
7
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
8
9
|
import styled from 'styled-components';
|
|
9
|
-
import { uiColors } from '../../constants/uiColors';
|
|
10
|
-
import { countItemFromInventory } from '../../libs/itemCounter';
|
|
11
10
|
import { ItemInfoWrapper } from '../Item/Cards/ItemInfoWrapper';
|
|
12
11
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
12
|
+
import { CraftingTooltip } from './CraftingTooltip';
|
|
13
|
+
import { modifyString } from './utils/modifyString';
|
|
13
14
|
|
|
14
15
|
interface ICraftingRecipeProps {
|
|
15
16
|
atlasJSON: any;
|
|
@@ -23,6 +24,44 @@ interface ICraftingRecipeProps {
|
|
|
23
24
|
skills?: ISkill | null;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
const RadioOptionsWrapper = styled.div`
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: stretch;
|
|
30
|
+
margin-bottom: 0.5rem;
|
|
31
|
+
padding: 8px;
|
|
32
|
+
position: relative;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const SpriteAtlasWrapper = styled.div`
|
|
37
|
+
margin-right: 40px;
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
width: 32px;
|
|
40
|
+
height: 32px;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const MainContent = styled.div`
|
|
47
|
+
flex: 1;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const ItemHeader = styled.div`
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
margin-bottom: 4px;
|
|
56
|
+
|
|
57
|
+
label {
|
|
58
|
+
font-size: 0.9rem;
|
|
59
|
+
font-weight: bold;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
26
65
|
export const CraftingRecipe: React.FC<ICraftingRecipeProps> = ({
|
|
27
66
|
atlasIMG,
|
|
28
67
|
atlasJSON,
|
|
@@ -34,32 +73,41 @@ export const CraftingRecipe: React.FC<ICraftingRecipeProps> = ({
|
|
|
34
73
|
inventory,
|
|
35
74
|
skills,
|
|
36
75
|
}) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
77
|
+
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
|
78
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
79
|
+
|
|
80
|
+
const isSelected = selectedCraftItemKey === recipe.key;
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
84
|
+
if (
|
|
85
|
+
wrapperRef.current &&
|
|
86
|
+
!wrapperRef.current.contains(event.target as Node)
|
|
87
|
+
) {
|
|
88
|
+
setShowTooltip(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
93
|
+
return () => {
|
|
94
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const handleClick = () => {
|
|
99
|
+
if (wrapperRef.current) {
|
|
100
|
+
const rect = wrapperRef.current.getBoundingClientRect();
|
|
101
|
+
setTooltipPosition({
|
|
102
|
+
x: rect.right + 10,
|
|
103
|
+
y: rect.top,
|
|
104
|
+
});
|
|
105
|
+
setShowTooltip(true);
|
|
106
|
+
}
|
|
54
107
|
};
|
|
55
108
|
|
|
56
|
-
const levelInSkill =
|
|
57
|
-
(skills?.[
|
|
58
|
-
(recipe?.minCraftingRequirements?.[0] ?? '') as keyof ISkill
|
|
59
|
-
] as any)?.level ?? 1;
|
|
60
|
-
|
|
61
109
|
return (
|
|
62
|
-
<RadioOptionsWrapper>
|
|
110
|
+
<RadioOptionsWrapper ref={wrapperRef} onClick={handleClick}>
|
|
63
111
|
<SpriteAtlasWrapper>
|
|
64
112
|
<ItemInfoWrapper
|
|
65
113
|
item={recipe}
|
|
@@ -77,85 +125,37 @@ export const CraftingRecipe: React.FC<ICraftingRecipeProps> = ({
|
|
|
77
125
|
/>
|
|
78
126
|
</ItemInfoWrapper>
|
|
79
127
|
</SpriteAtlasWrapper>
|
|
80
|
-
|
|
81
|
-
|
|
128
|
+
|
|
129
|
+
<MainContent>
|
|
130
|
+
<ItemHeader
|
|
131
|
+
onPointerUp={recipe.canCraft ? handleRecipeSelect : undefined}
|
|
132
|
+
>
|
|
82
133
|
<input
|
|
83
134
|
className="rpgui-radio"
|
|
84
135
|
type="radio"
|
|
85
136
|
value={recipe.name}
|
|
86
137
|
name="test"
|
|
87
138
|
disabled={!recipe.canCraft}
|
|
88
|
-
checked={
|
|
139
|
+
checked={isSelected}
|
|
89
140
|
onChange={handleRecipeSelect}
|
|
90
141
|
/>
|
|
91
|
-
<label
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<SpriteFromAtlas
|
|
109
|
-
atlasIMG={atlasIMG}
|
|
110
|
-
atlasJSON={atlasJSON}
|
|
111
|
-
spriteKey={ingredient.texturePath}
|
|
112
|
-
imgScale={1.2}
|
|
113
|
-
/>
|
|
114
|
-
<Ingredient isQuantityOk={ingredient.qty <= itemQtyInInventory}>
|
|
115
|
-
{modifyString(ingredient.key)} x{ingredient.qty} (
|
|
116
|
-
{itemQtyInInventory})
|
|
117
|
-
</Ingredient>
|
|
118
|
-
</Recipe>
|
|
119
|
-
);
|
|
120
|
-
})}
|
|
121
|
-
</div>
|
|
142
|
+
<label>{modifyString(recipe.name)}</label>
|
|
143
|
+
</ItemHeader>
|
|
144
|
+
</MainContent>
|
|
145
|
+
|
|
146
|
+
{showTooltip &&
|
|
147
|
+
createPortal(
|
|
148
|
+
<CraftingTooltip
|
|
149
|
+
x={tooltipPosition.x}
|
|
150
|
+
y={tooltipPosition.y}
|
|
151
|
+
recipe={recipe}
|
|
152
|
+
inventory={inventory}
|
|
153
|
+
skills={skills}
|
|
154
|
+
atlasIMG={atlasIMG}
|
|
155
|
+
atlasJSON={atlasJSON}
|
|
156
|
+
/>,
|
|
157
|
+
document.body
|
|
158
|
+
)}
|
|
122
159
|
</RadioOptionsWrapper>
|
|
123
160
|
);
|
|
124
161
|
};
|
|
125
|
-
|
|
126
|
-
const Ingredient = styled.p<{ isQuantityOk: boolean }>`
|
|
127
|
-
margin: 0;
|
|
128
|
-
margin-left: 14px;
|
|
129
|
-
color: ${({ isQuantityOk }) =>
|
|
130
|
-
isQuantityOk ? uiColors.lightGreen : uiColors.lightGray} !important;
|
|
131
|
-
`;
|
|
132
|
-
|
|
133
|
-
const Recipe = styled.div`
|
|
134
|
-
font-size: 0.6rem;
|
|
135
|
-
margin-bottom: 3px;
|
|
136
|
-
display: flex;
|
|
137
|
-
align-items: center;
|
|
138
|
-
margin-left: 4px;
|
|
139
|
-
|
|
140
|
-
.sprite-from-atlas-img {
|
|
141
|
-
top: 0px;
|
|
142
|
-
left: 0px;
|
|
143
|
-
}
|
|
144
|
-
`;
|
|
145
|
-
|
|
146
|
-
const SpriteAtlasWrapper = styled.div`
|
|
147
|
-
margin-right: 40px;
|
|
148
|
-
`;
|
|
149
|
-
|
|
150
|
-
const RadioOptionsWrapper = styled.div`
|
|
151
|
-
display: flex;
|
|
152
|
-
align-items: stretch;
|
|
153
|
-
margin-bottom: 40px;
|
|
154
|
-
`;
|
|
155
|
-
|
|
156
|
-
const MinCraftingRequirementsText = styled.p<{ levelIsOk: boolean }>`
|
|
157
|
-
font-size: 0.6rem !important;
|
|
158
|
-
margin: 0 5px 0 35px;
|
|
159
|
-
color: ${({ levelIsOk }) =>
|
|
160
|
-
levelIsOk ? uiColors.lightGreen : uiColors.lightGray} !important;
|
|
161
|
-
`;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ICraftableItem, IItemContainer, ISkill } from '@rpg-engine/shared';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { uiColors } from '../../constants/uiColors';
|
|
5
|
+
import { countItemFromInventory } from '../../libs/itemCounter';
|
|
6
|
+
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
7
|
+
import { modifyString } from './utils/modifyString';
|
|
8
|
+
|
|
9
|
+
interface ICraftingTooltipProps {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
recipe: ICraftableItem;
|
|
13
|
+
inventory?: IItemContainer | null;
|
|
14
|
+
skills?: ISkill | null;
|
|
15
|
+
atlasIMG: any;
|
|
16
|
+
atlasJSON: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TooltipContainer = styled.div<{ x: number; y: number }>`
|
|
20
|
+
position: fixed;
|
|
21
|
+
left: ${props => props.x}px;
|
|
22
|
+
top: ${props => props.y}px;
|
|
23
|
+
background-color: rgba(0, 0, 0, 0.95);
|
|
24
|
+
color: white;
|
|
25
|
+
text-align: left;
|
|
26
|
+
border-radius: 4px;
|
|
27
|
+
padding: 10px;
|
|
28
|
+
z-index: 1000;
|
|
29
|
+
font-family: 'Press Start 2P', cursive;
|
|
30
|
+
font-size: 0.6rem;
|
|
31
|
+
width: max-content;
|
|
32
|
+
max-width: 250px;
|
|
33
|
+
white-space: normal;
|
|
34
|
+
border: 1px solid ${uiColors.darkGray};
|
|
35
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
36
|
+
line-height: 1.4;
|
|
37
|
+
|
|
38
|
+
/* Arrow */
|
|
39
|
+
&:before {
|
|
40
|
+
content: '';
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 12px;
|
|
43
|
+
left: -6px;
|
|
44
|
+
border-width: 6px 6px 6px 0;
|
|
45
|
+
border-style: solid;
|
|
46
|
+
border-color: transparent rgba(0, 0, 0, 0.95) transparent transparent;
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const MinCraftingRequirementsText = styled.div<{ levelIsOk: boolean }>`
|
|
51
|
+
font-size: 0.55rem;
|
|
52
|
+
margin: 0;
|
|
53
|
+
margin-bottom: 12px;
|
|
54
|
+
color: ${({ levelIsOk }) =>
|
|
55
|
+
levelIsOk ? uiColors.lightGreen : uiColors.lightGray} !important;
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const IngredientsTitle = styled.div`
|
|
59
|
+
color: ${uiColors.yellow};
|
|
60
|
+
font-size: 0.6rem;
|
|
61
|
+
margin-bottom: 12px;
|
|
62
|
+
text-transform: uppercase;
|
|
63
|
+
letter-spacing: 0.5px;
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const Recipe = styled.div`
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
font-size: 0.55rem;
|
|
70
|
+
margin-bottom: 8px;
|
|
71
|
+
margin-left: 4px;
|
|
72
|
+
|
|
73
|
+
&:last-child {
|
|
74
|
+
margin-bottom: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.sprite-from-atlas-img {
|
|
78
|
+
top: 0px;
|
|
79
|
+
left: 0px;
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const Ingredient = styled.div<{ isQuantityOk: boolean }>`
|
|
84
|
+
margin: 0;
|
|
85
|
+
margin-left: 14px;
|
|
86
|
+
color: ${({ isQuantityOk }) =>
|
|
87
|
+
isQuantityOk ? uiColors.lightGreen : uiColors.lightGray} !important;
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export const CraftingTooltip: React.FC<ICraftingTooltipProps> = ({
|
|
91
|
+
x,
|
|
92
|
+
y,
|
|
93
|
+
recipe,
|
|
94
|
+
inventory,
|
|
95
|
+
skills,
|
|
96
|
+
atlasIMG,
|
|
97
|
+
atlasJSON,
|
|
98
|
+
}) => {
|
|
99
|
+
const levelInSkill =
|
|
100
|
+
(skills?.[
|
|
101
|
+
(recipe?.minCraftingRequirements?.[0] ?? '') as keyof ISkill
|
|
102
|
+
] as any)?.level ?? 1;
|
|
103
|
+
|
|
104
|
+
const levelIsOk = recipe?.levelIsOk ?? false;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<TooltipContainer x={x} y={y}>
|
|
108
|
+
<MinCraftingRequirementsText levelIsOk={levelIsOk}>
|
|
109
|
+
{modifyString(`${recipe?.minCraftingRequirements?.[0] ?? ''}`)} lvl{' '}
|
|
110
|
+
{recipe?.minCraftingRequirements?.[1] ?? 0} ({levelInSkill})
|
|
111
|
+
</MinCraftingRequirementsText>
|
|
112
|
+
|
|
113
|
+
<IngredientsTitle>Ingredients</IngredientsTitle>
|
|
114
|
+
{recipe.ingredients.map((ingredient, index) => {
|
|
115
|
+
const itemQtyInInventory = !inventory
|
|
116
|
+
? 0
|
|
117
|
+
: countItemFromInventory(ingredient.key, inventory);
|
|
118
|
+
const isQuantityOk = ingredient.qty <= itemQtyInInventory;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Recipe key={index}>
|
|
122
|
+
<SpriteFromAtlas
|
|
123
|
+
atlasIMG={atlasIMG}
|
|
124
|
+
atlasJSON={atlasJSON}
|
|
125
|
+
spriteKey={ingredient.texturePath}
|
|
126
|
+
imgScale={1.2}
|
|
127
|
+
/>
|
|
128
|
+
<Ingredient isQuantityOk={isQuantityOk}>
|
|
129
|
+
{modifyString(ingredient.key)} x{ingredient.qty} (
|
|
130
|
+
{itemQtyInInventory})
|
|
131
|
+
</Ingredient>
|
|
132
|
+
</Recipe>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</TooltipContainer>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FaSearch } from 'react-icons/fa';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { uiColors } from '../../../constants/uiColors';
|
|
5
|
+
import { Dropdown, IOptionsProps } from '../../Dropdown';
|
|
6
|
+
|
|
7
|
+
interface CraftBookHeaderProps {
|
|
8
|
+
categoryOptions: IOptionsProps[];
|
|
9
|
+
onCategoryChange: (value: string) => void;
|
|
10
|
+
onSearchToggle: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CraftBookHeader: React.FC<CraftBookHeaderProps> = ({
|
|
14
|
+
categoryOptions,
|
|
15
|
+
onCategoryChange,
|
|
16
|
+
onSearchToggle,
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<Container>
|
|
20
|
+
<Title>Craftbook</Title>
|
|
21
|
+
<Controls>
|
|
22
|
+
<DropdownWrapper>
|
|
23
|
+
<Dropdown
|
|
24
|
+
options={categoryOptions}
|
|
25
|
+
onChange={onCategoryChange}
|
|
26
|
+
width="200px"
|
|
27
|
+
/>
|
|
28
|
+
</DropdownWrapper>
|
|
29
|
+
<SearchButton onClick={onSearchToggle}>
|
|
30
|
+
<FaSearch size={16} />
|
|
31
|
+
</SearchButton>
|
|
32
|
+
</Controls>
|
|
33
|
+
</Container>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const Container = styled.div`
|
|
38
|
+
display: flex;
|
|
39
|
+
justify-content: space-between;
|
|
40
|
+
align-items: center;
|
|
41
|
+
width: 100%;
|
|
42
|
+
padding: 16px 16px 0;
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const Title = styled.h1`
|
|
46
|
+
font-size: 1.2rem;
|
|
47
|
+
color: ${uiColors.yellow} !important;
|
|
48
|
+
margin: 0;
|
|
49
|
+
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const Controls = styled.div`
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 16px;
|
|
56
|
+
position: relative;
|
|
57
|
+
left: -2rem;
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const DropdownWrapper = styled.div`
|
|
61
|
+
width: 200px;
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const SearchButton = styled.button`
|
|
65
|
+
background: none;
|
|
66
|
+
border: none;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
padding: 8px;
|
|
69
|
+
width: 32px;
|
|
70
|
+
height: 32px;
|
|
71
|
+
color: ${uiColors.yellow};
|
|
72
|
+
opacity: 0.8;
|
|
73
|
+
transition: opacity 0.2s;
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
|
|
78
|
+
&:hover {
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ICraftableItem } from '@rpg-engine/shared';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
interface UseCraftBookFiltersProps {
|
|
5
|
+
items: ICraftableItem[];
|
|
6
|
+
searchTerm: string;
|
|
7
|
+
selectedType: string;
|
|
8
|
+
pinnedItems: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useCraftBookFilters = ({
|
|
12
|
+
items,
|
|
13
|
+
searchTerm,
|
|
14
|
+
selectedType,
|
|
15
|
+
pinnedItems,
|
|
16
|
+
}: UseCraftBookFiltersProps): ICraftableItem[] => {
|
|
17
|
+
return useMemo(() => {
|
|
18
|
+
// First filter items
|
|
19
|
+
const filteredItems = items?.filter(item => {
|
|
20
|
+
const matchesSearch = item.name
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.includes(searchTerm.toLowerCase());
|
|
23
|
+
const matchesCategory =
|
|
24
|
+
selectedType === 'Suggested' ||
|
|
25
|
+
(selectedType === 'Pinned' && pinnedItems.includes(item.key)) ||
|
|
26
|
+
item.type === selectedType;
|
|
27
|
+
return matchesSearch && matchesCategory;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Then sort them (pinned items first)
|
|
31
|
+
return [...(filteredItems || [])].sort((a, b) => {
|
|
32
|
+
const aIsPinned = pinnedItems.includes(a.key);
|
|
33
|
+
const bIsPinned = pinnedItems.includes(b.key);
|
|
34
|
+
if (aIsPinned && !bIsPinned) return -1;
|
|
35
|
+
if (!aIsPinned && bIsPinned) return 1;
|
|
36
|
+
return 0;
|
|
37
|
+
});
|
|
38
|
+
}, [items, searchTerm, selectedType, pinnedItems]);
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ICraftableItem } from '@rpg-engine/shared';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
interface UseFilteredItemsProps {
|
|
5
|
+
items: ICraftableItem[];
|
|
6
|
+
searchTerm: string;
|
|
7
|
+
selectedType: string;
|
|
8
|
+
pinnedItems: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useFilteredItems = ({
|
|
12
|
+
items,
|
|
13
|
+
searchTerm,
|
|
14
|
+
selectedType,
|
|
15
|
+
pinnedItems,
|
|
16
|
+
}: UseFilteredItemsProps): ICraftableItem[] => {
|
|
17
|
+
return useMemo(() => {
|
|
18
|
+
// First filter items
|
|
19
|
+
const filteredItems = items?.filter(item => {
|
|
20
|
+
const matchesSearch = item.name
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.includes(searchTerm.toLowerCase());
|
|
23
|
+
const matchesCategory =
|
|
24
|
+
selectedType === 'Suggested' ||
|
|
25
|
+
(selectedType === 'Pinned' && pinnedItems.includes(item.key)) ||
|
|
26
|
+
item.type === selectedType;
|
|
27
|
+
return matchesSearch && matchesCategory;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Then sort them (pinned items first)
|
|
31
|
+
return [...(filteredItems || [])].sort((a, b) => {
|
|
32
|
+
const aIsPinned = pinnedItems.includes(a.key);
|
|
33
|
+
const bIsPinned = pinnedItems.includes(b.key);
|
|
34
|
+
if (aIsPinned && !bIsPinned) return -1;
|
|
35
|
+
if (!aIsPinned && bIsPinned) return 1;
|
|
36
|
+
return 0;
|
|
37
|
+
});
|
|
38
|
+
}, [items, searchTerm, selectedType, pinnedItems]);
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UsePaginationProps<T> {
|
|
4
|
+
items: T[];
|
|
5
|
+
itemsPerPage: number;
|
|
6
|
+
dependencies?: any[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UsePaginationReturn<T> {
|
|
10
|
+
currentPage: number;
|
|
11
|
+
setCurrentPage: (page: number) => void;
|
|
12
|
+
paginatedItems: T[];
|
|
13
|
+
totalPages: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const usePagination = <T>({
|
|
17
|
+
items,
|
|
18
|
+
itemsPerPage,
|
|
19
|
+
dependencies = [],
|
|
20
|
+
}: UsePaginationProps<T>): UsePaginationReturn<T> => {
|
|
21
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
22
|
+
const totalPages = Math.ceil(items.length / itemsPerPage);
|
|
23
|
+
|
|
24
|
+
const paginatedItems = items.slice(
|
|
25
|
+
(currentPage - 1) * itemsPerPage,
|
|
26
|
+
currentPage * itemsPerPage
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setCurrentPage(1);
|
|
31
|
+
}, [...dependencies]);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
currentPage,
|
|
35
|
+
setCurrentPage,
|
|
36
|
+
paginatedItems,
|
|
37
|
+
totalPages,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const desktop = {
|
|
4
|
+
width: 'min(900px, 80%)',
|
|
5
|
+
height: 'min(700px, 80%)',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const mobileLanscape = {
|
|
9
|
+
width: '800px',
|
|
10
|
+
height: '500px',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const mobilePortrait = {
|
|
14
|
+
width: '500px',
|
|
15
|
+
height: '700px',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface Size {
|
|
19
|
+
width: string;
|
|
20
|
+
height: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useResponsiveSize = (scale?: number): Size | undefined => {
|
|
24
|
+
const [size, setSize] = useState<Size>();
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleResize = (): void => {
|
|
28
|
+
if (
|
|
29
|
+
window.innerWidth < 500 &&
|
|
30
|
+
size?.width !== mobilePortrait.width &&
|
|
31
|
+
(!scale || scale < 1)
|
|
32
|
+
) {
|
|
33
|
+
setSize(mobilePortrait);
|
|
34
|
+
} else if (
|
|
35
|
+
(!scale || scale < 1) &&
|
|
36
|
+
size?.width !== mobileLanscape.width
|
|
37
|
+
) {
|
|
38
|
+
setSize(mobileLanscape);
|
|
39
|
+
} else if (size?.width !== desktop.width) {
|
|
40
|
+
setSize(desktop);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
handleResize();
|
|
44
|
+
|
|
45
|
+
window.addEventListener('resize', handleResize);
|
|
46
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
47
|
+
}, [scale]);
|
|
48
|
+
|
|
49
|
+
return size;
|
|
50
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const modifyString = (str: string): string => {
|
|
2
|
+
let parts = str.split('/');
|
|
3
|
+
let fileName = parts[parts.length - 1];
|
|
4
|
+
parts = fileName.split('.');
|
|
5
|
+
let name = parts[0];
|
|
6
|
+
name = name.replace(/-/g, ' ');
|
|
7
|
+
let words = name.split(' ');
|
|
8
|
+
let firstWord = words[0].slice(0, 1).toUpperCase() + words[0].slice(1);
|
|
9
|
+
let modifiedWords = [firstWord].concat(words.slice(1));
|
|
10
|
+
return modifiedWords.join(' ');
|
|
11
|
+
};
|