@blocklet/list 0.8.13 → 0.8.16
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/lib/base.js +21 -24
- package/lib/components/{aside.js → aside/aside.js} +11 -12
- package/lib/components/aside/category-link-list.js +78 -0
- package/lib/components/aside/index.js +13 -0
- package/lib/components/category-select.js +55 -0
- package/lib/components/{button.js → custom-select/button.js} +1 -1
- package/lib/components/{custom-select.js → custom-select/custom-select.js} +8 -6
- package/lib/components/custom-select/index.js +13 -0
- package/lib/components/filter-author.js +2 -3
- package/lib/components/{empty.js → list/empty.js} +6 -8
- package/lib/components/list/index.js +13 -0
- package/lib/components/{list.js → list/list.js} +12 -12
- package/lib/components/search.js +17 -18
- package/lib/contexts/{store.js → filter.js} +90 -159
- package/lib/index.js +6 -16
- package/lib/libs/prop-types.js +34 -0
- package/lib/{tools → libs}/utils.js +6 -6
- package/package.json +60 -64
- package/src/base.js +17 -18
- package/src/components/{aside.js → aside/aside.js} +7 -8
- package/src/components/aside/category-link-list.js +53 -0
- package/src/components/aside/index.js +3 -0
- package/src/components/category-select.js +43 -0
- package/src/components/{button.js → custom-select/button.js} +0 -0
- package/src/components/{custom-select.js → custom-select/custom-select.js} +5 -4
- package/src/components/custom-select/index.js +3 -0
- package/src/components/filter-author.js +2 -3
- package/src/components/{empty.js → list/empty.js} +7 -8
- package/src/components/list/index.js +3 -0
- package/src/components/{list.js → list/list.js} +10 -10
- package/src/components/search.js +12 -12
- package/src/contexts/filter.js +196 -0
- package/src/index.js +6 -16
- package/src/libs/prop-types.js +26 -0
- package/src/{tools → libs}/utils.js +6 -6
- package/lib/components/categories.js +0 -143
- package/lib/hooks/page-state.js +0 -69
- package/src/components/categories.js +0 -129
- package/src/contexts/store.js +0 -266
- package/src/hooks/page-state.js +0 -53
package/src/base.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
|
-
import
|
|
3
|
+
import SortIcon from '@mui/icons-material/Sort';
|
|
4
4
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
|
5
|
-
import { Box, Hidden
|
|
5
|
+
import { Box, Hidden } from '@mui/material';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { useFilterContext } from './contexts/filter';
|
|
8
8
|
import CustomSelect from './components/custom-select';
|
|
9
9
|
import FilterAuthor from './components/filter-author';
|
|
10
|
-
import { getSortOptions, getPrices } from './
|
|
10
|
+
import { getSortOptions, getPrices } from './libs/utils';
|
|
11
11
|
import BlockletList from './components/list';
|
|
12
12
|
import Aside from './components/aside';
|
|
13
13
|
import Search from './components/search';
|
|
14
|
-
import
|
|
14
|
+
import CategorySelect from './components/category-select';
|
|
15
15
|
|
|
16
16
|
const ListBase = () => {
|
|
17
|
-
const
|
|
18
|
-
const { sortParams, history, blockletList, queryParams, developerName, handleSort, t } = searchStore;
|
|
17
|
+
const { handleDeveloper, blockletList, filters, developerName, handleSort, t, handlePrice } = useFilterContext();
|
|
19
18
|
return (
|
|
20
19
|
<Box display="flex" alignItems="flex-start" height="100%">
|
|
21
20
|
<Hidden mdDown>
|
|
@@ -24,11 +23,11 @@ const ListBase = () => {
|
|
|
24
23
|
<StyledMin>
|
|
25
24
|
<Box className="marketplace-header" display="flex" alignItems="center">
|
|
26
25
|
<Hidden mdDown>
|
|
27
|
-
{!!
|
|
26
|
+
{!!filters.developer && (
|
|
28
27
|
<FilterAuthor
|
|
29
28
|
user={developerName}
|
|
30
29
|
deleteUserTag={() => {
|
|
31
|
-
|
|
30
|
+
handleDeveloper(null);
|
|
32
31
|
}}
|
|
33
32
|
/>
|
|
34
33
|
)}
|
|
@@ -43,10 +42,10 @@ const ListBase = () => {
|
|
|
43
42
|
<CategorySelect />
|
|
44
43
|
</Hidden>
|
|
45
44
|
<CustomSelect
|
|
46
|
-
value={
|
|
45
|
+
value={filters.sortBy}
|
|
47
46
|
options={getSortOptions(t)}
|
|
48
|
-
title={getSortOptions(t).find((f) => f.value ===
|
|
49
|
-
icon={<
|
|
47
|
+
title={getSortOptions(t).find((f) => f.value === filters.sortBy)?.name || t('sort.sort')}
|
|
48
|
+
icon={<SortIcon />}
|
|
50
49
|
onChange={(v) => {
|
|
51
50
|
handleSort(v);
|
|
52
51
|
}}
|
|
@@ -58,24 +57,24 @@ const ListBase = () => {
|
|
|
58
57
|
display="flex"
|
|
59
58
|
flexWrap="wrap"
|
|
60
59
|
alignItems="center"
|
|
61
|
-
justifyContent={
|
|
62
|
-
{!!
|
|
60
|
+
justifyContent={filters.developer ? 'space-between' : 'flex-end'}>
|
|
61
|
+
{!!filters.developer && (
|
|
63
62
|
<FilterAuthor
|
|
64
63
|
user={developerName}
|
|
65
64
|
deleteUserTag={() => {
|
|
66
|
-
|
|
65
|
+
handleDeveloper(null);
|
|
67
66
|
}}
|
|
68
67
|
style={{ marginBottom: '16px' }}
|
|
69
68
|
/>
|
|
70
69
|
)}
|
|
71
70
|
{/* 筛选付费/免费 */}
|
|
72
71
|
<CustomSelect
|
|
73
|
-
value={
|
|
72
|
+
value={filters.price}
|
|
74
73
|
icon={<FilterListIcon />}
|
|
75
74
|
options={getPrices(t)}
|
|
76
|
-
title={getPrices(t).find((f) => f.value ===
|
|
75
|
+
title={getPrices(t).find((f) => f.value === filters.price)?.name || t('common.price')}
|
|
77
76
|
onChange={(v) => {
|
|
78
|
-
|
|
77
|
+
handlePrice(v);
|
|
79
78
|
}}
|
|
80
79
|
style={{ marginBottom: '16px' }}
|
|
81
80
|
/>
|
|
@@ -2,14 +2,13 @@ import React from 'react';
|
|
|
2
2
|
import { Box, List, Checkbox, FormControlLabel } from '@mui/material';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { getPrices } from '
|
|
7
|
-
import Search from '
|
|
8
|
-
import
|
|
5
|
+
import { useFilterContext } from '../../contexts/filter';
|
|
6
|
+
import { getPrices } from '../../libs/utils';
|
|
7
|
+
import Search from '../search';
|
|
8
|
+
import CategoryLinkList from './category-link-list';
|
|
9
9
|
|
|
10
10
|
const Aside = () => {
|
|
11
|
-
const
|
|
12
|
-
const { queryParams, handlePriceFilter, t } = searchStore;
|
|
11
|
+
const { filters, handlePrice, t } = useFilterContext();
|
|
13
12
|
return (
|
|
14
13
|
<StyledAside>
|
|
15
14
|
<Search placeholder={t('common.searchStore')} />
|
|
@@ -21,10 +20,10 @@ const Aside = () => {
|
|
|
21
20
|
control={
|
|
22
21
|
<Checkbox
|
|
23
22
|
onClick={() => {
|
|
24
|
-
|
|
23
|
+
handlePrice(item.value);
|
|
25
24
|
}}
|
|
26
25
|
size="small"
|
|
27
|
-
checked={item.value ===
|
|
26
|
+
checked={item.value === filters.price}
|
|
28
27
|
/>
|
|
29
28
|
}
|
|
30
29
|
label={item.name}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ListItem, ListItemText, Link } from '@mui/material';
|
|
3
|
+
import joinUrl from 'url-join';
|
|
4
|
+
|
|
5
|
+
import { useFilterContext } from '../../contexts/filter';
|
|
6
|
+
import { urlStringify } from '../../libs/utils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 宽屏幕下的分类列表
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
const CategoryLinkList = () => {
|
|
13
|
+
const { categoryList, selectedCategory, filters, baseUrl, handleCategory, t, locale } = useFilterContext();
|
|
14
|
+
|
|
15
|
+
const handleClick = (e, name) => {
|
|
16
|
+
handleCategory(name);
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
return false;
|
|
19
|
+
};
|
|
20
|
+
const content = (
|
|
21
|
+
<div style={{ marginRight: '16px' }}>
|
|
22
|
+
<ListItem selected={!selectedCategory} button onClick={(e) => handleClick(e, 'all')}>
|
|
23
|
+
<ListItemText primary={t('category.all')} />
|
|
24
|
+
</ListItem>
|
|
25
|
+
{categoryList.map((item) => (
|
|
26
|
+
<ListItem
|
|
27
|
+
key={item._id}
|
|
28
|
+
title={item.locales[locale]}
|
|
29
|
+
component={Link}
|
|
30
|
+
to={`${joinUrl(baseUrl, '/search')}?${urlStringify({
|
|
31
|
+
...filters,
|
|
32
|
+
category: item.name,
|
|
33
|
+
})}`}
|
|
34
|
+
onClick={(e) => handleClick(e, item.name)}
|
|
35
|
+
button
|
|
36
|
+
selected={item.name === selectedCategory}>
|
|
37
|
+
<ListItemText primary={item.locales[locale]} />
|
|
38
|
+
</ListItem>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<ListItem>
|
|
46
|
+
<ListItemText className="category" primary={t('common.category')} />
|
|
47
|
+
</ListItem>
|
|
48
|
+
{content}
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default CategoryLinkList;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MenuItem } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
import { useFilterContext } from '../contexts/filter';
|
|
5
|
+
import CustomSelect from './custom-select';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 小屏幕下的分类选择器
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
const CategorySelect = () => {
|
|
12
|
+
const { categoryList, selectedCategory, handleCategory, t, locale } = useFilterContext();
|
|
13
|
+
|
|
14
|
+
const itemRender = (item) => {
|
|
15
|
+
return (
|
|
16
|
+
<MenuItem
|
|
17
|
+
className={['my-select__option', selectedCategory?.includes(item.name) ? 'my-select__option--active' : ''].join(
|
|
18
|
+
' '
|
|
19
|
+
)}
|
|
20
|
+
key={item._id}
|
|
21
|
+
value={item.name}
|
|
22
|
+
onClick={() => handleCategory(item.name)}>
|
|
23
|
+
{item.locales[locale]}
|
|
24
|
+
</MenuItem>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<CustomSelect
|
|
30
|
+
value={selectedCategory || 'all'}
|
|
31
|
+
options={categoryList}
|
|
32
|
+
title={categoryList.find((i) => i.name === selectedCategory)?.locales[locale] || t('category.all')}
|
|
33
|
+
itemRender={itemRender}
|
|
34
|
+
prepend={
|
|
35
|
+
<MenuItem value="all" onClick={() => handleCategory('all')}>
|
|
36
|
+
{t('category.all')}
|
|
37
|
+
</MenuItem>
|
|
38
|
+
}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default CategorySelect;
|
|
File without changes
|
|
@@ -4,7 +4,8 @@ import useTheme from '@mui/styles/useTheme';
|
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import PropTypes from 'prop-types';
|
|
6
6
|
import { ClickAwayListener, Grow, MenuItem, MenuList, Paper, Popper, SvgIcon, useMediaQuery } from '@mui/material';
|
|
7
|
-
import
|
|
7
|
+
import CheckIcon from '@mui/icons-material/Check';
|
|
8
|
+
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
8
9
|
import cloneDeep from 'lodash/cloneDeep';
|
|
9
10
|
import isEmpty from 'lodash/isEmpty';
|
|
10
11
|
import isEqual from 'lodash/isEqual';
|
|
@@ -81,9 +82,9 @@ const CustomSelect = ({
|
|
|
81
82
|
<div className="my-select__icon">{icon}</div>
|
|
82
83
|
{title}
|
|
83
84
|
{multiple && currentValue.length > 1 && ` (${currentValue.length})`}
|
|
84
|
-
<SvgIcon className="my-select__arrowdown" component={
|
|
85
|
+
<SvgIcon className="my-select__arrowdown" component={KeyboardArrowDownIcon} fontSize="small" />
|
|
85
86
|
</StyledButton>
|
|
86
|
-
<Popper open={open} anchorEl={anchorRef.current} transition style={{ zIndex: '
|
|
87
|
+
<Popper open={open} anchorEl={anchorRef.current} transition style={{ zIndex: '9999' }}>
|
|
87
88
|
{({ TransitionProps, placement }) => (
|
|
88
89
|
<Grow
|
|
89
90
|
{...TransitionProps}
|
|
@@ -106,7 +107,7 @@ const CustomSelect = ({
|
|
|
106
107
|
].join(' ')}>
|
|
107
108
|
{multiple && (
|
|
108
109
|
<SvgIcon
|
|
109
|
-
component={
|
|
110
|
+
component={CheckIcon}
|
|
110
111
|
className={[
|
|
111
112
|
'my-select__option__icon',
|
|
112
113
|
containsValue(option.value) ? 'my-select__option__icon--active' : '',
|
|
@@ -4,7 +4,7 @@ import styled from 'styled-components';
|
|
|
4
4
|
import { Chip } from '@mui/material';
|
|
5
5
|
import FaceIcon from '@mui/icons-material/Face';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { useFilterContext } from '../contexts/filter';
|
|
8
8
|
|
|
9
9
|
const StyleDiv = styled.div`
|
|
10
10
|
.MuiChip-root {
|
|
@@ -14,8 +14,7 @@ const StyleDiv = styled.div`
|
|
|
14
14
|
}
|
|
15
15
|
`;
|
|
16
16
|
const FilterAuthor = ({ user, deleteUserTag, ...containerProps }) => {
|
|
17
|
-
const
|
|
18
|
-
const { t } = searchStore;
|
|
17
|
+
const { t } = useFilterContext();
|
|
19
18
|
if (!user) return null;
|
|
20
19
|
return (
|
|
21
20
|
<StyleDiv {...containerProps}>
|
|
@@ -3,11 +3,10 @@ import PropTypes from 'prop-types';
|
|
|
3
3
|
import Box from '@mui/material/Box';
|
|
4
4
|
import Typography from '@mui/material/Typography';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useFilterContext } from '../../contexts/filter';
|
|
7
7
|
|
|
8
8
|
const NoResults = () => {
|
|
9
|
-
const
|
|
10
|
-
const { t } = searchStore;
|
|
9
|
+
const { t } = useFilterContext();
|
|
11
10
|
return (
|
|
12
11
|
<Typography style={{ textAlign: 'center' }} variant="subtitle2">
|
|
13
12
|
{t('blocklet.noResults')}
|
|
@@ -15,8 +14,8 @@ const NoResults = () => {
|
|
|
15
14
|
);
|
|
16
15
|
};
|
|
17
16
|
const NoResultsTips = ({ filterTip, keywordTip }) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
17
|
+
const { t, locale } = useFilterContext();
|
|
18
|
+
|
|
20
19
|
const getSplit = () => {
|
|
21
20
|
if (locale === 'zh') return '、';
|
|
22
21
|
return ' , ';
|
|
@@ -39,11 +38,11 @@ NoResultsTips.defaultProps = {
|
|
|
39
38
|
filterTip: false,
|
|
40
39
|
keywordTip: false,
|
|
41
40
|
};
|
|
42
|
-
const EmptyTitle = ({ primaryStart, primaryEnd,
|
|
41
|
+
const EmptyTitle = ({ primaryStart, primaryEnd, filter }) => {
|
|
43
42
|
return (
|
|
44
43
|
<Typography variant="subtitle2">
|
|
45
44
|
<span>{primaryStart}</span>
|
|
46
|
-
<span className="primary"> {
|
|
45
|
+
<span className="primary"> {filter} </span>
|
|
47
46
|
<span>{primaryEnd} </span>
|
|
48
47
|
</Typography>
|
|
49
48
|
);
|
|
@@ -51,7 +50,7 @@ const EmptyTitle = ({ primaryStart, primaryEnd, search }) => {
|
|
|
51
50
|
EmptyTitle.propTypes = {
|
|
52
51
|
primaryStart: PropTypes.string.isRequired,
|
|
53
52
|
primaryEnd: PropTypes.string.isRequired,
|
|
54
|
-
|
|
53
|
+
filter: PropTypes.string.isRequired,
|
|
55
54
|
};
|
|
56
55
|
|
|
57
56
|
export { NoResults, EmptyTitle, NoResultsTips };
|
|
@@ -8,14 +8,14 @@ import Grid from '@mui/material/Grid';
|
|
|
8
8
|
import CircularProgress from '@mui/material/CircularProgress';
|
|
9
9
|
|
|
10
10
|
import { NoResults, EmptyTitle, NoResultsTips } from './empty';
|
|
11
|
-
import {
|
|
12
|
-
import { formatError } from '
|
|
11
|
+
import { useFilterContext } from '../../contexts/filter';
|
|
12
|
+
import { formatError } from '../../libs/utils';
|
|
13
13
|
|
|
14
14
|
export default function BlockletList({ blocklets, ...rest }) {
|
|
15
|
-
const { blockletRender, errors, loadings, selectedCategory, blockletList, getCategoryLocale,
|
|
16
|
-
|
|
15
|
+
const { blockletRender, errors, loadings, selectedCategory, blockletList, getCategoryLocale, filters, t } =
|
|
16
|
+
useFilterContext();
|
|
17
17
|
|
|
18
|
-
const showFilterTip = !!selectedCategory || !!
|
|
18
|
+
const showFilterTip = !!selectedCategory || !!filters.price;
|
|
19
19
|
|
|
20
20
|
if (errors.fetchBlockletsError) {
|
|
21
21
|
return (
|
|
@@ -31,25 +31,25 @@ export default function BlockletList({ blocklets, ...rest }) {
|
|
|
31
31
|
</Box>
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
|
-
if (
|
|
34
|
+
if (filters.keyword && showFilterTip && blockletList.length === 0) {
|
|
35
35
|
return (
|
|
36
36
|
<CustomEmpty>
|
|
37
37
|
<EmptyTitle
|
|
38
38
|
primaryStart={t('blocklet.noBlockletPart1')}
|
|
39
39
|
primaryEnd={t('blocklet.noBlockletPart2')}
|
|
40
|
-
|
|
40
|
+
filter={filters.keyword}
|
|
41
41
|
/>
|
|
42
42
|
<NoResultsTips keywordTip filterTip />
|
|
43
43
|
</CustomEmpty>
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
-
if (
|
|
46
|
+
if (filters.keyword && blockletList.length === 0) {
|
|
47
47
|
return (
|
|
48
48
|
<CustomEmpty>
|
|
49
49
|
<EmptyTitle
|
|
50
50
|
primaryStart={t('blocklet.noBlockletPart1')}
|
|
51
51
|
primaryEnd={t('blocklet.noBlockletPart2')}
|
|
52
|
-
|
|
52
|
+
filter={filters.keyword}
|
|
53
53
|
/>
|
|
54
54
|
<NoResultsTips keywordTip />
|
|
55
55
|
</CustomEmpty>
|
|
@@ -63,7 +63,7 @@ export default function BlockletList({ blocklets, ...rest }) {
|
|
|
63
63
|
<EmptyTitle
|
|
64
64
|
primaryStart={t('blocklet.noCategoryResults1')}
|
|
65
65
|
primaryEnd={t('blocklet.noCategoryResults2')}
|
|
66
|
-
|
|
66
|
+
filter={categoryLocale}
|
|
67
67
|
/>
|
|
68
68
|
) : (
|
|
69
69
|
<NoResults />
|
package/src/components/search.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import
|
|
3
|
+
import SearchIcon from '@mui/icons-material/Search';
|
|
4
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
4
5
|
import { OutlinedInput, InputAdornment } from '@mui/material';
|
|
5
6
|
import { useDebounceFn } from 'ahooks';
|
|
6
7
|
import styled from 'styled-components';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { useFilterContext } from '../contexts/filter';
|
|
9
10
|
|
|
10
11
|
const Search = ({ placeholder, ...rest }) => {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const [searchStr, setSearchStr] = useState(queryParams.get('search') || '');
|
|
12
|
+
const { filters, handleKeyword } = useFilterContext();
|
|
13
|
+
const [searchStr, setSearchStr] = useState(filters.keyword || '');
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const debouncedSearch = useDebounceFn(handleKeyword, { wait: 300 });
|
|
16
16
|
const handleChange = (event) => {
|
|
17
17
|
const { value } = event.target;
|
|
18
18
|
setSearchStr(value);
|
|
19
|
-
|
|
19
|
+
debouncedSearch.run(value);
|
|
20
20
|
};
|
|
21
21
|
const handleClose = () => {
|
|
22
22
|
setSearchStr('');
|
|
23
|
-
|
|
23
|
+
handleKeyword();
|
|
24
24
|
};
|
|
25
25
|
return (
|
|
26
26
|
<StyledSearch
|
|
@@ -29,7 +29,7 @@ const Search = ({ placeholder, ...rest }) => {
|
|
|
29
29
|
}}
|
|
30
30
|
startAdornment={
|
|
31
31
|
<InputAdornment position="start">
|
|
32
|
-
<
|
|
32
|
+
<StyledSearchIcon />
|
|
33
33
|
</InputAdornment>
|
|
34
34
|
}
|
|
35
35
|
onChange={handleChange}
|
|
@@ -40,7 +40,7 @@ const Search = ({ placeholder, ...rest }) => {
|
|
|
40
40
|
endAdornment={
|
|
41
41
|
searchStr && (
|
|
42
42
|
<InputAdornment position="end">
|
|
43
|
-
<
|
|
43
|
+
<StyledCloseIcon data-cy="search-delete" onClick={handleClose} />
|
|
44
44
|
</InputAdornment>
|
|
45
45
|
)
|
|
46
46
|
}
|
|
@@ -80,7 +80,7 @@ const StyledSearch = styled(OutlinedInput)`
|
|
|
80
80
|
}
|
|
81
81
|
`;
|
|
82
82
|
|
|
83
|
-
const
|
|
83
|
+
const StyledSearchIcon = styled(SearchIcon)`
|
|
84
84
|
color: ${(props) => props.theme.palette.grey[500]};
|
|
85
85
|
font-size: 28px;
|
|
86
86
|
@media (max-width: ${(props) => props.theme.breakpoints.values.md}px) {
|
|
@@ -88,7 +88,7 @@ const StyledMagnify = styled(Magnify)`
|
|
|
88
88
|
}
|
|
89
89
|
`;
|
|
90
90
|
|
|
91
|
-
const
|
|
91
|
+
const StyledCloseIcon = styled(CloseIcon)`
|
|
92
92
|
color: ${(props) => props.theme.palette.grey[500]};
|
|
93
93
|
font-size: 16px;
|
|
94
94
|
cursor: pointer;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { useContext, createContext, useMemo, useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useRequest } from 'ahooks';
|
|
4
|
+
import orderBy from 'lodash/orderBy';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
// import joinUrl from 'url-join';
|
|
7
|
+
|
|
8
|
+
import { getCategories, filterBlockletByPrice, replaceTranslate } from '../libs/utils';
|
|
9
|
+
import translations from '../assets/locale';
|
|
10
|
+
import { propTypes, defaultProps } from '../libs/prop-types';
|
|
11
|
+
|
|
12
|
+
const Filter = createContext({});
|
|
13
|
+
const { Provider, Consumer } = Filter;
|
|
14
|
+
|
|
15
|
+
function FilterProvider({
|
|
16
|
+
onSelectBlocklet,
|
|
17
|
+
selectedBlocklets,
|
|
18
|
+
filters,
|
|
19
|
+
children,
|
|
20
|
+
baseUrl,
|
|
21
|
+
endpoint,
|
|
22
|
+
locale,
|
|
23
|
+
blockletRender,
|
|
24
|
+
onFilterChange,
|
|
25
|
+
}) {
|
|
26
|
+
const storeApi = axios.create({
|
|
27
|
+
baseURL: endpoint,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
data: allBlocklets,
|
|
32
|
+
error: fetchBlockletsError,
|
|
33
|
+
loading: fetchBlockletsLoading,
|
|
34
|
+
run: fetchBlocklets,
|
|
35
|
+
} = useRequest(
|
|
36
|
+
async () => {
|
|
37
|
+
const { data: list } = await storeApi.get('/api/blocklets.json');
|
|
38
|
+
return list;
|
|
39
|
+
},
|
|
40
|
+
{ initialData: [], manual: true }
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
data: allCategories,
|
|
45
|
+
error: fetchCategoriesError,
|
|
46
|
+
loading: fetchCategoriesLoading,
|
|
47
|
+
run: fetchCategories,
|
|
48
|
+
} = useRequest(
|
|
49
|
+
async () => {
|
|
50
|
+
const { data: list } = await storeApi.get('/api/blocklets/categories');
|
|
51
|
+
return list;
|
|
52
|
+
},
|
|
53
|
+
{ initialData: [], manual: true }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const selectedCategory = filters.category;
|
|
57
|
+
|
|
58
|
+
const hasDeveloperFilter = !!filters.developer;
|
|
59
|
+
const categoryState = useMemo(() => {
|
|
60
|
+
return !hasDeveloperFilter ? { data: allCategories } : getCategories(allBlocklets, filters.developer);
|
|
61
|
+
}, [hasDeveloperFilter, allCategories]);
|
|
62
|
+
|
|
63
|
+
const blockletList = useMemo(() => {
|
|
64
|
+
const sortByName = (x) => x?.title?.toLocaleLowerCase() || x?.name?.toLocaleLowerCase(); // 按名称排序
|
|
65
|
+
const sortByPopularity = (x) => x.stats.downloads; // 按下载量排序
|
|
66
|
+
const sortByPublish = (x) => x.lastPublishedAt; // 按发布时间
|
|
67
|
+
const sortMap = {
|
|
68
|
+
nameAsc: sortByName,
|
|
69
|
+
nameDesc: sortByName,
|
|
70
|
+
popularity: sortByPopularity,
|
|
71
|
+
publishAt: sortByPublish,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let result = allBlocklets || [];
|
|
75
|
+
// 按照付费/免费筛选
|
|
76
|
+
result = filterBlockletByPrice(result, filters.price);
|
|
77
|
+
// 按照分类筛选
|
|
78
|
+
result = result.filter((item) => (selectedCategory ? item?.category?.name === selectedCategory : true));
|
|
79
|
+
// 按照作者筛选
|
|
80
|
+
result = result.filter((item) => (filters?.developer ? item.owner.did === filters.developer : true));
|
|
81
|
+
const lowerSearch = filters?.keyword?.toLocaleLowerCase() || '';
|
|
82
|
+
// 按照搜索筛选
|
|
83
|
+
result = result.filter((item) => {
|
|
84
|
+
return (
|
|
85
|
+
(item?.title || item?.name)?.toLocaleLowerCase().includes(lowerSearch) ||
|
|
86
|
+
item.description?.toLocaleLowerCase().includes(lowerSearch) ||
|
|
87
|
+
item?.version?.toLocaleLowerCase().includes(lowerSearch)
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
// 排序
|
|
91
|
+
return orderBy(result, [sortMap[filters.sortBy]], [filters.sortDirection]);
|
|
92
|
+
}, [allBlocklets, filters]);
|
|
93
|
+
|
|
94
|
+
const categoryList = useMemo(() => {
|
|
95
|
+
const list = categoryState.data || [];
|
|
96
|
+
// 分类按照名称排序
|
|
97
|
+
return orderBy(list, [(i) => i.name], ['asc']);
|
|
98
|
+
}, [categoryState.data]);
|
|
99
|
+
|
|
100
|
+
const translate = (key, data) => {
|
|
101
|
+
if (!translations[locale] || !translations[locale][key]) {
|
|
102
|
+
console.warn(`Warning: no ${key} translation of ${locale}`);
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return replaceTranslate(translations[locale][key], data);
|
|
107
|
+
};
|
|
108
|
+
const filterStore = {
|
|
109
|
+
errors: { fetchBlockletsError, fetchCategoriesError },
|
|
110
|
+
loadings: { fetchBlockletsLoading, fetchCategoriesLoading },
|
|
111
|
+
endpoint,
|
|
112
|
+
blockletList,
|
|
113
|
+
t: translate,
|
|
114
|
+
filters: { sortBy: 'popularity', sortDirection: 'desc', ...filters },
|
|
115
|
+
selectedCategory,
|
|
116
|
+
categoryList,
|
|
117
|
+
baseUrl,
|
|
118
|
+
blockletRender,
|
|
119
|
+
locale,
|
|
120
|
+
selectedBlocklets,
|
|
121
|
+
onSelectBlocklet,
|
|
122
|
+
handleSort: (sort) => {
|
|
123
|
+
const changeData = {
|
|
124
|
+
...filters,
|
|
125
|
+
sortBy: sort,
|
|
126
|
+
sortDirection: sort === 'nameAsc' ? 'asc' : 'desc',
|
|
127
|
+
};
|
|
128
|
+
onFilterChange(changeData);
|
|
129
|
+
},
|
|
130
|
+
handleKeyword: (keyWord) => {
|
|
131
|
+
const changeData = { ...filters, keyword: keyWord || undefined };
|
|
132
|
+
onFilterChange(changeData);
|
|
133
|
+
},
|
|
134
|
+
handlePrice: (price) => {
|
|
135
|
+
const changeData = {
|
|
136
|
+
...filters,
|
|
137
|
+
price: price === filters.price ? undefined : price,
|
|
138
|
+
};
|
|
139
|
+
onFilterChange(changeData);
|
|
140
|
+
},
|
|
141
|
+
handleCategory: (category) => {
|
|
142
|
+
if (category === 'all') {
|
|
143
|
+
const changeData = { ...filters, category: undefined };
|
|
144
|
+
onFilterChange(changeData);
|
|
145
|
+
} else {
|
|
146
|
+
const changeData = { ...filters, category };
|
|
147
|
+
onFilterChange(changeData);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
handleDeveloper: (developer) => {
|
|
151
|
+
const changeData = { ...filters, developer: developer || undefined };
|
|
152
|
+
onFilterChange(changeData);
|
|
153
|
+
},
|
|
154
|
+
getCategoryLocale: (category) => {
|
|
155
|
+
if (!category) return null;
|
|
156
|
+
let result = null;
|
|
157
|
+
const find = categoryState.data.find((item) => item.name === category);
|
|
158
|
+
if (find) {
|
|
159
|
+
result = find.locales[locale];
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
},
|
|
163
|
+
get developerName() {
|
|
164
|
+
return allBlocklets.find((i) => i.owner.did === filters.developer)?.owner?.name || '';
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!hasDeveloperFilter) {
|
|
170
|
+
fetchCategories();
|
|
171
|
+
}
|
|
172
|
+
}, [!hasDeveloperFilter]);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
fetchBlocklets();
|
|
176
|
+
}, [endpoint]);
|
|
177
|
+
|
|
178
|
+
return <Provider value={filterStore}>{children}</Provider>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
FilterProvider.propTypes = {
|
|
182
|
+
...propTypes,
|
|
183
|
+
children: PropTypes.any.isRequired,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
FilterProvider.defaultProps = defaultProps;
|
|
187
|
+
|
|
188
|
+
function useFilterContext() {
|
|
189
|
+
const filterStore = useContext(Filter);
|
|
190
|
+
if (!filterStore) {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
return filterStore;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { FilterProvider, Consumer as FilterConsumer, useFilterContext };
|