@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.
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +24 -22
- package/src/components/auth/SignIn.tsx +43 -0
- package/src/components/auth/index.ts +1 -0
- package/src/components/box/CollapseHorizontal.tsx +18 -0
- package/src/components/box/ContentBox.tsx +17 -0
- package/src/components/box/ExpandOnMount.tsx +48 -0
- package/src/components/box/Expandable.tsx +96 -0
- package/src/components/box/FullSizeContainer.tsx +50 -0
- package/src/components/box/MobileFrame/index.tsx +145 -0
- package/src/components/box/MobileFrame/styles.css +35 -0
- package/src/components/box/index.ts +6 -0
- package/src/components/button/DeleteButton.tsx +178 -0
- package/src/components/button/Toggle.tsx +88 -0
- package/src/components/button/ViewButton.tsx +30 -0
- package/src/components/button/index.ts +3 -0
- package/src/components/feed/FeedContentPane.tsx +111 -0
- package/src/components/feed/MediaFeed.tsx +200 -0
- package/src/components/feed/MediaFeedBackground.tsx +127 -0
- package/src/components/feed/MediaFeedRefresh.tsx +78 -0
- package/src/components/feed/MediaFeedSwipeUp.tsx +34 -0
- package/src/components/feed/constants.ts +11 -0
- package/src/components/feed/context.tsx +19 -0
- package/src/components/feed/hooks.ts +290 -0
- package/src/components/feed/index.ts +2 -0
- package/src/components/feed/types.ts +50 -0
- package/src/components/form/Condition.tsx +26 -0
- package/src/components/form/Field.tsx +39 -0
- package/src/components/form/Form.tsx +425 -0
- package/src/components/form/FormFooter.tsx +82 -0
- package/src/components/form/UploadProgress/index.tsx +38 -0
- package/src/components/form/UploadProgress/styles.css +23 -0
- package/src/components/form/index.ts +4 -0
- package/src/components/form/input/Editable.tsx +129 -0
- package/src/components/form/input/InputSlider.tsx +75 -0
- package/src/components/form/input/OptionalTag.tsx +33 -0
- package/src/components/form/input/StandaloneInput.tsx +41 -0
- package/src/components/form/input/boolean/index.tsx +53 -0
- package/src/components/form/input/color/index.tsx +126 -0
- package/src/components/form/input/date/index.tsx +122 -0
- package/src/components/form/input/datetime/index.tsx +93 -0
- package/src/components/form/input/file.tsx +379 -0
- package/src/components/form/input/hooks/index.ts +2 -0
- package/src/components/form/input/hooks/useInputImperativeHandle.ts +16 -0
- package/src/components/form/input/hooks/useInputStyle.ts +39 -0
- package/src/components/form/input/index.ts +2 -0
- package/src/components/form/input/input.css +44 -0
- package/src/components/form/input/input.tsx +130 -0
- package/src/components/form/input/multipleSelect/index.tsx +55 -0
- package/src/components/form/input/number/index.tsx +83 -0
- package/src/components/form/input/number/styles.css +8 -0
- package/src/components/form/input/select/index.tsx +80 -0
- package/src/components/form/input/socialMedia/index.tsx +158 -0
- package/src/components/form/input/text/index.tsx +72 -0
- package/src/components/form/input/text/textarea.tsx +44 -0
- package/src/components/form/input/time/index.tsx +33 -0
- package/src/components/form/input/type.ts +0 -0
- package/src/components/form/input/types.ts +4 -0
- package/src/components/form/view/file.tsx +45 -0
- package/src/components/form/view/index.tsx +61 -0
- package/src/components/form/view/multipleSelect.tsx +38 -0
- package/src/components/form/view/select.tsx +33 -0
- package/src/components/index.ts +14 -0
- package/src/components/list/Body/InfiniteScrollGridBody.tsx +177 -0
- package/src/components/list/Body/InfiniteScrollListBody.tsx +114 -0
- package/src/components/list/Body/ListBody.tsx +23 -0
- package/src/components/list/Body/PagedGridBody.tsx +104 -0
- package/src/components/list/Body/PagedListBody.tsx +92 -0
- package/src/components/list/Body/hooks.ts +84 -0
- package/src/components/list/DataList.tsx +32 -0
- package/src/components/list/ListContainer.tsx +20 -0
- package/src/components/list/ListContent.tsx +54 -0
- package/src/components/list/ListCreate.tsx +57 -0
- package/src/components/list/ListFilters.tsx +182 -0
- package/src/components/list/ListFooter.tsx +458 -0
- package/src/components/list/ListHeader.tsx +180 -0
- package/src/components/list/ListItem/ListCell.tsx +48 -0
- package/src/components/list/ListItem/ListRow.tsx +38 -0
- package/src/components/list/ListItemView.tsx +53 -0
- package/src/components/list/ListSort.tsx +84 -0
- package/src/components/list/NoItems.tsx +33 -0
- package/src/components/list/constants.ts +1 -0
- package/src/components/list/index.ts +4 -0
- package/src/components/list/types.ts +29 -0
- package/src/components/list/utils.ts +62 -0
- package/src/components/loading/CircularProgress.tsx +11 -0
- package/src/components/loading/Loading.tsx +160 -0
- package/src/components/loading/LoadingImage.tsx +123 -0
- package/src/components/loading/LoadingSwitch.tsx +78 -0
- package/src/components/loading/index.ts +4 -0
- package/src/components/media/PlayButton.tsx +94 -0
- package/src/components/media/index.ts +1 -0
- package/src/components/modal/DefaultModal.tsx +18 -0
- package/src/components/modal/DesktopModal.tsx +11 -0
- package/src/components/modal/ForceMobile.tsx +7 -0
- package/src/components/modal/MobileModal.tsx +89 -0
- package/src/components/modal/index.ts +3 -0
- package/src/components/modal/type.ts +7 -0
- package/src/components/nav/NavBar.tsx +101 -0
- package/src/components/nav/index.ts +1 -0
- package/src/components/overlay/ImageViewOverlay.tsx +88 -0
- package/src/components/overlay/MobileOverlay.tsx +22 -0
- package/src/components/overlay/index.ts +2 -0
- package/src/components/text/GradientText/index.tsx +16 -0
- package/src/components/text/GradientText/styles.css +5 -0
- package/src/components/text/NumberTicker.tsx +28 -0
- package/src/components/text/index.ts +1 -0
- package/src/components/theme/colorMode/DarkModeToggle.tsx +40 -0
- package/src/components/theme/colorMode/index.ts +1 -0
- package/src/components/theme/index.ts +1 -0
- package/src/components/view/ErrorView.tsx +13 -0
- package/src/components/view/RedirectView.tsx +42 -0
- package/src/components/view/index.ts +2 -0
- package/src/contexts/index.ts +1 -0
- package/src/contexts/theme.ts +316 -0
- package/src/custom.d.ts +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/useBorderColor.ts +4 -0
- package/src/store/index.ts +1 -0
- package/src/store/usePlayer.ts +75 -0
- 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,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
|
+
}
|