@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,127 @@
1
+ import { Center, CenterProps } from '@chakra-ui/react'
2
+ import { AnyObject, WithId } from '@chem-po/core'
3
+ import { useObjectUrl } from '@chem-po/react'
4
+ import { useEffect, useMemo, useRef, useState } from 'react'
5
+ import { LoadingLogo } from '../loading'
6
+ import { GetBackgroundUrl, GetBackgroundValue } from './types'
7
+
8
+ const emptyPng =
9
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII='
10
+ const FillBackground = ({
11
+ background,
12
+ ...props
13
+ }: Omit<CenterProps, 'background'> & { background: string | null }) => (
14
+ <Center
15
+ position="absolute"
16
+ top="0"
17
+ left="0"
18
+ opacity={background ? 0.7 : 0}
19
+ transition="opacity 500ms"
20
+ right="0"
21
+ bottom="0"
22
+ zIndex={0}
23
+ transform="scale(1.075)"
24
+ pointerEvents="none"
25
+ backgroundImage={`url(${background ?? emptyPng})`}
26
+ backgroundSize="cover"
27
+ backgroundPosition="center"
28
+ {...props}
29
+ filter={props.filter ?? 'blur(15px) brightness(70%)'}
30
+ />
31
+ )
32
+
33
+ interface FileValueBackgroundProps<T extends AnyObject = AnyObject> {
34
+ getBackgroundValue: GetBackgroundValue<T>
35
+ item: WithId<T> | null
36
+ filter?: string
37
+ }
38
+ const FileValueBackground = <T extends AnyObject = AnyObject>({
39
+ getBackgroundValue,
40
+ item,
41
+ filter,
42
+ }: FileValueBackgroundProps<T>) => {
43
+ const background = useMemo(
44
+ () => (item ? getBackgroundValue(item) : null),
45
+ [getBackgroundValue, item],
46
+ )
47
+ const { loading, url } = useObjectUrl(background)
48
+ const usedIndex = useRef(0)
49
+ const [index, setIndex] = useState(0)
50
+ const [url1, setUrl1] = useState<string | null>(null)
51
+ const [url2, setUrl2] = useState<string | null>(null)
52
+
53
+ useEffect(() => {
54
+ const usedIdx = usedIndex.current
55
+ if (usedIdx === 0) {
56
+ setUrl1(url)
57
+ usedIndex.current = 1
58
+ setIndex(0)
59
+ } else {
60
+ setUrl2(url)
61
+ usedIndex.current = 0
62
+ setIndex(1)
63
+ }
64
+ }, [url])
65
+
66
+ return (
67
+ <Center position="absolute" top="0" left="0" right="0" bottom="0" zIndex={0}>
68
+ <FillBackground filter={filter} opacity={index === 0 ? 1 : 0} background={url1} />
69
+ <FillBackground filter={filter} opacity={index === 1 ? 1 : 0} background={url2} />
70
+ <LoadingLogo isLoading={loading} />
71
+ </Center>
72
+ )
73
+ }
74
+
75
+ const UrlBackground = <T extends AnyObject = AnyObject>({
76
+ getBackgroundUrl,
77
+ item,
78
+ filter,
79
+ }: {
80
+ getBackgroundUrl: GetBackgroundUrl<T>
81
+ item: T | null
82
+ filter?: string
83
+ }) => {
84
+ const background = useMemo(() => (item ? getBackgroundUrl(item) : null), [getBackgroundUrl, item])
85
+ const usedIndex = useRef(0)
86
+ const [index, setIndex] = useState(0)
87
+ const [url1, setUrl1] = useState<string | null>(null)
88
+ const [url2, setUrl2] = useState<string | null>(null)
89
+
90
+ useEffect(() => {
91
+ if (!background) return
92
+ if (usedIndex.current === 0) {
93
+ setUrl1(background)
94
+ usedIndex.current = 1
95
+ setIndex(0)
96
+ } else {
97
+ setUrl2(background)
98
+ usedIndex.current = 0
99
+ setIndex(1)
100
+ }
101
+ }, [background])
102
+
103
+ return (
104
+ <Center position="absolute" top="0" left="0" right="0" bottom="0" zIndex={0}>
105
+ <FillBackground filter={filter} opacity={index === 0 ? 1 : 0} background={url1} />
106
+ <FillBackground filter={filter} opacity={index === 1 ? 1 : 0} background={url2} />
107
+ </Center>
108
+ )
109
+ }
110
+
111
+ interface MediaFeedBackgroundProps<T extends AnyObject = AnyObject> {
112
+ getBackgroundValue?: GetBackgroundValue<T>
113
+ getBackgroundUrl?: GetBackgroundUrl<T>
114
+ filter?: string
115
+ item: WithId<T> | null
116
+ }
117
+
118
+ export const MediaFeedBackground = <T extends AnyObject = AnyObject>({
119
+ getBackgroundValue,
120
+ getBackgroundUrl,
121
+ ...props
122
+ }: MediaFeedBackgroundProps<T>) => {
123
+ if (getBackgroundValue)
124
+ return <FileValueBackground getBackgroundValue={getBackgroundValue} {...props} />
125
+ if (getBackgroundUrl) return <UrlBackground getBackgroundUrl={getBackgroundUrl} {...props} />
126
+ return null
127
+ }
@@ -0,0 +1,78 @@
1
+ import { RepeatIcon } from '@chakra-ui/icons'
2
+ import { Center } from '@chakra-ui/react'
3
+ import { motion, MotionValue, useSpring, useTransform } from 'framer-motion'
4
+ import { useEffect } from 'react'
5
+ import { CircularProgress } from '../loading/CircularProgress'
6
+ import { REFRESH_THRESHOLD, SWIPE_THRESHOLD } from './constants'
7
+
8
+ const WINDOW = REFRESH_THRESHOLD - SWIPE_THRESHOLD
9
+ export const MediaFeedRefresh = ({
10
+ offsetY,
11
+ refreshing,
12
+ canRefresh,
13
+ }: {
14
+ offsetY: MotionValue<number>
15
+ refreshing: boolean
16
+ canRefresh: boolean
17
+ }) => {
18
+ const progress = useTransform(offsetY, v => {
19
+ const dist = Math.max(0, v - SWIPE_THRESHOLD)
20
+ return dist / WINDOW
21
+ })
22
+ const baseY = useSpring(0)
23
+ const isIn = useSpring(0)
24
+ useEffect(() => {
25
+ if (refreshing) {
26
+ baseY.set(50)
27
+ } else {
28
+ baseY.set(0)
29
+ }
30
+ }, [refreshing, baseY])
31
+
32
+ useEffect(() => {
33
+ if (canRefresh) {
34
+ isIn.set(1)
35
+ } else {
36
+ isIn.set(0)
37
+ }
38
+ }, [isIn, canRefresh])
39
+
40
+ const y = useTransform(
41
+ () => isIn.get() * Math.min(20, Math.max(0, baseY.get() + progress.get() ** 0.5 * 20)),
42
+ )
43
+ const scale = useTransform(y, v => isIn.get() * Math.min(1, Math.max(0, v / 30)))
44
+ const rotate = useTransform(() => isIn.get() * progress.get() * 180)
45
+ return (
46
+ <motion.div
47
+ style={{
48
+ opacity: scale,
49
+ pointerEvents: 'none',
50
+ y,
51
+ position: 'absolute',
52
+ top: 0,
53
+ left: 0,
54
+ right: 0,
55
+ height: 'auto',
56
+ scale,
57
+ rotate,
58
+ }}>
59
+ <Center w="100%">
60
+ <RepeatIcon
61
+ opacity={refreshing ? 0 : 1}
62
+ transition={`opacity 300ms ease ${!refreshing ? 300 : 0}ms`}
63
+ w={8}
64
+ h={8}
65
+ color="white"
66
+ filter="drop-shadow(1px 1px 3px #000000aa)"
67
+ />
68
+ <CircularProgress
69
+ size={8}
70
+ position="absolute"
71
+ isIndeterminate
72
+ opacity={refreshing ? 1 : 0}
73
+ transition={`opacity 300ms ease ${refreshing ? 300 : 0}ms`}
74
+ />
75
+ </Center>
76
+ </motion.div>
77
+ )
78
+ }
@@ -0,0 +1,34 @@
1
+ import { ChevronUpIcon } from '@chakra-ui/icons'
2
+ import { Center } from '@chakra-ui/react'
3
+ import { motion, MotionValue, useTransform } from 'framer-motion'
4
+
5
+ export const MediaFeedSwipeUp = ({ offsetY }: { offsetY: MotionValue<number> }) => {
6
+ const progress = useTransform(offsetY, v => {
7
+ if (v > -30) return 0
8
+ return Math.max(0, -(v + 30) / 20)
9
+ })
10
+ const y = useTransform(progress, v => -(v ** 0.5) * 10)
11
+ return (
12
+ <motion.div
13
+ style={{
14
+ opacity: progress,
15
+ pointerEvents: 'none',
16
+ y,
17
+ position: 'absolute',
18
+ bottom: 0,
19
+ left: 0,
20
+ right: 0,
21
+ height: 'auto',
22
+ scale: progress,
23
+ }}>
24
+ <Center w="100%">
25
+ <ChevronUpIcon
26
+ w={8}
27
+ h={8}
28
+ color="whiteAlpha.700"
29
+ filter="drop-shadow(1px 1px 3px #000000aa)"
30
+ />
31
+ </Center>
32
+ </motion.div>
33
+ )
34
+ }
@@ -0,0 +1,11 @@
1
+ import { SpringOptions } from 'framer-motion'
2
+
3
+ export const springConfig: SpringOptions = {
4
+ damping: 25,
5
+ stiffness: 200,
6
+ bounce: 0.5,
7
+ // restSpeed: 0.1,
8
+ }
9
+
10
+ export const SWIPE_THRESHOLD = 75
11
+ export const REFRESH_THRESHOLD = 100
@@ -0,0 +1,19 @@
1
+ import { AnyObject, WithId } from '@chem-po/core'
2
+ import { createContext, PropsWithChildren, useContext, useMemo } from 'react'
3
+
4
+ interface MediaFeedContextValue<T extends AnyObject = AnyObject> {
5
+ curr: WithId<T> | null
6
+ }
7
+ export const MediaFeedContext = createContext<MediaFeedContextValue<any>>(
8
+ {} as MediaFeedContextValue<any>,
9
+ )
10
+
11
+ export const MediaFeedProvider = <T extends AnyObject = AnyObject>({
12
+ curr,
13
+ children,
14
+ }: PropsWithChildren<{ curr: WithId<T> | null }>) => {
15
+ const contextData = useMemo(() => ({ curr }), [curr])
16
+ return <MediaFeedContext.Provider value={contextData}>{children}</MediaFeedContext.Provider>
17
+ }
18
+ export const useMediaFeed = <T extends AnyObject = AnyObject>() =>
19
+ useContext(MediaFeedContext) as MediaFeedContextValue<T>
@@ -0,0 +1,290 @@
1
+ import { useToast } from '@chakra-ui/react'
2
+ import { FetchFeedFunction } from '@chem-po/core'
3
+ import { useAuth } from '@chem-po/react'
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
+ import { UpdatePanelsArgs } from './types'
6
+
7
+ const DEFAULT_LIMIT = 10
8
+ export const useMediaFeed = (
9
+ fetch: FetchFeedFunction,
10
+ onUpdatePanels: (items: UpdatePanelsArgs) => void,
11
+ limit: number = DEFAULT_LIMIT,
12
+ authRequired = false,
13
+ ) => {
14
+ // first element is prev ids, second element is curr ids, third element is next ids
15
+ const [ids, setIds] = useState<[string[] | null, string[] | null, string[] | null]>([
16
+ null,
17
+ null,
18
+ null,
19
+ ])
20
+ const [idIdx, setIdIdx] = useState<number>(0)
21
+ const user = useAuth(s => s.user)
22
+
23
+ const [loading, setLoading] = useState(false)
24
+ const [refreshing, setRefreshing] = useState(false)
25
+ const [error, setError] = useState<string | null>(null)
26
+ const [itemLoading, setItemLoading] = useState(false)
27
+ const [direction, setDirection] = useState<'next' | 'prev' | null>(null)
28
+
29
+ const toast = useToast()
30
+
31
+ const fetchingId = useRef<string | null>(null)
32
+
33
+ const idsRef = useRef<{ prev: string | null; curr: string | null; next: string | null }>({
34
+ prev: null,
35
+ curr: null,
36
+ next: null,
37
+ })
38
+
39
+ const updateCurr = useCallback(
40
+ (id: string | null) => {
41
+ if (id) {
42
+ if (idsRef.current.prev === id) {
43
+ idsRef.current.prev = null
44
+ }
45
+ if (idsRef.current.next === id) {
46
+ idsRef.current.next = null
47
+ }
48
+ }
49
+ idsRef.current.curr = id
50
+ onUpdatePanels({ ...idsRef.current })
51
+ },
52
+ [onUpdatePanels],
53
+ )
54
+
55
+ const updateNext = useCallback(
56
+ (id: string | null) => {
57
+ if (id) {
58
+ if (idsRef.current.prev === id) {
59
+ idsRef.current.prev = null
60
+ }
61
+ if (idsRef.current.curr === id) {
62
+ idsRef.current.curr = null
63
+ }
64
+ }
65
+ idsRef.current.next = id
66
+ onUpdatePanels({ ...idsRef.current })
67
+ },
68
+ [onUpdatePanels],
69
+ )
70
+
71
+ const updatePrev = useCallback(
72
+ (id: string | null) => {
73
+ if (id) {
74
+ if (idsRef.current.curr === id) {
75
+ idsRef.current.curr = null
76
+ }
77
+ if (idsRef.current.next === id) {
78
+ idsRef.current.next = null
79
+ }
80
+ }
81
+ idsRef.current.prev = id
82
+ onUpdatePanels({ ...idsRef.current })
83
+ },
84
+ [onUpdatePanels],
85
+ )
86
+
87
+ const fetchIds = useCallback(
88
+ async (
89
+ startBefore: string | null,
90
+ startAfter: string | null,
91
+ isPrefetch: boolean,
92
+ ): Promise<string[]> => {
93
+ if (!user && authRequired) return []
94
+ if (!isPrefetch) setLoading(true)
95
+
96
+ try {
97
+ const data = await fetch({ limit, startAfter, startBefore })
98
+ if (!isPrefetch) {
99
+ const firstId = data.ids[0]
100
+ const secondId = data.ids[1] || null
101
+ updateCurr(firstId)
102
+ updateNext(secondId)
103
+ updatePrev(startAfter)
104
+ if (startAfter) {
105
+ setIds(s => [s[1], data.ids, null])
106
+ } else if (startBefore) {
107
+ setIds(s => [null, data.ids, s[1]])
108
+ } else {
109
+ setIds([null, data.ids, null])
110
+ }
111
+ const newLastId = data.ids[data.ids.length - 1]
112
+ if (newLastId) {
113
+ fetchIds(null, newLastId, true)
114
+ }
115
+ } else if (startAfter) {
116
+ setIds(s => [s[0], s[1], data.ids])
117
+ } else if (startBefore) {
118
+ setIds(s => [data.ids, s[1], s[2]])
119
+ }
120
+ setLoading(false)
121
+ return data.ids || []
122
+ } catch (err: any) {
123
+ setError(err.message)
124
+ toast({
125
+ title: 'Error fetching feed',
126
+ description: err.message,
127
+ status: 'error',
128
+ duration: 9000,
129
+ isClosable: true,
130
+ })
131
+ }
132
+ setLoading(false)
133
+ return []
134
+ },
135
+ [fetch, toast, user, authRequired, updateCurr, updateNext, updatePrev, limit],
136
+ )
137
+
138
+ // handles prefetching of next items
139
+ const getNextId = useCallback(
140
+ async (currIdx: number, isPrefetch: boolean): Promise<{ id: string; idx: number } | null> => {
141
+ const currIds = ids[1] ?? []
142
+ const nextIds = ids[2] ?? []
143
+ const nextIdx = currIdx + 1
144
+ if (nextIdx < currIds.length) {
145
+ return { id: currIds[nextIdx], idx: nextIdx }
146
+ }
147
+ if (nextIds[0]) {
148
+ const newLastId = nextIds[nextIds.length - 1]
149
+ if (!isPrefetch) {
150
+ setIds(s => [s[1], s[2], null])
151
+ fetchIds(null, newLastId, true)
152
+ }
153
+ return { id: nextIds[0], idx: 0 }
154
+ }
155
+ const fetchedNextIds = await fetchIds(null, currIds[currIds.length - 1], isPrefetch)
156
+ const fetchedNextId = fetchedNextIds[0]
157
+ return fetchedNextId ? { id: fetchedNextId, idx: 0 } : null
158
+ },
159
+ [ids, fetchIds],
160
+ )
161
+
162
+ const getPrevId = useCallback(
163
+ async (currIdx: number, isPrefetch: boolean): Promise<{ id: string; idx: number } | null> => {
164
+ const currIds = ids[1] ?? []
165
+ const prevIds = ids[0] ?? []
166
+ const prevIdx = currIdx - 1
167
+ if (prevIdx >= 0) {
168
+ return { id: currIds[prevIdx], idx: prevIdx }
169
+ }
170
+ if (prevIds[prevIds.length - 1]) {
171
+ const newFirstId = prevIds[0]
172
+ if (!isPrefetch) {
173
+ setIds([null, ids[0], ids[1]])
174
+ fetchIds(newFirstId, null, true)
175
+ }
176
+ return { id: prevIds[prevIds.length - 1], idx: prevIds.length - 1 }
177
+ }
178
+ const fetchedPrevIds = await fetchIds(currIds[0], null, isPrefetch)
179
+ const fetchedPrevId = fetchedPrevIds[fetchedPrevIds.length - 1]
180
+ return fetchedPrevId ? { id: fetchedPrevId, idx: fetchedPrevIds.length - 1 } : null
181
+ },
182
+ [ids, fetchIds],
183
+ )
184
+
185
+ const goNext = useCallback(async () => {
186
+ if (itemLoading) return
187
+ // case 1: next item ID is fetched
188
+ const newPrev = idsRef.current.curr
189
+
190
+ setItemLoading(true)
191
+ const nextIdData = await getNextId(idIdx, false)
192
+ setDirection('next')
193
+ if (nextIdData) {
194
+ const { id: nextId, idx: nextIdx } = nextIdData
195
+ fetchingId.current = nextId
196
+ setIdIdx(nextIdx)
197
+ updateCurr(nextId)
198
+ updateNext(null)
199
+ updatePrev(newPrev)
200
+ getNextId(nextIdx, true).then(newNextId => updateNext(newNextId?.id ?? null))
201
+ } else {
202
+ toast({
203
+ title: 'No more items',
204
+ status: 'info',
205
+ duration: 3000,
206
+ isClosable: true,
207
+ })
208
+ }
209
+ setItemLoading(false)
210
+ }, [idIdx, getNextId, updateCurr, updateNext, updatePrev, toast, itemLoading])
211
+
212
+ const goPrev = useCallback(async () => {
213
+ if (itemLoading) return
214
+ // case 1: next item ID is fetched
215
+ const newNext = idsRef.current.curr
216
+
217
+ setDirection('prev')
218
+ setItemLoading(true)
219
+ const prevIdData = await getPrevId(idIdx, false)
220
+ if (prevIdData) {
221
+ const { id: prevId, idx: prevIdx } = prevIdData
222
+ fetchingId.current = prevId
223
+ setIdIdx(prevIdx)
224
+ updateCurr(prevId)
225
+ updatePrev(null)
226
+ updateNext(newNext)
227
+ getPrevId(prevIdx, true).then(newPrevId => updatePrev(newPrevId?.id ?? null))
228
+ } else {
229
+ toast({
230
+ title: 'No more items',
231
+ status: 'info',
232
+ duration: 3000,
233
+ isClosable: true,
234
+ })
235
+ }
236
+ setItemLoading(false)
237
+ }, [idIdx, getPrevId, updateCurr, updateNext, updatePrev, toast, itemLoading])
238
+
239
+ const refresh = useCallback(async () => {
240
+ setLoading(true)
241
+ setRefreshing(true)
242
+ setIds([null, null, null])
243
+ updateCurr(null)
244
+ updateNext(null)
245
+ updatePrev(null)
246
+ setDirection(null)
247
+ setIdIdx(0)
248
+ await fetchIds(null, null, false)
249
+ setRefreshing(false)
250
+ }, [fetchIds, updateCurr, updateNext, updatePrev])
251
+
252
+ const [initFetchIds] = useState(() => fetchIds)
253
+ useEffect(() => {
254
+ initFetchIds(null, null, false)
255
+ }, [initFetchIds])
256
+
257
+ const canGoPrev = useMemo(() => (idIdx > 0 && !!ids[1]?.length) || !!ids[0]?.length, [ids, idIdx])
258
+ const canGoNext = useMemo(
259
+ () => (ids[1] && idIdx < ids[1].length) ?? !!ids[2]?.length,
260
+ [ids, idIdx],
261
+ )
262
+ return useMemo(
263
+ () => ({
264
+ ids,
265
+ goNext,
266
+ goPrev,
267
+ error,
268
+ refresh,
269
+ refreshing,
270
+ loading,
271
+ canGoNext,
272
+ direction,
273
+ canGoPrev,
274
+ itemLoading,
275
+ }),
276
+ [
277
+ ids,
278
+ goNext,
279
+ goPrev,
280
+ loading,
281
+ error,
282
+ itemLoading,
283
+ canGoNext,
284
+ canGoPrev,
285
+ refresh,
286
+ refreshing,
287
+ direction,
288
+ ],
289
+ )
290
+ }
@@ -0,0 +1,2 @@
1
+ export { useMediaFeed } from './context'
2
+ export * from './MediaFeed'
@@ -0,0 +1,50 @@
1
+ import { AnyObject, FetchFeedFunction, FileValue, WithId } from '@chem-po/core'
2
+ import { JSX } from 'react'
3
+
4
+ export type GetBackgroundUrl<T extends AnyObject = AnyObject> = (
5
+ item: T,
6
+ ) => string | null | undefined
7
+ export type GetBackgroundValue<T extends AnyObject = AnyObject> = (
8
+ item: T,
9
+ ) => FileValue | null | undefined
10
+ export interface MediaFeedProps<T extends AnyObject = AnyObject> {
11
+ collection: string
12
+ fetch: FetchFeedFunction
13
+ authRequired?: boolean
14
+ limit?: number
15
+ // onSwipeUp?: () => void
16
+ swipeDisabled?: boolean
17
+ defaultBackground?: string
18
+ getBackgroundUrl?: GetBackgroundUrl<T>
19
+ getBackgroundValue?: GetBackgroundValue<T>
20
+ RenderItem: (item: WithId<T>) => JSX.Element
21
+ }
22
+
23
+ export type UseMediaFeedProps = Pick<MediaFeedProps, 'collection' | 'fetch'>
24
+
25
+ export interface UseMediaFeed<T extends AnyObject = AnyObject> {
26
+ ids: string[][]
27
+ goNext: () => void
28
+ goPrev: () => void
29
+ error: string | null
30
+ loading: boolean
31
+ itemLoading: boolean
32
+ currItem: WithId<T> | null
33
+ }
34
+ export type SwipeDirection = 'next' | 'prev'
35
+ export interface UpdatePanelsArgs {
36
+ prev: string | null
37
+ curr: string | null
38
+ next: string | null
39
+ }
40
+
41
+ export type PanelStatus = 'current' | 'next' | 'prev'
42
+
43
+ export interface MediaBackgroundRef<T extends AnyObject> {
44
+ onNewData: (data: WithId<T> | null) => void
45
+ }
46
+ export interface PanelData {
47
+ id: string
48
+ status: PanelStatus
49
+ prevStatus?: PanelStatus
50
+ }
@@ -0,0 +1,26 @@
1
+ import { PropsWithChildren, useMemo } from 'react'
2
+ import { useWatch } from 'react-hook-form'
3
+ import { ExpandOnMount } from '../box'
4
+
5
+ export const Condition = ({
6
+ path,
7
+ condition,
8
+ children,
9
+ }: PropsWithChildren<{
10
+ path: string
11
+ condition: (values: any) => boolean
12
+ }>) => {
13
+ // const { register } = useFormContext()
14
+ // const {
15
+ // input: { value },
16
+ // } = useField(path ?? '', { subscription: { value: true } })
17
+ const value = useWatch({ exact: true, name: path })
18
+ const isVisible = useMemo(() => condition(value), [value, condition])
19
+
20
+ if (!isVisible) return null
21
+ return (
22
+ <ExpandOnMount py="3px" animateOpacity in>
23
+ {children}
24
+ </ExpandOnMount>
25
+ )
26
+ }
@@ -0,0 +1,39 @@
1
+ import { InputRef } from '@chem-po/core'
2
+ import { ForwardedRef, forwardRef } from 'react'
3
+
4
+ import { Field, useField } from '@chem-po/react'
5
+ import { ChangeHandler, Controller } from 'react-hook-form'
6
+ import { Input } from './input/input'
7
+
8
+ const FieldComponentBase = (
9
+ { field, name }: { field: Field; name: string },
10
+ ref: ForwardedRef<InputRef>,
11
+ ) => {
12
+ const { control, getOnChange, meta, onFocus, onBlur: fieldBlur } = useField(name, field)
13
+ return (
14
+ <Controller<Field['defaultValue']>
15
+ control={control}
16
+ name={name}
17
+ render={({ field: { name: _, ref: _r, onBlur, ...inputProps } }) => {
18
+ return (
19
+ <Input
20
+ ref={ref}
21
+ field={field}
22
+ input={{
23
+ ...inputProps,
24
+ onFocus,
25
+ onBlur: () => {
26
+ fieldBlur()
27
+ onBlur()
28
+ },
29
+ onChange: getOnChange(inputProps.onChange as ChangeHandler),
30
+ }}
31
+ meta={meta}
32
+ />
33
+ )
34
+ }}
35
+ />
36
+ )
37
+ }
38
+
39
+ export const FieldComponent = forwardRef(FieldComponentBase)