@chem-po/react-web 0.0.5 → 0.0.7
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.js +2 -2
- package/package.json +23 -20
- 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,94 @@
|
|
|
1
|
+
import { ButtonProps, Center, IconButton, Image } from '@chakra-ui/react'
|
|
2
|
+
import { SpotifyTrack, WithId } from '@chem-po/core'
|
|
3
|
+
import { usePlaylist } from '@chem-po/react'
|
|
4
|
+
import { useActiveMedia, usePlayer } from '../../store/usePlayer'
|
|
5
|
+
|
|
6
|
+
export const PlayButton = ({
|
|
7
|
+
media,
|
|
8
|
+
size = 50,
|
|
9
|
+
opacity = 0.8,
|
|
10
|
+
// withThumbnail,
|
|
11
|
+
buttonProps,
|
|
12
|
+
}: {
|
|
13
|
+
media: WithId<SpotifyTrack> | null
|
|
14
|
+
size?: number
|
|
15
|
+
opacity?: number
|
|
16
|
+
withThumbnail?: boolean
|
|
17
|
+
buttonProps?: ButtonProps
|
|
18
|
+
}) => {
|
|
19
|
+
const { id: mediaId } = media ?? {}
|
|
20
|
+
const { playlist } = usePlaylist()
|
|
21
|
+
const api = usePlayer(s => s.api)
|
|
22
|
+
const playerIsPlaying = usePlayer(s => s.isPlaying)
|
|
23
|
+
const playerIsLoading = usePlayer(s => s.isLoading)
|
|
24
|
+
const currentMedia = useActiveMedia()
|
|
25
|
+
const isPlaying = playerIsPlaying && currentMedia?.id === mediaId
|
|
26
|
+
const isFetching = playerIsLoading && currentMedia?.id === mediaId
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<IconButton
|
|
30
|
+
boxShadow="md"
|
|
31
|
+
p={1}
|
|
32
|
+
height={`${size}px`}
|
|
33
|
+
width={`${size}px`}
|
|
34
|
+
minW="0"
|
|
35
|
+
minH="0"
|
|
36
|
+
borderRadius="full"
|
|
37
|
+
isLoading={isFetching}
|
|
38
|
+
aria-label="play/pause"
|
|
39
|
+
onClick={e => {
|
|
40
|
+
e.stopPropagation()
|
|
41
|
+
if (!media) return
|
|
42
|
+
if (currentMedia?.id === mediaId) {
|
|
43
|
+
if (playerIsPlaying) api.pause()
|
|
44
|
+
else api.play()
|
|
45
|
+
} else if (playlist) {
|
|
46
|
+
api.setMedia({
|
|
47
|
+
playlist: {
|
|
48
|
+
...playlist,
|
|
49
|
+
currentId: mediaId ?? null,
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
} else {
|
|
53
|
+
api.setMedia({ media })
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
style={{ background: '#eee' }}
|
|
57
|
+
{...buttonProps}
|
|
58
|
+
icon={
|
|
59
|
+
<Center borderRadius="full" height={`${size}px`} width={`${size}px`}>
|
|
60
|
+
{/* {
|
|
61
|
+
media ? (
|
|
62
|
+
<StorageImage
|
|
63
|
+
borderRadius='full'
|
|
64
|
+
position='absolute'
|
|
65
|
+
objectFit='cover'
|
|
66
|
+
width='100%'
|
|
67
|
+
height='100%'
|
|
68
|
+
storagePath={`${media.storageDir}/photoFile`}
|
|
69
|
+
alt={media.title}
|
|
70
|
+
/>
|
|
71
|
+
) : null
|
|
72
|
+
} */}
|
|
73
|
+
<Image
|
|
74
|
+
opacity={isPlaying ? opacity : 0}
|
|
75
|
+
width="100%"
|
|
76
|
+
transition="all 0.2s ease-in-out"
|
|
77
|
+
src="/svg/pause.svg"
|
|
78
|
+
filter="invert(100%) drop-shadow(0 0 4px black)"
|
|
79
|
+
transform={`scale(${isPlaying ? 0.8 : 1})`}
|
|
80
|
+
/>
|
|
81
|
+
<Image
|
|
82
|
+
position="absolute"
|
|
83
|
+
width="100%"
|
|
84
|
+
opacity={isPlaying ? 0 : opacity}
|
|
85
|
+
transition="all 0.2s ease-in-out"
|
|
86
|
+
filter="invert(100%) drop-shadow(0 0 3px black)"
|
|
87
|
+
src="/svg/play.svg"
|
|
88
|
+
transform={`scale(${!isPlaying ? 0.8 : 1})`}
|
|
89
|
+
/>
|
|
90
|
+
</Center>
|
|
91
|
+
}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PlayButton'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useScreen } from '@chem-po/react'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { DesktopModal } from './DesktopModal'
|
|
4
|
+
import { useForceMobile } from './ForceMobile'
|
|
5
|
+
import { MobileModal } from './MobileModal'
|
|
6
|
+
import { DefaultModalProps } from './type'
|
|
7
|
+
|
|
8
|
+
export const DefaultModal = (props: DefaultModalProps) => {
|
|
9
|
+
const isMobile = useScreen(s => s.isMobile)
|
|
10
|
+
|
|
11
|
+
const forceMobile = useForceMobile()
|
|
12
|
+
const Component = useMemo(
|
|
13
|
+
() => (isMobile || forceMobile ? MobileModal : DesktopModal),
|
|
14
|
+
[isMobile, forceMobile],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
return <Component {...props} />
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Box, Modal, ModalContent, ModalOverlay } from '@chakra-ui/react'
|
|
2
|
+
import { DefaultModalProps } from './type'
|
|
3
|
+
|
|
4
|
+
export const DesktopModal = ({ children, isOpen, contentProps, ...props }: DefaultModalProps) => (
|
|
5
|
+
<Modal scrollBehavior="inside" isCentered isOpen={isOpen} {...props}>
|
|
6
|
+
<ModalOverlay />
|
|
7
|
+
<ModalContent position="relative" overflowY="auto" bg="background.100" {...contentProps}>
|
|
8
|
+
<Box>{children}</Box>
|
|
9
|
+
</ModalContent>
|
|
10
|
+
</Modal>
|
|
11
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React, { PropsWithChildren, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
const ForceMobileContext = React.createContext(false)
|
|
4
|
+
export const ForceMobile = ({ children }: PropsWithChildren) => (
|
|
5
|
+
<ForceMobileContext.Provider value={true}>{children}</ForceMobileContext.Provider>
|
|
6
|
+
)
|
|
7
|
+
export const useForceMobile = () => useContext(ForceMobileContext)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Flex, useColorModeValue } from '@chakra-ui/react'
|
|
2
|
+
import { useMobileFrame } from '@chem-po/react'
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { MobileOverlay } from '../overlay/MobileOverlay'
|
|
5
|
+
import { DefaultModalProps } from './type'
|
|
6
|
+
|
|
7
|
+
export const MobileOverlayBackground = ({ onClick }: { onClick?: () => void }) => (
|
|
8
|
+
<Flex
|
|
9
|
+
onClick={onClick}
|
|
10
|
+
w="100%"
|
|
11
|
+
h="100%"
|
|
12
|
+
position="absolute"
|
|
13
|
+
bg="blackAlpha.400"
|
|
14
|
+
transition="opacity 300ms"
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export const MobileModal = ({
|
|
19
|
+
isOpen,
|
|
20
|
+
onClose,
|
|
21
|
+
children,
|
|
22
|
+
contentProps,
|
|
23
|
+
onContentMounted,
|
|
24
|
+
closeOnOverlayClick,
|
|
25
|
+
}: DefaultModalProps) => {
|
|
26
|
+
const { height, width } = useMobileFrame()
|
|
27
|
+
|
|
28
|
+
const bg = useColorModeValue('gray.100', '#454545')
|
|
29
|
+
|
|
30
|
+
const [isMounted, setIsMounted] = useState(false)
|
|
31
|
+
const [contentMounted, setContentMounted] = useState(false)
|
|
32
|
+
|
|
33
|
+
const unmountTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
34
|
+
const contentMountTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (unmountTimeout.current) {
|
|
37
|
+
clearTimeout(unmountTimeout.current)
|
|
38
|
+
unmountTimeout.current = null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (contentMountTimeout.current) {
|
|
42
|
+
clearTimeout(contentMountTimeout.current)
|
|
43
|
+
contentMountTimeout.current = null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isOpen) {
|
|
47
|
+
setIsMounted(true)
|
|
48
|
+
contentMountTimeout.current = setTimeout(() => {
|
|
49
|
+
setContentMounted(true)
|
|
50
|
+
if (onContentMounted) onContentMounted(true)
|
|
51
|
+
}, 50)
|
|
52
|
+
} else {
|
|
53
|
+
setContentMounted(false)
|
|
54
|
+
if (onContentMounted) onContentMounted(false)
|
|
55
|
+
unmountTimeout.current = setTimeout(() => {
|
|
56
|
+
setIsMounted(false)
|
|
57
|
+
}, 300)
|
|
58
|
+
}
|
|
59
|
+
return () => {
|
|
60
|
+
if (unmountTimeout.current) {
|
|
61
|
+
clearTimeout(unmountTimeout.current)
|
|
62
|
+
unmountTimeout.current = null
|
|
63
|
+
}
|
|
64
|
+
if (contentMountTimeout.current) {
|
|
65
|
+
clearTimeout(contentMountTimeout.current)
|
|
66
|
+
contentMountTimeout.current = null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, [isOpen, onContentMounted])
|
|
70
|
+
return isMounted ? (
|
|
71
|
+
<MobileOverlay
|
|
72
|
+
opacity={contentMounted ? 1 : 0}
|
|
73
|
+
pointerEvents={contentMounted ? 'auto' : 'none'}>
|
|
74
|
+
<MobileOverlayBackground onClick={closeOnOverlayClick !== false ? onClose : undefined} />
|
|
75
|
+
<Flex
|
|
76
|
+
width={`${width - 10}px`}
|
|
77
|
+
maxH={`${height - 10}px`}
|
|
78
|
+
overflowY="auto"
|
|
79
|
+
overflowX="hidden"
|
|
80
|
+
minH="100px"
|
|
81
|
+
bg={bg}
|
|
82
|
+
borderRadius={6}
|
|
83
|
+
position="relative"
|
|
84
|
+
{...contentProps}>
|
|
85
|
+
{children}
|
|
86
|
+
</Flex>
|
|
87
|
+
</MobileOverlay>
|
|
88
|
+
) : null
|
|
89
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ModalContentProps, ModalProps } from '@chakra-ui/react'
|
|
2
|
+
import { PropsWithChildren } from 'react'
|
|
3
|
+
|
|
4
|
+
export type DefaultModalProps = PropsWithChildren<Omit<ModalProps, 'render'>> & {
|
|
5
|
+
contentProps?: ModalContentProps
|
|
6
|
+
onContentMounted?: (mounted: boolean) => void
|
|
7
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Button, Flex, Image, useColorModeValue } from '@chakra-ui/react'
|
|
2
|
+
import { palette } from '@chem-po/core'
|
|
3
|
+
import { useViews, View } from '@chem-po/react'
|
|
4
|
+
import { PropsWithChildren, useMemo } from 'react'
|
|
5
|
+
import { Link, useLocation } from 'react-router-dom'
|
|
6
|
+
|
|
7
|
+
export const NAV_BAR_HEIGHT = 50
|
|
8
|
+
|
|
9
|
+
const NavBarLink = ({ view, absolute }: { view: View; absolute: boolean }) => {
|
|
10
|
+
const { path: paths, name, icon, iconScale } = view
|
|
11
|
+
const path = Array.isArray(paths) ? paths[0] : paths
|
|
12
|
+
|
|
13
|
+
// const hoverBg = useColorModeValue('#00000033', '#f7f7f777')
|
|
14
|
+
// const textColor = useColorModeValue('#777', '#cdcdcd')
|
|
15
|
+
const iconBrightness = useColorModeValue(1, 1.8)
|
|
16
|
+
const usedBrightness = useMemo(
|
|
17
|
+
() => (absolute ? 2.4 : iconBrightness),
|
|
18
|
+
[absolute, iconBrightness],
|
|
19
|
+
)
|
|
20
|
+
const { pathname } = useLocation()
|
|
21
|
+
const hoverBg = useColorModeValue('#ffffff33', '#00000033')
|
|
22
|
+
const body = (
|
|
23
|
+
<Button
|
|
24
|
+
pointerEvents={pathname === path ? 'none' : 'auto'}
|
|
25
|
+
opacity={pathname === path ? 1 : 0.7}
|
|
26
|
+
width="100%"
|
|
27
|
+
height="100%"
|
|
28
|
+
borderRadius={0}
|
|
29
|
+
flexFlow="column"
|
|
30
|
+
_hover={{
|
|
31
|
+
bg: hoverBg,
|
|
32
|
+
}}
|
|
33
|
+
variant="unstyled"
|
|
34
|
+
display="flex"
|
|
35
|
+
alignItems="center"
|
|
36
|
+
justifyContent="center">
|
|
37
|
+
<Image
|
|
38
|
+
height={`${34 * (iconScale ?? 1)}px`}
|
|
39
|
+
filter={`brightness(${usedBrightness})${
|
|
40
|
+
absolute ? ' drop-shadow(1px 1px 3px #00000099 )' : ''
|
|
41
|
+
}`}
|
|
42
|
+
src={icon}
|
|
43
|
+
alt={name}
|
|
44
|
+
/>
|
|
45
|
+
{/* <Text color={textColor} lineHeight={1} fontWeight={400} fontSize='xs'>
|
|
46
|
+
{name}
|
|
47
|
+
</Text> */}
|
|
48
|
+
</Button>
|
|
49
|
+
)
|
|
50
|
+
return pathname === path ? (
|
|
51
|
+
<Flex key={path} flex={1} h="100%" justify="center">
|
|
52
|
+
{body}
|
|
53
|
+
</Flex>
|
|
54
|
+
) : (
|
|
55
|
+
<Link
|
|
56
|
+
key={path}
|
|
57
|
+
style={{
|
|
58
|
+
flex: 1,
|
|
59
|
+
display: 'flex',
|
|
60
|
+
height: '100%',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
}}
|
|
63
|
+
to={path}>
|
|
64
|
+
{body}
|
|
65
|
+
</Link>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const NavBar = ({
|
|
70
|
+
selectedView,
|
|
71
|
+
children,
|
|
72
|
+
}: PropsWithChildren<{ selectedView?: View | null }>) => {
|
|
73
|
+
const bg = useColorModeValue('#dedede', palette.gray.dark)
|
|
74
|
+
const defaultBorderColor = useColorModeValue('#00000022', '#ffffff33')
|
|
75
|
+
const views = useViews()
|
|
76
|
+
const menuViews = useMemo(() => views.filter(v => !!v.view.icon), [views])
|
|
77
|
+
const isAbsolute = useMemo(() => !!selectedView?.navBar?.absolute, [selectedView])
|
|
78
|
+
const background = useMemo(() => selectedView?.navBar?.backgroundColor ?? bg, [bg, selectedView])
|
|
79
|
+
const borderColor = useMemo(() => {
|
|
80
|
+
if (selectedView?.navBar?.borderColor) return !!selectedView?.navBar?.borderColor
|
|
81
|
+
return isAbsolute ? '#ffffff33' : defaultBorderColor
|
|
82
|
+
}, [defaultBorderColor, selectedView, isAbsolute])
|
|
83
|
+
return (
|
|
84
|
+
<Flex
|
|
85
|
+
position="absolute"
|
|
86
|
+
bottom={0}
|
|
87
|
+
left={0}
|
|
88
|
+
bg={background}
|
|
89
|
+
w="100%"
|
|
90
|
+
transition="all 500ms"
|
|
91
|
+
h={`${NAV_BAR_HEIGHT}px`}
|
|
92
|
+
borderTop={`1px solid ${borderColor}`}>
|
|
93
|
+
<Flex position="relative" w="100%" justify="space-around" align="center">
|
|
94
|
+
{menuViews.map(({ view }) => (
|
|
95
|
+
<NavBarLink absolute={isAbsolute} key={view.name} view={view} />
|
|
96
|
+
))}
|
|
97
|
+
{children}
|
|
98
|
+
</Flex>
|
|
99
|
+
</Flex>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './NavBar'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CloseIcon } from '@chakra-ui/icons'
|
|
2
|
+
import { Center, IconButton, Image, Modal, ModalContent, ModalOverlay } from '@chakra-ui/react'
|
|
3
|
+
import { useScreen } from '@chem-po/react'
|
|
4
|
+
import { SyntheticEvent, useCallback, useMemo, useState } from 'react'
|
|
5
|
+
import { LoadingLogo } from '../loading/Loading'
|
|
6
|
+
|
|
7
|
+
export const ImageViewOverlay = ({ src, onClose }: { src: string; onClose: () => void }) => {
|
|
8
|
+
const [loading, setLoading] = useState(true)
|
|
9
|
+
const screenWidth = useScreen(s => s.width)
|
|
10
|
+
const screenHeight = useScreen(s => s.height)
|
|
11
|
+
const [imageSize, setImageSize] = useState({ width: screenWidth / 2, height: screenHeight / 2 })
|
|
12
|
+
|
|
13
|
+
const { height, width } = useMemo(() => {
|
|
14
|
+
if (loading) return imageSize
|
|
15
|
+
const ratio = imageSize.width / imageSize.height
|
|
16
|
+
let h = Math.min(imageSize.height, screenHeight * 0.9)
|
|
17
|
+
let w = h * ratio
|
|
18
|
+
if (w > screenWidth * 0.9) {
|
|
19
|
+
w = Math.min(imageSize.width, screenWidth * 0.9)
|
|
20
|
+
h = w / ratio
|
|
21
|
+
}
|
|
22
|
+
return { height: h, width: w }
|
|
23
|
+
}, [screenHeight, screenWidth, imageSize, loading])
|
|
24
|
+
|
|
25
|
+
const onLoadStart = useCallback(() => setLoading(true), [])
|
|
26
|
+
const onLoad = useCallback((e: SyntheticEvent<HTMLImageElement>) => {
|
|
27
|
+
const { naturalWidth, naturalHeight } = e.currentTarget
|
|
28
|
+
setImageSize({ width: naturalWidth, height: naturalHeight })
|
|
29
|
+
setLoading(false)
|
|
30
|
+
}, [])
|
|
31
|
+
return (
|
|
32
|
+
<Modal size="full" isOpen onClose={onClose}>
|
|
33
|
+
<ModalOverlay bg="blackAlpha.700" />
|
|
34
|
+
<ModalContent
|
|
35
|
+
style={{ background: 'transparent' }}
|
|
36
|
+
pointerEvents="none"
|
|
37
|
+
width="100%"
|
|
38
|
+
height="100%">
|
|
39
|
+
<Center
|
|
40
|
+
pointerEvents="none"
|
|
41
|
+
position="fixed"
|
|
42
|
+
p={[4, 6, 8]}
|
|
43
|
+
top={0}
|
|
44
|
+
left={0}
|
|
45
|
+
right={0}
|
|
46
|
+
bottom={0}
|
|
47
|
+
zIndex={4}>
|
|
48
|
+
<Center
|
|
49
|
+
opacity={loading ? 0 : 1}
|
|
50
|
+
transition="all 500ms"
|
|
51
|
+
overflow="hidden"
|
|
52
|
+
w={`${width}px`}
|
|
53
|
+
height={`${height}px`}>
|
|
54
|
+
<Image
|
|
55
|
+
onLoadStart={onLoadStart}
|
|
56
|
+
onLoad={onLoad}
|
|
57
|
+
transition="opacity 300ms"
|
|
58
|
+
height="100%"
|
|
59
|
+
objectFit="contain"
|
|
60
|
+
borderRadius={4}
|
|
61
|
+
src={src}
|
|
62
|
+
/>
|
|
63
|
+
</Center>
|
|
64
|
+
<IconButton
|
|
65
|
+
borderRadius="full"
|
|
66
|
+
position="absolute"
|
|
67
|
+
top={4}
|
|
68
|
+
right={4}
|
|
69
|
+
aria-label="close"
|
|
70
|
+
icon={<CloseIcon />}
|
|
71
|
+
onClick={onClose}
|
|
72
|
+
/>
|
|
73
|
+
</Center>
|
|
74
|
+
<Center
|
|
75
|
+
position="absolute"
|
|
76
|
+
top={0}
|
|
77
|
+
left={0}
|
|
78
|
+
right={0}
|
|
79
|
+
bottom={0}
|
|
80
|
+
pointerEvents="none"
|
|
81
|
+
opacity={loading ? 1 : 0}
|
|
82
|
+
transition="opacity 300ms">
|
|
83
|
+
<LoadingLogo isLoading={loading} size={70} />
|
|
84
|
+
</Center>
|
|
85
|
+
</ModalContent>
|
|
86
|
+
</Modal>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Flex, FlexProps, Portal } from '@chakra-ui/react'
|
|
2
|
+
import { useMobileFrame } from '@chem-po/react'
|
|
3
|
+
|
|
4
|
+
export const MobileOverlay = (props: FlexProps) => {
|
|
5
|
+
const { overlayRef } = useMobileFrame()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Portal containerRef={overlayRef}>
|
|
9
|
+
<Flex
|
|
10
|
+
justify="center"
|
|
11
|
+
align="center"
|
|
12
|
+
transition="opacity 300ms"
|
|
13
|
+
position="absolute"
|
|
14
|
+
left={0}
|
|
15
|
+
top={0}
|
|
16
|
+
height="100%"
|
|
17
|
+
width="100%"
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
</Portal>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Box, BoxProps, Text, TextProps } from '@chakra-ui/react'
|
|
2
|
+
import './styles.css'
|
|
3
|
+
|
|
4
|
+
export const GradientText = ({
|
|
5
|
+
children,
|
|
6
|
+
color,
|
|
7
|
+
background,
|
|
8
|
+
boxProps,
|
|
9
|
+
...props
|
|
10
|
+
}: TextProps & { boxProps?: BoxProps }) => (
|
|
11
|
+
<Box background={background} {...boxProps}>
|
|
12
|
+
<Text cursor="default" className="gradient-text" background={color} {...props}>
|
|
13
|
+
{children}
|
|
14
|
+
</Text>
|
|
15
|
+
</Box>
|
|
16
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export const NumberTicker = ({ value, duration = 30 }: { value: number; duration?: number }) => {
|
|
4
|
+
const spanRef = useRef<HTMLSpanElement>(null)
|
|
5
|
+
const helperValue = useRef(value)
|
|
6
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
intervalRef.current = setInterval(() => {
|
|
9
|
+
if (spanRef.current) {
|
|
10
|
+
if (helperValue.current === value && intervalRef.current)
|
|
11
|
+
return clearInterval(intervalRef.current)
|
|
12
|
+
if (helperValue.current < value) {
|
|
13
|
+
helperValue.current += 1
|
|
14
|
+
spanRef.current.innerText = helperValue.current.toString()
|
|
15
|
+
} else if (helperValue.current > value) {
|
|
16
|
+
helperValue.current -= 1
|
|
17
|
+
spanRef.current.innerText = helperValue.current.toString()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return () => {}
|
|
21
|
+
}, duration)
|
|
22
|
+
return () => {
|
|
23
|
+
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
24
|
+
}
|
|
25
|
+
}, [value, duration])
|
|
26
|
+
|
|
27
|
+
return <span ref={spanRef}>{value}</span>
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './NumberTicker'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MoonIcon, SunIcon } from '@chakra-ui/icons'
|
|
2
|
+
import { Flex, FlexProps, IconButton, useColorMode, useColorModeValue } from '@chakra-ui/react'
|
|
3
|
+
|
|
4
|
+
export const DarkModeToggle = () => {
|
|
5
|
+
const { colorMode, toggleColorMode } = useColorMode()
|
|
6
|
+
const bg = useColorModeValue('gray.200', 'whiteAlpha.200')
|
|
7
|
+
const color = useColorModeValue('blackAlpha.700', 'whiteAlpha.800')
|
|
8
|
+
const hoverBg = useColorModeValue('gray.300', 'whiteAlpha.300')
|
|
9
|
+
const hoverColor = useColorModeValue('gray.800', 'whiteAlpha.800')
|
|
10
|
+
return (
|
|
11
|
+
<IconButton
|
|
12
|
+
aria-label="Toggle dark mode"
|
|
13
|
+
bg={bg}
|
|
14
|
+
color={color}
|
|
15
|
+
borderRadius="full"
|
|
16
|
+
_hover={{
|
|
17
|
+
bg: hoverBg,
|
|
18
|
+
color: hoverColor,
|
|
19
|
+
}}
|
|
20
|
+
w={8}
|
|
21
|
+
h={8}
|
|
22
|
+
minW={0}
|
|
23
|
+
icon={
|
|
24
|
+
colorMode === 'light' ? (
|
|
25
|
+
<SunIcon w={4} h={4} />
|
|
26
|
+
) : (
|
|
27
|
+
<MoonIcon w={4} h={4} filter="drop-shadow(1px 1px 2px #000000aa)" />
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
onClick={toggleColorMode}
|
|
31
|
+
variant="ghost"
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const AbsoluteDarkModeToggle = (props: FlexProps) => (
|
|
37
|
+
<Flex position="absolute" bottom={3} right={3} {...props}>
|
|
38
|
+
<DarkModeToggle />
|
|
39
|
+
</Flex>
|
|
40
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './DarkModeToggle'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './colorMode'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Flex, Text, useColorModeValue } from '@chakra-ui/react'
|
|
2
|
+
import { ContentBox } from '../box/ContentBox'
|
|
3
|
+
|
|
4
|
+
export const ErrorView = ({ message }: { message?: string }) => {
|
|
5
|
+
const color = useColorModeValue('gray.600', 'gray.100')
|
|
6
|
+
return (
|
|
7
|
+
<Flex pt={4}>
|
|
8
|
+
<ContentBox>
|
|
9
|
+
<Text color={color}>{message ?? 'Sorry, something went wrong.'}</Text>
|
|
10
|
+
</ContentBox>
|
|
11
|
+
</Flex>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Center, HStack, Text, VStack } from '@chakra-ui/react'
|
|
2
|
+
import { useAuth } from '@chem-po/react'
|
|
3
|
+
import React, { useEffect, useRef } from 'react'
|
|
4
|
+
import { useNavigate } from 'react-router-dom'
|
|
5
|
+
import { ContentBox } from '../box/ContentBox'
|
|
6
|
+
import { CircularProgress } from '../loading/CircularProgress'
|
|
7
|
+
import { Loading } from '../loading/Loading'
|
|
8
|
+
|
|
9
|
+
export const RedirectView: React.FC<{ loading?: boolean }> = ({ loading }) => {
|
|
10
|
+
const timer = useRef<any>()
|
|
11
|
+
const authLoading = useAuth(s => s.loading)
|
|
12
|
+
|
|
13
|
+
const navigate = useNavigate()
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!loading && !authLoading) {
|
|
16
|
+
timer.current = setTimeout(() => {
|
|
17
|
+
navigate('/')
|
|
18
|
+
}, 1500)
|
|
19
|
+
}
|
|
20
|
+
return () => {
|
|
21
|
+
if (timer.current) clearTimeout(timer.current)
|
|
22
|
+
}
|
|
23
|
+
}, [loading, navigate, authLoading])
|
|
24
|
+
|
|
25
|
+
return !loading && !authLoading ? (
|
|
26
|
+
<Center minH="100%" w="100%">
|
|
27
|
+
<ContentBox maxW="500px">
|
|
28
|
+
<HStack spacing={3}>
|
|
29
|
+
<CircularProgress size={6} />
|
|
30
|
+
<VStack spacing={0} align="flex-start">
|
|
31
|
+
<Text>404</Text>
|
|
32
|
+
<Text fontSize="sm" opacity={0.7}>
|
|
33
|
+
Page not found - redirecting to Home...
|
|
34
|
+
</Text>
|
|
35
|
+
</VStack>
|
|
36
|
+
</HStack>
|
|
37
|
+
</ContentBox>
|
|
38
|
+
</Center>
|
|
39
|
+
) : (
|
|
40
|
+
<Loading />
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './theme'
|