@chem-po/react-web 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.js +2 -2
  3. package/package.json +22 -20
  4. package/src/components/auth/SignIn.tsx +43 -0
  5. package/src/components/auth/index.ts +1 -0
  6. package/src/components/box/CollapseHorizontal.tsx +18 -0
  7. package/src/components/box/ContentBox.tsx +17 -0
  8. package/src/components/box/ExpandOnMount.tsx +48 -0
  9. package/src/components/box/Expandable.tsx +96 -0
  10. package/src/components/box/FullSizeContainer.tsx +50 -0
  11. package/src/components/box/MobileFrame/index.tsx +145 -0
  12. package/src/components/box/MobileFrame/styles.css +35 -0
  13. package/src/components/box/index.ts +6 -0
  14. package/src/components/button/DeleteButton.tsx +178 -0
  15. package/src/components/button/Toggle.tsx +88 -0
  16. package/src/components/button/ViewButton.tsx +30 -0
  17. package/src/components/button/index.ts +3 -0
  18. package/src/components/feed/FeedContentPane.tsx +111 -0
  19. package/src/components/feed/MediaFeed.tsx +200 -0
  20. package/src/components/feed/MediaFeedBackground.tsx +127 -0
  21. package/src/components/feed/MediaFeedRefresh.tsx +78 -0
  22. package/src/components/feed/MediaFeedSwipeUp.tsx +34 -0
  23. package/src/components/feed/constants.ts +11 -0
  24. package/src/components/feed/context.tsx +19 -0
  25. package/src/components/feed/hooks.ts +290 -0
  26. package/src/components/feed/index.ts +2 -0
  27. package/src/components/feed/types.ts +50 -0
  28. package/src/components/form/Condition.tsx +26 -0
  29. package/src/components/form/Field.tsx +39 -0
  30. package/src/components/form/Form.tsx +425 -0
  31. package/src/components/form/FormFooter.tsx +82 -0
  32. package/src/components/form/UploadProgress/index.tsx +38 -0
  33. package/src/components/form/UploadProgress/styles.css +23 -0
  34. package/src/components/form/index.ts +4 -0
  35. package/src/components/form/input/Editable.tsx +129 -0
  36. package/src/components/form/input/InputSlider.tsx +75 -0
  37. package/src/components/form/input/OptionalTag.tsx +33 -0
  38. package/src/components/form/input/StandaloneInput.tsx +41 -0
  39. package/src/components/form/input/boolean/index.tsx +53 -0
  40. package/src/components/form/input/color/index.tsx +126 -0
  41. package/src/components/form/input/date/index.tsx +122 -0
  42. package/src/components/form/input/datetime/index.tsx +93 -0
  43. package/src/components/form/input/file.tsx +379 -0
  44. package/src/components/form/input/hooks/index.ts +2 -0
  45. package/src/components/form/input/hooks/useInputImperativeHandle.ts +16 -0
  46. package/src/components/form/input/hooks/useInputStyle.ts +39 -0
  47. package/src/components/form/input/index.ts +2 -0
  48. package/src/components/form/input/input.css +44 -0
  49. package/src/components/form/input/input.tsx +130 -0
  50. package/src/components/form/input/multipleSelect/index.tsx +55 -0
  51. package/src/components/form/input/number/index.tsx +83 -0
  52. package/src/components/form/input/number/styles.css +8 -0
  53. package/src/components/form/input/select/index.tsx +80 -0
  54. package/src/components/form/input/socialMedia/index.tsx +158 -0
  55. package/src/components/form/input/text/index.tsx +72 -0
  56. package/src/components/form/input/text/textarea.tsx +44 -0
  57. package/src/components/form/input/time/index.tsx +33 -0
  58. package/src/components/form/input/type.ts +0 -0
  59. package/src/components/form/input/types.ts +4 -0
  60. package/src/components/form/view/file.tsx +45 -0
  61. package/src/components/form/view/index.tsx +61 -0
  62. package/src/components/form/view/multipleSelect.tsx +38 -0
  63. package/src/components/form/view/select.tsx +33 -0
  64. package/src/components/index.ts +14 -0
  65. package/src/components/list/Body/InfiniteScrollGridBody.tsx +177 -0
  66. package/src/components/list/Body/InfiniteScrollListBody.tsx +114 -0
  67. package/src/components/list/Body/ListBody.tsx +23 -0
  68. package/src/components/list/Body/PagedGridBody.tsx +104 -0
  69. package/src/components/list/Body/PagedListBody.tsx +92 -0
  70. package/src/components/list/Body/hooks.ts +84 -0
  71. package/src/components/list/DataList.tsx +32 -0
  72. package/src/components/list/ListContainer.tsx +20 -0
  73. package/src/components/list/ListContent.tsx +54 -0
  74. package/src/components/list/ListCreate.tsx +57 -0
  75. package/src/components/list/ListFilters.tsx +182 -0
  76. package/src/components/list/ListFooter.tsx +458 -0
  77. package/src/components/list/ListHeader.tsx +180 -0
  78. package/src/components/list/ListItem/ListCell.tsx +48 -0
  79. package/src/components/list/ListItem/ListRow.tsx +38 -0
  80. package/src/components/list/ListItemView.tsx +53 -0
  81. package/src/components/list/ListSort.tsx +84 -0
  82. package/src/components/list/NoItems.tsx +33 -0
  83. package/src/components/list/constants.ts +1 -0
  84. package/src/components/list/index.ts +4 -0
  85. package/src/components/list/types.ts +29 -0
  86. package/src/components/list/utils.ts +62 -0
  87. package/src/components/loading/CircularProgress.tsx +11 -0
  88. package/src/components/loading/Loading.tsx +160 -0
  89. package/src/components/loading/LoadingImage.tsx +123 -0
  90. package/src/components/loading/LoadingSwitch.tsx +78 -0
  91. package/src/components/loading/index.ts +4 -0
  92. package/src/components/media/PlayButton.tsx +94 -0
  93. package/src/components/media/index.ts +1 -0
  94. package/src/components/modal/DefaultModal.tsx +18 -0
  95. package/src/components/modal/DesktopModal.tsx +11 -0
  96. package/src/components/modal/ForceMobile.tsx +7 -0
  97. package/src/components/modal/MobileModal.tsx +89 -0
  98. package/src/components/modal/index.ts +3 -0
  99. package/src/components/modal/type.ts +7 -0
  100. package/src/components/nav/NavBar.tsx +101 -0
  101. package/src/components/nav/index.ts +1 -0
  102. package/src/components/overlay/ImageViewOverlay.tsx +88 -0
  103. package/src/components/overlay/MobileOverlay.tsx +22 -0
  104. package/src/components/overlay/index.ts +2 -0
  105. package/src/components/text/GradientText/index.tsx +16 -0
  106. package/src/components/text/GradientText/styles.css +5 -0
  107. package/src/components/text/NumberTicker.tsx +28 -0
  108. package/src/components/text/index.ts +1 -0
  109. package/src/components/theme/colorMode/DarkModeToggle.tsx +40 -0
  110. package/src/components/theme/colorMode/index.ts +1 -0
  111. package/src/components/theme/index.ts +1 -0
  112. package/src/components/view/ErrorView.tsx +13 -0
  113. package/src/components/view/RedirectView.tsx +42 -0
  114. package/src/components/view/index.ts +2 -0
  115. package/src/contexts/index.ts +1 -0
  116. package/src/contexts/theme.ts +316 -0
  117. package/src/custom.d.ts +4 -0
  118. package/src/hooks/index.ts +1 -0
  119. package/src/hooks/ui/index.ts +1 -0
  120. package/src/hooks/ui/useBorderColor.ts +4 -0
  121. package/src/store/index.ts +1 -0
  122. package/src/store/usePlayer.ts +75 -0
  123. package/src/store/useScreen.ts +22 -0
@@ -0,0 +1,458 @@
1
+ import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
2
+ import {
3
+ Box,
4
+ Button,
5
+ Collapse,
6
+ Flex,
7
+ IconButton,
8
+ Popover,
9
+ PopoverBody,
10
+ PopoverContent,
11
+ PopoverTrigger,
12
+ Progress,
13
+ Text,
14
+ Tooltip,
15
+ useColorModeValue,
16
+ } from '@chakra-ui/react'
17
+ import { toPlural } from '@chem-po/core'
18
+ import { useDataList, usePaginatedList } from '@chem-po/react'
19
+ import useResizeObserver from '@react-hook/resize-observer'
20
+ import { useCallback, useMemo, useRef } from 'react'
21
+ import { useBorderColor } from '../../hooks'
22
+ import { ExpandOnMount } from '../box'
23
+ import { DataListFooterProps } from './types'
24
+
25
+ const NextButton = ({
26
+ goNext,
27
+ canGoNext,
28
+ mobileLayout,
29
+ }: {
30
+ goNext?: () => void
31
+ canGoNext: boolean
32
+ mobileLayout?: boolean
33
+ }) => (
34
+ <Button
35
+ opacity={canGoNext ? 1 : 0.6}
36
+ pointerEvents={canGoNext ? 'auto' : 'none'}
37
+ borderRadius="full"
38
+ onClick={goNext}
39
+ aria-label="next"
40
+ variant="ghost"
41
+ size={mobileLayout ? 'xs' : 'sm'}
42
+ pl={3}
43
+ pr={1}
44
+ gap={1}>
45
+ <Text>Next</Text>
46
+ <ChevronRightIcon w={5} h={5} />
47
+ </Button>
48
+ )
49
+
50
+ const PrevButton = ({
51
+ goPrev,
52
+ canGoPrev,
53
+ mobileLayout,
54
+ }: {
55
+ goPrev?: () => void
56
+ canGoPrev: boolean
57
+ mobileLayout?: boolean
58
+ }) => (
59
+ <Button
60
+ opacity={canGoPrev ? 1 : 0.5}
61
+ pointerEvents={canGoPrev ? 'auto' : 'none'}
62
+ onClick={goPrev}
63
+ borderRadius="full"
64
+ aria-label="back"
65
+ variant="ghost"
66
+ pr={3}
67
+ pl={1}
68
+ gap={1}
69
+ size={mobileLayout ? 'xs' : 'sm'}>
70
+ <ChevronLeftIcon w={5} h={5} />
71
+ <Text>Back</Text>
72
+ </Button>
73
+ )
74
+
75
+ const FooterLoadingAndError = ({
76
+ progressBg,
77
+ borderColor,
78
+ }: {
79
+ progressBg: string
80
+ borderColor: string
81
+ }) => {
82
+ const {
83
+ data: { isLoading, error },
84
+ } = usePaginatedList()
85
+ return (
86
+ <Box position="absolute" bottom="100%" w="100%" left={0}>
87
+ <Collapse style={{ width: '100%' }} in={!!error} animateOpacity>
88
+ <Box borderTopRadius={6} bg="red.600" maxW="100%" p={1}>
89
+ <Tooltip placement="top" hasArrow label={error?.message ?? ''} aria-label="error">
90
+ <Text
91
+ textShadow="1px 1px 3px #00000077"
92
+ _dark={{ textShadow: 'none' }}
93
+ px={1}
94
+ fontWeight={600}
95
+ isTruncated
96
+ maxW="100%"
97
+ fontSize="sm"
98
+ color="white">
99
+ {error?.message}
100
+ </Text>
101
+ </Tooltip>
102
+ </Box>
103
+ </Collapse>
104
+ <ExpandOnMount in={isLoading} animateOpacity>
105
+ <Flex
106
+ bg={progressBg}
107
+ borderTop="1px solid"
108
+ borderBottom="1px solid"
109
+ borderColor={borderColor}
110
+ w="100%">
111
+ <Progress borderRadius={0} w="100%" height="8px" isIndeterminate={isLoading} />
112
+ </Flex>
113
+ </ExpandOnMount>
114
+ </Box>
115
+ )
116
+ }
117
+
118
+ interface PageIndicator {
119
+ index: number
120
+ isActive: boolean
121
+ }
122
+ interface PageIndicatorGroup {
123
+ indicators: PageIndicator[]
124
+ collapsed: boolean
125
+ }
126
+
127
+ /*
128
+ page nav groups:
129
+ 0 - first group - pages 1:3 if not in current group
130
+ 1 - second group (collapsed) - pages 4:i-1 if not in current group, where i is the selected page
131
+ 2 - current group - pages i:i+2
132
+ 3 - fourth group (collapsed) - pages i+3:n-3 if not in current group, where n is the last page
133
+ 4 - last group - pages n-2:n
134
+ */
135
+ const getGroupIndex = (index: number, selected: number, numPages: number) => {
136
+ if (Math.abs(index - selected) <= 1) return 2
137
+ if (index < 3) return selected === 0 ? 2 : 0
138
+ if (index > numPages - 4) return selected > numPages - 3 ? 2 : 4
139
+ return index < selected ? 1 : 3
140
+ }
141
+
142
+ const NAV_BUTTON_SIZE = 28
143
+ const MOBILE_NAV_BUTTON_SIZE = 24
144
+
145
+ const PageNavButton = ({
146
+ index,
147
+ isActive,
148
+ onClick,
149
+ borderColor,
150
+ isLoading,
151
+ mobileLayout,
152
+ }: {
153
+ index: number
154
+ isActive: boolean
155
+ onClick: () => void
156
+ borderColor: string
157
+ isLoading: boolean
158
+ mobileLayout: boolean
159
+ }) => {
160
+ return (
161
+ <Button
162
+ key={index}
163
+ onClick={onClick}
164
+ variant="unstyled"
165
+ display="flex"
166
+ border="1px solid"
167
+ borderColor={isActive ? 'transparent' : borderColor}
168
+ justifyContent="center"
169
+ alignItems="center"
170
+ width={`${mobileLayout ? MOBILE_NAV_BUTTON_SIZE : NAV_BUTTON_SIZE}px`}
171
+ height={`${mobileLayout ? MOBILE_NAV_BUTTON_SIZE : NAV_BUTTON_SIZE}px`}
172
+ fontFamily="fonts.heading"
173
+ lineHeight={1}
174
+ fontWeight={600}
175
+ minW="0"
176
+ bg={isActive ? 'accent.400' : 'transparent'}
177
+ _hover={{
178
+ bg: isActive ? 'accent.500' : 'whiteAlpha.500',
179
+ }}
180
+ color={isActive ? 'white' : borderColor}
181
+ textShadow={isActive ? '1px 1px 3px #00000088' : 'none'}
182
+ size="sm"
183
+ disabled={isLoading}
184
+ aria-label={`Go to page ${index + 1}`}>
185
+ {index + 1}
186
+ </Button>
187
+ )
188
+ }
189
+
190
+ const PageNavGroup = ({
191
+ group: { indicators, collapsed },
192
+ mobileLayout,
193
+ }: {
194
+ group: PageIndicatorGroup
195
+ mobileLayout: boolean
196
+ }) => {
197
+ const {
198
+ data: { isLoading },
199
+ goToPage,
200
+ } = usePaginatedList()
201
+ const borderColor = useBorderColor()
202
+
203
+ if (collapsed) {
204
+ return (
205
+ <Popover trigger="hover" gutter={1} variant="fixed">
206
+ {({ onClose }) => (
207
+ <>
208
+ <PopoverTrigger>
209
+ <IconButton
210
+ variant="unstyled"
211
+ width={`${mobileLayout ? MOBILE_NAV_BUTTON_SIZE : NAV_BUTTON_SIZE}px`}
212
+ height={`${mobileLayout ? MOBILE_NAV_BUTTON_SIZE : NAV_BUTTON_SIZE}px`}
213
+ minW="0"
214
+ opacity={0.7}
215
+ _hover={{
216
+ opacity: 1,
217
+ }}
218
+ aria-label="more pages"
219
+ icon={
220
+ <Text fontSize="md" position="relative" bottom="2px">
221
+ ...
222
+ </Text>
223
+ }
224
+ />
225
+ </PopoverTrigger>
226
+ <PopoverContent
227
+ bg="background.200"
228
+ w="auto"
229
+ border="1px solid"
230
+ borderColor={borderColor}
231
+ boxShadow="none">
232
+ <PopoverBody w="auto" maxW="166px" p={0.5}>
233
+ <Flex align="center" justify="center" flexFlow="row wrap">
234
+ {indicators.map(({ index, isActive }) => (
235
+ <Box key={`ind-${index}`} m={0.5}>
236
+ <PageNavButton
237
+ mobileLayout={mobileLayout}
238
+ index={index}
239
+ isActive={isActive}
240
+ onClick={() => {
241
+ goToPage(index)
242
+ onClose()
243
+ }}
244
+ borderColor={borderColor}
245
+ isLoading={isLoading}
246
+ />
247
+ </Box>
248
+ ))}
249
+ </Flex>
250
+ </PopoverBody>
251
+ </PopoverContent>
252
+ </>
253
+ )}
254
+ </Popover>
255
+ )
256
+ }
257
+ return (
258
+ <>
259
+ {indicators.map(({ index, isActive }) => (
260
+ <PageNavButton
261
+ mobileLayout={mobileLayout}
262
+ key={index}
263
+ index={index}
264
+ isActive={isActive}
265
+ onClick={() => goToPage(index)}
266
+ borderColor={borderColor}
267
+ isLoading={isLoading}
268
+ />
269
+ ))}
270
+ </>
271
+ )
272
+ }
273
+
274
+ const PageNav = () => {
275
+ const { pageIndex, numPages } = usePaginatedList()
276
+ const { mobileLayout } = useDataList()
277
+ const groups = useMemo<PageIndicatorGroup[]>(() => {
278
+ const nPages = numPages ?? 0
279
+ if (nPages <= 9)
280
+ return [
281
+ {
282
+ indicators: Array.from({ length: nPages }, (_, i) => ({
283
+ index: i,
284
+ isActive: i === pageIndex,
285
+ })),
286
+ collapsed: false,
287
+ },
288
+ ]
289
+ const g: PageIndicatorGroup[] = Array.from({ length: 5 }, (_, i) => ({
290
+ indicators: [],
291
+ collapsed: i % 2 === 1,
292
+ }))
293
+
294
+ for (let i = 0; i < nPages; i += 1) {
295
+ const groupIndex = getGroupIndex(i, pageIndex, nPages)
296
+ if (!g[groupIndex]) {
297
+ g[groupIndex] = { indicators: [], collapsed: groupIndex !== 2 }
298
+ }
299
+ g[groupIndex].indicators.push({ index: i, isActive: i === pageIndex })
300
+ }
301
+ return g
302
+ .filter(group => group.indicators.length)
303
+ .map(g => ({ ...g, collapsed: g.collapsed && g.indicators.length > 1 }))
304
+ }, [numPages, pageIndex])
305
+ return (
306
+ <Flex gap={1} align="center">
307
+ {groups.map((group, i) => (
308
+ <PageNavGroup mobileLayout={mobileLayout} key={`group-${i}`} group={group} />
309
+ ))}
310
+ </Flex>
311
+ )
312
+ }
313
+
314
+ const FOOTER_TEXT_WIDTH = 120
315
+ export const ListFooter = ({ onResize, flexProps, noFooter }: DataListFooterProps) => {
316
+ const borderColor = useColorModeValue('#cdcdcd', '#2d3748')
317
+
318
+ const {
319
+ list,
320
+ mobileLayout,
321
+ infiniteScroll,
322
+ search: { search, debounced },
323
+ } = useDataList()
324
+ const {
325
+ data: { data, isLoading },
326
+ totalCount,
327
+ pageIndex,
328
+ limit,
329
+ numFetched,
330
+ goNext,
331
+ goPrev,
332
+ hasMoreData,
333
+ } = usePaginatedList()
334
+ const { itemName, pluralItemName, searchRequired } = list
335
+
336
+ const { itemsText, pageText } = useMemo(() => {
337
+ if (isLoading && !infiniteScroll)
338
+ return { itemsText: mobileLayout ? 'Loading...' : '', pageText: '' }
339
+ if (totalCount === null) return { itemsText: 'Loading...', pageText: '' }
340
+ if (totalCount === 0 && !isLoading && search === debounced) {
341
+ return {
342
+ itemsText:
343
+ !search && searchRequired
344
+ ? `Search ${pluralItemName ?? toPlural(itemName)}`
345
+ : `No ${pluralItemName ?? toPlural(itemName)} found`,
346
+ pageText: '',
347
+ }
348
+ }
349
+ const firstItemIndex = infiniteScroll ? 1 : (pageIndex || 0) * limit + 1
350
+ // return `Displaying ${pluralItemName || toPlural(itemName)} ${firstItemIndex} - ${
351
+ // infiniteScroll ? numFetched : pageIndex * limit + data.length
352
+ // } of ${totalCount}`
353
+ return {
354
+ itemsText: `${firstItemIndex} - ${
355
+ infiniteScroll ? numFetched : pageIndex * limit + data.length
356
+ } of ${totalCount}`,
357
+ pageText: `Page ${pageIndex + 1} of ${limit ? Math.ceil((totalCount ?? 0) / limit) : 0}`,
358
+ }
359
+ }, [
360
+ totalCount,
361
+ isLoading,
362
+ itemName,
363
+ pluralItemName,
364
+ limit,
365
+ pageIndex,
366
+ search,
367
+ searchRequired,
368
+ mobileLayout,
369
+ debounced,
370
+ data,
371
+ infiniteScroll,
372
+ numFetched,
373
+ ])
374
+ const hasPrev = useMemo(() => !infiniteScroll && goPrev, [infiniteScroll, goPrev])
375
+ const hasNext = useMemo(() => !infiniteScroll && goNext, [infiniteScroll, goNext])
376
+ const progressBg = useColorModeValue('#efefef', '#212121')
377
+
378
+ const ref = useRef<HTMLDivElement>(null)
379
+ const handleResize = useCallback(
380
+ (e: ResizeObserverEntry) => {
381
+ const height = e.borderBoxSize[0].blockSize
382
+ const width = e.borderBoxSize[0].inlineSize
383
+ onResize({ height, width })
384
+ },
385
+ [onResize],
386
+ )
387
+ useResizeObserver(ref, handleResize)
388
+
389
+ return (
390
+ <Flex
391
+ ref={ref}
392
+ py={noFooter ? 0 : 2}
393
+ px={noFooter ? 0 : 3}
394
+ borderTop={`1px solid ${borderColor}`}
395
+ position="relative"
396
+ flexFlow={mobileLayout ? 'column' : 'row'}
397
+ gap={mobileLayout ? 1 : 0}
398
+ w="100%"
399
+ {...flexProps}>
400
+ {noFooter || (mobileLayout && infiniteScroll) ? null : (
401
+ <Flex
402
+ align="center"
403
+ justify={mobileLayout ? 'center' : 'space-between'}
404
+ position="relative"
405
+ w="100%">
406
+ {hasPrev && !mobileLayout ? (
407
+ <PrevButton goPrev={goPrev} canGoPrev={!isLoading && !!pageIndex} />
408
+ ) : null}
409
+ <Flex gap={1} align="center">
410
+ {infiniteScroll ? null : (
411
+ <>
412
+ {!mobileLayout ? (
413
+ <Text
414
+ fontFamily="fonts.heading"
415
+ whiteSpace="nowrap"
416
+ textAlign="center"
417
+ w={`${FOOTER_TEXT_WIDTH}px`}
418
+ fontSize="sm">
419
+ {pageText}
420
+ </Text>
421
+ ) : null}
422
+ <PageNav />
423
+ </>
424
+ )}
425
+ {!mobileLayout ? (
426
+ <Text
427
+ fontFamily="fonts.heading"
428
+ textAlign="center"
429
+ w={mobileLayout || infiniteScroll ? 'auto' : `${FOOTER_TEXT_WIDTH}px`}
430
+ whiteSpace="nowrap"
431
+ fontSize="sm">
432
+ {itemsText}
433
+ </Text>
434
+ ) : null}
435
+ </Flex>
436
+ {hasNext && !mobileLayout ? (
437
+ <NextButton goNext={goNext} canGoNext={!isLoading && hasMoreData} />
438
+ ) : null}
439
+ </Flex>
440
+ )}
441
+ {mobileLayout && !noFooter ? (
442
+ <Flex gap={2} w="100%" justify="space-between" align="center">
443
+ {hasPrev ? <PrevButton mobileLayout goPrev={goPrev} canGoPrev={!!pageIndex} /> : null}
444
+ <Text
445
+ fontFamily="fonts.heading"
446
+ textAlign="center"
447
+ w={mobileLayout || infiniteScroll ? 'auto' : `${FOOTER_TEXT_WIDTH}px`}
448
+ whiteSpace="nowrap"
449
+ fontSize="sm">
450
+ {itemsText}
451
+ </Text>
452
+ {hasNext ? <NextButton mobileLayout goNext={goNext} canGoNext={hasMoreData} /> : null}
453
+ </Flex>
454
+ ) : null}
455
+ <FooterLoadingAndError progressBg={progressBg} borderColor={borderColor} />
456
+ </Flex>
457
+ )
458
+ }
@@ -0,0 +1,180 @@
1
+ import { AddIcon, RepeatIcon, SearchIcon } from '@chakra-ui/icons'
2
+ import {
3
+ Box,
4
+ Flex,
5
+ IconButton,
6
+ Image,
7
+ Input,
8
+ InputGroup,
9
+ InputLeftElement,
10
+ Tooltip,
11
+ useColorModeValue,
12
+ } from '@chakra-ui/react'
13
+ import { getHasAccess, toPlural } from '@chem-po/core'
14
+ import { useAuth, useDataList } from '@chem-po/react'
15
+ import useResizeObserver from '@react-hook/resize-observer'
16
+ import { useCallback, useMemo, useRef } from 'react'
17
+ import { useBorderColor } from '../../hooks/ui/useBorderColor'
18
+ import { Toggle, ToggleOption } from '../button/Toggle'
19
+ import { PresetFilters } from './ListFilters'
20
+ import { ListSortView } from './ListSort'
21
+ import { DataListHeaderProps } from './types'
22
+
23
+ const ListSearch = () => {
24
+ const {
25
+ list,
26
+ search: { search, update },
27
+ } = useDataList()
28
+ const { searchIcon, itemName, pluralItemName } = list
29
+ const iconFilter = useColorModeValue('invert(0)', 'invert(1)')
30
+
31
+ return (
32
+ <Flex p={1} flex={1} minW="0">
33
+ <InputGroup>
34
+ <InputLeftElement pointerEvents="none">
35
+ {searchIcon ? (
36
+ <Image filter={iconFilter} src={searchIcon} w="24px" />
37
+ ) : (
38
+ <SearchIcon w="24px" opacity={0.5} />
39
+ )}
40
+ </InputLeftElement>
41
+ <Input
42
+ bg="whiteAlpha.700"
43
+ pl={9}
44
+ borderRadius={6}
45
+ _focus={{ boxShadow: 'none', outline: 'none' }}
46
+ border="none"
47
+ _dark={{
48
+ bg: 'blackAlpha.400',
49
+ color: '#cdcdcd',
50
+ _placeholder: {
51
+ color: '#ababab',
52
+ },
53
+ border: 'none',
54
+ }}
55
+ value={search}
56
+ placeholder={`Search for ${pluralItemName ?? toPlural(itemName)}`}
57
+ onChange={e => update(e.target.value)}
58
+ />
59
+ </InputGroup>
60
+ </Flex>
61
+ )
62
+ }
63
+
64
+ const HeaderButtons = ({ refetch }: { refetch?: () => void }) => {
65
+ const { list, setNewItemOpen } = useDataList()
66
+ const auth = useAuth()
67
+ const { itemName, field, access } = list
68
+
69
+ const canCreate = useMemo(() => getHasAccess(auth, access?.create, null), [access, auth])
70
+ return (
71
+ <Flex gap={2} ml="auto" align="center">
72
+ {refetch ? (
73
+ <Tooltip placement="left" hasArrow label="Refresh" aria-label="Refresh">
74
+ <IconButton size="xs" icon={<RepeatIcon />} aria-label="Refresh" onClick={refetch} />
75
+ </Tooltip>
76
+ ) : null}
77
+ {field && canCreate ? (
78
+ <Tooltip
79
+ placement="left"
80
+ hasArrow
81
+ label={`Create new ${itemName}`}
82
+ aria-label={`Create new ${itemName}`}>
83
+ <IconButton
84
+ size="xs"
85
+ icon={<AddIcon />}
86
+ aria-label={`Create new ${itemName}`}
87
+ onClick={() => setNewItemOpen(true)}
88
+ />
89
+ </Tooltip>
90
+ ) : null}
91
+ </Flex>
92
+ )
93
+ }
94
+
95
+ const gridToggleOptions: ToggleOption<'grid' | 'list'>[] = [
96
+ {
97
+ id: 'grid',
98
+ label: 'Grid View',
99
+ Render: selected => <Image w={5} opacity={selected ? 1 : 0.7} src="/icons/grid_view.svg" />,
100
+ },
101
+ {
102
+ id: 'list',
103
+ label: 'List View',
104
+ Render: selected => <Image w={6} opacity={selected ? 1 : 0.7} src="/icons/list.svg" />,
105
+ },
106
+ ]
107
+
108
+ const GridViewToggle = () => {
109
+ const { toggleGridLayout, gridLayout } = useDataList()
110
+ if (!toggleGridLayout) return null
111
+ return (
112
+ <Toggle
113
+ onChange={toggleGridLayout}
114
+ options={gridToggleOptions}
115
+ value={gridLayout ? 'grid' : 'list'}
116
+ />
117
+ )
118
+ }
119
+
120
+ export const DataListHeader = ({ onResize, refetch, boxProps }: DataListHeaderProps) => {
121
+ const { list, mobileLayout, toggleGridLayout } = useDataList()
122
+ const { searchPath, sortPresets, filterPresets, Header } = list
123
+
124
+ const borderColor = useBorderColor()
125
+ const ref = useRef<HTMLDivElement>(null)
126
+ const handleResize = useCallback(
127
+ (entry: ResizeObserverEntry) => {
128
+ onResize({ height: entry.contentRect.height, width: entry.contentRect.width })
129
+ },
130
+ [onResize],
131
+ )
132
+ const hasSearch = useMemo(() => !!Object.keys(searchPath ?? {}).length, [searchPath])
133
+
134
+ useResizeObserver(ref, handleResize)
135
+
136
+ return (
137
+ <Box
138
+ ref={ref}
139
+ bg="background.200"
140
+ borderBottom={`1px solid ${borderColor}`}
141
+ w="100%"
142
+ {...boxProps}>
143
+ <Flex px={2} align="center" w="100%">
144
+ {sortPresets?.length || filterPresets?.length || hasSearch ? (
145
+ <Flex w="100%" flexFlow={mobileLayout ? 'column' : 'row'}>
146
+ {mobileLayout && !Header ? null : (
147
+ <Flex flex={1} px={2} py={mobileLayout ? 0 : 1} gap={1} align="center">
148
+ {mobileLayout ? null : (
149
+ <Flex flex={1} minW="0" align="center" gap={2}>
150
+ {toggleGridLayout ? <GridViewToggle /> : null}
151
+ <PresetFilters />
152
+ <ListSortView />
153
+ {hasSearch ? <ListSearch /> : null}
154
+ </Flex>
155
+ )}
156
+ <Flex align="center" minW="0" gap={3} ml="auto">
157
+ {Header ? (
158
+ <Box minW="0" flex={1}>
159
+ <Header />
160
+ </Box>
161
+ ) : null}
162
+ {mobileLayout ? null : <HeaderButtons refetch={refetch} />}
163
+ </Flex>
164
+ </Flex>
165
+ )}
166
+ {hasSearch && mobileLayout ? <ListSearch /> : null}
167
+ {mobileLayout && (sortPresets?.length || filterPresets?.length) ? (
168
+ <Flex pl={1} py={1} gap={1.5} align="center" w="100%">
169
+ {toggleGridLayout ? <GridViewToggle /> : null}
170
+ <PresetFilters />
171
+ <ListSortView />
172
+ <HeaderButtons refetch={refetch} />
173
+ </Flex>
174
+ ) : null}
175
+ </Flex>
176
+ ) : null}
177
+ </Flex>
178
+ </Box>
179
+ )
180
+ }
@@ -0,0 +1,48 @@
1
+ import { Flex } from '@chakra-ui/react'
2
+ import { AnyObject, ColorMode, DBItem } from '@chem-po/core'
3
+ import { DataList, ListGridOptions } from '@chem-po/react'
4
+ import { GridChildComponentProps } from 'react-window'
5
+
6
+ export interface GridItemProps<T extends AnyObject> {
7
+ list: DataList<T>
8
+ grid: ListGridOptions<T>
9
+ items: Array<DBItem<T>>
10
+ colorMode: ColorMode
11
+ numCols: number
12
+ mobileLayout: boolean
13
+ onSelect: (item: DBItem<T>) => void
14
+ refetch?: (id: string) => Promise<void>
15
+ }
16
+ export const ListCell = <T extends AnyObject>({
17
+ data,
18
+ rowIndex,
19
+ columnIndex,
20
+ style,
21
+ }: GridChildComponentProps<GridItemProps<T>>) => {
22
+ const { items, onSelect, refetch, grid, numCols, colorMode } = data || {}
23
+ const { ItemPreview: Render } = grid
24
+
25
+ const itemIdx = rowIndex * numCols + columnIndex
26
+ const item = items[itemIdx]
27
+ if (!item) return null
28
+ return (
29
+ <Flex
30
+ display="flex"
31
+ cursor="pointer"
32
+ aria-label="list-item"
33
+ onClick={() => {
34
+ onSelect(item)
35
+ }}
36
+ style={style}
37
+ px={1}
38
+ py={0.25}
39
+ key={itemIdx}>
40
+ {Render({
41
+ index: itemIdx,
42
+ item,
43
+ refetch: refetch ? () => refetch(item._id) : undefined,
44
+ colorMode,
45
+ })}
46
+ </Flex>
47
+ )
48
+ }