@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/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;