@blocklet/list 0.8.32 → 0.8.35

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
  },
@@ -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',
@@ -142,6 +158,10 @@ function FilterProvider(_ref) {
142
158
 
143
159
  return (0, _orderBy.default)(blocklets, [sortMap[finalFilters.sortBy]], [finalFilters.sortDirection]);
144
160
  }, [allBlocklets, finalFilters]);
161
+ const finalBlockletList = (0, _react.useMemo)(() => {
162
+ // 前端分页 currentPage 当前页数 pageSize 每页条数
163
+ return blockletList.slice((paginateState.defaultCurrentPage - 1) * paginateState.pageSize, paginateState.currentPage * paginateState.pageSize);
164
+ }, [blockletList, paginateState]);
145
165
  const categoryList = (0, _react.useMemo)(() => {
146
166
  const list = categoryState.data || []; // 分类按照名称排序
147
167
 
@@ -169,16 +189,16 @@ function FilterProvider(_ref) {
169
189
  fetchCategoriesLoading
170
190
  },
171
191
  endpoint,
172
- blockletList,
192
+ finalBlockletList,
173
193
  t: translate,
174
194
  filters: finalFilters,
175
195
  selectedCategory,
176
196
  categoryList,
177
- baseUrl,
178
197
  blockletRender,
179
198
  locale,
180
199
  categoryOptions,
181
200
  priceOptions,
201
+ hasNextPage: blockletList.length >= paginateState.pageSize * paginateState.currentPage,
182
202
  handleSort: sort => {
183
203
  const changeData = _objectSpread(_objectSpread({}, finalFilters), {}, {
184
204
  sortBy: sort,
@@ -221,6 +241,18 @@ function FilterProvider(_ref) {
221
241
 
222
242
  onFilterChange(changeData);
223
243
  },
244
+ handlePage: page => {
245
+ const changeData = _objectSpread(_objectSpread({}, finalFilters), {}, {
246
+ currentPage: page
247
+ });
248
+
249
+ onFilterChange(changeData);
250
+ },
251
+ loadMore: () => {
252
+ setTimeout(() => {
253
+ paginateState.currentPage += 1;
254
+ }, 1000);
255
+ },
224
256
  getCategoryLocale: category => {
225
257
  if (!category) return null;
226
258
  let result = null;
@@ -233,10 +265,15 @@ function FilterProvider(_ref) {
233
265
  return result;
234
266
  },
235
267
 
268
+ get allBlocklets() {
269
+ return allBlocklets || [];
270
+ },
271
+
236
272
  get developerName() {
237
- var _allBlocklets$find, _allBlocklets$find$ow;
273
+ var _blocklets$find, _blocklets$find$owner;
238
274
 
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) || '';
275
+ const blocklets = allBlocklets || [];
276
+ 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
277
  }
241
278
 
242
279
  };
@@ -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.getCategories = exports.formatLogoPath = exports.formatError = exports.filterBlockletByPrice = void 0;
7
7
 
8
8
  var _urlJoin = _interopRequireDefault(require("url-join"));
9
9
 
@@ -68,7 +68,9 @@ const getCategoryOptions = function getCategoryOptions() {
68
68
 
69
69
  exports.getCategoryOptions = getCategoryOptions;
70
70
 
71
- const getCategories = (list, developerDid) => {
71
+ const getCategories = function getCategories() {
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();
@@ -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.32",
3
+ "version": "0.8.35",
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.16",
41
+ "@arcblock/ux": "^2.1.21",
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": "18d5f7c6f7979e76da75d6b42f1cdcc6581814fb"
67
+ "gitHead": "f6a52e3c84e6eb4cd5625f688a55d7da1b477740"
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
  }
@@ -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
+ getCategories,
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,12 +45,16 @@ 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;
@@ -59,7 +72,6 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
59
72
  popularity: sortByPopularity,
60
73
  publishAt: sortByPublish,
61
74
  };
62
-
63
75
  let blocklets = allBlocklets || [];
64
76
  // 按照付费/免费筛选
65
77
  blocklets = filterBlockletByPrice(blocklets, finalFilters.price);
@@ -84,6 +96,14 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
84
96
  return orderBy(blocklets, [sortMap[finalFilters.sortBy]], [finalFilters.sortDirection]);
85
97
  }, [allBlocklets, finalFilters]);
86
98
 
99
+ const finalBlockletList = useMemo(() => {
100
+ // 前端分页 currentPage 当前页数 pageSize 每页条数
101
+ return blockletList.slice(
102
+ (paginateState.defaultCurrentPage - 1) * paginateState.pageSize,
103
+ paginateState.currentPage * paginateState.pageSize
104
+ );
105
+ }, [blockletList, paginateState]);
106
+
87
107
  const categoryList = useMemo(() => {
88
108
  const list = categoryState.data || [];
89
109
  // 分类按照名称排序
@@ -98,24 +118,22 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
98
118
 
99
119
  return replaceTranslate(translations[locale][key], data);
100
120
  };
101
-
102
121
  const categoryOptions = useMemo(() => getCategoryOptions(categoryList, locale), [categoryList, locale]);
103
122
  const priceOptions = getPrices(translate);
104
-
105
123
  const filterStore = {
106
124
  errors: { fetchBlockletsError, fetchCategoriesError },
107
125
  loadings: { fetchBlockletsLoading, fetchCategoriesLoading },
108
126
  endpoint,
109
- blockletList,
127
+ finalBlockletList,
110
128
  t: translate,
111
129
  filters: finalFilters,
112
130
  selectedCategory,
113
131
  categoryList,
114
- baseUrl,
115
132
  blockletRender,
116
133
  locale,
117
134
  categoryOptions,
118
135
  priceOptions,
136
+ hasNextPage: blockletList.length >= paginateState.pageSize * paginateState.currentPage,
119
137
  handleSort: (sort) => {
120
138
  const changeData = {
121
139
  ...finalFilters,
@@ -147,6 +165,15 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
147
165
  const changeData = { ...finalFilters, developer: developer || undefined };
148
166
  onFilterChange(changeData);
149
167
  },
168
+ handlePage: (page) => {
169
+ const changeData = { ...finalFilters, currentPage: page };
170
+ onFilterChange(changeData);
171
+ },
172
+ loadMore: () => {
173
+ setTimeout(() => {
174
+ paginateState.currentPage += 1;
175
+ }, 1000);
176
+ },
150
177
  getCategoryLocale: (category) => {
151
178
  if (!category) return null;
152
179
  let result = null;
@@ -156,8 +183,12 @@ function FilterProvider({ filters, children, baseUrl, endpoint, locale, blocklet
156
183
  }
157
184
  return result;
158
185
  },
186
+ get allBlocklets() {
187
+ return allBlocklets || [];
188
+ },
159
189
  get developerName() {
160
- return allBlocklets.find((blocklet) => blocklet.owner.did === finalFilters.developer)?.owner?.name || '';
190
+ const blocklets = allBlocklets || [];
191
+ return blocklets.find((blocklet) => blocklet.owner.did === finalFilters.developer)?.owner?.name || '';
161
192
  },
162
193
  };
163
194
 
@@ -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
  {
@@ -44,7 +44,7 @@ const getCategoryOptions = (list = [], locale = 'en') => {
44
44
  * @param {*} developerDid
45
45
  * @returns
46
46
  */
47
- const getCategories = (list, developerDid) => {
47
+ const getCategories = (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,6 +106,10 @@ 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,
@@ -118,4 +122,5 @@ export {
118
122
  removeUndefined,
119
123
  urlStringify,
120
124
  getCategoryOptions,
125
+ isMobileScreen,
121
126
  };