@blocklet/list 0.8.6
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/README.md +0 -0
- package/lib/assets/locale.js +78 -0
- package/lib/base.js +131 -0
- package/lib/components/aside.js +65 -0
- package/lib/components/button.js +64 -0
- package/lib/components/categories.js +145 -0
- package/lib/components/custom-select.js +188 -0
- package/lib/components/empty.js +88 -0
- package/lib/components/filter-author.js +64 -0
- package/lib/components/list.js +132 -0
- package/lib/components/search.js +99 -0
- package/lib/contexts/store.js +333 -0
- package/lib/hooks/page-state.js +69 -0
- package/lib/index.js +33 -0
- package/lib/tools/utils.js +125 -0
- package/package.json +66 -0
- package/src/assets/locale.js +72 -0
- package/src/base.js +148 -0
- package/src/components/aside.js +91 -0
- package/src/components/button.js +35 -0
- package/src/components/categories.js +111 -0
- package/src/components/custom-select.js +207 -0
- package/src/components/empty.js +57 -0
- package/src/components/filter-author.js +39 -0
- package/src/components/list.js +117 -0
- package/src/components/search.js +93 -0
- package/src/contexts/store.js +250 -0
- package/src/hooks/page-state.js +53 -0
- package/src/index.js +27 -0
- package/src/tools/utils.js +97 -0
package/src/base.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import Sort from 'mdi-material-ui/Sort';
|
|
4
|
+
import FilterListIcon from '@mui/icons-material/FilterList';
|
|
5
|
+
import { Box, Hidden, SvgIcon } from '@mui/material';
|
|
6
|
+
|
|
7
|
+
import { useSearchContext } from './contexts/store';
|
|
8
|
+
import CustomSelect from './components/custom-select';
|
|
9
|
+
import FilterAuthor from './components/filter-author';
|
|
10
|
+
import { getSortOptions, getPrices } from './tools/utils';
|
|
11
|
+
import BlockletList from './components/list';
|
|
12
|
+
import Aside from './components/aside';
|
|
13
|
+
import Search from './components/search';
|
|
14
|
+
import { CategorySelect } from './components/categories';
|
|
15
|
+
|
|
16
|
+
const ListBase = () => {
|
|
17
|
+
const searchStore = useSearchContext();
|
|
18
|
+
const { sortParams, history, blockletList, queryParams, developerName, handleSort, t } = searchStore;
|
|
19
|
+
return (
|
|
20
|
+
<Box display="flex" alignItems="flex-start" height="100%">
|
|
21
|
+
<Hidden mdDown>
|
|
22
|
+
<Aside />
|
|
23
|
+
</Hidden>
|
|
24
|
+
<StyledMin>
|
|
25
|
+
<Box className="marketplace-header" display="flex" alignItems="center">
|
|
26
|
+
<Hidden mdDown>
|
|
27
|
+
{!!queryParams.developer && (
|
|
28
|
+
<FilterAuthor
|
|
29
|
+
user={developerName}
|
|
30
|
+
deleteUserTag={() => {
|
|
31
|
+
history.push('/');
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
)}
|
|
35
|
+
</Hidden>
|
|
36
|
+
<Box mt={0} className="searchContainer">
|
|
37
|
+
<Hidden mdUp>
|
|
38
|
+
<StyledSearch className="search" placeholder={t('common.searchStore')} />
|
|
39
|
+
</Hidden>
|
|
40
|
+
</Box>
|
|
41
|
+
<Box mt={0} ml="10px" className="filterContainer">
|
|
42
|
+
<Hidden mdUp>
|
|
43
|
+
<CategorySelect />
|
|
44
|
+
</Hidden>
|
|
45
|
+
<CustomSelect
|
|
46
|
+
value={sortParams.sort}
|
|
47
|
+
options={getSortOptions(t)}
|
|
48
|
+
title={getSortOptions(t).find((f) => f.value === sortParams.sort)?.name || t('sort.sort')}
|
|
49
|
+
icon={<SvgIcon component={Sort} />}
|
|
50
|
+
onChange={(v) => {
|
|
51
|
+
handleSort(v);
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
</Box>
|
|
55
|
+
</Box>
|
|
56
|
+
<Hidden mdUp>
|
|
57
|
+
<Box
|
|
58
|
+
display="flex"
|
|
59
|
+
flexWrap="wrap"
|
|
60
|
+
alignItems="center"
|
|
61
|
+
justifyContent={queryParams.developer ? 'space-between' : 'flex-end'}>
|
|
62
|
+
{!!queryParams.developer && (
|
|
63
|
+
<FilterAuthor
|
|
64
|
+
user={developerName}
|
|
65
|
+
deleteUserTag={() => {
|
|
66
|
+
history.push('/');
|
|
67
|
+
}}
|
|
68
|
+
style={{ marginBottom: '16px' }}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
{/* 筛选付费/免费 */}
|
|
72
|
+
<CustomSelect
|
|
73
|
+
value={queryParams.price}
|
|
74
|
+
icon={<FilterListIcon />}
|
|
75
|
+
options={getPrices(t)}
|
|
76
|
+
title={getPrices(t).find((f) => f.value === queryParams.price)?.name || t('common.price')}
|
|
77
|
+
onChange={(v) => {
|
|
78
|
+
searchStore.handlePriceFilter(v);
|
|
79
|
+
}}
|
|
80
|
+
style={{ marginBottom: '16px' }}
|
|
81
|
+
/>
|
|
82
|
+
</Box>
|
|
83
|
+
</Hidden>
|
|
84
|
+
<BlockletList blocklets={blockletList} />
|
|
85
|
+
</StyledMin>
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const StyledMin = styled.main`
|
|
91
|
+
flex: 1;
|
|
92
|
+
width: 100%;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
.marketplace-header {
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
margin-bottom: 20px;
|
|
97
|
+
}
|
|
98
|
+
.sort-button {
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
101
|
+
.search {
|
|
102
|
+
margin-left: 0px;
|
|
103
|
+
}
|
|
104
|
+
@media (max-width: ${(props) => props.theme.breakpoints.values.md}px) {
|
|
105
|
+
.searchContainer {
|
|
106
|
+
flex: 1;
|
|
107
|
+
}
|
|
108
|
+
.search {
|
|
109
|
+
width: 100%;
|
|
110
|
+
}
|
|
111
|
+
.filterContainer {
|
|
112
|
+
flex: 1;
|
|
113
|
+
display: flex;
|
|
114
|
+
justify-content: flex-end;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
@media (max-width: 750px) {
|
|
118
|
+
.searchContainer {
|
|
119
|
+
width: 100%;
|
|
120
|
+
}
|
|
121
|
+
.filterContainer {
|
|
122
|
+
width: 100%;
|
|
123
|
+
margin-left: 0;
|
|
124
|
+
display: flex;
|
|
125
|
+
justify-content: space-between;
|
|
126
|
+
}
|
|
127
|
+
.search {
|
|
128
|
+
margin-bottom: 20px;
|
|
129
|
+
width: 100%;
|
|
130
|
+
}
|
|
131
|
+
.marketplace-header {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
align-items: flex-start;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
@media (max-width: ${(props) => props.theme.breakpoints.values.md}px) {
|
|
138
|
+
.sort-button {
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const StyledSearch = styled(Search)`
|
|
145
|
+
background-color: ${(props) => props.theme.palette.grey[50]};
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
export default ListBase;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, List, Checkbox, FormControlLabel } from '@mui/material';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
|
|
5
|
+
import { useSearchContext } from '../contexts/store';
|
|
6
|
+
import { getPrices } from '../tools/utils';
|
|
7
|
+
import Search from './search';
|
|
8
|
+
import { CategoryLinkList } from './categories';
|
|
9
|
+
|
|
10
|
+
const Aside = () => {
|
|
11
|
+
const searchStore = useSearchContext();
|
|
12
|
+
const { queryParams, handlePriceFilter, t } = searchStore;
|
|
13
|
+
return (
|
|
14
|
+
<StyledAside>
|
|
15
|
+
<Search placeholder={t('common.searchStore')} />
|
|
16
|
+
<Box marginTop="24px">
|
|
17
|
+
{getPrices(t).map((item) => (
|
|
18
|
+
<FormControlLabel
|
|
19
|
+
className="payments"
|
|
20
|
+
key={item.value}
|
|
21
|
+
control={
|
|
22
|
+
<Checkbox
|
|
23
|
+
onClick={() => {
|
|
24
|
+
handlePriceFilter(item.value);
|
|
25
|
+
}}
|
|
26
|
+
size="small"
|
|
27
|
+
checked={item.value === queryParams.price}
|
|
28
|
+
/>
|
|
29
|
+
}
|
|
30
|
+
label={item.name}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</Box>
|
|
34
|
+
<List component="nav" color="primary">
|
|
35
|
+
{/* 分割线 */}
|
|
36
|
+
<div style={{ border: '1px solid #dadce0', margin: '16px 16px' }} />
|
|
37
|
+
<CategoryLinkList />
|
|
38
|
+
</List>
|
|
39
|
+
</StyledAside>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const StyledAside = styled.aside`
|
|
44
|
+
width: 220px;
|
|
45
|
+
margin-right: 20px;
|
|
46
|
+
background-color: ${(props) => props.theme.palette.grey[50]};
|
|
47
|
+
height: 100%;
|
|
48
|
+
padding: 15px 0px;
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
.MuiListItemText-root {
|
|
51
|
+
width: 100%;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
text-overflow: ellipsis;
|
|
55
|
+
}
|
|
56
|
+
.payments {
|
|
57
|
+
display: flex;
|
|
58
|
+
padding: 0 16px;
|
|
59
|
+
.MuiFormControlLabel-label {
|
|
60
|
+
color: #5f6368;
|
|
61
|
+
font-size: 1rem;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
.MuiListItem-root {
|
|
65
|
+
text-transform: capitalize;
|
|
66
|
+
.category > span {
|
|
67
|
+
font-weight: ${(props) => props.theme.typography.fontWeightBold};
|
|
68
|
+
}
|
|
69
|
+
> :not(.category) {
|
|
70
|
+
.MuiListItemText-primary {
|
|
71
|
+
color: #5f6368;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
&.Mui-selected {
|
|
75
|
+
background-color: ${(props) => props.theme.palette.primary.main}!important;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
.MuiListItemText-primary {
|
|
78
|
+
color: #fff !important;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
@media (max-width: ${(props) => props.theme.breakpoints.values.md}px) {
|
|
83
|
+
.MuiListItemText-primary {
|
|
84
|
+
font-size: 16px;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
Aside.propTypes = {};
|
|
90
|
+
Aside.defaultProps = {};
|
|
91
|
+
export default Aside;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Button as MuiButton, CircularProgress } from '@mui/material';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
|
|
6
|
+
const StyledButton = styled(MuiButton)`
|
|
7
|
+
border-radius: 4px;
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
const Button = forwardRef(({ children, rounded, loading, disabled, ...rest }, ref) => {
|
|
11
|
+
return (
|
|
12
|
+
<StyledButton
|
|
13
|
+
ref={ref}
|
|
14
|
+
disableElevation
|
|
15
|
+
disabled={disabled || loading}
|
|
16
|
+
{...rest}
|
|
17
|
+
startIcon={loading && <CircularProgress size="1em" />}>
|
|
18
|
+
{children}
|
|
19
|
+
</StyledButton>
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
Button.propTypes = {
|
|
24
|
+
children: PropTypes.any,
|
|
25
|
+
rounded: PropTypes.bool,
|
|
26
|
+
loading: PropTypes.bool,
|
|
27
|
+
disabled: PropTypes.bool,
|
|
28
|
+
};
|
|
29
|
+
Button.defaultProps = {
|
|
30
|
+
children: null,
|
|
31
|
+
rounded: false,
|
|
32
|
+
loading: false,
|
|
33
|
+
disabled: false,
|
|
34
|
+
};
|
|
35
|
+
export default Button;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { MenuItem, ListItem, ListItemText } from '@mui/material';
|
|
4
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
5
|
+
import qs from 'qs';
|
|
6
|
+
|
|
7
|
+
import { useSearchContext } from '../contexts/store';
|
|
8
|
+
import CustomSelect from './custom-select';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 小屏幕下的分类选择器
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
const CategorySelect = () => {
|
|
15
|
+
const searchStore = useSearchContext();
|
|
16
|
+
const { categoryList, selectedCategory, handleCategorySelect, t, locale } = searchStore;
|
|
17
|
+
const itemRender = (item) => {
|
|
18
|
+
return (
|
|
19
|
+
<MenuItem
|
|
20
|
+
className={['my-select__option', selectedCategory?.includes(item.name) ? 'my-select__option--active' : ''].join(
|
|
21
|
+
' '
|
|
22
|
+
)}
|
|
23
|
+
key={item._id}
|
|
24
|
+
value={item.name}
|
|
25
|
+
onClick={() => handleCategorySelect(item.name)}>
|
|
26
|
+
{item.locales[locale]}
|
|
27
|
+
</MenuItem>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<CustomSelect
|
|
33
|
+
value={selectedCategory || 'all'}
|
|
34
|
+
options={categoryList}
|
|
35
|
+
title={categoryList.find((i) => i.name === selectedCategory)?.locales[locale] || t('category.all')}
|
|
36
|
+
itemRender={itemRender}
|
|
37
|
+
prepend={
|
|
38
|
+
<MenuItem value="all" onClick={() => handleCategorySelect('all')}>
|
|
39
|
+
{t('category.all')}
|
|
40
|
+
</MenuItem>
|
|
41
|
+
}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* 宽屏幕下的分类列表
|
|
47
|
+
* @returns
|
|
48
|
+
*/
|
|
49
|
+
const CategoryLinkList = () => {
|
|
50
|
+
const { t, locale } = useLocaleContext();
|
|
51
|
+
const searchStore = useSearchContext();
|
|
52
|
+
const { queryParams, categoryList, isSearchPage, selectedCategory, isPageMode, handleCategorySelect, baseUrl } =
|
|
53
|
+
searchStore;
|
|
54
|
+
|
|
55
|
+
let content = null;
|
|
56
|
+
if (isPageMode) {
|
|
57
|
+
content = (
|
|
58
|
+
<div style={{ marginRight: '16px' }}>
|
|
59
|
+
<ListItem
|
|
60
|
+
selected={!selectedCategory}
|
|
61
|
+
button
|
|
62
|
+
component={Link}
|
|
63
|
+
to={!isSearchPage ? baseUrl : `${baseUrl}search?${qs.stringify({ ...queryParams, category: undefined })}`}>
|
|
64
|
+
<ListItemText primary={t('category.all')} />
|
|
65
|
+
</ListItem>
|
|
66
|
+
{categoryList.map((item) => (
|
|
67
|
+
<ListItem
|
|
68
|
+
key={item._id}
|
|
69
|
+
title={item.locales[locale]}
|
|
70
|
+
button
|
|
71
|
+
selected={item.name === selectedCategory}
|
|
72
|
+
component={Link}
|
|
73
|
+
to={
|
|
74
|
+
!isSearchPage
|
|
75
|
+
? `${baseUrl}category/${item.name}`
|
|
76
|
+
: `${baseUrl}search?${qs.stringify({ ...queryParams, category: item.name })}`
|
|
77
|
+
}>
|
|
78
|
+
<ListItemText primary={item.locales[locale]} />
|
|
79
|
+
</ListItem>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
content = (
|
|
85
|
+
<div style={{ marginRight: '16px' }}>
|
|
86
|
+
<ListItem selected={!selectedCategory} button onClick={() => handleCategorySelect('all')}>
|
|
87
|
+
<ListItemText primary={t('category.all')} />
|
|
88
|
+
</ListItem>
|
|
89
|
+
{categoryList.map((item) => (
|
|
90
|
+
<ListItem
|
|
91
|
+
key={item._id}
|
|
92
|
+
title={item.locales[locale]}
|
|
93
|
+
onClick={() => handleCategorySelect(item.name)}
|
|
94
|
+
button
|
|
95
|
+
selected={item.name === selectedCategory}>
|
|
96
|
+
<ListItemText primary={item.locales[locale]} />
|
|
97
|
+
</ListItem>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<ListItem>
|
|
105
|
+
<ListItemText className="category" primary={t('common.category')} />
|
|
106
|
+
</ListItem>
|
|
107
|
+
{content}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
export { CategorySelect, CategoryLinkList };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* eslint-disable no-unused-expressions */
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import useTheme from '@mui/styles/useTheme';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
import PropTypes from 'prop-types';
|
|
6
|
+
import { ClickAwayListener, Grow, MenuItem, MenuList, Paper, Popper, SvgIcon, useMediaQuery } from '@mui/material';
|
|
7
|
+
import { Check, ChevronDown } from 'mdi-material-ui';
|
|
8
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
9
|
+
import isEmpty from 'lodash/isEmpty';
|
|
10
|
+
import isEqual from 'lodash/isEqual';
|
|
11
|
+
|
|
12
|
+
import Button from './button';
|
|
13
|
+
|
|
14
|
+
const CustomSelect = ({
|
|
15
|
+
title,
|
|
16
|
+
value,
|
|
17
|
+
icon,
|
|
18
|
+
prepend,
|
|
19
|
+
options,
|
|
20
|
+
multiple,
|
|
21
|
+
onClose,
|
|
22
|
+
onShow,
|
|
23
|
+
onChange,
|
|
24
|
+
onInput,
|
|
25
|
+
itemRender,
|
|
26
|
+
...buttonProps
|
|
27
|
+
}) => {
|
|
28
|
+
const anchorRef = useRef(null);
|
|
29
|
+
const theme = useTheme();
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
const [currentValue, setCurrentValue] = value !== null ? useState(value) : useState(multiple ? [] : '');
|
|
32
|
+
const isSm = useMediaQuery(theme.breakpoints.down('sm'));
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// eslint-disable-next-line no-nested-ternary
|
|
36
|
+
setCurrentValue(value !== null ? value : multiple ? [] : '');
|
|
37
|
+
}, [value]);
|
|
38
|
+
|
|
39
|
+
function closeMenu() {
|
|
40
|
+
isEqual(value, currentValue) || onInput(currentValue);
|
|
41
|
+
onClose();
|
|
42
|
+
setOpen(false);
|
|
43
|
+
}
|
|
44
|
+
function openMenu() {
|
|
45
|
+
setOpen(true);
|
|
46
|
+
onShow();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toggle(option) {
|
|
50
|
+
if (multiple) {
|
|
51
|
+
const copyValue = cloneDeep(currentValue);
|
|
52
|
+
const index = copyValue.indexOf(option.value);
|
|
53
|
+
if (index >= 0) {
|
|
54
|
+
copyValue.splice(index, 1);
|
|
55
|
+
} else {
|
|
56
|
+
copyValue.push(option.value);
|
|
57
|
+
}
|
|
58
|
+
setCurrentValue(copyValue);
|
|
59
|
+
onChange(copyValue);
|
|
60
|
+
} else {
|
|
61
|
+
setCurrentValue(option.value);
|
|
62
|
+
onChange(option.value);
|
|
63
|
+
if (isSm) {
|
|
64
|
+
closeMenu();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function containsValue(optionValue) {
|
|
69
|
+
return multiple ? currentValue.includes(optionValue) : optionValue === currentValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<StyledButton
|
|
75
|
+
ref={anchorRef}
|
|
76
|
+
onClick={openMenu}
|
|
77
|
+
variant="outlined"
|
|
78
|
+
size="small"
|
|
79
|
+
className={['my-select__selector', isEmpty(currentValue) ? '' : 'my-select__selector--active'].join(' ')}
|
|
80
|
+
{...buttonProps}>
|
|
81
|
+
<div className="my-select__icon">{icon}</div>
|
|
82
|
+
{title}
|
|
83
|
+
{multiple && currentValue.length > 1 && ` (${currentValue.length})`}
|
|
84
|
+
<SvgIcon className="my-select__arrowdown" component={ChevronDown} fontSize="small" />
|
|
85
|
+
</StyledButton>
|
|
86
|
+
<Popper open={open} anchorEl={anchorRef.current} transition style={{ zIndex: '1' }}>
|
|
87
|
+
{({ TransitionProps, placement }) => (
|
|
88
|
+
<Grow
|
|
89
|
+
{...TransitionProps}
|
|
90
|
+
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}>
|
|
91
|
+
<Paper>
|
|
92
|
+
<ClickAwayListener onClickAway={closeMenu}>
|
|
93
|
+
<StyledMenuList autoFocusItem={open} onMouseEnter={openMenu} onMouseLeave={closeMenu}>
|
|
94
|
+
{prepend}
|
|
95
|
+
{options.map((option) => {
|
|
96
|
+
if (itemRender) {
|
|
97
|
+
return itemRender(option);
|
|
98
|
+
}
|
|
99
|
+
return (
|
|
100
|
+
<MenuItem
|
|
101
|
+
key={option.value}
|
|
102
|
+
onClick={() => toggle(option)}
|
|
103
|
+
className={[
|
|
104
|
+
'my-select__option',
|
|
105
|
+
containsValue(option.value) ? 'my-select__option--active' : '',
|
|
106
|
+
].join(' ')}>
|
|
107
|
+
{multiple && (
|
|
108
|
+
<SvgIcon
|
|
109
|
+
component={Check}
|
|
110
|
+
className={[
|
|
111
|
+
'my-select__option__icon',
|
|
112
|
+
containsValue(option.value) ? 'my-select__option__icon--active' : '',
|
|
113
|
+
].join(' ')}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
{option.name}
|
|
117
|
+
</MenuItem>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</StyledMenuList>
|
|
121
|
+
</ClickAwayListener>
|
|
122
|
+
</Paper>
|
|
123
|
+
</Grow>
|
|
124
|
+
)}
|
|
125
|
+
</Popper>
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
CustomSelect.propTypes = {
|
|
131
|
+
options: PropTypes.array.isRequired,
|
|
132
|
+
multiple: PropTypes.bool,
|
|
133
|
+
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
|
134
|
+
onShow: PropTypes.func,
|
|
135
|
+
onClose: PropTypes.func,
|
|
136
|
+
onInput: PropTypes.func,
|
|
137
|
+
onChange: PropTypes.func,
|
|
138
|
+
itemRender: PropTypes.func,
|
|
139
|
+
title: PropTypes.string.isRequired,
|
|
140
|
+
icon: PropTypes.any,
|
|
141
|
+
prepend: PropTypes.any,
|
|
142
|
+
};
|
|
143
|
+
CustomSelect.defaultProps = {
|
|
144
|
+
multiple: false,
|
|
145
|
+
value: null,
|
|
146
|
+
icon: null,
|
|
147
|
+
prepend: null,
|
|
148
|
+
itemRender: null,
|
|
149
|
+
onShow: () => {},
|
|
150
|
+
onClose: () => {},
|
|
151
|
+
onInput: () => {},
|
|
152
|
+
onChange: () => {},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const StyledButton = styled(Button)`
|
|
156
|
+
border: 1px solid #f0f0f0;
|
|
157
|
+
padding: 6px 8px 6px 12px;
|
|
158
|
+
font-weight: ${(props) => props.theme.typography.fontWeightBold};
|
|
159
|
+
font-size: 16px;
|
|
160
|
+
color: #666;
|
|
161
|
+
line-height: 1;
|
|
162
|
+
text-transform: none;
|
|
163
|
+
min-width: 100px;
|
|
164
|
+
& + & {
|
|
165
|
+
margin-left: 10px;
|
|
166
|
+
}
|
|
167
|
+
&.my-select__selector--active {
|
|
168
|
+
&,
|
|
169
|
+
.my-select__arrowdown,
|
|
170
|
+
.my-select__icon svg {
|
|
171
|
+
color: ${(props) => props.theme.palette.primary.main};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
.my-select__arrowdown {
|
|
175
|
+
color: #999;
|
|
176
|
+
font-size: 16px;
|
|
177
|
+
margin-left: 6px;
|
|
178
|
+
}
|
|
179
|
+
.my-select__icon {
|
|
180
|
+
font-size: 0;
|
|
181
|
+
svg {
|
|
182
|
+
color: #999;
|
|
183
|
+
font-size: 18px;
|
|
184
|
+
margin-right: 3px;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const StyledMenuList = styled(MenuList)`
|
|
190
|
+
.my-select__option__icon {
|
|
191
|
+
color: transparent;
|
|
192
|
+
font-size: 16px;
|
|
193
|
+
margin: 0 3px 0 -5px;
|
|
194
|
+
}
|
|
195
|
+
.my-select__option {
|
|
196
|
+
font-size: 16px;
|
|
197
|
+
color: #999;
|
|
198
|
+
}
|
|
199
|
+
.my-select__option--active {
|
|
200
|
+
&,
|
|
201
|
+
.my-select__option__icon {
|
|
202
|
+
color: ${(props) => props.theme.palette.primary.main};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
export default CustomSelect;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import Typography from '@mui/material/Typography';
|
|
5
|
+
|
|
6
|
+
import { useSearchContext } from '../contexts/store';
|
|
7
|
+
|
|
8
|
+
const NoResults = () => {
|
|
9
|
+
const searchStore = useSearchContext();
|
|
10
|
+
const { t } = searchStore;
|
|
11
|
+
return (
|
|
12
|
+
<Typography style={{ textAlign: 'center' }} variant="subtitle2">
|
|
13
|
+
{t('blocklet.noResults')}
|
|
14
|
+
</Typography>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
const NoResultsTips = ({ filterTip, keywordTip }) => {
|
|
18
|
+
const searchStore = useSearchContext();
|
|
19
|
+
const { t, locale } = searchStore;
|
|
20
|
+
const getSplit = () => {
|
|
21
|
+
if (locale === 'zh') return '、';
|
|
22
|
+
return ' , ';
|
|
23
|
+
};
|
|
24
|
+
return (
|
|
25
|
+
<Box className="tips">
|
|
26
|
+
<span style={{ marginRight: '16px' }}>{t('blocklet.emptyTip')}</span>
|
|
27
|
+
{filterTip && <span>{t('blocklet.filterTip')}</span>}
|
|
28
|
+
{filterTip && keywordTip && getSplit()}
|
|
29
|
+
{keywordTip && <span>{t('blocklet.keywordTip')}</span>}
|
|
30
|
+
</Box>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
NoResultsTips.propTypes = {
|
|
34
|
+
filterTip: PropTypes.bool,
|
|
35
|
+
keywordTip: PropTypes.bool,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
NoResultsTips.defaultProps = {
|
|
39
|
+
filterTip: false,
|
|
40
|
+
keywordTip: false,
|
|
41
|
+
};
|
|
42
|
+
const EmptyTitle = ({ primaryStart, primaryEnd, search }) => {
|
|
43
|
+
return (
|
|
44
|
+
<Typography variant="subtitle2">
|
|
45
|
+
<span>{primaryStart}</span>
|
|
46
|
+
<span className="primary"> {search} </span>
|
|
47
|
+
<span>{primaryEnd} </span>
|
|
48
|
+
</Typography>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
EmptyTitle.propTypes = {
|
|
52
|
+
primaryStart: PropTypes.string.isRequired,
|
|
53
|
+
primaryEnd: PropTypes.string.isRequired,
|
|
54
|
+
search: PropTypes.string.isRequired,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export { NoResults, EmptyTitle, NoResultsTips };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { Chip } from '@mui/material';
|
|
5
|
+
import FaceIcon from '@mui/icons-material/Face';
|
|
6
|
+
|
|
7
|
+
import { useSearchContext } from '../contexts/store';
|
|
8
|
+
|
|
9
|
+
const StyleDiv = styled.div`
|
|
10
|
+
.MuiChip-root {
|
|
11
|
+
border-radius: 4px;
|
|
12
|
+
height: initial;
|
|
13
|
+
text-transform: capitalize;
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
const FilterAuthor = ({ user, deleteUserTag, ...containerProps }) => {
|
|
17
|
+
const searchStore = useSearchContext();
|
|
18
|
+
const { t } = searchStore;
|
|
19
|
+
if (!user) return null;
|
|
20
|
+
return (
|
|
21
|
+
<StyleDiv {...containerProps}>
|
|
22
|
+
<Chip
|
|
23
|
+
icon={<FaceIcon />}
|
|
24
|
+
label={t('blocklet.owner', { name: user })}
|
|
25
|
+
onDelete={() => {
|
|
26
|
+
deleteUserTag();
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
</StyleDiv>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
FilterAuthor.propTypes = {
|
|
33
|
+
user: PropTypes.string.isRequired,
|
|
34
|
+
deleteUserTag: PropTypes.func,
|
|
35
|
+
};
|
|
36
|
+
FilterAuthor.defaultProps = {
|
|
37
|
+
deleteUserTag: () => {},
|
|
38
|
+
};
|
|
39
|
+
export default FilterAuthor;
|