@janrankenhohn/react-thumbnail-list 0.5.0 → 0.5.1

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/dist/index.cjs.js CHANGED
@@ -1,65 +1,34 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var material = require('@mui/material');
6
- var React = require('react');
7
- var jsxRuntime = require('react/jsx-runtime');
8
- var system = require('@mui/system');
9
- var SearchIcon = require('@mui/icons-material/Search');
10
- var ClearIcon = require('@mui/icons-material/Clear');
11
- var SwapVertIcon = require('@mui/icons-material/SwapVert');
12
- var SortIcon = require('@mui/icons-material/Sort');
13
-
14
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
15
-
16
- var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
17
- var SearchIcon__default = /*#__PURE__*/_interopDefaultLegacy(SearchIcon);
18
- var ClearIcon__default = /*#__PURE__*/_interopDefaultLegacy(ClearIcon);
19
- var SwapVertIcon__default = /*#__PURE__*/_interopDefaultLegacy(SwapVertIcon);
20
- var SortIcon__default = /*#__PURE__*/_interopDefaultLegacy(SortIcon);
21
-
22
- function HelloWorld() {
23
- return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("h1", null, "Hello World"), /*#__PURE__*/React__default["default"].createElement(material.Button, {
24
- variant: "contained"
25
- }, "Contained"));
26
- }
27
-
28
- /* eslint-disable @typescript-eslint/no-explicit-any */
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var material = require('@mui/material');
7
+ var React = require('react');
8
+ var system = require('@mui/system');
9
+ var SearchIcon = require('@mui/icons-material/Search');
10
+ var ClearIcon = require('@mui/icons-material/Clear');
11
+ var lodash = require('lodash');
12
+ var SwapVertIcon = require('@mui/icons-material/SwapVert');
13
+ var SortIcon = require('@mui/icons-material/Sort');
14
+
15
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
16
+
17
+ var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
18
+ var SearchIcon__default = /*#__PURE__*/_interopDefaultLegacy(SearchIcon);
19
+ var ClearIcon__default = /*#__PURE__*/_interopDefaultLegacy(ClearIcon);
20
+ var SwapVertIcon__default = /*#__PURE__*/_interopDefaultLegacy(SwapVertIcon);
21
+ var SortIcon__default = /*#__PURE__*/_interopDefaultLegacy(SortIcon);
22
+
29
23
  const ThumbnailListItemContext = React.createContext(undefined);
30
- // Create a custom hook to consume the context
31
24
  const useThumbnailListItemContext = () => {
32
25
  const context = React.useContext(ThumbnailListItemContext);
33
26
  if (!context) {
34
- throw new Error('useMyContext must be used within a MyContextProvider');
27
+ throw new Error('no context provider available');
35
28
  }
36
29
  return context;
37
- };
38
-
39
- /**
40
- * Can be used as parent component to crop a wrapped image
41
- * @param props width: width for cropping
42
- * height: height for cropping
43
- * seperate xs and sm values for mui breakpoints
44
- * @returns component
45
- */
46
- function ImageCropper(props) {
47
- const ThumbnailImageCrop = material.styled('div')((p) => ({
48
- [p.theme.breakpoints.up('xs')]: {
49
- minWidth: props.width.xs,
50
- maxWidth: props.width.xs,
51
- height: props.height.xs,
52
- overflow: 'hidden'
53
- },
54
- [p.theme.breakpoints.up('sm')]: {
55
- minWidth: props.width.sm,
56
- maxwWidth: props.width.sm,
57
- height: props.height.sm,
58
- },
59
- }));
60
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(ThumbnailImageCrop, { children: props.children }) }));
61
- }
62
-
30
+ };
31
+
63
32
  /**
64
33
  * Creates a ellipies text with webkit css styles
65
34
  * @param props lineClamp: lines till ellipses
@@ -70,128 +39,64 @@ function EllipsisContainer(props) {
70
39
  [p.theme.breakpoints.up('xs')]: {
71
40
  overflow: 'hidden',
72
41
  display: '-webkit-box',
73
- WebkitLineClamp: props.lineClamp.xs.toString(),
42
+ WebkitLineClamp: props.lineClamp.xs.toString() /* number of lines to show */,
74
43
  WebkitBoxOrient: 'vertical',
75
44
  },
76
45
  [p.theme.breakpoints.up('sm')]: {
77
46
  overflow: 'hidden',
78
47
  display: '-webkit-box',
79
- WebkitLineClamp: props.lineClamp.sm.toString(),
80
- WebkitBoxOrient: 'vertical', /* number of lines to show */
48
+ WebkitLineClamp: props.lineClamp.sm.toString() /* number of lines to show */,
49
+ WebkitBoxOrient: 'vertical' /* number of lines to show */,
81
50
  },
82
51
  }));
83
- return (jsxRuntime.jsx(EllipsisContainer, { children: props.children }));
84
- }
85
-
52
+ return jsxRuntime.jsx(EllipsisContainer, { children: props.children });
53
+ }
54
+
86
55
  function ThumbnailListItemTitle(props) {
87
- const StyledCardContent = material.styled('div')((props) => ({
88
- [props.theme.breakpoints.up('xs')]: {
89
- 'padding': props.theme.spacing(1),
90
- 'flex': '1 0 auto',
56
+ const StyledCardContent = material.styled('div')((p) => ({
57
+ [p.theme.breakpoints.up('xs')]: {
58
+ padding: p.theme.spacing(1),
59
+ flex: '1 0 auto',
91
60
  '&:last-child': { paddingBottom: 0 },
92
- 'overflow': 'hidden',
61
+ overflow: 'hidden',
93
62
  },
94
63
  }));
95
- const children = React.Children.toArray(props.children);
96
- return jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Box, { children: jsxRuntime.jsxs(StyledCardContent, { children: [jsxRuntime.jsx(EllipsisContainer, { lineClamp: { xs: 1, sm: 2 }, children: jsxRuntime.jsx(material.Typography, { variant: 'h4', children: props.title }) }), jsxRuntime.jsx(system.Stack, { direction: "row", gap: 1, children: children.map((child, index) => jsxRuntime.jsx(material.Typography, { variant: "subtitle2", color: "text.secondary", children: child }, index)) })] }) }) });
97
- }
98
-
99
- function ThumbnailListItemInfoLabel(props) {
100
- return jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsxs(material.Stack, { textAlign: "right", justifyContent: "space-between", children: [jsxRuntime.jsx(material.Box, { textAlign: "right", padding: 1, children: props.topContent }), jsxRuntime.jsx(material.Box, { textAlign: "right", padding: 1, children: props.bottomContent })] }) });
101
- }
102
-
103
- // import {Link} from 'react-router-dom';
64
+ console.log('item title rerenders');
65
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Box, { children: jsxRuntime.jsxs(StyledCardContent, { children: [jsxRuntime.jsx(EllipsisContainer, { lineClamp: { xs: 1, sm: 2 }, children: jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { fontWeight: 'bold' }, children: props.title }) }), jsxRuntime.jsx(system.Stack, { direction: "row", gap: 1, children: jsxRuntime.jsx(EllipsisContainer, { lineClamp: { xs: 1, sm: 2 }, children: jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { fontSize: '0.84rem' }, color: "text.secondary", children: props.subTitle }) }) })] }) }) }));
66
+ }
67
+
104
68
  const ThumbnailListItem = (props) => {
105
- return jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Card, { sx: { display: 'flex' }, children: jsxRuntime.jsx(material.CardActionArea, { children: jsxRuntime.jsxs(material.Stack, { direction: "row", width: "100%", children: [jsxRuntime.jsx(ImageCropper, { width: { xs: '98px', sm: '160px' }, height: { xs: '54px', sm: '90px' }, children: jsxRuntime.jsx(material.Box, { children: "Placeholder for image" }) }), jsxRuntime.jsx(material.Stack, { direction: "row", justifyContent: "space-between", width: "100%", gap: 1, children: props.children })] }) }) }) });
69
+ console.log('ThumbnailListItems renders');
70
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Card, { sx: { display: 'flex' }, children: jsxRuntime.jsx(material.CardActionArea, { disabled: !props.onClick, onClick: () => props.onClick(props.id), children: jsxRuntime.jsxs(material.Stack, { direction: "row", width: "100%", children: [jsxRuntime.jsx("img", { src: props.thumbnailUrl, width: '45%' }), jsxRuntime.jsxs(material.Stack, { direction: "row", justifyContent: "space-between", width: "100%", gap: 1, children: [jsxRuntime.jsx(ThumbnailListItemTitle, { title: props.title, subTitle: props.subTitle }), props.infoLabel] })] }) }) }) }));
106
71
  };
107
- ThumbnailListItem.Title = ThumbnailListItemTitle;
108
- ThumbnailListItem.InfoLabel = ThumbnailListItemInfoLabel;
109
-
110
- function ThumbnailListMainContent( /* props: {children: ReactNode}*/) {
111
- // const children = Children.toArray(props.children);
112
- const { items } = useThumbnailListItemContext();
113
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: items.map((item) => {
114
- return (jsxRuntime.jsx(material.Grid, { item: true, xs: 12, lg: 6, xl: 3, children: jsxRuntime.jsxs(ThumbnailListItem, { id: item.id,
115
- // link={item.link}
116
- thumbnailUrl: item.thumbnailUrl, children: [jsxRuntime.jsx(ThumbnailListItem.Title, { title: item.title, children: item.subTitle }), item.label] }) }, item.id));
117
- }) }));
118
- }
119
-
120
- const ThumbnailListSearchField = () => {
121
- const [input, setInput] = React.useState('');
122
- const [showClearIcon, setShowClearIcon] = React.useState('hidden');
123
- const { setSearchTerm } = useThumbnailListItemContext();
124
- const handleChange = (value) => {
125
- setInput(value);
126
- setShowClearIcon(value === '' ? 'hidden' : '');
127
- setSearchTerm(value);
128
- };
129
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.FormControl, { children: jsxRuntime.jsx(material.TextField, { sx: { input: { color: 'white' } }, fullWidth: true, value: input, size: "small", variant: "outlined", onChange: (event) => handleChange(event.target.value), InputProps: {
130
- startAdornment: (jsxRuntime.jsx(material.InputAdornment, { position: "start", children: jsxRuntime.jsx(SearchIcon__default["default"], {}) })),
131
- endAdornment: (jsxRuntime.jsx(material.InputAdornment, { position: "end", children: jsxRuntime.jsx(material.IconButton, { onClick: () => handleChange(''), sx: { visibility: showClearIcon, padding: 0 }, children: jsxRuntime.jsx(ClearIcon__default["default"], {}) }) })),
132
- } }) }) }));
133
- };
134
-
135
- function ThumbnailListFilterTag(props) {
136
- const theme = material.useTheme();
137
- const handleOnClick = (value) => {
138
- if (props.onClickCallback) {
139
- props.onClickCallback(value);
140
- }
141
- };
142
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: material.useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) || !props.icon ?
143
- jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Chip, { label: props.label, variant: props.variant, onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined }) }) :
144
- jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Tooltip, { title: props.label, children: jsxRuntime.jsx(material.IconButton, { onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined, children: props.icon }) }) }) }));
72
+ var ThumbnailListItem$1 = React__default["default"].memo(ThumbnailListItem);
73
+
74
+ const RatioWrapper = material.styled('div')(() => ({
75
+ // Assuming a 16:9 aspect ratio
76
+ paddingTop: '27.75%',
77
+ position: 'relative',
78
+ width: '100%',
79
+ '& > *': {
80
+ position: 'absolute',
81
+ top: 0,
82
+ left: 0,
83
+ right: 0,
84
+ bottom: 0,
85
+ },
86
+ }));
87
+ function ThumbnailListMainContent(props) {
88
+ const { items, isLoading } = useThumbnailListItemContext();
89
+ console.log('main content rerenders');
90
+ const memoizedItems = React.useMemo(() => {
91
+ return items.map((item) => (jsxRuntime.jsx(material.Grid, { item: true, xs: props.muiBreakpoints.xs, sm: props.muiBreakpoints.sm, md: props.muiBreakpoints.md, lg: props.muiBreakpoints.lg, xl: props.muiBreakpoints.xl, children: jsxRuntime.jsx(RatioWrapper, { children: jsxRuntime.jsx(ThumbnailListItem$1, { id: item.id, thumbnailUrl: item.thumbnailUrl, title: item.title, subTitle: item.subTitle, infoLabel: item.label, onClick: item.onClick }) }) }, item.id)));
92
+ }, [items, props.muiBreakpoints]);
93
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(material.Box, { sx: { mt: 0.75, mb: 0.75 }, children: jsxRuntime.jsx(material.LinearProgress, { sx: { opacity: isLoading ? 1 : 0 } }) }), jsxRuntime.jsx(material.Grid, { container: true, spacing: props.spacing, children: memoizedItems })] }));
145
94
  }
146
-
147
- function ThumbnailListFilterTags(props) {
148
- const { tagFilterCallback, tagAndCondition } = useThumbnailListItemContext();
149
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: props.tags.map((tag) => {
150
- return (jsxRuntime.jsx(ThumbnailListFilterTag, { label: tag.label, value: tag.value, variant: tagAndCondition.tag === tag.value ? 'filled' : 'outlined', collapseBreakpoint: props.collapseBreakpoint, onClickCallback: (value) => tagFilterCallback({ tag: value, condition: tag.condition }), icon: tag.icon }));
151
- }) }));
152
- }
153
-
154
- /**
155
- * Displays a generic MUI select dropdown.
156
- * Optinal collapses to a sort icon at a certain breakpoint
157
- * @param props.label Select Label
158
- * @param props.width * Width of the input field
159
- * @param props.collapseBreakPoint * MUI breakpoint after that the select will collapse to the sort icon
160
- * @param props.onChangeCallback * Callback function that gets triggered once a item is selected
161
- * @param props.items * Array of items (name-value-pairs) that will be the select options
162
- * @returns Drowpdown Input Component
163
- */
164
- function DropdownInput(props) {
165
- const [value, setValue] = React.useState(props.defaultValue ?? '');
166
- const theme = material.useTheme();
167
- const [anchorEl, setAnchorEl] = React.useState(null);
168
- const handleChange = (value, name) => {
169
- setValue(value);
170
- setAnchorEl(null);
171
- props.onChangeCallback(value, name);
172
- };
173
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [material.useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) ?
174
- jsxRuntime.jsxs(material.FormControl, { sx: { width: props.width, textAlign: 'start' }, children: [jsxRuntime.jsx(material.InputLabel, { size: 'small', id: "demo-simple-select-label", children: props.label }), jsxRuntime.jsx(material.Select, { value: value, size: 'small', label: props.label, onChange: (event) => handleChange(event.target.value, event.target.name), children: props.items.map((item) => {
175
- return jsxRuntime.jsx(material.MenuItem, { value: item.value, children: item.name }, item.value);
176
- }) })] }) : (jsxRuntime.jsx(material.IconButton
177
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
- , {
179
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
- onClick: (event) => setAnchorEl(event.currentTarget), children: props.icon })), jsxRuntime.jsx(material.Menu, { anchorEl: anchorEl, open: Boolean(anchorEl), onClose: () => setAnchorEl(null), children: props.items.map((item) => (jsxRuntime.jsx(material.MenuItem, { onClick: () => handleChange(item.value), children: item.name }, item.value))) })] }));
181
- }
182
-
183
- function ThumbnailListHeaderSort(props) {
184
- const { setSortAscending, sortAscending, setSortBy } = useThumbnailListItemContext();
185
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsxs(material.Box, { sx: { width: '-webkit-fill-available', textAlign: 'end' }, children: [jsxRuntime.jsx(material.Tooltip, { title: "asc/desc", children: jsxRuntime.jsx(material.IconButton, { onClick: () => setSortAscending(!sortAscending), children: jsxRuntime.jsx(SwapVertIcon__default["default"], {}) }) }), jsxRuntime.jsx(DropdownInput, { width: "130px", collapseBreakpoint: 'md', label: 'sort', defaultValue: "creationTimeStamp", icon: jsxRuntime.jsx(material.Tooltip, { title: 'sort', children: jsxRuntime.jsx(SortIcon__default["default"], {}) }), items: props.items, onChangeCallback: (value) => setSortBy(value) })] }) }));
186
- }
187
-
188
- const ThumbnailListHeader = function (props) {
189
- return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Grid, { item: true, xs: 12, children: jsxRuntime.jsx(material.Stack, { direction: "row", alignItems: "center", justifyContent: props.justifyContent ?? 'space-between', gap: 2, children: props.children }) }) }));
190
- };
191
- ThumbnailListHeader.SearchField = ThumbnailListSearchField;
192
- ThumbnailListHeader.FilterTags = ThumbnailListFilterTags;
193
- ThumbnailListHeader.Sort = ThumbnailListHeaderSort;
194
-
95
+ ThumbnailListMainContent.defaultProps = {
96
+ spacing: 2,
97
+ muiBreakpoints: { xs: 12, sm: 6, md: 6, lg: 4, xl: 3 },
98
+ };
99
+
195
100
  /**
196
101
  * Generic method that sorts an array of items based on an item key
197
102
  * @param values The array that should be sorted
@@ -229,20 +134,18 @@ function filterByTag(array, tagType, condition) {
229
134
  console.log('filter array');
230
135
  console.log(filteredArray);
231
136
  return [...filteredArray];
232
- }
233
-
234
- // /* eslint-disable @typescript-eslint/no-explicit-any */
235
- const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialCondition }) => {
137
+ }
138
+
139
+ const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialCondition, }) => {
236
140
  const [tagAndCondition, setTagAndCondition] = React.useState({ tag: initialTag, condition: initialCondition });
237
- const [tagFilteredItems, setTagFilteredItems] = React.useState(allItems);
238
141
  const setTagWithCondition = (t, c) => {
239
142
  setTagAndCondition({ tag: t, condition: c });
240
143
  };
241
- React.useEffect(() => {
242
- const tagFiltered = tagAndCondition.tag === 'id' ?
243
- allItems :
244
- filterByTag(allItems, tagAndCondition.tag, tagAndCondition.condition);
245
- setTagFilteredItems(tagFiltered);
144
+ const tagFilteredItems = React.useMemo(() => {
145
+ const tagFiltered = tagAndCondition.tag === 'id'
146
+ ? allItems
147
+ : filterByTag(allItems, tagAndCondition.tag, tagAndCondition.condition);
148
+ return tagFiltered;
246
149
  }, [allItems, tagAndCondition]);
247
150
  return {
248
151
  tagAndCondition,
@@ -250,8 +153,8 @@ const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialConditi
250
153
  tagFilteredItems,
251
154
  setTagWithCondition,
252
155
  };
253
- };
254
-
156
+ };
157
+
255
158
  /**
256
159
  * Filters a list of event by a search term
257
160
  * @param allEvents event list that will be formatted
@@ -260,48 +163,159 @@ const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialConditi
260
163
  */
261
164
  const useFilteredThumbnailListItems = (allItems, initialSearchTerm = '') => {
262
165
  const [searchTerm, setSearchTerm] = React.useState(initialSearchTerm);
263
- const [filteredItems, setFilteredItems] = React.useState(allItems);
264
- React.useEffect(() => {
265
- const filterEvents = () => {
266
- const filtered = [...allItems].filter((item) => item.title.toLowerCase().includes(searchTerm.toLowerCase()));
267
- setFilteredItems(filtered);
268
- };
269
- filterEvents();
166
+ const filteredItems = React.useMemo(() => {
167
+ const filtered = [...allItems].filter((item) => item.title.toLowerCase().includes(searchTerm.toLowerCase()));
168
+ return filtered;
270
169
  }, [allItems, searchTerm]);
271
170
  return { searchTerm, setSearchTerm, filteredItems };
272
- };
273
-
171
+ };
172
+
274
173
  const useSortedThumbnailListItems = (allItems, initialSortBy, initialSortAscending) => {
275
174
  const [sortBy, setSortBy] = React.useState(initialSortBy);
276
175
  const [sortAscending, setSortAscending] = React.useState(initialSortAscending);
277
- const [sortedItems, setSortedItems] = React.useState(allItems);
278
- React.useEffect(() => {
176
+ const sortedItems = React.useMemo(() => {
279
177
  let sorted = orderByArray(allItems, sortBy);
280
178
  if (!sortAscending) {
281
179
  sorted = sorted.reverse();
282
180
  }
283
- setSortedItems(sorted);
284
- }, [allItems, sortBy, sortAscending, initialSortBy, initialSortAscending]);
181
+ return sorted;
182
+ }, [allItems, sortBy, sortAscending]);
285
183
  return { sortBy, sortAscending, setSortBy, setSortAscending, sortedItems };
286
- };
287
-
288
- const ThumbnailList = function (props) {
289
- // const [originalItems, setOriginalItems] = useState(props.items);
290
- const [listItems, setListItems] = React.useState(props.items);
291
- const { sortedItems, setSortBy, setSortAscending, sortAscending } = useSortedThumbnailListItems(listItems, 'creationTimeStamp', false);
292
- const { setSearchTerm, filteredItems } = useFilteredThumbnailListItems(sortedItems);
293
- const { tagFilteredItems, setTagAndCondition, tagAndCondition } = useTagFilteredThumbnailListItems({ allItems: filteredItems, initialTag: 'id' });
294
- console.log('tag filterd');
295
- console.log(tagFilteredItems);
296
- console.log(listItems);
184
+ };
185
+
186
+ const defaultConfiguration = {
187
+ sortBy: 'id',
188
+ sortAscending: true,
189
+ tag: 'id',
190
+ };
191
+
192
+ const ThumbnailListSearchField = () => {
193
+ const [input, setInput] = React.useState('');
194
+ const [showClearIcon, setShowClearIcon] = React.useState('hidden');
195
+ const { setSearchTerm } = useThumbnailListItemContext();
196
+ console.log('Searchfield rerenders');
197
+ const handleChange = (value) => {
198
+ setInput(value);
199
+ setShowClearIcon(value === '' ? 'hidden' : '');
200
+ };
201
+ const debouncedSetSearchTerm = React.useCallback(lodash.debounce(setSearchTerm, 50), []);
297
202
  React.useEffect(() => {
298
- if (props.items) {
299
- // setOriginalItems(props.items);
300
- setListItems(props.items);
203
+ debouncedSetSearchTerm(input);
204
+ return () => {
205
+ debouncedSetSearchTerm.cancel();
206
+ };
207
+ }, [input, debouncedSetSearchTerm]);
208
+ return (jsxRuntime.jsx(material.Box, { sx: { marginLeft: 'auto' }, children: jsxRuntime.jsx(material.FormControl, { children: jsxRuntime.jsx(material.TextField, { fullWidth: true, value: input, size: "small", variant: "outlined", onChange: (event) => handleChange(event.target.value), InputProps: {
209
+ startAdornment: (jsxRuntime.jsx(material.InputAdornment, { position: "start", children: jsxRuntime.jsx(SearchIcon__default["default"], {}) })),
210
+ endAdornment: (jsxRuntime.jsx(material.InputAdornment, { position: "end", children: jsxRuntime.jsx(material.IconButton, { onClick: () => handleChange(''), sx: { visibility: showClearIcon, padding: 0 }, children: jsxRuntime.jsx(ClearIcon__default["default"], {}) }) })),
211
+ } }) }) }));
212
+ };
213
+ ThumbnailListSearchField.defaultProps = {
214
+ align: 'start',
215
+ };
216
+ var ThumbnailListSearchField$1 = React__default["default"].memo(ThumbnailListSearchField);
217
+
218
+ function ThumbnailListFilterTag(props) {
219
+ const theme = material.useTheme();
220
+ const handleOnClick = (value) => {
221
+ if (props.onClickCallback) {
222
+ props.onClickCallback(value);
223
+ }
224
+ };
225
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: material.useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) || !props.icon ? (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Chip, { label: props.label, variant: props.variant, onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined }) })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(material.Tooltip, { title: props.label, children: jsxRuntime.jsx(material.IconButton, { onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined, children: props.icon }) }) })) }));
226
+ }
227
+
228
+ function ThumbnailListFilterTags(props) {
229
+ const { tagFilterCallback, tagAndCondition } = useThumbnailListItemContext();
230
+ console.log('filter tags rerenders');
231
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: props.tags.map((tag, index) => {
232
+ return (jsxRuntime.jsx(ThumbnailListFilterTag, { label: tag.label, value: tag.key.toString(), variant: tagAndCondition.tag === tag.key ? 'filled' : 'outlined', collapseBreakpoint: props.muiCollapseBreakpoint, onClickCallback: (value) => tagFilterCallback({ tag: value, condition: tag.condition }), icon: tag.icon }, `${index}_${tag.key.toString()}`));
233
+ }) }));
234
+ }
235
+ ThumbnailListFilterTags.defaultProps = {
236
+ align: 'start',
237
+ muiCollapseBreakpoint: 'md',
238
+ };
239
+
240
+ /**
241
+ * Displays a generic MUI select dropdown.
242
+ * Optinal collapses to a sort icon at a certain breakpoint
243
+ * @param props.label Select Label
244
+ * @param props.width * Width of the input field
245
+ * @param props.collapseBreakPoint * MUI breakpoint after that the select will collapse to the sort icon
246
+ * @param props.onChangeCallback * Callback function that gets triggered once a item is selected
247
+ * @param props.items * Array of items (name-value-pairs) that will be the select options
248
+ * @returns Drowpdown Input Component
249
+ */
250
+ function DropdownInput(props) {
251
+ const [value, setValue] = React.useState(props.defaultValue ?? '');
252
+ const theme = material.useTheme();
253
+ const [anchorEl, setAnchorEl] = React.useState(null);
254
+ const handleChange = (value, name) => {
255
+ setValue(value);
256
+ setAnchorEl(null);
257
+ props.onChangeCallback(value, name);
258
+ };
259
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [material.useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) ? (jsxRuntime.jsxs(material.FormControl, { sx: { width: props.width, textAlign: 'start' }, children: [jsxRuntime.jsx(material.InputLabel, { size: "small", children: props.label }), jsxRuntime.jsx(material.Select, { value: value, size: "small", label: props.label, onChange: (event) => handleChange(event.target.value, event.target.name), children: props.items.map((item) => {
260
+ return (jsxRuntime.jsx(material.MenuItem, { value: item.value, children: item.name }, item.value));
261
+ }) })] })) : (jsxRuntime.jsx(material.IconButton
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ , {
264
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
+ onClick: (event) => setAnchorEl(event.currentTarget), children: props.icon })), jsxRuntime.jsx(material.Menu, { anchorEl: anchorEl, open: Boolean(anchorEl), onClose: () => setAnchorEl(null), children: props.items.map((item) => (jsxRuntime.jsx(material.MenuItem, { onClick: () => handleChange(item.value), children: item.name }, item.value))) })] }));
266
+ }
267
+
268
+ function ThumbnailListHeaderSort(props) {
269
+ const { setSortAscending, sortAscending, setSortBy, sortBy } = useThumbnailListItemContext();
270
+ console.log('Header sort rerenders');
271
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsxs(material.Box, { sx: { minWidth: '80px' }, children: [jsxRuntime.jsx(material.Tooltip, { title: "asc/desc", children: jsxRuntime.jsx(material.IconButton, { onClick: () => setSortAscending(!sortAscending), children: jsxRuntime.jsx(SwapVertIcon__default["default"], {}) }) }), jsxRuntime.jsx(DropdownInput, { width: "130px", collapseBreakpoint: props.muiBreakpoint, label: 'sort', defaultValue: sortBy, icon: jsxRuntime.jsx(material.Tooltip, { title: 'sort', children: jsxRuntime.jsx(SortIcon__default["default"], {}) }), items: props.items.map((i) => {
272
+ return { name: i.label, value: i.key.toString() };
273
+ }), onChangeCallback: (value) => setSortBy(value) })] }) }));
274
+ }
275
+ ThumbnailListHeaderSort.defaultProps = {
276
+ align: 'start',
277
+ muiBreakpoint: 'md',
278
+ };
279
+
280
+ const ThumbnailListHeader = function (props) {
281
+ const startAlignedItems = [];
282
+ const endAlignedItems = [];
283
+ // Iterate through each child to categorize them based on their 'align' prop
284
+ React.Children.forEach(props.children, (child) => {
285
+ if (React__default["default"].isValidElement(child)) {
286
+ const alignment = child.props.align || 'start';
287
+ if (alignment === 'end') {
288
+ endAlignedItems.push(child);
289
+ }
290
+ else {
291
+ startAlignedItems.push(child);
292
+ }
301
293
  }
302
- }, [props.items]);
294
+ });
295
+ return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsxs(material.Stack, { direction: "row", alignItems: "center", justifyContent: props.justifyContent ?? 'space-between', gap: 2, children: [jsxRuntime.jsx(material.Stack, { direction: "row", alignItems: "center", gap: 2, justifyContent: "start", children: startAlignedItems }), endAlignedItems] }) }));
296
+ };
297
+ ThumbnailListHeader.SearchField = ThumbnailListSearchField$1;
298
+ ThumbnailListHeader.FilterTags = ThumbnailListFilterTags;
299
+ ThumbnailListHeader.Sort = ThumbnailListHeaderSort;
300
+
301
+ /**
302
+ * Main Component: Renders all sub components
303
+ * Includes ThumbnailList Provider for context data
304
+ * @param props react children, items
305
+ * @returns component
306
+ */
307
+ function ThumbnailList(props) {
308
+ const combinedConfig = {
309
+ ...defaultConfiguration,
310
+ ...props.config, // This will override the defaults with any props that are not undefined
311
+ };
312
+ const [listItems, setListItems] = React.useState(props.items);
313
+ const { sortedItems, setSortBy, setSortAscending, sortAscending } = useSortedThumbnailListItems(listItems, combinedConfig.sortBy.toString(), combinedConfig.sortAscending);
314
+ const { tagFilteredItems, setTagAndCondition, tagAndCondition } = useTagFilteredThumbnailListItems({ allItems: sortedItems, initialTag: combinedConfig.tag.toString() });
315
+ const { setSearchTerm, filteredItems } = useFilteredThumbnailListItems(tagFilteredItems);
316
+ console.log('Thumbnaillist renders');
303
317
  return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(ThumbnailListItemContext.Provider, { value: {
304
- items: tagFilteredItems,
318
+ items: filteredItems,
305
319
  setItems: setListItems,
306
320
  originalItems: listItems,
307
321
  setOriginalItems: setListItems,
@@ -311,10 +325,11 @@ const ThumbnailList = function (props) {
311
325
  setSortAscending: setSortAscending,
312
326
  sortAscending: sortAscending,
313
327
  setSortBy: setSortBy,
314
- }, children: jsxRuntime.jsx(material.Grid, { container: true, spacing: 2, children: props.children }) }) }));
315
- };
328
+ sortBy: combinedConfig.sortBy.toString(),
329
+ isLoading: false,
330
+ }, children: jsxRuntime.jsx(material.Stack, { direction: "column", sx: { width: '100%', minWidth: '425px' }, children: props.children }) }) }));
331
+ }
316
332
  ThumbnailList.MainContent = ThumbnailListMainContent;
317
- ThumbnailList.Header = ThumbnailListHeader;
318
-
319
- exports.HelloWorld = HelloWorld;
320
- exports.ThumbnailList = ThumbnailList;
333
+ ThumbnailList.Header = ThumbnailListHeader;
334
+
335
+ exports.ThumbnailList = ThumbnailList;
package/dist/index.esm.js CHANGED
@@ -1,53 +1,22 @@
1
- import { Button, styled, Box, Typography, Stack as Stack$1, Card, CardActionArea, Grid, FormControl, TextField, InputAdornment, IconButton, useTheme, useMediaQuery, Chip, Tooltip, InputLabel, Select, MenuItem, Menu } from '@mui/material';
2
- import React, { createContext, useContext, Children, useState, useEffect } from 'react';
3
- import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
- import { Stack } from '@mui/system';
5
- import SearchIcon from '@mui/icons-material/Search';
6
- import ClearIcon from '@mui/icons-material/Clear';
7
- import SwapVertIcon from '@mui/icons-material/SwapVert';
8
- import SortIcon from '@mui/icons-material/Sort';
9
-
10
- function HelloWorld() {
11
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "Hello World"), /*#__PURE__*/React.createElement(Button, {
12
- variant: "contained"
13
- }, "Contained"));
14
- }
15
-
16
- /* eslint-disable @typescript-eslint/no-explicit-any */
1
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
+ import { styled, Box, Typography, Card, CardActionArea, Stack as Stack$1, Grid, LinearProgress, FormControl, TextField, InputAdornment, IconButton, useTheme, useMediaQuery, Chip, Tooltip, InputLabel, Select, MenuItem, Menu } from '@mui/material';
3
+ import React, { createContext, useContext, useMemo, useState, useCallback, useEffect, Children } from 'react';
4
+ import { Stack } from '@mui/system';
5
+ import SearchIcon from '@mui/icons-material/Search';
6
+ import ClearIcon from '@mui/icons-material/Clear';
7
+ import { debounce } from 'lodash';
8
+ import SwapVertIcon from '@mui/icons-material/SwapVert';
9
+ import SortIcon from '@mui/icons-material/Sort';
10
+
17
11
  const ThumbnailListItemContext = createContext(undefined);
18
- // Create a custom hook to consume the context
19
12
  const useThumbnailListItemContext = () => {
20
13
  const context = useContext(ThumbnailListItemContext);
21
14
  if (!context) {
22
- throw new Error('useMyContext must be used within a MyContextProvider');
15
+ throw new Error('no context provider available');
23
16
  }
24
17
  return context;
25
- };
26
-
27
- /**
28
- * Can be used as parent component to crop a wrapped image
29
- * @param props width: width for cropping
30
- * height: height for cropping
31
- * seperate xs and sm values for mui breakpoints
32
- * @returns component
33
- */
34
- function ImageCropper(props) {
35
- const ThumbnailImageCrop = styled('div')((p) => ({
36
- [p.theme.breakpoints.up('xs')]: {
37
- minWidth: props.width.xs,
38
- maxWidth: props.width.xs,
39
- height: props.height.xs,
40
- overflow: 'hidden'
41
- },
42
- [p.theme.breakpoints.up('sm')]: {
43
- minWidth: props.width.sm,
44
- maxwWidth: props.width.sm,
45
- height: props.height.sm,
46
- },
47
- }));
48
- return (jsx(Fragment, { children: jsx(ThumbnailImageCrop, { children: props.children }) }));
49
- }
50
-
18
+ };
19
+
51
20
  /**
52
21
  * Creates a ellipies text with webkit css styles
53
22
  * @param props lineClamp: lines till ellipses
@@ -58,128 +27,64 @@ function EllipsisContainer(props) {
58
27
  [p.theme.breakpoints.up('xs')]: {
59
28
  overflow: 'hidden',
60
29
  display: '-webkit-box',
61
- WebkitLineClamp: props.lineClamp.xs.toString(),
30
+ WebkitLineClamp: props.lineClamp.xs.toString() /* number of lines to show */,
62
31
  WebkitBoxOrient: 'vertical',
63
32
  },
64
33
  [p.theme.breakpoints.up('sm')]: {
65
34
  overflow: 'hidden',
66
35
  display: '-webkit-box',
67
- WebkitLineClamp: props.lineClamp.sm.toString(),
68
- WebkitBoxOrient: 'vertical', /* number of lines to show */
36
+ WebkitLineClamp: props.lineClamp.sm.toString() /* number of lines to show */,
37
+ WebkitBoxOrient: 'vertical' /* number of lines to show */,
69
38
  },
70
39
  }));
71
- return (jsx(EllipsisContainer, { children: props.children }));
72
- }
73
-
40
+ return jsx(EllipsisContainer, { children: props.children });
41
+ }
42
+
74
43
  function ThumbnailListItemTitle(props) {
75
- const StyledCardContent = styled('div')((props) => ({
76
- [props.theme.breakpoints.up('xs')]: {
77
- 'padding': props.theme.spacing(1),
78
- 'flex': '1 0 auto',
44
+ const StyledCardContent = styled('div')((p) => ({
45
+ [p.theme.breakpoints.up('xs')]: {
46
+ padding: p.theme.spacing(1),
47
+ flex: '1 0 auto',
79
48
  '&:last-child': { paddingBottom: 0 },
80
- 'overflow': 'hidden',
49
+ overflow: 'hidden',
81
50
  },
82
51
  }));
83
- const children = Children.toArray(props.children);
84
- return jsx(Fragment, { children: jsx(Box, { children: jsxs(StyledCardContent, { children: [jsx(EllipsisContainer, { lineClamp: { xs: 1, sm: 2 }, children: jsx(Typography, { variant: 'h4', children: props.title }) }), jsx(Stack, { direction: "row", gap: 1, children: children.map((child, index) => jsx(Typography, { variant: "subtitle2", color: "text.secondary", children: child }, index)) })] }) }) });
85
- }
86
-
87
- function ThumbnailListItemInfoLabel(props) {
88
- return jsx(Fragment, { children: jsxs(Stack$1, { textAlign: "right", justifyContent: "space-between", children: [jsx(Box, { textAlign: "right", padding: 1, children: props.topContent }), jsx(Box, { textAlign: "right", padding: 1, children: props.bottomContent })] }) });
89
- }
90
-
91
- // import {Link} from 'react-router-dom';
52
+ console.log('item title rerenders');
53
+ return (jsx(Fragment, { children: jsx(Box, { children: jsxs(StyledCardContent, { children: [jsx(EllipsisContainer, { lineClamp: { xs: 1, sm: 2 }, children: jsx(Typography, { variant: "subtitle2", sx: { fontWeight: 'bold' }, children: props.title }) }), jsx(Stack, { direction: "row", gap: 1, children: jsx(EllipsisContainer, { lineClamp: { xs: 1, sm: 2 }, children: jsx(Typography, { variant: "subtitle2", sx: { fontSize: '0.84rem' }, color: "text.secondary", children: props.subTitle }) }) })] }) }) }));
54
+ }
55
+
92
56
  const ThumbnailListItem = (props) => {
93
- return jsx(Fragment, { children: jsx(Card, { sx: { display: 'flex' }, children: jsx(CardActionArea, { children: jsxs(Stack$1, { direction: "row", width: "100%", children: [jsx(ImageCropper, { width: { xs: '98px', sm: '160px' }, height: { xs: '54px', sm: '90px' }, children: jsx(Box, { children: "Placeholder for image" }) }), jsx(Stack$1, { direction: "row", justifyContent: "space-between", width: "100%", gap: 1, children: props.children })] }) }) }) });
57
+ console.log('ThumbnailListItems renders');
58
+ return (jsx(Fragment, { children: jsx(Card, { sx: { display: 'flex' }, children: jsx(CardActionArea, { disabled: !props.onClick, onClick: () => props.onClick(props.id), children: jsxs(Stack$1, { direction: "row", width: "100%", children: [jsx("img", { src: props.thumbnailUrl, width: '45%' }), jsxs(Stack$1, { direction: "row", justifyContent: "space-between", width: "100%", gap: 1, children: [jsx(ThumbnailListItemTitle, { title: props.title, subTitle: props.subTitle }), props.infoLabel] })] }) }) }) }));
94
59
  };
95
- ThumbnailListItem.Title = ThumbnailListItemTitle;
96
- ThumbnailListItem.InfoLabel = ThumbnailListItemInfoLabel;
97
-
98
- function ThumbnailListMainContent( /* props: {children: ReactNode}*/) {
99
- // const children = Children.toArray(props.children);
100
- const { items } = useThumbnailListItemContext();
101
- return (jsx(Fragment, { children: items.map((item) => {
102
- return (jsx(Grid, { item: true, xs: 12, lg: 6, xl: 3, children: jsxs(ThumbnailListItem, { id: item.id,
103
- // link={item.link}
104
- thumbnailUrl: item.thumbnailUrl, children: [jsx(ThumbnailListItem.Title, { title: item.title, children: item.subTitle }), item.label] }) }, item.id));
105
- }) }));
106
- }
107
-
108
- const ThumbnailListSearchField = () => {
109
- const [input, setInput] = useState('');
110
- const [showClearIcon, setShowClearIcon] = useState('hidden');
111
- const { setSearchTerm } = useThumbnailListItemContext();
112
- const handleChange = (value) => {
113
- setInput(value);
114
- setShowClearIcon(value === '' ? 'hidden' : '');
115
- setSearchTerm(value);
116
- };
117
- return (jsx(Fragment, { children: jsx(FormControl, { children: jsx(TextField, { sx: { input: { color: 'white' } }, fullWidth: true, value: input, size: "small", variant: "outlined", onChange: (event) => handleChange(event.target.value), InputProps: {
118
- startAdornment: (jsx(InputAdornment, { position: "start", children: jsx(SearchIcon, {}) })),
119
- endAdornment: (jsx(InputAdornment, { position: "end", children: jsx(IconButton, { onClick: () => handleChange(''), sx: { visibility: showClearIcon, padding: 0 }, children: jsx(ClearIcon, {}) }) })),
120
- } }) }) }));
121
- };
122
-
123
- function ThumbnailListFilterTag(props) {
124
- const theme = useTheme();
125
- const handleOnClick = (value) => {
126
- if (props.onClickCallback) {
127
- props.onClickCallback(value);
128
- }
129
- };
130
- return (jsx(Fragment, { children: useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) || !props.icon ?
131
- jsx(Fragment, { children: jsx(Chip, { label: props.label, variant: props.variant, onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined }) }) :
132
- jsx(Fragment, { children: jsx(Tooltip, { title: props.label, children: jsx(IconButton, { onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined, children: props.icon }) }) }) }));
60
+ var ThumbnailListItem$1 = React.memo(ThumbnailListItem);
61
+
62
+ const RatioWrapper = styled('div')(() => ({
63
+ // Assuming a 16:9 aspect ratio
64
+ paddingTop: '27.75%',
65
+ position: 'relative',
66
+ width: '100%',
67
+ '& > *': {
68
+ position: 'absolute',
69
+ top: 0,
70
+ left: 0,
71
+ right: 0,
72
+ bottom: 0,
73
+ },
74
+ }));
75
+ function ThumbnailListMainContent(props) {
76
+ const { items, isLoading } = useThumbnailListItemContext();
77
+ console.log('main content rerenders');
78
+ const memoizedItems = useMemo(() => {
79
+ return items.map((item) => (jsx(Grid, { item: true, xs: props.muiBreakpoints.xs, sm: props.muiBreakpoints.sm, md: props.muiBreakpoints.md, lg: props.muiBreakpoints.lg, xl: props.muiBreakpoints.xl, children: jsx(RatioWrapper, { children: jsx(ThumbnailListItem$1, { id: item.id, thumbnailUrl: item.thumbnailUrl, title: item.title, subTitle: item.subTitle, infoLabel: item.label, onClick: item.onClick }) }) }, item.id)));
80
+ }, [items, props.muiBreakpoints]);
81
+ return (jsxs(Fragment, { children: [jsx(Box, { sx: { mt: 0.75, mb: 0.75 }, children: jsx(LinearProgress, { sx: { opacity: isLoading ? 1 : 0 } }) }), jsx(Grid, { container: true, spacing: props.spacing, children: memoizedItems })] }));
133
82
  }
134
-
135
- function ThumbnailListFilterTags(props) {
136
- const { tagFilterCallback, tagAndCondition } = useThumbnailListItemContext();
137
- return (jsx(Fragment, { children: props.tags.map((tag) => {
138
- return (jsx(ThumbnailListFilterTag, { label: tag.label, value: tag.value, variant: tagAndCondition.tag === tag.value ? 'filled' : 'outlined', collapseBreakpoint: props.collapseBreakpoint, onClickCallback: (value) => tagFilterCallback({ tag: value, condition: tag.condition }), icon: tag.icon }));
139
- }) }));
140
- }
141
-
142
- /**
143
- * Displays a generic MUI select dropdown.
144
- * Optinal collapses to a sort icon at a certain breakpoint
145
- * @param props.label Select Label
146
- * @param props.width * Width of the input field
147
- * @param props.collapseBreakPoint * MUI breakpoint after that the select will collapse to the sort icon
148
- * @param props.onChangeCallback * Callback function that gets triggered once a item is selected
149
- * @param props.items * Array of items (name-value-pairs) that will be the select options
150
- * @returns Drowpdown Input Component
151
- */
152
- function DropdownInput(props) {
153
- const [value, setValue] = useState(props.defaultValue ?? '');
154
- const theme = useTheme();
155
- const [anchorEl, setAnchorEl] = useState(null);
156
- const handleChange = (value, name) => {
157
- setValue(value);
158
- setAnchorEl(null);
159
- props.onChangeCallback(value, name);
160
- };
161
- return (jsxs(Fragment, { children: [useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) ?
162
- jsxs(FormControl, { sx: { width: props.width, textAlign: 'start' }, children: [jsx(InputLabel, { size: 'small', id: "demo-simple-select-label", children: props.label }), jsx(Select, { value: value, size: 'small', label: props.label, onChange: (event) => handleChange(event.target.value, event.target.name), children: props.items.map((item) => {
163
- return jsx(MenuItem, { value: item.value, children: item.name }, item.value);
164
- }) })] }) : (jsx(IconButton
165
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
- , {
167
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
- onClick: (event) => setAnchorEl(event.currentTarget), children: props.icon })), jsx(Menu, { anchorEl: anchorEl, open: Boolean(anchorEl), onClose: () => setAnchorEl(null), children: props.items.map((item) => (jsx(MenuItem, { onClick: () => handleChange(item.value), children: item.name }, item.value))) })] }));
169
- }
170
-
171
- function ThumbnailListHeaderSort(props) {
172
- const { setSortAscending, sortAscending, setSortBy } = useThumbnailListItemContext();
173
- return (jsx(Fragment, { children: jsxs(Box, { sx: { width: '-webkit-fill-available', textAlign: 'end' }, children: [jsx(Tooltip, { title: "asc/desc", children: jsx(IconButton, { onClick: () => setSortAscending(!sortAscending), children: jsx(SwapVertIcon, {}) }) }), jsx(DropdownInput, { width: "130px", collapseBreakpoint: 'md', label: 'sort', defaultValue: "creationTimeStamp", icon: jsx(Tooltip, { title: 'sort', children: jsx(SortIcon, {}) }), items: props.items, onChangeCallback: (value) => setSortBy(value) })] }) }));
174
- }
175
-
176
- const ThumbnailListHeader = function (props) {
177
- return (jsx(Fragment, { children: jsx(Grid, { item: true, xs: 12, children: jsx(Stack$1, { direction: "row", alignItems: "center", justifyContent: props.justifyContent ?? 'space-between', gap: 2, children: props.children }) }) }));
178
- };
179
- ThumbnailListHeader.SearchField = ThumbnailListSearchField;
180
- ThumbnailListHeader.FilterTags = ThumbnailListFilterTags;
181
- ThumbnailListHeader.Sort = ThumbnailListHeaderSort;
182
-
83
+ ThumbnailListMainContent.defaultProps = {
84
+ spacing: 2,
85
+ muiBreakpoints: { xs: 12, sm: 6, md: 6, lg: 4, xl: 3 },
86
+ };
87
+
183
88
  /**
184
89
  * Generic method that sorts an array of items based on an item key
185
90
  * @param values The array that should be sorted
@@ -217,20 +122,18 @@ function filterByTag(array, tagType, condition) {
217
122
  console.log('filter array');
218
123
  console.log(filteredArray);
219
124
  return [...filteredArray];
220
- }
221
-
222
- // /* eslint-disable @typescript-eslint/no-explicit-any */
223
- const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialCondition }) => {
125
+ }
126
+
127
+ const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialCondition, }) => {
224
128
  const [tagAndCondition, setTagAndCondition] = useState({ tag: initialTag, condition: initialCondition });
225
- const [tagFilteredItems, setTagFilteredItems] = useState(allItems);
226
129
  const setTagWithCondition = (t, c) => {
227
130
  setTagAndCondition({ tag: t, condition: c });
228
131
  };
229
- useEffect(() => {
230
- const tagFiltered = tagAndCondition.tag === 'id' ?
231
- allItems :
232
- filterByTag(allItems, tagAndCondition.tag, tagAndCondition.condition);
233
- setTagFilteredItems(tagFiltered);
132
+ const tagFilteredItems = useMemo(() => {
133
+ const tagFiltered = tagAndCondition.tag === 'id'
134
+ ? allItems
135
+ : filterByTag(allItems, tagAndCondition.tag, tagAndCondition.condition);
136
+ return tagFiltered;
234
137
  }, [allItems, tagAndCondition]);
235
138
  return {
236
139
  tagAndCondition,
@@ -238,8 +141,8 @@ const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialConditi
238
141
  tagFilteredItems,
239
142
  setTagWithCondition,
240
143
  };
241
- };
242
-
144
+ };
145
+
243
146
  /**
244
147
  * Filters a list of event by a search term
245
148
  * @param allEvents event list that will be formatted
@@ -248,48 +151,159 @@ const useTagFilteredThumbnailListItems = ({ allItems, initialTag, initialConditi
248
151
  */
249
152
  const useFilteredThumbnailListItems = (allItems, initialSearchTerm = '') => {
250
153
  const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
251
- const [filteredItems, setFilteredItems] = useState(allItems);
252
- useEffect(() => {
253
- const filterEvents = () => {
254
- const filtered = [...allItems].filter((item) => item.title.toLowerCase().includes(searchTerm.toLowerCase()));
255
- setFilteredItems(filtered);
256
- };
257
- filterEvents();
154
+ const filteredItems = useMemo(() => {
155
+ const filtered = [...allItems].filter((item) => item.title.toLowerCase().includes(searchTerm.toLowerCase()));
156
+ return filtered;
258
157
  }, [allItems, searchTerm]);
259
158
  return { searchTerm, setSearchTerm, filteredItems };
260
- };
261
-
159
+ };
160
+
262
161
  const useSortedThumbnailListItems = (allItems, initialSortBy, initialSortAscending) => {
263
162
  const [sortBy, setSortBy] = useState(initialSortBy);
264
163
  const [sortAscending, setSortAscending] = useState(initialSortAscending);
265
- const [sortedItems, setSortedItems] = useState(allItems);
266
- useEffect(() => {
164
+ const sortedItems = useMemo(() => {
267
165
  let sorted = orderByArray(allItems, sortBy);
268
166
  if (!sortAscending) {
269
167
  sorted = sorted.reverse();
270
168
  }
271
- setSortedItems(sorted);
272
- }, [allItems, sortBy, sortAscending, initialSortBy, initialSortAscending]);
169
+ return sorted;
170
+ }, [allItems, sortBy, sortAscending]);
273
171
  return { sortBy, sortAscending, setSortBy, setSortAscending, sortedItems };
274
- };
275
-
276
- const ThumbnailList = function (props) {
277
- // const [originalItems, setOriginalItems] = useState(props.items);
278
- const [listItems, setListItems] = useState(props.items);
279
- const { sortedItems, setSortBy, setSortAscending, sortAscending } = useSortedThumbnailListItems(listItems, 'creationTimeStamp', false);
280
- const { setSearchTerm, filteredItems } = useFilteredThumbnailListItems(sortedItems);
281
- const { tagFilteredItems, setTagAndCondition, tagAndCondition } = useTagFilteredThumbnailListItems({ allItems: filteredItems, initialTag: 'id' });
282
- console.log('tag filterd');
283
- console.log(tagFilteredItems);
284
- console.log(listItems);
172
+ };
173
+
174
+ const defaultConfiguration = {
175
+ sortBy: 'id',
176
+ sortAscending: true,
177
+ tag: 'id',
178
+ };
179
+
180
+ const ThumbnailListSearchField = () => {
181
+ const [input, setInput] = useState('');
182
+ const [showClearIcon, setShowClearIcon] = useState('hidden');
183
+ const { setSearchTerm } = useThumbnailListItemContext();
184
+ console.log('Searchfield rerenders');
185
+ const handleChange = (value) => {
186
+ setInput(value);
187
+ setShowClearIcon(value === '' ? 'hidden' : '');
188
+ };
189
+ const debouncedSetSearchTerm = useCallback(debounce(setSearchTerm, 50), []);
285
190
  useEffect(() => {
286
- if (props.items) {
287
- // setOriginalItems(props.items);
288
- setListItems(props.items);
191
+ debouncedSetSearchTerm(input);
192
+ return () => {
193
+ debouncedSetSearchTerm.cancel();
194
+ };
195
+ }, [input, debouncedSetSearchTerm]);
196
+ return (jsx(Box, { sx: { marginLeft: 'auto' }, children: jsx(FormControl, { children: jsx(TextField, { fullWidth: true, value: input, size: "small", variant: "outlined", onChange: (event) => handleChange(event.target.value), InputProps: {
197
+ startAdornment: (jsx(InputAdornment, { position: "start", children: jsx(SearchIcon, {}) })),
198
+ endAdornment: (jsx(InputAdornment, { position: "end", children: jsx(IconButton, { onClick: () => handleChange(''), sx: { visibility: showClearIcon, padding: 0 }, children: jsx(ClearIcon, {}) }) })),
199
+ } }) }) }));
200
+ };
201
+ ThumbnailListSearchField.defaultProps = {
202
+ align: 'start',
203
+ };
204
+ var ThumbnailListSearchField$1 = React.memo(ThumbnailListSearchField);
205
+
206
+ function ThumbnailListFilterTag(props) {
207
+ const theme = useTheme();
208
+ const handleOnClick = (value) => {
209
+ if (props.onClickCallback) {
210
+ props.onClickCallback(value);
211
+ }
212
+ };
213
+ return (jsx(Fragment, { children: useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) || !props.icon ? (jsx(Fragment, { children: jsx(Chip, { label: props.label, variant: props.variant, onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined }) })) : (jsx(Fragment, { children: jsx(Tooltip, { title: props.label, children: jsx(IconButton, { onClick: props.onClickCallback ? () => handleOnClick(props.value) : undefined, children: props.icon }) }) })) }));
214
+ }
215
+
216
+ function ThumbnailListFilterTags(props) {
217
+ const { tagFilterCallback, tagAndCondition } = useThumbnailListItemContext();
218
+ console.log('filter tags rerenders');
219
+ return (jsx(Fragment, { children: props.tags.map((tag, index) => {
220
+ return (jsx(ThumbnailListFilterTag, { label: tag.label, value: tag.key.toString(), variant: tagAndCondition.tag === tag.key ? 'filled' : 'outlined', collapseBreakpoint: props.muiCollapseBreakpoint, onClickCallback: (value) => tagFilterCallback({ tag: value, condition: tag.condition }), icon: tag.icon }, `${index}_${tag.key.toString()}`));
221
+ }) }));
222
+ }
223
+ ThumbnailListFilterTags.defaultProps = {
224
+ align: 'start',
225
+ muiCollapseBreakpoint: 'md',
226
+ };
227
+
228
+ /**
229
+ * Displays a generic MUI select dropdown.
230
+ * Optinal collapses to a sort icon at a certain breakpoint
231
+ * @param props.label Select Label
232
+ * @param props.width * Width of the input field
233
+ * @param props.collapseBreakPoint * MUI breakpoint after that the select will collapse to the sort icon
234
+ * @param props.onChangeCallback * Callback function that gets triggered once a item is selected
235
+ * @param props.items * Array of items (name-value-pairs) that will be the select options
236
+ * @returns Drowpdown Input Component
237
+ */
238
+ function DropdownInput(props) {
239
+ const [value, setValue] = useState(props.defaultValue ?? '');
240
+ const theme = useTheme();
241
+ const [anchorEl, setAnchorEl] = useState(null);
242
+ const handleChange = (value, name) => {
243
+ setValue(value);
244
+ setAnchorEl(null);
245
+ props.onChangeCallback(value, name);
246
+ };
247
+ return (jsxs(Fragment, { children: [useMediaQuery(theme.breakpoints.up(props.collapseBreakpoint ?? 0)) ? (jsxs(FormControl, { sx: { width: props.width, textAlign: 'start' }, children: [jsx(InputLabel, { size: "small", children: props.label }), jsx(Select, { value: value, size: "small", label: props.label, onChange: (event) => handleChange(event.target.value, event.target.name), children: props.items.map((item) => {
248
+ return (jsx(MenuItem, { value: item.value, children: item.name }, item.value));
249
+ }) })] })) : (jsx(IconButton
250
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
251
+ , {
252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
+ onClick: (event) => setAnchorEl(event.currentTarget), children: props.icon })), jsx(Menu, { anchorEl: anchorEl, open: Boolean(anchorEl), onClose: () => setAnchorEl(null), children: props.items.map((item) => (jsx(MenuItem, { onClick: () => handleChange(item.value), children: item.name }, item.value))) })] }));
254
+ }
255
+
256
+ function ThumbnailListHeaderSort(props) {
257
+ const { setSortAscending, sortAscending, setSortBy, sortBy } = useThumbnailListItemContext();
258
+ console.log('Header sort rerenders');
259
+ return (jsx(Fragment, { children: jsxs(Box, { sx: { minWidth: '80px' }, children: [jsx(Tooltip, { title: "asc/desc", children: jsx(IconButton, { onClick: () => setSortAscending(!sortAscending), children: jsx(SwapVertIcon, {}) }) }), jsx(DropdownInput, { width: "130px", collapseBreakpoint: props.muiBreakpoint, label: 'sort', defaultValue: sortBy, icon: jsx(Tooltip, { title: 'sort', children: jsx(SortIcon, {}) }), items: props.items.map((i) => {
260
+ return { name: i.label, value: i.key.toString() };
261
+ }), onChangeCallback: (value) => setSortBy(value) })] }) }));
262
+ }
263
+ ThumbnailListHeaderSort.defaultProps = {
264
+ align: 'start',
265
+ muiBreakpoint: 'md',
266
+ };
267
+
268
+ const ThumbnailListHeader = function (props) {
269
+ const startAlignedItems = [];
270
+ const endAlignedItems = [];
271
+ // Iterate through each child to categorize them based on their 'align' prop
272
+ Children.forEach(props.children, (child) => {
273
+ if (React.isValidElement(child)) {
274
+ const alignment = child.props.align || 'start';
275
+ if (alignment === 'end') {
276
+ endAlignedItems.push(child);
277
+ }
278
+ else {
279
+ startAlignedItems.push(child);
280
+ }
289
281
  }
290
- }, [props.items]);
282
+ });
283
+ return (jsx(Fragment, { children: jsxs(Stack$1, { direction: "row", alignItems: "center", justifyContent: props.justifyContent ?? 'space-between', gap: 2, children: [jsx(Stack$1, { direction: "row", alignItems: "center", gap: 2, justifyContent: "start", children: startAlignedItems }), endAlignedItems] }) }));
284
+ };
285
+ ThumbnailListHeader.SearchField = ThumbnailListSearchField$1;
286
+ ThumbnailListHeader.FilterTags = ThumbnailListFilterTags;
287
+ ThumbnailListHeader.Sort = ThumbnailListHeaderSort;
288
+
289
+ /**
290
+ * Main Component: Renders all sub components
291
+ * Includes ThumbnailList Provider for context data
292
+ * @param props react children, items
293
+ * @returns component
294
+ */
295
+ function ThumbnailList(props) {
296
+ const combinedConfig = {
297
+ ...defaultConfiguration,
298
+ ...props.config, // This will override the defaults with any props that are not undefined
299
+ };
300
+ const [listItems, setListItems] = useState(props.items);
301
+ const { sortedItems, setSortBy, setSortAscending, sortAscending } = useSortedThumbnailListItems(listItems, combinedConfig.sortBy.toString(), combinedConfig.sortAscending);
302
+ const { tagFilteredItems, setTagAndCondition, tagAndCondition } = useTagFilteredThumbnailListItems({ allItems: sortedItems, initialTag: combinedConfig.tag.toString() });
303
+ const { setSearchTerm, filteredItems } = useFilteredThumbnailListItems(tagFilteredItems);
304
+ console.log('Thumbnaillist renders');
291
305
  return (jsx(Fragment, { children: jsx(ThumbnailListItemContext.Provider, { value: {
292
- items: tagFilteredItems,
306
+ items: filteredItems,
293
307
  setItems: setListItems,
294
308
  originalItems: listItems,
295
309
  setOriginalItems: setListItems,
@@ -299,9 +313,11 @@ const ThumbnailList = function (props) {
299
313
  setSortAscending: setSortAscending,
300
314
  sortAscending: sortAscending,
301
315
  setSortBy: setSortBy,
302
- }, children: jsx(Grid, { container: true, spacing: 2, children: props.children }) }) }));
303
- };
316
+ sortBy: combinedConfig.sortBy.toString(),
317
+ isLoading: false,
318
+ }, children: jsx(Stack$1, { direction: "column", sx: { width: '100%', minWidth: '425px' }, children: props.children }) }) }));
319
+ }
304
320
  ThumbnailList.MainContent = ThumbnailListMainContent;
305
- ThumbnailList.Header = ThumbnailListHeader;
306
-
307
- export { HelloWorld, ThumbnailList };
321
+ ThumbnailList.Header = ThumbnailListHeader;
322
+
323
+ export { ThumbnailList };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janrankenhohn/react-thumbnail-list",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.esm.js",
6
6
  "files": [
@@ -43,6 +43,7 @@
43
43
  "@babel/core": "^7.23.9",
44
44
  "@babel/preset-env": "^7.23.9",
45
45
  "@babel/preset-react": "^7.23.3",
46
+ "@eslint/js": "^9.1.1",
46
47
  "@rollup/plugin-babel": "^6.0.4",
47
48
  "@rollup/plugin-commonjs": "^25.0.7",
48
49
  "@rollup/plugin-node-resolve": "^15.2.3",
@@ -51,22 +52,32 @@
51
52
  "@testing-library/react": "^13.4.0",
52
53
  "@testing-library/user-event": "^13.5.0",
53
54
  "@types/lodash": "^4.17.0",
55
+ "@typescript-eslint/eslint-plugin": "^7.8.0",
56
+ "@typescript-eslint/parser": "^7.8.0",
57
+ "eslint": "^8.57.0",
58
+ "eslint-config-prettier": "^9.1.0",
59
+ "eslint-plugin-prettier": "^5.1.3",
54
60
  "eslint-plugin-react": "^7.34.1",
61
+ "globals": "^15.1.0",
62
+ "jest": "^27.5.1",
55
63
  "npm-run-all": "^4.1.5",
64
+ "prettier": "^3.2.5",
65
+ "react-router": "^6.26.1",
56
66
  "rollup": "^2.79.1",
57
67
  "rollup-plugin-delete": "^2.0.0",
58
68
  "rollup-plugin-peer-deps-external": "^2.2.4",
59
- "rollup-plugin-ts": "^3.4.5"
69
+ "rollup-plugin-ts": "^3.4.5",
70
+ "typescript-eslint": "^7.8.0"
60
71
  },
61
72
  "peerDependencies": {
62
73
  "@emotion/react": "^11.11.4",
63
74
  "@emotion/styled": "^11.11.0",
64
75
  "@mui/icons-material": "^5.15.11",
65
76
  "@mui/material": "^5.15.11",
77
+ "lodash.debounce": "^4.0.8",
66
78
  "react": "^18.0.0",
67
79
  "react-dom": "^18.0.0",
68
80
  "react-scripts": "5.0.1",
69
- "web-vitals": "^2.1.4",
70
- "lodash.debounce": "^4.0.8"
81
+ "web-vitals": "^2.1.4"
71
82
  }
72
83
  }