@blocklet/list 0.8.33 → 0.8.36

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 CHANGED
@@ -13,6 +13,10 @@ var _material = require("@mui/material");
13
13
 
14
14
  var _Face = _interopRequireDefault(require("@mui/icons-material/Face"));
15
15
 
16
+ var _reactErrorBoundary = require("react-error-boundary");
17
+
18
+ var _ErrorBoundary = require("@arcblock/ux/lib/ErrorBoundary");
19
+
16
20
  var _filter = require("./contexts/filter");
17
21
 
18
22
  var _customSelect = _interopRequireDefault(require("./components/custom-select"));
@@ -36,7 +40,7 @@ function ListBase() {
36
40
 
37
41
  const {
38
42
  handleDeveloper,
39
- blockletList,
43
+ finalBlockletList,
40
44
  filters,
41
45
  developerName,
42
46
  handleSort,
@@ -104,8 +108,11 @@ function ListBase() {
104
108
  handlePrice(null);
105
109
  }
106
110
  })]
107
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_list.default, {
108
- blocklets: blockletList
111
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactErrorBoundary.ErrorBoundary, {
112
+ FallbackComponent: _ErrorBoundary.ErrorFallback,
113
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_list.default, {
114
+ blocklets: finalBlockletList
115
+ })
109
116
  })]
110
117
  })]
111
118
  });
@@ -33,7 +33,7 @@ function Aside() {
33
33
  value: filters.price,
34
34
  onChange: handlePrice
35
35
  })
36
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
36
+ }), categoryOptions.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
37
37
  style: {
38
38
  marginTop: '16px'
39
39
  },
@@ -43,6 +43,7 @@ function FilterChip(_ref) {
43
43
  if (!label) return null;
44
44
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(StyleDiv, _objectSpread(_objectSpread({}, containerProps), {}, {
45
45
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_material.Chip, {
46
+ "data-cy": "filter-tag",
46
47
  icon: icon,
47
48
  label: label,
48
49
  onDelete: onDelete
@@ -68,7 +68,7 @@ function FilterIcon() {
68
68
  onChange: v => {
69
69
  handelChange('price', v);
70
70
  }
71
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
71
+ }), categoryOptions.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
72
72
  style: {
73
73
  marginTop: '16px'
74
74
  },
@@ -5,26 +5,28 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = BlockletList;
7
7
 
8
+ var _react = require("react");
9
+
8
10
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
11
 
10
12
  var _styledComponents = _interopRequireDefault(require("styled-components"));
11
13
 
12
14
  var _Empty = _interopRequireDefault(require("@arcblock/ux/lib/Empty"));
13
15
 
14
- var _Alert = _interopRequireDefault(require("@arcblock/ux/lib/Alert"));
15
-
16
16
  var _Box = _interopRequireDefault(require("@mui/material/Box"));
17
17
 
18
18
  var _Grid = _interopRequireDefault(require("@mui/material/Grid"));
19
19
 
20
20
  var _CircularProgress = _interopRequireDefault(require("@mui/material/CircularProgress"));
21
21
 
22
+ var _reactInfiniteScrollHook = _interopRequireDefault(require("react-infinite-scroll-hook"));
23
+
24
+ var _ErrorBoundary = require("@arcblock/ux/lib/ErrorBoundary");
25
+
22
26
  var _empty = require("./empty");
23
27
 
24
28
  var _filter = require("../../contexts/filter");
25
29
 
26
- var _utils = require("../../libs/utils");
27
-
28
30
  var _jsxRuntime = require("react/jsx-runtime");
29
31
 
30
32
  const _excluded = ["blocklets"];
@@ -52,20 +54,25 @@ function BlockletList(_ref) {
52
54
  errors,
53
55
  loadings,
54
56
  selectedCategory,
55
- blockletList,
57
+ finalBlockletList,
56
58
  getCategoryLocale,
57
59
  filters,
58
- t
60
+ t,
61
+ hasNextPage,
62
+ loadMore,
63
+ endpoint
59
64
  } = (0, _filter.useFilterContext)();
60
65
  const showFilterTip = !!selectedCategory || !!filters.price;
66
+ const [sentryRef] = (0, _reactInfiniteScrollHook.default)({
67
+ loading: loadings.fetchBlockletsLoading,
68
+ hasNextPage,
69
+ onLoadMore: loadMore,
70
+ rootMargin: '0px 0px 400px 0px'
71
+ });
61
72
 
62
73
  if (errors.fetchBlockletsError) {
63
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Alert.default, {
64
- type: "error",
65
- variant: "icon",
66
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
67
- children: (0, _utils.formatError)(errors.fetchBlockletsError)
68
- })
74
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ErrorBoundary.ErrorFallback, {
75
+ error: new Error("Failed to fetch blocklets from ".concat(endpoint, ": ").concat(errors.fetchBlockletsError.message))
69
76
  });
70
77
  }
71
78
 
@@ -78,7 +85,7 @@ function BlockletList(_ref) {
78
85
  });
79
86
  }
80
87
 
81
- if (filters.keyword && showFilterTip && blockletList.length === 0) {
88
+ if (filters.keyword && showFilterTip && finalBlockletList.length === 0) {
82
89
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(CustomEmpty, {
83
90
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_empty.EmptyTitle, {
84
91
  primaryStart: t('blocklet.noBlockletPart1'),
@@ -91,7 +98,7 @@ function BlockletList(_ref) {
91
98
  });
92
99
  }
93
100
 
94
- if (filters.keyword && blockletList.length === 0) {
101
+ if (filters.keyword && finalBlockletList.length === 0) {
95
102
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(CustomEmpty, {
96
103
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_empty.EmptyTitle, {
97
104
  primaryStart: t('blocklet.noBlockletPart1'),
@@ -103,7 +110,7 @@ function BlockletList(_ref) {
103
110
  });
104
111
  }
105
112
 
106
- if (showFilterTip && blockletList.length === 0) {
113
+ if (showFilterTip && finalBlockletList.length === 0) {
107
114
  const categoryLocale = getCategoryLocale(selectedCategory);
108
115
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(CustomEmpty, {
109
116
  children: [categoryLocale ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_empty.EmptyTitle, {
@@ -116,16 +123,16 @@ function BlockletList(_ref) {
116
123
  });
117
124
  }
118
125
 
119
- if (blockletList.length === 0) {
126
+ if (finalBlockletList.length === 0) {
120
127
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(CustomEmpty, {
121
128
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_empty.NoResults, {})
122
129
  });
123
130
  }
124
131
 
125
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(StyledGrid, _objectSpread(_objectSpread({
132
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(StyledGrid, _objectSpread(_objectSpread({
126
133
  container: true
127
134
  }, rest), {}, {
128
- children: blocklets.map(blocklet => /*#__PURE__*/(0, _jsxRuntime.jsx)(StyledGridItem, {
135
+ children: [blocklets.map(blocklet => /*#__PURE__*/(0, _jsxRuntime.jsx)(StyledGridItem, {
129
136
  item: true,
130
137
  lg: 4,
131
138
  md: 6,
@@ -134,9 +141,20 @@ function BlockletList(_ref) {
134
141
  "data-blocklet-did": blocklet.did,
135
142
  children: blockletRender({
136
143
  blocklet,
137
- blocklets: blockletList
144
+ blocklets: finalBlockletList
145
+ })
146
+ }, blocklet.did)), hasNextPage && /*#__PURE__*/(0, _jsxRuntime.jsx)(StyledGridItem, {
147
+ item: true,
148
+ md: 12,
149
+ sm: 12,
150
+ xs: 12,
151
+ ref: sentryRef,
152
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Box.default, {
153
+ display: "flex",
154
+ justifyContent: "center",
155
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_CircularProgress.default, {})
138
156
  })
139
- }, blocklet.did))
157
+ })]
140
158
  }));
141
159
  }
142
160
 
@@ -148,10 +166,10 @@ const StyledGrid = (0, _styledComponents.default)(_Grid.default).withConfig({
148
166
  displayName: "list__StyledGrid",
149
167
  componentId: "sc-1guvpon-0"
150
168
  })(["&.MuiGrid-root{width:auto;margin:0 -16px;}"]);
151
- const StyledGridItem = (0, _styledComponents.default)(_Grid.default).withConfig({
169
+ const StyledGridItem = /*#__PURE__*/(0, _react.memo)((0, _styledComponents.default)(_Grid.default).withConfig({
152
170
  displayName: "list__StyledGridItem",
153
171
  componentId: "sc-1guvpon-1"
154
- })(["@media (max-width:", "px){&.MuiGrid-item{padding-bottom:0px;}}@media (min-width:", "px){&.MuiGrid-item{margin-bottom:", ";}}"], props => props.theme.breakpoints.values.sm, props => props.theme.breakpoints.values.sm, props => props.theme.spacing(2));
172
+ })(["@media (max-width:", "px){&.MuiGrid-item{padding-bottom:0px;}}@media (min-width:", "px){&.MuiGrid-item{margin-bottom:", ";}}"], props => props.theme.breakpoints.values.sm, props => props.theme.breakpoints.values.sm, props => props.theme.spacing(2)));
155
173
  const CustomEmpty = (0, _styledComponents.default)(_Empty.default).withConfig({
156
174
  displayName: "list__CustomEmpty",
157
175
  componentId: "sc-1guvpon-2"
@@ -17,6 +17,8 @@ var _orderBy = _interopRequireDefault(require("lodash/orderBy"));
17
17
 
18
18
  var _axios = _interopRequireDefault(require("axios"));
19
19
 
20
+ var _isArray = _interopRequireDefault(require("lodash/isArray"));
21
+
20
22
  var _utils = require("../libs/utils");
21
23
 
22
24
  var _locale = _interopRequireDefault(require("../assets/locale"));
@@ -44,7 +46,6 @@ function FilterProvider(_ref) {
44
46
  let {
45
47
  filters,
46
48
  children,
47
- baseUrl,
48
49
  endpoint,
49
50
  locale,
50
51
  blockletRender,
@@ -63,9 +64,14 @@ function FilterProvider(_ref) {
63
64
  run: fetchBlocklets
64
65
  } = (0, _ahooks.useRequest)(async () => {
65
66
  const {
66
- data: list
67
+ data
67
68
  } = await storeApi.get('/api/blocklets.json');
68
- return list;
69
+
70
+ if (!(0, _isArray.default)(data)) {
71
+ throw new Error('/api/blocklets.json response is not array');
72
+ }
73
+
74
+ return data;
69
75
  }, {
70
76
  initialData: [],
71
77
  manual: true
@@ -77,13 +83,23 @@ function FilterProvider(_ref) {
77
83
  run: fetchCategories
78
84
  } = (0, _ahooks.useRequest)(async () => {
79
85
  const {
80
- data: list
86
+ data
81
87
  } = await storeApi.get('/api/blocklets/categories');
82
- return list;
88
+
89
+ if (!(0, _isArray.default)(data)) {
90
+ throw new Error('/api/blocklets/categories response is not array');
91
+ }
92
+
93
+ return data;
83
94
  }, {
84
95
  initialData: [],
85
96
  manual: true
86
97
  });
98
+ const paginateState = (0, _ahooks.useReactive)({
99
+ currentPage: 1,
100
+ pageSize: (0, _utils.isMobileScreen)() ? 10 : 18,
101
+ defaultCurrentPage: 1
102
+ });
87
103
 
88
104
  const finalFilters = _objectSpread({
89
105
  sortBy: 'popularity',
@@ -93,10 +109,11 @@ function FilterProvider(_ref) {
93
109
  const selectedCategory = finalFilters.category;
94
110
  const hasDeveloperFilter = !!finalFilters.developer;
95
111
  const categoryState = (0, _react.useMemo)(() => {
112
+ // 当按作者过滤时,需要从所有blocklets中找出属于作者的分类
96
113
  return !hasDeveloperFilter ? {
97
114
  data: allCategories
98
- } : (0, _utils.getCategories)(allBlocklets, finalFilters.developer);
99
- }, [hasDeveloperFilter, allCategories]);
115
+ } : (0, _utils.getCategoriesByAuthor)(allBlocklets, finalFilters.developer);
116
+ }, [hasDeveloperFilter, allCategories, allBlocklets]);
100
117
  const blockletList = (0, _react.useMemo)(() => {
101
118
  var _finalFilters$keyword;
102
119
 
@@ -142,6 +159,10 @@ function FilterProvider(_ref) {
142
159
 
143
160
  return (0, _orderBy.default)(blocklets, [sortMap[finalFilters.sortBy]], [finalFilters.sortDirection]);
144
161
  }, [allBlocklets, finalFilters]);
162
+ const finalBlockletList = (0, _react.useMemo)(() => {
163
+ // 前端分页 currentPage 当前页数 pageSize 每页条数
164
+ return blockletList.slice((paginateState.defaultCurrentPage - 1) * paginateState.pageSize, paginateState.currentPage * paginateState.pageSize);
165
+ }, [blockletList, paginateState]);
145
166
  const categoryList = (0, _react.useMemo)(() => {
146
167
  const list = categoryState.data || []; // 分类按照名称排序
147
168
 
@@ -169,16 +190,16 @@ function FilterProvider(_ref) {
169
190
  fetchCategoriesLoading
170
191
  },
171
192
  endpoint,
172
- blockletList,
193
+ finalBlockletList,
173
194
  t: translate,
174
195
  filters: finalFilters,
175
196
  selectedCategory,
176
197
  categoryList,
177
- baseUrl,
178
198
  blockletRender,
179
199
  locale,
180
200
  categoryOptions,
181
201
  priceOptions,
202
+ hasNextPage: blockletList.length >= paginateState.pageSize * paginateState.currentPage,
182
203
  handleSort: sort => {
183
204
  const changeData = _objectSpread(_objectSpread({}, finalFilters), {}, {
184
205
  sortBy: sort,
@@ -221,6 +242,18 @@ function FilterProvider(_ref) {
221
242
 
222
243
  onFilterChange(changeData);
223
244
  },
245
+ handlePage: page => {
246
+ const changeData = _objectSpread(_objectSpread({}, finalFilters), {}, {
247
+ currentPage: page
248
+ });
249
+
250
+ onFilterChange(changeData);
251
+ },
252
+ loadMore: () => {
253
+ setTimeout(() => {
254
+ paginateState.currentPage += 1;
255
+ }, 1000);
256
+ },
224
257
  getCategoryLocale: category => {
225
258
  if (!category) return null;
226
259
  let result = null;
@@ -233,10 +266,15 @@ function FilterProvider(_ref) {
233
266
  return result;
234
267
  },
235
268
 
269
+ get allBlocklets() {
270
+ return allBlocklets || [];
271
+ },
272
+
236
273
  get developerName() {
237
- var _allBlocklets$find, _allBlocklets$find$ow;
274
+ var _blocklets$find, _blocklets$find$owner;
238
275
 
239
- return ((_allBlocklets$find = allBlocklets.find(blocklet => blocklet.owner.did === finalFilters.developer)) === null || _allBlocklets$find === void 0 ? void 0 : (_allBlocklets$find$ow = _allBlocklets$find.owner) === null || _allBlocklets$find$ow === void 0 ? void 0 : _allBlocklets$find$ow.name) || '';
276
+ const blocklets = allBlocklets || [];
277
+ return ((_blocklets$find = blocklets.find(blocklet => blocklet.owner.did === finalFilters.developer)) === null || _blocklets$find === void 0 ? void 0 : (_blocklets$find$owner = _blocklets$find.owner) === null || _blocklets$find$owner === void 0 ? void 0 : _blocklets$find$owner.name) || '';
240
278
  }
241
279
 
242
280
  };
@@ -22,12 +22,10 @@ const propTypes = {
22
22
  endpoint: _propTypes.default.string.isRequired,
23
23
  blockletRender: _propTypes.default.func.isRequired,
24
24
  onFilterChange: _propTypes.default.func,
25
- baseUrl: _propTypes.default.string,
26
25
  locale: _propTypes.default.oneOf(['zh', 'en'])
27
26
  };
28
27
  exports.propTypes = propTypes;
29
28
  const defaultProps = {
30
- baseUrl: null,
31
29
  locale: 'zh',
32
30
  filters: {},
33
31
  onFilterChange: () => {},
package/lib/libs/utils.js CHANGED
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.urlStringify = exports.replaceTranslate = exports.removeUndefined = exports.getStoreDetail = exports.getSortOptions = exports.getPrices = exports.getCategoryOptions = exports.getCategories = exports.formatLogoPath = exports.formatError = exports.filterBlockletByPrice = void 0;
6
+ exports.urlStringify = exports.replaceTranslate = exports.removeUndefined = exports.isMobileScreen = exports.getStoreDetail = exports.getSortOptions = exports.getPrices = exports.getCategoryOptions = exports.getCategoriesByAuthor = exports.formatLogoPath = exports.formatError = exports.filterBlockletByPrice = void 0;
7
7
 
8
8
  var _urlJoin = _interopRequireDefault(require("url-join"));
9
9
 
@@ -59,7 +59,7 @@ const getCategoryOptions = function getCategoryOptions() {
59
59
  }));
60
60
  };
61
61
  /**
62
- * 从开发者所属 blocklets 中的得到 Categories
62
+ * blocklets 中的得到属于作者的 Categories
63
63
  * @param {*} list
64
64
  * @param {*} developerDid
65
65
  * @returns
@@ -68,7 +68,9 @@ const getCategoryOptions = function getCategoryOptions() {
68
68
 
69
69
  exports.getCategoryOptions = getCategoryOptions;
70
70
 
71
- const getCategories = (list, developerDid) => {
71
+ const getCategoriesByAuthor = function getCategoriesByAuthor() {
72
+ let list = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
73
+ let developerDid = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
72
74
  const filterList = list.filter(item => developerDid ? item.owner.did === developerDid : true);
73
75
  const Categories = filterList.map(item => item.category);
74
76
  const res = new Map();
@@ -85,7 +87,7 @@ const getCategories = (list, developerDid) => {
85
87
  */
86
88
 
87
89
 
88
- exports.getCategories = getCategories;
90
+ exports.getCategoriesByAuthor = getCategoriesByAuthor;
89
91
 
90
92
  const filterBlockletByPrice = function filterBlockletByPrice() {
91
93
  let list = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
@@ -157,4 +159,10 @@ const urlStringify = obj => {
157
159
  return new URLSearchParams(removeUndefined(obj)).toString();
158
160
  };
159
161
 
160
- exports.urlStringify = urlStringify;
162
+ exports.urlStringify = urlStringify;
163
+
164
+ const isMobileScreen = () => {
165
+ return window.innerWidth <= 600;
166
+ };
167
+
168
+ exports.isMobileScreen = isMobileScreen;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/list",
3
- "version": "0.8.33",
3
+ "version": "0.8.36",
4
4
  "description": "Common ux components of blocklet",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -38,7 +38,7 @@
38
38
  "react": ">=18.1.0"
39
39
  },
40
40
  "dependencies": {
41
- "@arcblock/ux": "^2.1.17",
41
+ "@arcblock/ux": "^2.1.23",
42
42
  "@emotion/react": "^11.9.0",
43
43
  "@emotion/styled": "^11.8.1",
44
44
  "@mui/icons-material": "^5.6.2",
@@ -47,6 +47,8 @@
47
47
  "flat": "^5.0.2",
48
48
  "lodash": "^4.17.21",
49
49
  "prop-types": "^15.7.2",
50
+ "react-error-boundary": "^3.1.4",
51
+ "react-infinite-scroll-hook": "^4.0.3",
50
52
  "styled-components": "5.3.5",
51
53
  "url-join": "^4.0.1"
52
54
  },
@@ -62,5 +64,5 @@
62
64
  "eslint": "^8.16.0",
63
65
  "prettier": "^2.6.2"
64
66
  },
65
- "gitHead": "5e7c02cb3dfd0edff3f8290509f754b10bbc61bc"
67
+ "gitHead": "d7102af63503db57cb89f856a50fd83e03d1dbdb"
66
68
  }
package/src/base.js CHANGED
@@ -2,6 +2,8 @@ import styled from 'styled-components';
2
2
  import SortIcon from '@mui/icons-material/Sort';
3
3
  import { Box, Hidden } from '@mui/material';
4
4
  import FaceIcon from '@mui/icons-material/Face';
5
+ import { ErrorBoundary } from 'react-error-boundary';
6
+ import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
5
7
 
6
8
  import { useFilterContext } from './contexts/filter';
7
9
  import CustomSelect from './components/custom-select';
@@ -14,7 +16,7 @@ import Search from './components/search';
14
16
  function ListBase() {
15
17
  const {
16
18
  handleDeveloper,
17
- blockletList,
19
+ finalBlockletList,
18
20
  filters,
19
21
  developerName,
20
22
  handleSort,
@@ -76,7 +78,9 @@ function ListBase() {
76
78
  }}
77
79
  />
78
80
  </Box>
79
- <BlockletList blocklets={blockletList} />
81
+ <ErrorBoundary FallbackComponent={ErrorFallback}>
82
+ <BlockletList blocklets={finalBlockletList} />
83
+ </ErrorBoundary>
80
84
  </StyledMin>
81
85
  </Box>
82
86
  );
@@ -12,14 +12,16 @@ function Aside() {
12
12
  <div>
13
13
  <FilterGroup title={t('common.price')} options={priceOptions} value={filters.price} onChange={handlePrice} />
14
14
  </div>
15
- <div style={{ marginTop: '16px' }}>
16
- <FilterGroup
17
- title={t('common.category')}
18
- options={categoryOptions}
19
- value={selectedCategory}
20
- onChange={handleCategory}
21
- />
22
- </div>
15
+ {categoryOptions.length > 0 && (
16
+ <div style={{ marginTop: '16px' }}>
17
+ <FilterGroup
18
+ title={t('common.category')}
19
+ options={categoryOptions}
20
+ value={selectedCategory}
21
+ onChange={handleCategory}
22
+ />
23
+ </div>
24
+ )}
23
25
  </StyledAside>
24
26
  );
25
27
  }
@@ -15,7 +15,7 @@ function FilterChip({ label, icon, onDelete, ...containerProps }) {
15
15
  if (!label) return null;
16
16
  return (
17
17
  <StyleDiv {...containerProps}>
18
- <Chip icon={icon} label={label} onDelete={onDelete} />
18
+ <Chip data-cy="filter-tag" icon={icon} label={label} onDelete={onDelete} />
19
19
  </StyleDiv>
20
20
  );
21
21
  }
@@ -36,16 +36,18 @@ function FilterIcon() {
36
36
  handelChange('price', v);
37
37
  }}
38
38
  />
39
- <div style={{ marginTop: '16px' }}>
40
- <FilterGroup
41
- title={t('common.category')}
42
- options={categoryOptions}
43
- value={selectedCategory}
44
- onChange={(v) => {
45
- handelChange('category', v);
46
- }}
47
- />
48
- </div>
39
+ {categoryOptions.length > 0 && (
40
+ <div style={{ marginTop: '16px' }}>
41
+ <FilterGroup
42
+ title={t('common.category')}
43
+ options={categoryOptions}
44
+ value={selectedCategory}
45
+ onChange={(v) => {
46
+ handelChange('category', v);
47
+ }}
48
+ />
49
+ </div>
50
+ )}
49
51
  </Dialog>
50
52
  </StyledDiv>
51
53
  );
@@ -1,26 +1,45 @@
1
+ import { memo } from 'react';
1
2
  import PropTypes from 'prop-types';
2
3
  import styled from 'styled-components';
3
4
  import Empty from '@arcblock/ux/lib/Empty';
4
- import Alert from '@arcblock/ux/lib/Alert';
5
5
  import Box from '@mui/material/Box';
6
6
  import Grid from '@mui/material/Grid';
7
7
  import CircularProgress from '@mui/material/CircularProgress';
8
+ import useInfiniteScroll from 'react-infinite-scroll-hook';
9
+ import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
8
10
 
9
11
  import { NoResults, EmptyTitle, NoResultsTips } from './empty';
10
12
  import { useFilterContext } from '../../contexts/filter';
11
- import { formatError } from '../../libs/utils';
12
13
 
13
14
  export default function BlockletList({ blocklets, ...rest }) {
14
- const { blockletRender, errors, loadings, selectedCategory, blockletList, getCategoryLocale, filters, t } =
15
- useFilterContext();
15
+ const {
16
+ blockletRender,
17
+ errors,
18
+ loadings,
19
+ selectedCategory,
20
+ finalBlockletList,
21
+ getCategoryLocale,
22
+ filters,
23
+ t,
24
+ hasNextPage,
25
+ loadMore,
26
+ endpoint,
27
+ } = useFilterContext();
16
28
 
17
29
  const showFilterTip = !!selectedCategory || !!filters.price;
18
30
 
31
+ const [sentryRef] = useInfiniteScroll({
32
+ loading: loadings.fetchBlockletsLoading,
33
+ hasNextPage,
34
+ onLoadMore: loadMore,
35
+ rootMargin: '0px 0px 400px 0px',
36
+ });
37
+
19
38
  if (errors.fetchBlockletsError) {
20
39
  return (
21
- <Alert type="error" variant="icon">
22
- <div>{formatError(errors.fetchBlockletsError)}</div>
23
- </Alert>
40
+ <ErrorFallback
41
+ error={new Error(`Failed to fetch blocklets from ${endpoint}: ${errors.fetchBlockletsError.message}`)}
42
+ />
24
43
  );
25
44
  }
26
45
  if (loadings.fetchBlockletsLoading) {
@@ -30,7 +49,7 @@ export default function BlockletList({ blocklets, ...rest }) {
30
49
  </Box>
31
50
  );
32
51
  }
33
- if (filters.keyword && showFilterTip && blockletList.length === 0) {
52
+ if (filters.keyword && showFilterTip && finalBlockletList.length === 0) {
34
53
  return (
35
54
  <CustomEmpty>
36
55
  <EmptyTitle
@@ -42,7 +61,7 @@ export default function BlockletList({ blocklets, ...rest }) {
42
61
  </CustomEmpty>
43
62
  );
44
63
  }
45
- if (filters.keyword && blockletList.length === 0) {
64
+ if (filters.keyword && finalBlockletList.length === 0) {
46
65
  return (
47
66
  <CustomEmpty>
48
67
  <EmptyTitle
@@ -54,7 +73,7 @@ export default function BlockletList({ blocklets, ...rest }) {
54
73
  </CustomEmpty>
55
74
  );
56
75
  }
57
- if (showFilterTip && blockletList.length === 0) {
76
+ if (showFilterTip && finalBlockletList.length === 0) {
58
77
  const categoryLocale = getCategoryLocale(selectedCategory);
59
78
  return (
60
79
  <CustomEmpty>
@@ -71,7 +90,7 @@ export default function BlockletList({ blocklets, ...rest }) {
71
90
  </CustomEmpty>
72
91
  );
73
92
  }
74
- if (blockletList.length === 0) {
93
+ if (finalBlockletList.length === 0) {
75
94
  return (
76
95
  <CustomEmpty>
77
96
  <NoResults />
@@ -83,9 +102,16 @@ export default function BlockletList({ blocklets, ...rest }) {
83
102
  <StyledGrid container {...rest}>
84
103
  {blocklets.map((blocklet) => (
85
104
  <StyledGridItem item lg={4} md={6} sm={6} xs={12} key={blocklet.did} data-blocklet-did={blocklet.did}>
86
- {blockletRender({ blocklet, blocklets: blockletList })}
105
+ {blockletRender({ blocklet, blocklets: finalBlockletList })}
87
106
  </StyledGridItem>
88
107
  ))}
108
+ {hasNextPage && (
109
+ <StyledGridItem item md={12} sm={12} xs={12} ref={sentryRef}>
110
+ <Box display="flex" justifyContent="center">
111
+ <CircularProgress />
112
+ </Box>
113
+ </StyledGridItem>
114
+ )}
89
115
  </StyledGrid>
90
116
  );
91
117
  }
@@ -103,7 +129,7 @@ const StyledGrid = styled(Grid)`
103
129
  }
104
130
  `;
105
131
 
106
- const StyledGridItem = styled(Grid)`
132
+ const StyledGridItem = memo(styled(Grid)`
107
133
  @media (max-width: ${(props) => props.theme.breakpoints.values.sm}px) {
108
134
  &.MuiGrid-item {
109
135
  padding-bottom: 0px;
@@ -114,7 +140,7 @@ const StyledGridItem = styled(Grid)`
114
140
  margin-bottom: ${(props) => props.theme.spacing(2)};
115
141
  }
116
142
  }
117
- `;
143
+ `);
118
144
  const CustomEmpty = styled(Empty)`
119
145
  text-align: center;
120
146
  .primary {
@@ -1,18 +1,24 @@
1
1
  import { useContext, createContext, useMemo, useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { useRequest } from 'ahooks';
3
+ import { useRequest, useReactive } from 'ahooks';
4
4
  import orderBy from 'lodash/orderBy';
5
5
  import axios from 'axios';
6
- // import joinUrl from 'url-join';
7
-
8
- import { getCategories, filterBlockletByPrice, replaceTranslate, getPrices, getCategoryOptions } from '../libs/utils';
6
+ import isArray from 'lodash/isArray';
7
+ import {
8
+ getCategoriesByAuthor,
9
+ filterBlockletByPrice,
10
+ replaceTranslate,
11
+ getPrices,
12
+ getCategoryOptions,
13
+ isMobileScreen,
14
+ } from '../libs/utils';
9
15
  import translations from '../assets/locale';
10
16
  import { propTypes, defaultProps } from '../libs/prop-types';
11
17
 
12
18
  const Filter = createContext({});
13
19
  const { Provider, Consumer } = Filter;
14
20
 
15
- function FilterProvider({ filters, children, baseUrl, endpoint, locale, blockletRender, onFilterChange, extraFilter }) {
21
+ function FilterProvider({ filters, children, endpoint, locale, blockletRender, onFilterChange, extraFilter }) {
16
22
  const storeApi = axios.create({
17
23
  baseURL: endpoint,
18
24
  });
@@ -23,8 +29,11 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
23
29
  run: fetchBlocklets,
24
30
  } = useRequest(
25
31
  async () => {
26
- const { data: list } = await storeApi.get('/api/blocklets.json');
27
- return list;
32
+ const { data } = await storeApi.get('/api/blocklets.json');
33
+ if (!isArray(data)) {
34
+ throw new Error('/api/blocklets.json response is not array');
35
+ }
36
+ return data;
28
37
  },
29
38
  { initialData: [], manual: true }
30
39
  );
@@ -36,18 +45,23 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
36
45
  run: fetchCategories,
37
46
  } = useRequest(
38
47
  async () => {
39
- const { data: list } = await storeApi.get('/api/blocklets/categories');
40
- return list;
48
+ const { data } = await storeApi.get('/api/blocklets/categories');
49
+ if (!isArray(data)) {
50
+ throw new Error('/api/blocklets/categories response is not array');
51
+ }
52
+ return data;
41
53
  },
42
54
  { initialData: [], manual: true }
43
55
  );
44
56
 
57
+ const paginateState = useReactive({ currentPage: 1, pageSize: isMobileScreen() ? 10 : 18, defaultCurrentPage: 1 });
45
58
  const finalFilters = { sortBy: 'popularity', sortDirection: 'desc', ...filters };
46
59
  const selectedCategory = finalFilters.category;
47
60
  const hasDeveloperFilter = !!finalFilters.developer;
48
61
  const categoryState = useMemo(() => {
49
- return !hasDeveloperFilter ? { data: allCategories } : getCategories(allBlocklets, finalFilters.developer);
50
- }, [hasDeveloperFilter, allCategories]);
62
+ // 当按作者过滤时,需要从所有blocklets中找出属于作者的分类
63
+ return !hasDeveloperFilter ? { data: allCategories } : getCategoriesByAuthor(allBlocklets, finalFilters.developer);
64
+ }, [hasDeveloperFilter, allCategories, allBlocklets]);
51
65
 
52
66
  const blockletList = useMemo(() => {
53
67
  const sortByName = (x) => x?.title?.toLocaleLowerCase() || x?.name?.toLocaleLowerCase(); // 按名称排序
@@ -59,7 +73,6 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
59
73
  popularity: sortByPopularity,
60
74
  publishAt: sortByPublish,
61
75
  };
62
-
63
76
  let blocklets = allBlocklets || [];
64
77
  // 按照付费/免费筛选
65
78
  blocklets = filterBlockletByPrice(blocklets, finalFilters.price);
@@ -84,6 +97,14 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
84
97
  return orderBy(blocklets, [sortMap[finalFilters.sortBy]], [finalFilters.sortDirection]);
85
98
  }, [allBlocklets, finalFilters]);
86
99
 
100
+ const finalBlockletList = useMemo(() => {
101
+ // 前端分页 currentPage 当前页数 pageSize 每页条数
102
+ return blockletList.slice(
103
+ (paginateState.defaultCurrentPage - 1) * paginateState.pageSize,
104
+ paginateState.currentPage * paginateState.pageSize
105
+ );
106
+ }, [blockletList, paginateState]);
107
+
87
108
  const categoryList = useMemo(() => {
88
109
  const list = categoryState.data || [];
89
110
  // 分类按照名称排序
@@ -98,24 +119,22 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
98
119
 
99
120
  return replaceTranslate(translations[locale][key], data);
100
121
  };
101
-
102
122
  const categoryOptions = useMemo(() => getCategoryOptions(categoryList, locale), [categoryList, locale]);
103
123
  const priceOptions = getPrices(translate);
104
-
105
124
  const filterStore = {
106
125
  errors: { fetchBlockletsError, fetchCategoriesError },
107
126
  loadings: { fetchBlockletsLoading, fetchCategoriesLoading },
108
127
  endpoint,
109
- blockletList,
128
+ finalBlockletList,
110
129
  t: translate,
111
130
  filters: finalFilters,
112
131
  selectedCategory,
113
132
  categoryList,
114
- baseUrl,
115
133
  blockletRender,
116
134
  locale,
117
135
  categoryOptions,
118
136
  priceOptions,
137
+ hasNextPage: blockletList.length >= paginateState.pageSize * paginateState.currentPage,
119
138
  handleSort: (sort) => {
120
139
  const changeData = {
121
140
  ...finalFilters,
@@ -147,6 +166,15 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
147
166
  const changeData = { ...finalFilters, developer: developer || undefined };
148
167
  onFilterChange(changeData);
149
168
  },
169
+ handlePage: (page) => {
170
+ const changeData = { ...finalFilters, currentPage: page };
171
+ onFilterChange(changeData);
172
+ },
173
+ loadMore: () => {
174
+ setTimeout(() => {
175
+ paginateState.currentPage += 1;
176
+ }, 1000);
177
+ },
150
178
  getCategoryLocale: (category) => {
151
179
  if (!category) return null;
152
180
  let result = null;
@@ -156,8 +184,12 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
156
184
  }
157
185
  return result;
158
186
  },
187
+ get allBlocklets() {
188
+ return allBlocklets || [];
189
+ },
159
190
  get developerName() {
160
- return allBlocklets.find((blocklet) => blocklet.owner.did === finalFilters.developer)?.owner?.name || '';
191
+ const blocklets = allBlocklets || [];
192
+ return blocklets.find((blocklet) => blocklet.owner.did === finalFilters.developer)?.owner?.name || '';
161
193
  },
162
194
  };
163
195
 
@@ -13,12 +13,10 @@ const propTypes = {
13
13
  endpoint: PropTypes.string.isRequired,
14
14
  blockletRender: PropTypes.func.isRequired,
15
15
  onFilterChange: PropTypes.func,
16
- baseUrl: PropTypes.string,
17
16
  locale: PropTypes.oneOf(['zh', 'en']),
18
17
  };
19
18
 
20
19
  const defaultProps = {
21
- baseUrl: null,
22
20
  locale: 'zh',
23
21
  filters: {},
24
22
  onFilterChange: () => {},
package/src/libs/utils.js CHANGED
@@ -5,10 +5,10 @@ const isFreeBlocklet = (meta) => {
5
5
  if (!meta.payment) {
6
6
  return true;
7
7
  }
8
-
9
8
  const priceList = (meta.payment.price || []).map((x) => x.value || 0);
10
9
  return priceList.every((x) => x === 0);
11
10
  };
11
+
12
12
  const getSortOptions = (t) => {
13
13
  return [
14
14
  {
@@ -39,12 +39,12 @@ const getCategoryOptions = (list = [], locale = 'en') => {
39
39
  return list.map((item) => ({ name: item.locales[locale], value: item.name }));
40
40
  };
41
41
  /**
42
- * 从开发者所属 blocklets 中的得到 Categories
42
+ * blocklets 中的得到属于作者的 Categories
43
43
  * @param {*} list
44
44
  * @param {*} developerDid
45
45
  * @returns
46
46
  */
47
- const getCategories = (list, developerDid) => {
47
+ const getCategoriesByAuthor = (list = [], developerDid = null) => {
48
48
  const filterList = list.filter((item) => (developerDid ? item.owner.did === developerDid : true));
49
49
  const Categories = filterList.map((item) => item.category);
50
50
  const res = new Map();
@@ -106,10 +106,14 @@ const urlStringify = (obj) => {
106
106
  return new URLSearchParams(removeUndefined(obj)).toString();
107
107
  };
108
108
 
109
+ const isMobileScreen = () => {
110
+ return window.innerWidth <= 600;
111
+ };
112
+
109
113
  export {
110
114
  getSortOptions,
111
115
  getPrices,
112
- getCategories,
116
+ getCategoriesByAuthor,
113
117
  filterBlockletByPrice,
114
118
  getStoreDetail,
115
119
  formatLogoPath,
@@ -118,4 +122,5 @@ export {
118
122
  removeUndefined,
119
123
  urlStringify,
120
124
  getCategoryOptions,
125
+ isMobileScreen,
121
126
  };