@redocly/theme 0.12.1 → 0.12.3

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.
@@ -0,0 +1,3 @@
1
+ export * from './Catalog';
2
+ export * from './CatalogCard';
3
+ export * from './useCatalog';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./Catalog"), exports);
18
+ __exportStar(require("./CatalogCard"), exports);
19
+ __exportStar(require("./useCatalog"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -34,6 +34,13 @@ function useCatalog(items, config) {
34
34
  const [filtersState, setFiltersState] = React.useState(() => {
35
35
  var _a;
36
36
  return ((_a = config.filters) !== null && _a !== void 0 ? _a : []).map((f) => {
37
+ var _a, _b;
38
+ if (f.type === 'date-range') {
39
+ const [from, to] = (_b = (_a = searchParams.get(f.property)) === null || _a === void 0 ? void 0 : _a.split('--')) !== null && _b !== void 0 ? _b : [];
40
+ if (!from && !to)
41
+ return {};
42
+ return { from, to };
43
+ }
37
44
  return new Set(searchParams.getAll(f.property));
38
45
  });
39
46
  });
@@ -43,6 +50,8 @@ function useCatalog(items, config) {
43
50
  const toggleOption = React.useCallback((filterIdx, option) => {
44
51
  setFiltersState((prev) => {
45
52
  const newFilterOptions = prev[filterIdx] ? prev[filterIdx] : new Set();
53
+ if (!(newFilterOptions instanceof Set))
54
+ return prev;
46
55
  if (newFilterOptions.has(option)) {
47
56
  newFilterOptions.delete(option);
48
57
  }
@@ -55,7 +64,12 @@ function useCatalog(items, config) {
55
64
  }, []);
56
65
  const selectOption = React.useCallback((filterIdx, option) => {
57
66
  setFiltersState((prev) => {
58
- const newFilterOptions = new Set(option ? [option] : []);
67
+ const newFilterOptions = prev[filterIdx] instanceof Set
68
+ ? new Set(option ? [option] : [])
69
+ : {
70
+ from: option === null || option === void 0 ? void 0 : option.from,
71
+ to: option === null || option === void 0 ? void 0 : option.to,
72
+ };
59
73
  const filter = filtersWithOptions[filterIdx];
60
74
  return prev.map((f, idx) => idx === filterIdx
61
75
  ? newFilterOptions
@@ -72,11 +86,16 @@ function useCatalog(items, config) {
72
86
  if (!filter)
73
87
  return;
74
88
  searchParams.delete(filter.property);
75
- filterValues.forEach((value) => {
76
- searchParams.append(filter.property, value);
77
- });
89
+ if (filterValues instanceof Set) {
90
+ filterValues.forEach((value) => {
91
+ searchParams.append(filter.property, value);
92
+ });
93
+ }
94
+ else if (filterValues.from || filterValues.to) {
95
+ searchParams.append(filter.property, `${filterValues.from || ''}--${filterValues.to || ''}`);
96
+ return;
97
+ }
78
98
  });
79
- searchParams;
80
99
  if (filterTerm) {
81
100
  searchParams.set('filter', filterTerm);
82
101
  }
@@ -84,14 +103,17 @@ function useCatalog(items, config) {
84
103
  searchParams.delete('filter');
85
104
  }
86
105
  const newSearch = searchParams.toString();
106
+ if (newSearch === location.search.substring(1))
107
+ return;
87
108
  navigate({ search: newSearch });
88
- }, [config.filters, searchParams, filtersState, filterTerm, navigate]);
109
+ }, [config.filters, searchParams, filtersState, filterTerm, navigate, location]);
89
110
  // filterParents[i] is a Set with indexes of parents of this filter
90
111
  const filterParents = React.useMemo(() => collectFilterParents(config.filters), [config.filters]);
91
112
  return React.useMemo(() => {
113
+ var _a;
92
114
  const filters = filtersWithOptions.map((filter, idx) => {
93
115
  var _a, _b, _c;
94
- return (Object.assign(Object.assign({}, filter), { toggleOption: (value) => toggleOption(idx, value), selectOption: (value) => selectOption(idx, value), selectedOptions: (_a = filtersState[idx]) !== null && _a !== void 0 ? _a : new Set(), isFilterUsed: ((_c = (_b = filtersState[idx]) === null || _b === void 0 ? void 0 : _b.size) !== null && _c !== void 0 ? _c : 0) > 0 }));
116
+ return (Object.assign(Object.assign({}, filter), { toggleOption: (value) => toggleOption(idx, value), selectOption: (value) => selectOption(idx, value), selectedOptions: (_a = filtersState[idx]) !== null && _a !== void 0 ? _a : new Set(), isFilterUsed: ((_c = (_b = filtersState[idx]) === null || _b === void 0 ? void 0 : _b.size) !== null && _c !== void 0 ? _c : 0) > 0 || !!filtersState[idx].from }));
95
117
  });
96
118
  const filteredItems = filterItems(normalizedItems, filters, filterTerm);
97
119
  // add more information to filters state which is known only after filtering items
@@ -99,7 +121,8 @@ function useCatalog(items, config) {
99
121
  var _a, _b;
100
122
  const parentFilterIdx = filters.findIndex((f) => f.property === filter.parentFilter);
101
123
  const parentUsed = filter.parentFilter
102
- ? ((_b = (_a = filtersState[parentFilterIdx]) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0) > 0
124
+ ? ((_b = (_a = filtersState[parentFilterIdx]) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0) > 0 ||
125
+ !!filtersState[parentFilterIdx].from
103
126
  : true;
104
127
  // we filter items with all the other independent filters current state
105
128
  // then we can calculate the options for this filter after other filters are applied
@@ -110,7 +133,7 @@ function useCatalog(items, config) {
110
133
  const adjustedFilterOptions = collectFilterOptions(filteredWithOtherFilters.map((m) => ({ metadata: m })), config.filters);
111
134
  return Object.assign(Object.assign({}, filter), { parentUsed, filteredOptions: adjustedFilterOptions[idx].options });
112
135
  });
113
- const groups = config.groupByFirstFilter && filters[0].selectedOptions.size > 0
136
+ const groups = config.groupByFirstFilter && ((_a = filters[0].selectedOptions) === null || _a === void 0 ? void 0 : _a.size) > 0
114
137
  ? groupByFirstFilter(resolvedFilters, filteredItems)
115
138
  : [{ title: 'APIs', items: filteredItems }];
116
139
  return { groups, filters: resolvedFilters, setFilterTerm, filterTerm };
@@ -171,7 +194,7 @@ function normalizeItems(items, config) {
171
194
  const metadata = item.metadata || {};
172
195
  const link = item.link || ((_a = (0, utils_1.findDeepFirst)(item.items || [], (i) => 'link' in i && !!i.link)) === null || _a === void 0 ? void 0 : _a.link);
173
196
  const firstSidebarItem = (_b = item.sidebar) === null || _b === void 0 ? void 0 : _b[0];
174
- return Object.assign(Object.assign({}, metadata), { title: (0, utils_1.toStringIfDefined)(metadata === null || metadata === void 0 ? void 0 : metadata.title) || item.label || 'Untitled', description: (0, utils_1.toStringIfDefined)(metadata === null || metadata === void 0 ? void 0 : metadata.description), link: (_c = (0, utils_1.withoutHash)(link)) !== null && _c !== void 0 ? _c : '#', docsLink: (0, utils_1.withoutHash)(firstSidebarItem === null || firstSidebarItem === void 0 ? void 0 : firstSidebarItem.link), image: (0, utils_1.toStringIfDefined)(metadata === null || metadata === void 0 ? void 0 : metadata.image) });
197
+ return Object.assign(Object.assign({}, metadata), { publishedAt: metadata.publishedAt || metadata.createdAt, title: (0, utils_1.toStringIfDefined)(metadata === null || metadata === void 0 ? void 0 : metadata.title) || item.label || 'Untitled', description: (0, utils_1.toStringIfDefined)(metadata === null || metadata === void 0 ? void 0 : metadata.description), link: (_c = (0, utils_1.withoutHash)(link)) !== null && _c !== void 0 ? _c : '#', docsLink: (0, utils_1.withoutHash)(firstSidebarItem === null || firstSidebarItem === void 0 ? void 0 : firstSidebarItem.link), image: (0, utils_1.toStringIfDefined)(metadata === null || metadata === void 0 ? void 0 : metadata.image) });
175
198
  });
176
199
  }
177
200
  function collectFilterParents(filtersWithOptions) {
@@ -233,6 +256,17 @@ function filterItems(normalizedItems, filters, term) {
233
256
  // filter by filters first, and then by search term
234
257
  const filteredByFilters = normalizedItems.filter((item) => {
235
258
  return filters.every((filter) => {
259
+ var _a, _b;
260
+ if (filter.selectedOptions && !(filter.selectedOptions instanceof Set)) {
261
+ try {
262
+ const date = new Date(item[filter.property]).toISOString().split('T')[0];
263
+ return (date >= ((_a = filter.selectedOptions.from) !== null && _a !== void 0 ? _a : '') &&
264
+ date <= ((_b = filter.selectedOptions.to) !== null && _b !== void 0 ? _b : 'Z'));
265
+ }
266
+ catch (e) {
267
+ return true;
268
+ }
269
+ }
236
270
  if (filter.selectedOptions.size === 0) {
237
271
  return true;
238
272
  }
@@ -1,5 +1,7 @@
1
1
  /// <reference types="react" />
2
2
  import type { ResolvedFilter } from '../../types/portal/src/shared/types/catalog';
3
3
  export declare function Filter({ filter }: {
4
- filter: ResolvedFilter;
4
+ filter: ResolvedFilter & {
5
+ selectedOptions: any;
6
+ };
5
7
  }): JSX.Element | null;
@@ -6,19 +6,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Filter = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const styled_components_1 = __importDefault(require("styled-components"));
9
+ const react_date_picker_1 = require("react-date-picker");
9
10
  const Checkbox_1 = require("../../ui/Checkbox");
10
11
  const hooks_1 = require("../../mocks/hooks");
11
12
  function Filter({ filter }) {
12
- var _a;
13
+ var _a, _b;
13
14
  const { translate } = (0, hooks_1.useTranslate)();
14
15
  const translationKeys = {
15
16
  selectAll: 'theme.catalog.filters.select.all',
17
+ clear: 'theme.catalog.filters.clear',
16
18
  };
17
19
  if (!filter.parentUsed)
18
20
  return null;
19
21
  return (react_1.default.createElement(FilterGroup, { key: filter.property + filter.title },
20
- react_1.default.createElement(FilterTitle, null, translate(filter.titleTranslationKey, filter.title)),
21
- filter.type === 'select' ? (react_1.default.createElement(StyledSelect, { onChange: (e) => filter.selectOption(e.target.value), value: ((_a = filter.selectedOptions.values().next()) === null || _a === void 0 ? void 0 : _a.value) || '' },
22
+ react_1.default.createElement(FilterTitle, null,
23
+ translate(filter.titleTranslationKey, filter.title),
24
+ ' ',
25
+ ((_a = filter.selectedOptions) === null || _a === void 0 ? void 0 : _a.size) ? (react_1.default.createElement("a", { "data-translation-key": translationKeys.clear, onClick: () => filter.selectOption('') }, translate(translationKeys.clear, 'Clear'))) : null),
26
+ filter.type === 'select' ? (react_1.default.createElement(StyledSelect, { onChange: (e) => filter.selectOption(e.target.value), value: ((_b = filter.selectedOptions.values().next()) === null || _b === void 0 ? void 0 : _b.value) || '' },
22
27
  react_1.default.createElement("option", { key: "none", value: "", "data-translation-key": translationKeys.selectAll }, translate(translationKeys.selectAll, 'All')),
23
28
  filter.filteredOptions.map((value) => {
24
29
  return (react_1.default.createElement("option", { key: value.value, value: value.value },
@@ -26,7 +31,21 @@ function Filter({ filter }) {
26
31
  " (",
27
32
  value.count,
28
33
  ")"));
29
- }))) : (filter.filteredOptions.map((value) => {
34
+ }))) : filter.type === 'date-range' ? (react_1.default.createElement(react_1.default.Fragment, null,
35
+ react_1.default.createElement(DatePickerWrapper, null,
36
+ react_1.default.createElement("span", null, "From:"),
37
+ react_1.default.createElement(react_date_picker_1.DatePicker, { closeCalendar: true, format: "y-MM-dd", dayPlaceholder: "DD", monthPlaceholder: "MM", yearPlaceholder: "YYYY", value: filter.selectedOptions.from ? new Date(filter.selectedOptions.from) : null, minDetail: "decade", maxDate: new Date(), onChange: (from) => {
38
+ if (Array.isArray(from))
39
+ return;
40
+ filter.selectOption(Object.assign(Object.assign({}, filter.selectedOptions), { from: formatDateWithNoTimeZone(from) }));
41
+ } })),
42
+ react_1.default.createElement(DatePickerWrapper, null,
43
+ react_1.default.createElement("span", null, "To:"),
44
+ react_1.default.createElement(react_date_picker_1.DatePicker, { closeCalendar: true, dayPlaceholder: "DD", monthPlaceholder: "MM", yearPlaceholder: "YYYY", format: "y-MM-dd", minDate: filter.selectedOptions.from ? new Date(filter.selectedOptions.from) : undefined, value: filter.selectedOptions.to ? new Date(filter.selectedOptions.to) : null, minDetail: "decade", onChange: (to) => {
45
+ if (Array.isArray(to))
46
+ return;
47
+ filter.selectOption(Object.assign(Object.assign({}, filter.selectedOptions), { to: formatDateWithNoTimeZone(to) }));
48
+ } })))) : (filter.filteredOptions.map((value) => {
30
49
  const id = 'filter--' + filter.property + '--' + slug(value.value);
31
50
  return (react_1.default.createElement(FilterValue, { key: id },
32
51
  react_1.default.createElement(Checkbox_1.Checkbox, { type: "checkbox", id: id, checked: filter.selectedOptions.has(value.value), onChange: () => filter.toggleOption(value.value) }),
@@ -51,6 +70,11 @@ const FilterTitle = styled_components_1.default.h4 `
51
70
  font-weight: var(--font-weight-bold);
52
71
  margin: 0;
53
72
  margin-bottom: 16px;
73
+
74
+ > a {
75
+ font-size: 14px;
76
+ cursor: pointer;
77
+ }
54
78
  `;
55
79
  const FilterValue = styled_components_1.default.label `
56
80
  display: block;
@@ -85,9 +109,66 @@ const StyledSelect = styled_components_1.default.select `
85
109
  background-position: right 10px center;
86
110
  background-size: 1em;
87
111
  width: 100%;
112
+ padding-right: 25px;
88
113
  `;
89
114
  // TODO: import from portal
90
115
  function slug(str) {
91
116
  return str.replace(/\s/g, '-').toLowerCase();
92
117
  }
118
+ const DatePickerWrapper = styled_components_1.default.div `
119
+ display: flex;
120
+ flex-direction: row;
121
+ margin-bottom: 5px;
122
+
123
+ align-items: center;
124
+ gap: 10px;
125
+
126
+ > span {
127
+ width: 50px;
128
+ }
129
+
130
+ .react-date-picker {
131
+ flex: 1;
132
+ }
133
+
134
+ .react-calendar__tile--now {
135
+ background: #cbf7f1;
136
+ color: black;
137
+
138
+ &:enabled:hover,
139
+ &:enabled:focus {
140
+ background: #b1efe7;
141
+ color: black;
142
+ }
143
+ }
144
+
145
+ .react-date-picker__inputGroup__input:invalid {
146
+ background: rgb(255 125 0 / 10%);
147
+ }
148
+
149
+ .react-date-picker__button {
150
+ padding: 4px 4px;
151
+ svg {
152
+ width: 12px;
153
+ }
154
+ }
155
+
156
+ .react-date-picker__wrapper {
157
+ border: 1px solid rgba(0, 0, 0, 0.23);
158
+ border-radius: var(--border-radius);
159
+ padding: var(--input-padding);
160
+ }
161
+
162
+ .react-date-picker__inputGroup__input {
163
+ width: 20px;
164
+ }
165
+ `;
166
+ function padZero(num) {
167
+ return num < 10 ? '0' + num : num;
168
+ }
169
+ function formatDateWithNoTimeZone(date) {
170
+ if (!date)
171
+ return date;
172
+ return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())}`;
173
+ }
93
174
  //# sourceMappingURL=Filter.js.map
@@ -26,3 +26,4 @@ export * from './Menu';
26
26
  export * from './Separator';
27
27
  export * from './Cards';
28
28
  export * from './Tiles';
29
+ export * from './Catalog';
@@ -42,4 +42,5 @@ __exportStar(require("./Menu"), exports);
42
42
  __exportStar(require("./Separator"), exports);
43
43
  __exportStar(require("./Cards"), exports);
44
44
  __exportStar(require("./Tiles"), exports);
45
+ __exportStar(require("./Catalog"), exports);
45
46
  //# sourceMappingURL=index.js.map
@@ -9,7 +9,7 @@ export type FilteredCatalog = {
9
9
  setFilterTerm: (term: string) => void;
10
10
  };
11
11
  export type Filter = {
12
- type?: 'select' | 'checkboxes';
12
+ type?: 'select' | 'checkboxes' | 'date-range';
13
13
  title: string;
14
14
  titleTranslationKey?: string;
15
15
  property: string;
@@ -42,12 +42,16 @@ export type ResolvedFilter = Omit<Filter, 'options'> & {
42
42
  toggleOption: (option: string) => void;
43
43
  selectOption: (option: string) => void;
44
44
  parentUsed: boolean;
45
- selectedOptions: Set<string>;
45
+ selectedOptions: Set<string> | {
46
+ from?: string;
47
+ to?: string;
48
+ };
46
49
  };
47
50
  export type CatalogItem = {
48
51
  title: string;
49
52
  link: string;
50
53
  description?: string;
51
54
  image?: string;
55
+ docsLink?: string;
52
56
  [k: string]: unknown;
53
57
  };
package/lib/ui/index.d.ts CHANGED
@@ -5,5 +5,6 @@ export * from '../ui/Dropdown';
5
5
  export * from '../ui/Flex';
6
6
  export * from '../ui/Jumbotron';
7
7
  export * from '../ui/ArrowBack';
8
+ export * from '../ui/Highlight';
8
9
  export declare const LandingLayout: ({ children }: React.PropsWithChildren<object>) => React.ReactNode;
9
10
  export declare const EmptyLayout: ({ children }: React.PropsWithChildren<object>) => React.ReactNode;
package/lib/ui/index.js CHANGED
@@ -21,6 +21,7 @@ __exportStar(require("../ui/Dropdown"), exports);
21
21
  __exportStar(require("../ui/Flex"), exports);
22
22
  __exportStar(require("../ui/Jumbotron"), exports);
23
23
  __exportStar(require("../ui/ArrowBack"), exports);
24
+ __exportStar(require("../ui/Highlight"), exports);
24
25
  const LandingLayout = ({ children }) => children;
25
26
  exports.LandingLayout = LandingLayout;
26
27
  exports.EmptyLayout = exports.LandingLayout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Shared UI components library",
5
5
  "keywords": [],
6
6
  "author": "team@redocly.com",
@@ -82,6 +82,8 @@
82
82
  "@redocly/ajv": "^8.11.0",
83
83
  "copy-to-clipboard": "^3.3.3",
84
84
  "highlight-words-core": "^1.2.2",
85
+ "react-date-picker": "10.0.3",
86
+ "react-calendar": "4.2.1",
85
87
  "hotkeys-js": "^3.10.1",
86
88
  "timeago.js": "^4.0.2"
87
89
  },
@@ -0,0 +1,3 @@
1
+ export * from './Catalog';
2
+ export * from './CatalogCard';
3
+ export * from './useCatalog';
@@ -6,10 +6,10 @@ import type { Location } from 'react-router-dom';
6
6
  import type { ResolvedNavItem } from '@theme/types/portal';
7
7
  import type {
8
8
  CatalogConfig,
9
- FilteredCatalog,
10
- ResolvedFilter,
11
9
  CatalogItem,
12
10
  Filter,
11
+ FilteredCatalog,
12
+ ResolvedFilter,
13
13
  } from '@theme/types/portal/src/shared/types/catalog';
14
14
  import { findDeepFirst, toStringIfDefined, withoutHash } from '@theme/utils';
15
15
 
@@ -19,6 +19,11 @@ export function useCatalog(items: ResolvedNavItem[], config: CatalogConfig): Fil
19
19
  const searchParams = useSearchParams(location);
20
20
  const [filtersState, setFiltersState] = React.useState(() =>
21
21
  (config.filters ?? []).map((f) => {
22
+ if (f.type === 'date-range') {
23
+ const [from, to] = searchParams.get(f.property)?.split('--') ?? [];
24
+ if (!from && !to) return {};
25
+ return { from, to };
26
+ }
22
27
  return new Set(searchParams.getAll(f.property));
23
28
  }),
24
29
  );
@@ -33,6 +38,7 @@ export function useCatalog(items: ResolvedNavItem[], config: CatalogConfig): Fil
33
38
  const toggleOption = React.useCallback((filterIdx, option) => {
34
39
  setFiltersState((prev) => {
35
40
  const newFilterOptions = prev[filterIdx] ? prev[filterIdx] : new Set<string>();
41
+ if (!(newFilterOptions instanceof Set)) return prev;
36
42
  if (newFilterOptions.has(option)) {
37
43
  newFilterOptions.delete(option);
38
44
  } else {
@@ -46,7 +52,13 @@ export function useCatalog(items: ResolvedNavItem[], config: CatalogConfig): Fil
46
52
  const selectOption = React.useCallback(
47
53
  (filterIdx, option) => {
48
54
  setFiltersState((prev) => {
49
- const newFilterOptions = new Set<string>(option ? [option] : []);
55
+ const newFilterOptions =
56
+ prev[filterIdx] instanceof Set
57
+ ? new Set<string>(option ? [option] : [])
58
+ : {
59
+ from: option?.from,
60
+ to: option?.to,
61
+ };
50
62
  const filter = filtersWithOptions[filterIdx];
51
63
 
52
64
  return prev.map((f, idx) =>
@@ -67,19 +79,28 @@ export function useCatalog(items: ResolvedNavItem[], config: CatalogConfig): Fil
67
79
  const filter = config.filters?.[filterIdx];
68
80
  if (!filter) return;
69
81
  searchParams.delete(filter.property);
70
- filterValues.forEach((value) => {
71
- searchParams.append(filter.property, value);
72
- });
82
+ if (filterValues instanceof Set) {
83
+ filterValues.forEach((value) => {
84
+ searchParams.append(filter.property, value);
85
+ });
86
+ } else if (filterValues.from || filterValues.to) {
87
+ searchParams.append(
88
+ filter.property,
89
+ `${filterValues.from || ''}--${filterValues.to || ''}`,
90
+ );
91
+ return;
92
+ }
73
93
  });
74
- searchParams;
94
+
75
95
  if (filterTerm) {
76
96
  searchParams.set('filter', filterTerm);
77
97
  } else {
78
98
  searchParams.delete('filter');
79
99
  }
80
100
  const newSearch = searchParams.toString();
101
+ if (newSearch === location.search.substring(1)) return;
81
102
  navigate({ search: newSearch });
82
- }, [config.filters, searchParams, filtersState, filterTerm, navigate]);
103
+ }, [config.filters, searchParams, filtersState, filterTerm, navigate, location]);
83
104
 
84
105
  // filterParents[i] is a Set with indexes of parents of this filter
85
106
  const filterParents = React.useMemo(() => collectFilterParents(config.filters), [config.filters]);
@@ -90,7 +111,8 @@ export function useCatalog(items: ResolvedNavItem[], config: CatalogConfig): Fil
90
111
  toggleOption: (value: string) => toggleOption(idx, value),
91
112
  selectOption: (value: string) => selectOption(idx, value),
92
113
  selectedOptions: filtersState[idx] ?? new Set<string>(),
93
- isFilterUsed: (filtersState[idx]?.size ?? 0) > 0, // TODO: rename to isUsed
114
+ isFilterUsed:
115
+ ((filtersState[idx] as any)?.size ?? 0) > 0 || !!(filtersState[idx] as any).from,
94
116
  }));
95
117
 
96
118
  const filteredItems = filterItems(normalizedItems, filters, filterTerm);
@@ -99,7 +121,8 @@ export function useCatalog(items: ResolvedNavItem[], config: CatalogConfig): Fil
99
121
  const resolvedFilters = filters.map((filter, idx) => {
100
122
  const parentFilterIdx = filters.findIndex((f) => f.property === filter.parentFilter);
101
123
  const parentUsed = filter.parentFilter
102
- ? (filtersState[parentFilterIdx]?.size ?? 0) > 0
124
+ ? ((filtersState[parentFilterIdx] as any)?.size ?? 0) > 0 ||
125
+ !!(filtersState[parentFilterIdx] as any).from
103
126
  : true;
104
127
 
105
128
  // we filter items with all the other independent filters current state
@@ -127,7 +150,7 @@ export function useCatalog(items: ResolvedNavItem[], config: CatalogConfig): Fil
127
150
  });
128
151
 
129
152
  const groups =
130
- config.groupByFirstFilter && filters[0].selectedOptions.size > 0
153
+ config.groupByFirstFilter && (filters[0].selectedOptions as any)?.size > 0
131
154
  ? groupByFirstFilter(resolvedFilters, filteredItems)
132
155
  : [{ title: 'APIs', items: filteredItems }];
133
156
 
@@ -193,6 +216,7 @@ function normalizeItems(items: ResolvedNavItem[], config: CatalogConfig): Catalo
193
216
  const firstSidebarItem = (item as any).sidebar?.[0];
194
217
  return {
195
218
  ...metadata,
219
+ publishedAt: metadata.publishedAt || metadata.createdAt,
196
220
  title: toStringIfDefined(metadata?.title) || item.label || 'Untitled',
197
221
  description: toStringIfDefined(metadata?.description),
198
222
  link: withoutHash(link) ?? '#',
@@ -272,12 +296,25 @@ function filterItems(
272
296
  // filter by filters first, and then by search term
273
297
  const filteredByFilters = normalizedItems.filter((item) => {
274
298
  return filters.every((filter) => {
299
+ if (filter.selectedOptions && !(filter.selectedOptions instanceof Set)) {
300
+ try {
301
+ const date = new Date(item[filter.property] as string).toISOString().split('T')[0];
302
+ return (
303
+ date >= (filter.selectedOptions.from ?? '') &&
304
+ date <= (filter.selectedOptions.to ?? 'Z')
305
+ );
306
+ } catch (e) {
307
+ return true;
308
+ }
309
+ }
310
+
275
311
  if (filter.selectedOptions.size === 0) {
276
312
  return true;
277
313
  }
314
+
278
315
  const itemValue = item?.[filter.property] || filter.missingCategoryName || 'Others';
279
316
  if (Array.isArray(itemValue)) {
280
- return itemValue.some((value) => filter.selectedOptions.has(value));
317
+ return itemValue.some((value) => (filter.selectedOptions as Set<any>).has(value));
281
318
  }
282
319
  return filter.selectedOptions.has(itemValue as string);
283
320
  });
@@ -1,24 +1,33 @@
1
1
  import React from 'react';
2
2
  import styled from 'styled-components';
3
+ import { DatePicker } from 'react-date-picker';
3
4
 
4
5
  import type { ResolvedFilter } from '@theme/types/portal/src/shared/types/catalog';
5
6
  import { Checkbox } from '@theme/ui/Checkbox';
6
7
  import { useTranslate } from '@portal/hooks';
7
8
 
8
- export function Filter({ filter }: { filter: ResolvedFilter }) {
9
+ export function Filter({ filter }: { filter: ResolvedFilter & { selectedOptions: any } }) {
9
10
  const { translate } = useTranslate();
10
11
  const translationKeys = {
11
12
  selectAll: 'theme.catalog.filters.select.all',
13
+ clear: 'theme.catalog.filters.clear',
12
14
  };
13
15
 
14
16
  if (!filter.parentUsed) return null;
15
17
  return (
16
18
  <FilterGroup key={filter.property + filter.title}>
17
- <FilterTitle>{translate(filter.titleTranslationKey, filter.title)}</FilterTitle>
19
+ <FilterTitle>
20
+ {translate(filter.titleTranslationKey, filter.title)}{' '}
21
+ {filter.selectedOptions?.size ? (
22
+ <a data-translation-key={translationKeys.clear} onClick={() => filter.selectOption('')}>
23
+ {translate(translationKeys.clear, 'Clear')}
24
+ </a>
25
+ ) : null}
26
+ </FilterTitle>
18
27
  {filter.type === 'select' ? (
19
28
  <StyledSelect
20
29
  onChange={(e) => filter.selectOption(e.target.value)}
21
- value={filter.selectedOptions.values().next()?.value || ''}
30
+ value={(filter.selectedOptions as Set<any>).values().next()?.value || ''}
22
31
  >
23
32
  <option key="none" value="" data-translation-key={translationKeys.selectAll}>
24
33
  {translate(translationKeys.selectAll, 'All')}
@@ -31,6 +40,51 @@ export function Filter({ filter }: { filter: ResolvedFilter }) {
31
40
  );
32
41
  })}
33
42
  </StyledSelect>
43
+ ) : filter.type === 'date-range' ? (
44
+ <>
45
+ <DatePickerWrapper>
46
+ <span>From:</span>
47
+ <DatePicker
48
+ closeCalendar={true}
49
+ format="y-MM-dd"
50
+ dayPlaceholder="DD"
51
+ monthPlaceholder="MM"
52
+ yearPlaceholder="YYYY"
53
+ value={filter.selectedOptions.from ? new Date(filter.selectedOptions.from) : null}
54
+ minDetail="decade"
55
+ maxDate={new Date()}
56
+ onChange={(from) => {
57
+ if (Array.isArray(from)) return;
58
+ filter.selectOption({
59
+ ...(filter.selectedOptions as any),
60
+ from: formatDateWithNoTimeZone(from),
61
+ });
62
+ }}
63
+ />
64
+ </DatePickerWrapper>
65
+ <DatePickerWrapper>
66
+ <span>To:</span>
67
+ <DatePicker
68
+ closeCalendar={true}
69
+ dayPlaceholder="DD"
70
+ monthPlaceholder="MM"
71
+ yearPlaceholder="YYYY"
72
+ format="y-MM-dd"
73
+ minDate={
74
+ filter.selectedOptions.from ? new Date(filter.selectedOptions.from) : undefined
75
+ }
76
+ value={filter.selectedOptions.to ? new Date(filter.selectedOptions.to) : null}
77
+ minDetail="decade"
78
+ onChange={(to) => {
79
+ if (Array.isArray(to)) return;
80
+ filter.selectOption({
81
+ ...filter.selectedOptions,
82
+ to: formatDateWithNoTimeZone(to),
83
+ });
84
+ }}
85
+ />
86
+ </DatePickerWrapper>
87
+ </>
34
88
  ) : (
35
89
  filter.filteredOptions.map((value: any) => {
36
90
  const id = 'filter--' + filter.property + '--' + slug(value.value);
@@ -67,6 +121,11 @@ const FilterTitle = styled.h4`
67
121
  font-weight: var(--font-weight-bold);
68
122
  margin: 0;
69
123
  margin-bottom: 16px;
124
+
125
+ > a {
126
+ font-size: 14px;
127
+ cursor: pointer;
128
+ }
70
129
  `;
71
130
 
72
131
  const FilterValue = styled.label`
@@ -103,9 +162,68 @@ const StyledSelect = styled.select`
103
162
  background-position: right 10px center;
104
163
  background-size: 1em;
105
164
  width: 100%;
165
+ padding-right: 25px;
106
166
  `;
107
167
 
108
168
  // TODO: import from portal
109
169
  function slug(str: string): string {
110
170
  return str.replace(/\s/g, '-').toLowerCase();
111
171
  }
172
+
173
+ const DatePickerWrapper = styled.div`
174
+ display: flex;
175
+ flex-direction: row;
176
+ margin-bottom: 5px;
177
+
178
+ align-items: center;
179
+ gap: 10px;
180
+
181
+ > span {
182
+ width: 50px;
183
+ }
184
+
185
+ .react-date-picker {
186
+ flex: 1;
187
+ }
188
+
189
+ .react-calendar__tile--now {
190
+ background: #cbf7f1;
191
+ color: black;
192
+
193
+ &:enabled:hover,
194
+ &:enabled:focus {
195
+ background: #b1efe7;
196
+ color: black;
197
+ }
198
+ }
199
+
200
+ .react-date-picker__inputGroup__input:invalid {
201
+ background: rgb(255 125 0 / 10%);
202
+ }
203
+
204
+ .react-date-picker__button {
205
+ padding: 4px 4px;
206
+ svg {
207
+ width: 12px;
208
+ }
209
+ }
210
+
211
+ .react-date-picker__wrapper {
212
+ border: 1px solid rgba(0, 0, 0, 0.23);
213
+ border-radius: var(--border-radius);
214
+ padding: var(--input-padding);
215
+ }
216
+
217
+ .react-date-picker__inputGroup__input {
218
+ width: 20px;
219
+ }
220
+ `;
221
+
222
+ function padZero(num: number) {
223
+ return num < 10 ? '0' + num : num;
224
+ }
225
+
226
+ function formatDateWithNoTimeZone(date?: Date | null) {
227
+ if (!date) return date;
228
+ return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())}`;
229
+ }
@@ -26,3 +26,4 @@ export * from './Menu';
26
26
  export * from './Separator';
27
27
  export * from './Cards';
28
28
  export * from './Tiles';
29
+ export * from './Catalog';
@@ -8,7 +8,7 @@ export type FilteredCatalog = {
8
8
  };
9
9
 
10
10
  export type Filter = {
11
- type?: 'select' | 'checkboxes';
11
+ type?: 'select' | 'checkboxes' | 'date-range';
12
12
  title: string;
13
13
  titleTranslationKey?: string;
14
14
  property: string;
@@ -43,7 +43,12 @@ export type ResolvedFilter = Omit<Filter, 'options'> & {
43
43
  toggleOption: (option: string) => void;
44
44
  selectOption: (option: string) => void;
45
45
  parentUsed: boolean;
46
- selectedOptions: Set<string>;
46
+ selectedOptions:
47
+ | Set<string>
48
+ | {
49
+ from?: string;
50
+ to?: string;
51
+ };
47
52
  };
48
53
 
49
54
  export type CatalogItem = {
@@ -51,5 +56,6 @@ export type CatalogItem = {
51
56
  link: string;
52
57
  description?: string;
53
58
  image?: string;
59
+ docsLink?: string;
54
60
  [k: string]: unknown;
55
61
  };
package/src/ui/index.tsx CHANGED
@@ -6,6 +6,7 @@ export * from '@theme/ui/Dropdown';
6
6
  export * from '@theme/ui/Flex';
7
7
  export * from '@theme/ui/Jumbotron';
8
8
  export * from '@theme/ui/ArrowBack';
9
+ export * from '@theme/ui/Highlight';
9
10
 
10
11
  export const LandingLayout = ({ children }: React.PropsWithChildren<object>): React.ReactNode =>
11
12
  children;