@chem-po/react-web 0.0.4 → 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 (127) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +5 -5
  4. package/dist/index.d.ts +5 -5
  5. package/dist/index.js +2 -2
  6. package/dist/index.js.map +1 -1
  7. package/package.json +24 -22
  8. package/src/components/auth/SignIn.tsx +43 -0
  9. package/src/components/auth/index.ts +1 -0
  10. package/src/components/box/CollapseHorizontal.tsx +18 -0
  11. package/src/components/box/ContentBox.tsx +17 -0
  12. package/src/components/box/ExpandOnMount.tsx +48 -0
  13. package/src/components/box/Expandable.tsx +96 -0
  14. package/src/components/box/FullSizeContainer.tsx +50 -0
  15. package/src/components/box/MobileFrame/index.tsx +145 -0
  16. package/src/components/box/MobileFrame/styles.css +35 -0
  17. package/src/components/box/index.ts +6 -0
  18. package/src/components/button/DeleteButton.tsx +178 -0
  19. package/src/components/button/Toggle.tsx +88 -0
  20. package/src/components/button/ViewButton.tsx +30 -0
  21. package/src/components/button/index.ts +3 -0
  22. package/src/components/feed/FeedContentPane.tsx +111 -0
  23. package/src/components/feed/MediaFeed.tsx +200 -0
  24. package/src/components/feed/MediaFeedBackground.tsx +127 -0
  25. package/src/components/feed/MediaFeedRefresh.tsx +78 -0
  26. package/src/components/feed/MediaFeedSwipeUp.tsx +34 -0
  27. package/src/components/feed/constants.ts +11 -0
  28. package/src/components/feed/context.tsx +19 -0
  29. package/src/components/feed/hooks.ts +290 -0
  30. package/src/components/feed/index.ts +2 -0
  31. package/src/components/feed/types.ts +50 -0
  32. package/src/components/form/Condition.tsx +26 -0
  33. package/src/components/form/Field.tsx +39 -0
  34. package/src/components/form/Form.tsx +425 -0
  35. package/src/components/form/FormFooter.tsx +82 -0
  36. package/src/components/form/UploadProgress/index.tsx +38 -0
  37. package/src/components/form/UploadProgress/styles.css +23 -0
  38. package/src/components/form/index.ts +4 -0
  39. package/src/components/form/input/Editable.tsx +129 -0
  40. package/src/components/form/input/InputSlider.tsx +75 -0
  41. package/src/components/form/input/OptionalTag.tsx +33 -0
  42. package/src/components/form/input/StandaloneInput.tsx +41 -0
  43. package/src/components/form/input/boolean/index.tsx +53 -0
  44. package/src/components/form/input/color/index.tsx +126 -0
  45. package/src/components/form/input/date/index.tsx +122 -0
  46. package/src/components/form/input/datetime/index.tsx +93 -0
  47. package/src/components/form/input/file.tsx +379 -0
  48. package/src/components/form/input/hooks/index.ts +2 -0
  49. package/src/components/form/input/hooks/useInputImperativeHandle.ts +16 -0
  50. package/src/components/form/input/hooks/useInputStyle.ts +39 -0
  51. package/src/components/form/input/index.ts +2 -0
  52. package/src/components/form/input/input.css +44 -0
  53. package/src/components/form/input/input.tsx +130 -0
  54. package/src/components/form/input/multipleSelect/index.tsx +55 -0
  55. package/src/components/form/input/number/index.tsx +83 -0
  56. package/src/components/form/input/number/styles.css +8 -0
  57. package/src/components/form/input/select/index.tsx +80 -0
  58. package/src/components/form/input/socialMedia/index.tsx +158 -0
  59. package/src/components/form/input/text/index.tsx +72 -0
  60. package/src/components/form/input/text/textarea.tsx +44 -0
  61. package/src/components/form/input/time/index.tsx +33 -0
  62. package/src/components/form/input/type.ts +0 -0
  63. package/src/components/form/input/types.ts +4 -0
  64. package/src/components/form/view/file.tsx +45 -0
  65. package/src/components/form/view/index.tsx +61 -0
  66. package/src/components/form/view/multipleSelect.tsx +38 -0
  67. package/src/components/form/view/select.tsx +33 -0
  68. package/src/components/index.ts +14 -0
  69. package/src/components/list/Body/InfiniteScrollGridBody.tsx +177 -0
  70. package/src/components/list/Body/InfiniteScrollListBody.tsx +114 -0
  71. package/src/components/list/Body/ListBody.tsx +23 -0
  72. package/src/components/list/Body/PagedGridBody.tsx +104 -0
  73. package/src/components/list/Body/PagedListBody.tsx +92 -0
  74. package/src/components/list/Body/hooks.ts +84 -0
  75. package/src/components/list/DataList.tsx +32 -0
  76. package/src/components/list/ListContainer.tsx +20 -0
  77. package/src/components/list/ListContent.tsx +54 -0
  78. package/src/components/list/ListCreate.tsx +57 -0
  79. package/src/components/list/ListFilters.tsx +182 -0
  80. package/src/components/list/ListFooter.tsx +458 -0
  81. package/src/components/list/ListHeader.tsx +180 -0
  82. package/src/components/list/ListItem/ListCell.tsx +48 -0
  83. package/src/components/list/ListItem/ListRow.tsx +38 -0
  84. package/src/components/list/ListItemView.tsx +53 -0
  85. package/src/components/list/ListSort.tsx +84 -0
  86. package/src/components/list/NoItems.tsx +33 -0
  87. package/src/components/list/constants.ts +1 -0
  88. package/src/components/list/index.ts +4 -0
  89. package/src/components/list/types.ts +29 -0
  90. package/src/components/list/utils.ts +62 -0
  91. package/src/components/loading/CircularProgress.tsx +11 -0
  92. package/src/components/loading/Loading.tsx +160 -0
  93. package/src/components/loading/LoadingImage.tsx +123 -0
  94. package/src/components/loading/LoadingSwitch.tsx +78 -0
  95. package/src/components/loading/index.ts +4 -0
  96. package/src/components/media/PlayButton.tsx +94 -0
  97. package/src/components/media/index.ts +1 -0
  98. package/src/components/modal/DefaultModal.tsx +18 -0
  99. package/src/components/modal/DesktopModal.tsx +11 -0
  100. package/src/components/modal/ForceMobile.tsx +7 -0
  101. package/src/components/modal/MobileModal.tsx +89 -0
  102. package/src/components/modal/index.ts +3 -0
  103. package/src/components/modal/type.ts +7 -0
  104. package/src/components/nav/NavBar.tsx +101 -0
  105. package/src/components/nav/index.ts +1 -0
  106. package/src/components/overlay/ImageViewOverlay.tsx +88 -0
  107. package/src/components/overlay/MobileOverlay.tsx +22 -0
  108. package/src/components/overlay/index.ts +2 -0
  109. package/src/components/text/GradientText/index.tsx +16 -0
  110. package/src/components/text/GradientText/styles.css +5 -0
  111. package/src/components/text/NumberTicker.tsx +28 -0
  112. package/src/components/text/index.ts +1 -0
  113. package/src/components/theme/colorMode/DarkModeToggle.tsx +40 -0
  114. package/src/components/theme/colorMode/index.ts +1 -0
  115. package/src/components/theme/index.ts +1 -0
  116. package/src/components/view/ErrorView.tsx +13 -0
  117. package/src/components/view/RedirectView.tsx +42 -0
  118. package/src/components/view/index.ts +2 -0
  119. package/src/contexts/index.ts +1 -0
  120. package/src/contexts/theme.ts +316 -0
  121. package/src/custom.d.ts +4 -0
  122. package/src/hooks/index.ts +1 -0
  123. package/src/hooks/ui/index.ts +1 -0
  124. package/src/hooks/ui/useBorderColor.ts +4 -0
  125. package/src/store/index.ts +1 -0
  126. package/src/store/usePlayer.ts +75 -0
  127. package/src/store/useScreen.ts +22 -0
@@ -0,0 +1,38 @@
1
+ import { Flex } from '@chakra-ui/react'
2
+ import { AnyObject, ColorMode, DBItem } from '@chem-po/core'
3
+ import { DataList } from '@chem-po/react'
4
+ import { ListChildComponentProps } from 'react-window'
5
+
6
+ export interface ListItemProps<T extends AnyObject = AnyObject> {
7
+ list: DataList<T>
8
+ items: Array<DBItem<T>>
9
+ mobileLayout: boolean
10
+ onSelect: (item: DBItem<T>) => void
11
+ colorMode: ColorMode
12
+ refetch?: (id: string) => Promise<void>
13
+ }
14
+ export const ListRow = <T extends AnyObject>({
15
+ data,
16
+ index,
17
+ style,
18
+ }: ListChildComponentProps<ListItemProps<T>>) => {
19
+ const { list, items, onSelect, refetch, mobileLayout, colorMode } = data || {}
20
+ const { ItemPreview: RenderItem, mobile } = list
21
+
22
+ const Render = mobileLayout ? (mobile?.ItemPreview ?? RenderItem) : RenderItem
23
+ const item = items[index]
24
+ if (!item) return null
25
+ return (
26
+ <Flex
27
+ display="flex"
28
+ cursor="pointer"
29
+ aria-label="list-item"
30
+ onClick={() => {
31
+ onSelect(items[index])
32
+ }}
33
+ style={style}
34
+ key={index}>
35
+ {Render({ index, item, refetch: refetch ? () => refetch(item._id) : undefined, colorMode })}
36
+ </Flex>
37
+ )
38
+ }
@@ -0,0 +1,53 @@
1
+ import { Text, useColorMode, useColorModeValue } from '@chakra-ui/react'
2
+ import { AnyObject, DBItem } from '@chem-po/core'
3
+ import { useDataList, usePaginatedList } from '@chem-po/react'
4
+ import { useEffect, useMemo, useState } from 'react'
5
+ import { DefaultModal } from '../modal'
6
+
7
+ export const ListItemView = () => {
8
+ const { selectedItemId, deselectItem, list } = useDataList()
9
+ const { ItemView, itemName } = list || {}
10
+ const [contentOffset, setContentOffset] = useState({ x: 0, y: 0 })
11
+ const {
12
+ data: { data },
13
+ // refetchItem,
14
+ } = usePaginatedList()
15
+ const item = useMemo(
16
+ () => data.find(d => d._id === selectedItemId) ?? null,
17
+ [data, selectedItemId],
18
+ )
19
+ const { colorMode } = useColorMode()
20
+ const errorColor = useColorModeValue('red.600', 'red.300')
21
+ const [prevItem, setPrevItem] = useState<DBItem<AnyObject> | null>(null)
22
+ useEffect(() => {
23
+ if (item) {
24
+ setPrevItem(item)
25
+ }
26
+ }, [item])
27
+
28
+ return (
29
+ <DefaultModal
30
+ contentProps={{
31
+ p: 0,
32
+ transition: 'all 300ms',
33
+ transform: `translate(${contentOffset.x}px, ${contentOffset.y}px)`,
34
+ }}
35
+ scrollBehavior="inside"
36
+ onClose={deselectItem}
37
+ isOpen={!!ItemView && !!selectedItemId}>
38
+ {ItemView && prevItem ? (
39
+ <ItemView
40
+ item={prevItem}
41
+ colorMode={colorMode}
42
+ clearContentOffset={() => setContentOffset({ x: 0, y: 0 })}
43
+ updateContentOffset={(x, y) => setContentOffset({ x, y })}
44
+ // onEdited={onEdited}
45
+ />
46
+ ) : (
47
+ <Text color={errorColor} p={4}>
48
+ Error displaying {itemName}
49
+ </Text>
50
+ )}
51
+ </DefaultModal>
52
+ )
53
+ }
@@ -0,0 +1,84 @@
1
+ import { ArrowDownIcon, ArrowUpIcon } from '@chakra-ui/icons'
2
+ import { Button, HStack, Text, useColorMode } from '@chakra-ui/react'
3
+ import { SortDirection } from '@chem-po/core'
4
+ import { SortPreset, useDataList } from '@chem-po/react'
5
+ import { useMemo } from 'react'
6
+ import { CollapseHorizontal } from '../box/CollapseHorizontal'
7
+
8
+ const SortButton = ({ preset }: { preset: SortPreset }) => {
9
+ const { setSort: setSortFromPreset, query } = useDataList()
10
+ const { sort } = query
11
+ const { label, key, Render } = preset
12
+ const active = useMemo<SortDirection | null>(
13
+ () => (sort?.key === key ? sort.direction : null),
14
+ [sort, key],
15
+ )
16
+ const { colorMode } = useColorMode()
17
+
18
+ const color = useMemo(() => {
19
+ if (active) return 'white'
20
+ return colorMode === 'light' ? 'gray.500' : 'gray.400'
21
+ }, [active, colorMode])
22
+ const icon = useMemo(
23
+ () => (sort?.direction === 'asc' ? <ArrowUpIcon w={4} h={4} /> : <ArrowDownIcon w={4} h={4} />),
24
+ [sort],
25
+ )
26
+
27
+ return (
28
+ <Button
29
+ size="xs"
30
+ variant="outline"
31
+ bg={active ? 'accent.400' : 'transparent'}
32
+ transition="all 300ms"
33
+ textShadow={active ? '0 0 2px rgba(0,0,0,0.7)' : 'none'}
34
+ boxShadow={`0px 0px 1px 1px ${active ? 'transparent' : '#00000033'}`}
35
+ border="none"
36
+ // borderColor={active ? 'transparent' : 'blackAlpha.200'}
37
+ _dark={{
38
+ // borderColor: active ? 'transparent' : 'whiteAlpha.300',
39
+ boxShadow: `0px 0px 1px 1px ${active ? 'transparent' : '#ffffff66'}`,
40
+ color,
41
+ textShadow: active ? '0 0 2px rgba(0,0,0,0.7)' : 'none',
42
+ }}
43
+ aria-label={label}
44
+ opacity={active ? 1 : 0.8}
45
+ height={6}
46
+ pl={active ? 3 : 2}
47
+ pr={2}
48
+ _hover={{ opacity: 1 }}
49
+ gap={0}
50
+ color={color}
51
+ onClick={() =>
52
+ setSortFromPreset({
53
+ direction: active && sort?.direction === 'asc' ? 'desc' : 'asc',
54
+ key,
55
+ })
56
+ }>
57
+ {Render ? (
58
+ <Render active={active} colorMode={colorMode} preset={preset} />
59
+ ) : (
60
+ <Text fontWeight={600} fontSize="sm" color={color}>
61
+ {label}
62
+ </Text>
63
+ )}
64
+ <CollapseHorizontal active={!!active} h="16px" width={20}>
65
+ {icon}
66
+ </CollapseHorizontal>
67
+ </Button>
68
+ )
69
+ }
70
+
71
+ export const ListSortView = () => {
72
+ const {
73
+ list: { sortPresets },
74
+ } = useDataList()
75
+
76
+ if (!sortPresets) return null
77
+ return (
78
+ <HStack spacing={2}>
79
+ {sortPresets.map(preset => (
80
+ <SortButton key={preset.label} preset={preset} />
81
+ ))}
82
+ </HStack>
83
+ )
84
+ }
@@ -0,0 +1,33 @@
1
+ import { Flex, Text } from '@chakra-ui/react'
2
+ import { toPlural } from '@chem-po/core'
3
+ import { useDataList, usePaginatedList } from '@chem-po/react'
4
+ import { useMemo } from 'react'
5
+ import { Loading } from '../loading'
6
+
7
+ export const NoItemsRow = () => {
8
+ const {
9
+ list,
10
+ search: { debounced, search },
11
+ } = useDataList()
12
+ const {
13
+ data: { isLoading },
14
+ } = usePaginatedList()
15
+ const showLoading = useMemo(
16
+ () => isLoading || debounced !== search,
17
+ [isLoading, debounced, search],
18
+ )
19
+ const { itemName, pluralItemName, noItemsMessage, searchRequired } = list
20
+ return (
21
+ <Flex justify="center" align="flex-start" w="100%" h="100%">
22
+ {showLoading ? (
23
+ <Loading text={`Loading ${pluralItemName ?? toPlural(itemName)}`} />
24
+ ) : (
25
+ <Text textAlign="center" flex={1} p={3} fontStyle="italic" opacity={0.7}>
26
+ {!debounced && searchRequired
27
+ ? `Search ${pluralItemName ?? toPlural(itemName)}`
28
+ : (noItemsMessage ?? `No ${pluralItemName ?? toPlural(itemName)}`)}
29
+ </Text>
30
+ )}
31
+ </Flex>
32
+ )
33
+ }
@@ -0,0 +1 @@
1
+ export const FETCH_LIMIT = 15
@@ -0,0 +1,4 @@
1
+ export * from './DataList'
2
+ export * from './ListContainer'
3
+ export * from './ListContent'
4
+ export * from './types'
@@ -0,0 +1,29 @@
1
+ import { BoxProps, FlexProps } from '@chakra-ui/react'
2
+ import { AnyObject } from '@chem-po/core'
3
+ import { ListProviderProps } from '@chem-po/react'
4
+ import { ReactNode } from 'react'
5
+
6
+ export interface DataListHeaderProps {
7
+ onResize: (size: { height: number; width: number }) => void
8
+ refetch?: () => void
9
+ boxProps?: BoxProps
10
+ }
11
+
12
+ export interface DataListFooterProps {
13
+ flexProps?: FlexProps
14
+ noFooter?: boolean
15
+ onResize: (size: { height: number; width: number }) => void
16
+ }
17
+
18
+ export type ListViewProps<T extends AnyObject> = ListProviderProps<T> & {
19
+ flexProps?: FlexProps
20
+ footerProps?: FlexProps
21
+ noFooter?: boolean
22
+ headerProps?: BoxProps
23
+ modals?: ReactNode
24
+ }
25
+
26
+ export type ListContentProps<T extends AnyObject> = Omit<
27
+ ListViewProps<T>,
28
+ 'list' | 'basePath' | 'flexProps'
29
+ >
@@ -0,0 +1,62 @@
1
+ import {
2
+ AnyObject,
3
+ arrayOperators,
4
+ BaseQuery,
5
+ QueryFilter,
6
+ SearchPath,
7
+ stringTransforms,
8
+ } from '@chem-po/core'
9
+ import { FilterPreset } from '@chem-po/react'
10
+ import { Query, query, where } from 'firebase/firestore'
11
+
12
+ export const getIsDynamicSize = (itemHeight: number | ((item: any) => number)) =>
13
+ typeof itemHeight === 'function'
14
+
15
+ export const getTextSearchQuery = <T extends AnyObject>(
16
+ baseQuery: Query<T>,
17
+ searchQuery: string,
18
+ searchPath: SearchPath<T>,
19
+ ) => {
20
+ let trimmed = searchQuery.trim()
21
+ if (!trimmed) return baseQuery
22
+ if (searchPath.transform) trimmed = stringTransforms[searchPath.transform](trimmed)
23
+ return query(
24
+ baseQuery,
25
+ where(searchPath.prop, '>=', trimmed),
26
+ where(searchPath.prop, '<=', `${trimmed}\uf8ff`),
27
+ )
28
+ }
29
+ export const getSearchQueries = <T extends AnyObject>(
30
+ baseQuery: Query<T>,
31
+ searchData: BaseQuery<T>['search'],
32
+ ): Array<Query<T>> => {
33
+ const { paths, query: search } = searchData
34
+ if (!paths?.length || !search) return [baseQuery]
35
+ return paths.map(path => getTextSearchQuery(baseQuery, search, path))
36
+ }
37
+ const nativeFilterMatchesPreset = <T extends AnyObject>(
38
+ nativeFilter: QueryFilter<T>,
39
+ presetFilter: QueryFilter<T>,
40
+ ) => {
41
+ if (nativeFilter.key !== presetFilter.key) return false
42
+ if (nativeFilter.operator !== presetFilter.operator) return false
43
+ if (arrayOperators.includes(nativeFilter.operator)) {
44
+ const nativeValues = Array.isArray(nativeFilter.value)
45
+ ? nativeFilter.value
46
+ : [nativeFilter.value]
47
+ const presetValues = Array.isArray(presetFilter.value)
48
+ ? presetFilter.value
49
+ : [presetFilter.value]
50
+ if (presetValues.every(value => !nativeValues.includes(value))) return false
51
+ return true
52
+ }
53
+ return nativeFilter.value === presetFilter.value
54
+ }
55
+
56
+ export const filterMatchesPreset = <T extends AnyObject>(
57
+ filter: QueryFilter<T>,
58
+ { nativeFilter }: FilterPreset<T>,
59
+ ) => {
60
+ const nativeFilters = Array.isArray(nativeFilter) ? nativeFilter : [nativeFilter]
61
+ return nativeFilters.every(nativeFilter => nativeFilterMatchesPreset(nativeFilter, filter))
62
+ }
@@ -0,0 +1,11 @@
1
+ import {
2
+ CircularProgressProps,
3
+ CircularProgress as Orig,
4
+ useColorModeValue,
5
+ } from '@chakra-ui/react'
6
+
7
+ export const CircularProgress = (props: CircularProgressProps) => {
8
+ const color = useColorModeValue('gray.400', 'gray.400')
9
+ const bg = useColorModeValue('gray.300', 'gray.600')
10
+ return <Orig isIndeterminate trackColor={bg} color={color} {...props} />
11
+ }
@@ -0,0 +1,160 @@
1
+ import { Center, CenterProps, HStack, StackProps, Text, useColorModeValue } from '@chakra-ui/react'
2
+ import { ThemedAsset, useAppAssets } from '@chem-po/react'
3
+ import { LottieOptions, useLottie } from 'lottie-react'
4
+ import { useEffect, useMemo } from 'react'
5
+ import { ContentBox } from '../box/ContentBox'
6
+ // import loadingAnimation from './circles_loading.json'
7
+ // import loadingAnimationLight from './circles_loading_light.json'
8
+
9
+ export const LottieLoadingLogo = ({
10
+ size = 30,
11
+ isLoading,
12
+ speed = 2,
13
+ inFeed,
14
+ asset,
15
+ }: {
16
+ size?: number | string
17
+ color?: string
18
+ isLoading: boolean
19
+ asset: ThemedAsset
20
+ inFeed?: boolean
21
+ speed?: number
22
+ }) => {
23
+ const _animationData = useColorModeValue(asset.default, asset.dark ?? asset.default)
24
+ const animationData = useMemo(
25
+ () => (inFeed ? (asset.dark ?? asset.default) : _animationData),
26
+ [_animationData, asset, inFeed],
27
+ )
28
+ const options = useMemo<LottieOptions>(
29
+ () => ({
30
+ // animationData: getLoaderJson(propColor || defaultColor),
31
+ animationData,
32
+ loop: true,
33
+ }),
34
+ [animationData],
35
+ )
36
+ const style = useMemo(
37
+ () => ({
38
+ width: size,
39
+ height: size,
40
+ opacity: isLoading ? 0.85 : 0,
41
+ transition: 'opacity 300ms ease-in-out',
42
+ }),
43
+ [size, isLoading],
44
+ )
45
+ const { View, pause, play, setSpeed } = useLottie(options, style)
46
+ useEffect(() => {
47
+ setSpeed(speed)
48
+ }, [setSpeed, speed])
49
+ useEffect(() => {
50
+ if (isLoading) {
51
+ play()
52
+ } else pause()
53
+ }, [isLoading, play, pause])
54
+
55
+ return <>{View}</>
56
+ }
57
+
58
+ const SVGLoadingLogo = ({
59
+ size = 30,
60
+ isLoading,
61
+ asset,
62
+ inFeed,
63
+ }: {
64
+ size?: number | string
65
+ isLoading: boolean
66
+ asset: ThemedAsset
67
+ inFeed?: boolean
68
+ }) => {
69
+ const _svg = useColorModeValue(asset.default, asset.dark ?? asset.default)
70
+ const svg = useMemo(() => (inFeed ? (asset.dark ?? asset.default) : _svg), [_svg, asset, inFeed])
71
+ const style = useMemo(
72
+ () => ({
73
+ width: size,
74
+ height: size,
75
+ opacity: isLoading ? 0.85 : 0,
76
+ transition: 'opacity 300ms ease-in-out',
77
+ }),
78
+ [size, isLoading],
79
+ )
80
+ return <img src={svg} style={style} alt="loading" />
81
+ }
82
+
83
+ const LoadingBody = ({ isLoading, size }: { isLoading: boolean; size?: number | string }) => {
84
+ const { loading } = useAppAssets()
85
+ if (loading.lottieJson)
86
+ return <LottieLoadingLogo isLoading={isLoading} size={size} asset={loading.lottieJson} />
87
+ if (loading.svg) return <SVGLoadingLogo isLoading={isLoading} size={size} asset={loading.svg} />
88
+ return null
89
+ }
90
+
91
+ export const LoadingLogo = ({
92
+ isLoading,
93
+ size,
94
+ inFeed,
95
+ }: {
96
+ isLoading: boolean
97
+ size?: number | string
98
+ inFeed?: boolean
99
+ }) => {
100
+ const { loading } = useAppAssets()
101
+ if (loading.lottieJson)
102
+ return (
103
+ <LottieLoadingLogo
104
+ inFeed={inFeed}
105
+ isLoading={isLoading}
106
+ size={size}
107
+ asset={loading.lottieJson}
108
+ />
109
+ )
110
+ if (loading.svg)
111
+ return <SVGLoadingLogo inFeed={inFeed} isLoading={isLoading} size={size} asset={loading.svg} />
112
+ return <Text color="red">ERROR: No loading animation found</Text>
113
+ }
114
+
115
+ export const Loading = ({
116
+ text = 'Loading...',
117
+ inBox,
118
+ stackProps,
119
+ }: {
120
+ text?: string
121
+ inBox?: boolean
122
+ stackProps?: StackProps
123
+ }) => {
124
+ const body = (
125
+ <HStack p={2} {...stackProps}>
126
+ <LoadingBody isLoading />
127
+ <Text opacity={0.8} fontSize="sm">
128
+ {text}
129
+ </Text>
130
+ </HStack>
131
+ )
132
+ return inBox ? <ContentBox>{body}</ContentBox> : body
133
+ }
134
+
135
+ export const LoadingOverlay = ({
136
+ isLoading,
137
+ text,
138
+ inFeed,
139
+ ...props
140
+ }: { isLoading: boolean; text?: string; inFeed?: boolean } & CenterProps) => {
141
+ const bg = useColorModeValue('#ffffffaa', '#00000088')
142
+ return (
143
+ <Center
144
+ pos={'absolute'}
145
+ top={0}
146
+ left={0}
147
+ right={0}
148
+ bottom={0}
149
+ bg={bg}
150
+ pointerEvents={isLoading ? 'auto' : 'none'}
151
+ opacity={isLoading ? 1 : 0}
152
+ {...props}>
153
+ {!text ? (
154
+ <LoadingLogo inFeed={inFeed} size={100} isLoading={isLoading} />
155
+ ) : (
156
+ <Loading text={text} />
157
+ )}
158
+ </Center>
159
+ )
160
+ }
@@ -0,0 +1,123 @@
1
+ import { BoxProps, Center, IconButton, Image, ImageProps } from '@chakra-ui/react'
2
+ import { useMounted } from '@chem-po/react'
3
+ import { SyntheticEvent, useCallback, useEffect, useRef, useState } from 'react'
4
+ import { ImageViewOverlay } from '../overlay/ImageViewOverlay'
5
+ import { LoadingLogo } from './Loading'
6
+
7
+ export interface LoadingImageProps {
8
+ src?: string | null
9
+ loadingOverride?: boolean
10
+ onLoad?: ImageProps['onLoad']
11
+ alt?: string
12
+ noFullView?: boolean
13
+ buttonFullView?: boolean
14
+ imageProps?: Omit<ImageProps, 'onLoad'>
15
+ }
16
+
17
+ const emptyPng =
18
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII='
19
+ export const LoadingImage = ({
20
+ src,
21
+ loadingOverride,
22
+ alt,
23
+ onLoad,
24
+ width,
25
+ height,
26
+ noFullView,
27
+ buttonFullView,
28
+ imageProps,
29
+ ...boxProps
30
+ }: LoadingImageProps & Omit<BoxProps, 'onLoad'>) => {
31
+ const [imageLoading, setImageLoading] = useState(!!src)
32
+ const imageRef = useRef<HTMLImageElement>(null)
33
+
34
+ const [viewing, setViewing] = useState(false)
35
+ const mounted = useMounted(100)
36
+ useEffect(() => {
37
+ if (imageRef.current?.complete) {
38
+ setImageLoading(true)
39
+ }
40
+ }, [imageRef])
41
+
42
+ const handleImageLoad = useCallback(
43
+ (e: SyntheticEvent<HTMLImageElement>) => {
44
+ setImageLoading(false)
45
+ if (onLoad) onLoad(e)
46
+ },
47
+ [onLoad],
48
+ )
49
+
50
+ const [prevSrc, setPrevSrc] = useState(src)
51
+ useEffect(() => {
52
+ if (src) setPrevSrc(src)
53
+ }, [src, prevSrc])
54
+
55
+ const loading = imageLoading || !!loadingOverride
56
+ return (
57
+ <Center
58
+ opacity={mounted ? 1 : 0}
59
+ transition="opacity 333ms"
60
+ w={width ?? '100%'}
61
+ h={height ?? '100%'}
62
+ overflow="hidden"
63
+ position="relative"
64
+ {...boxProps}>
65
+ <Image
66
+ src={src ?? emptyPng}
67
+ alt={alt}
68
+ onClick={noFullView || buttonFullView ? undefined : () => setViewing(true)}
69
+ onLoad={handleImageLoad}
70
+ onLoadStart={() => setImageLoading(true)}
71
+ top={0}
72
+ left={0}
73
+ cursor={noFullView || buttonFullView ? 'default' : 'pointer'}
74
+ draggable={false}
75
+ opacity={src && !loading ? 1 : 0}
76
+ transition="opacity 300ms ease-in-out"
77
+ w="100%"
78
+ h="100%"
79
+ objectFit={src ? 'cover' : 'contain'}
80
+ zIndex={0}
81
+ {...imageProps}
82
+ />
83
+ {buttonFullView ? (
84
+ <IconButton
85
+ pos="absolute"
86
+ top={2}
87
+ right={2}
88
+ zIndex={1}
89
+ w={7}
90
+ minW={0}
91
+ borderRadius={10}
92
+ h={7}
93
+ size="sm"
94
+ aria-label="View Image"
95
+ icon={
96
+ <Image
97
+ height="20px"
98
+ src="/icons/open_in_full.svg"
99
+ opacity={0.8}
100
+ filter="brightness(300%) drop-shadow(1px 1px 3px #00000066)"
101
+ />
102
+ }
103
+ onClick={() => setViewing(true)}
104
+ />
105
+ ) : null}
106
+ <Center
107
+ pos="absolute"
108
+ top={0}
109
+ left={0}
110
+ zIndex={2}
111
+ pointerEvents="none"
112
+ transition="opacity 0.5s ease-in-out"
113
+ opacity={loading || !src ? 1 : 0}
114
+ w="100%"
115
+ h="100%">
116
+ <LoadingLogo isLoading={loading} size="60%" />
117
+ </Center>
118
+ {viewing ? (
119
+ <ImageViewOverlay src={src ?? emptyPng} onClose={() => setViewing(false)} />
120
+ ) : null}
121
+ </Center>
122
+ )
123
+ }
@@ -0,0 +1,78 @@
1
+ import { Center, Flex, Switch, Text, useToast } from '@chakra-ui/react'
2
+ import { useCallback, useState } from 'react'
3
+ import { Loading } from './Loading'
4
+
5
+ export const LoadingSwitch = ({
6
+ value,
7
+ label,
8
+ onChange,
9
+ }: {
10
+ value: boolean
11
+ label: string
12
+ onChange: (updated: boolean) => Promise<void>
13
+ }) => {
14
+ const toast = useToast()
15
+ const [isLoading, setIsLoading] = useState(false)
16
+
17
+ const reportComplete = useCallback((fetchedOn: number) => {
18
+ const buffer = Math.max(0, 1000 - (Date.now() - fetchedOn))
19
+ setTimeout(() => {
20
+ setIsLoading(false)
21
+ }, buffer)
22
+ }, [])
23
+
24
+ return (
25
+ <Flex
26
+ py={1}
27
+ pr={2}
28
+ pl={3}
29
+ bg="gray.700"
30
+ borderRadius={12}
31
+ boxShadow="0 0 4px black"
32
+ pos="relative"
33
+ align="center">
34
+ <Flex
35
+ opacity={isLoading ? 0 : 1}
36
+ transition={`opacity 300ms ${isLoading ? 'ease-out' : 'ease-in'}`}
37
+ pointerEvents={isLoading ? 'none' : 'auto'}
38
+ align="center"
39
+ gap={2}>
40
+ <Text style={{ fontSize: '1.2rem', height: '24px' }}>{label}</Text>
41
+ <Switch
42
+ size="md"
43
+ isChecked={value}
44
+ onChange={e => {
45
+ setIsLoading(true)
46
+ const fetchedOn = Date.now()
47
+ onChange(e.target.checked).catch(err => {
48
+ console.error(err)
49
+ toast({
50
+ title: 'Error',
51
+ description: err?.message ?? 'Error occurred',
52
+ status: 'error',
53
+ duration: 5000,
54
+ isClosable: true,
55
+ })
56
+ })
57
+
58
+ reportComplete(fetchedOn)
59
+ }}
60
+ />
61
+ </Flex>
62
+ <Center
63
+ opacity={isLoading ? 1 : 0}
64
+ transition={`opacity 300ms ${!isLoading ? 'ease-out' : 'ease-in'}`}
65
+ pointerEvents={isLoading ? 'auto' : 'none'}
66
+ pos="absolute"
67
+ top="-10%"
68
+ left="-10%"
69
+ w="120%"
70
+ borderRadius={8}
71
+ p={1}
72
+ gap={2}
73
+ h="120%">
74
+ <Loading />
75
+ </Center>
76
+ </Flex>
77
+ )
78
+ }
@@ -0,0 +1,4 @@
1
+ export * from './CircularProgress'
2
+ export * from './Loading'
3
+ export * from './LoadingImage'
4
+ export * from './LoadingSwitch'