@chem-po/react-web 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.js +2 -2
  3. package/package.json +22 -20
  4. package/src/components/auth/SignIn.tsx +43 -0
  5. package/src/components/auth/index.ts +1 -0
  6. package/src/components/box/CollapseHorizontal.tsx +18 -0
  7. package/src/components/box/ContentBox.tsx +17 -0
  8. package/src/components/box/ExpandOnMount.tsx +48 -0
  9. package/src/components/box/Expandable.tsx +96 -0
  10. package/src/components/box/FullSizeContainer.tsx +50 -0
  11. package/src/components/box/MobileFrame/index.tsx +145 -0
  12. package/src/components/box/MobileFrame/styles.css +35 -0
  13. package/src/components/box/index.ts +6 -0
  14. package/src/components/button/DeleteButton.tsx +178 -0
  15. package/src/components/button/Toggle.tsx +88 -0
  16. package/src/components/button/ViewButton.tsx +30 -0
  17. package/src/components/button/index.ts +3 -0
  18. package/src/components/feed/FeedContentPane.tsx +111 -0
  19. package/src/components/feed/MediaFeed.tsx +200 -0
  20. package/src/components/feed/MediaFeedBackground.tsx +127 -0
  21. package/src/components/feed/MediaFeedRefresh.tsx +78 -0
  22. package/src/components/feed/MediaFeedSwipeUp.tsx +34 -0
  23. package/src/components/feed/constants.ts +11 -0
  24. package/src/components/feed/context.tsx +19 -0
  25. package/src/components/feed/hooks.ts +290 -0
  26. package/src/components/feed/index.ts +2 -0
  27. package/src/components/feed/types.ts +50 -0
  28. package/src/components/form/Condition.tsx +26 -0
  29. package/src/components/form/Field.tsx +39 -0
  30. package/src/components/form/Form.tsx +425 -0
  31. package/src/components/form/FormFooter.tsx +82 -0
  32. package/src/components/form/UploadProgress/index.tsx +38 -0
  33. package/src/components/form/UploadProgress/styles.css +23 -0
  34. package/src/components/form/index.ts +4 -0
  35. package/src/components/form/input/Editable.tsx +129 -0
  36. package/src/components/form/input/InputSlider.tsx +75 -0
  37. package/src/components/form/input/OptionalTag.tsx +33 -0
  38. package/src/components/form/input/StandaloneInput.tsx +41 -0
  39. package/src/components/form/input/boolean/index.tsx +53 -0
  40. package/src/components/form/input/color/index.tsx +126 -0
  41. package/src/components/form/input/date/index.tsx +122 -0
  42. package/src/components/form/input/datetime/index.tsx +93 -0
  43. package/src/components/form/input/file.tsx +379 -0
  44. package/src/components/form/input/hooks/index.ts +2 -0
  45. package/src/components/form/input/hooks/useInputImperativeHandle.ts +16 -0
  46. package/src/components/form/input/hooks/useInputStyle.ts +39 -0
  47. package/src/components/form/input/index.ts +2 -0
  48. package/src/components/form/input/input.css +44 -0
  49. package/src/components/form/input/input.tsx +130 -0
  50. package/src/components/form/input/multipleSelect/index.tsx +55 -0
  51. package/src/components/form/input/number/index.tsx +83 -0
  52. package/src/components/form/input/number/styles.css +8 -0
  53. package/src/components/form/input/select/index.tsx +80 -0
  54. package/src/components/form/input/socialMedia/index.tsx +158 -0
  55. package/src/components/form/input/text/index.tsx +72 -0
  56. package/src/components/form/input/text/textarea.tsx +44 -0
  57. package/src/components/form/input/time/index.tsx +33 -0
  58. package/src/components/form/input/type.ts +0 -0
  59. package/src/components/form/input/types.ts +4 -0
  60. package/src/components/form/view/file.tsx +45 -0
  61. package/src/components/form/view/index.tsx +61 -0
  62. package/src/components/form/view/multipleSelect.tsx +38 -0
  63. package/src/components/form/view/select.tsx +33 -0
  64. package/src/components/index.ts +14 -0
  65. package/src/components/list/Body/InfiniteScrollGridBody.tsx +177 -0
  66. package/src/components/list/Body/InfiniteScrollListBody.tsx +114 -0
  67. package/src/components/list/Body/ListBody.tsx +23 -0
  68. package/src/components/list/Body/PagedGridBody.tsx +104 -0
  69. package/src/components/list/Body/PagedListBody.tsx +92 -0
  70. package/src/components/list/Body/hooks.ts +84 -0
  71. package/src/components/list/DataList.tsx +32 -0
  72. package/src/components/list/ListContainer.tsx +20 -0
  73. package/src/components/list/ListContent.tsx +54 -0
  74. package/src/components/list/ListCreate.tsx +57 -0
  75. package/src/components/list/ListFilters.tsx +182 -0
  76. package/src/components/list/ListFooter.tsx +458 -0
  77. package/src/components/list/ListHeader.tsx +180 -0
  78. package/src/components/list/ListItem/ListCell.tsx +48 -0
  79. package/src/components/list/ListItem/ListRow.tsx +38 -0
  80. package/src/components/list/ListItemView.tsx +53 -0
  81. package/src/components/list/ListSort.tsx +84 -0
  82. package/src/components/list/NoItems.tsx +33 -0
  83. package/src/components/list/constants.ts +1 -0
  84. package/src/components/list/index.ts +4 -0
  85. package/src/components/list/types.ts +29 -0
  86. package/src/components/list/utils.ts +62 -0
  87. package/src/components/loading/CircularProgress.tsx +11 -0
  88. package/src/components/loading/Loading.tsx +160 -0
  89. package/src/components/loading/LoadingImage.tsx +123 -0
  90. package/src/components/loading/LoadingSwitch.tsx +78 -0
  91. package/src/components/loading/index.ts +4 -0
  92. package/src/components/media/PlayButton.tsx +94 -0
  93. package/src/components/media/index.ts +1 -0
  94. package/src/components/modal/DefaultModal.tsx +18 -0
  95. package/src/components/modal/DesktopModal.tsx +11 -0
  96. package/src/components/modal/ForceMobile.tsx +7 -0
  97. package/src/components/modal/MobileModal.tsx +89 -0
  98. package/src/components/modal/index.ts +3 -0
  99. package/src/components/modal/type.ts +7 -0
  100. package/src/components/nav/NavBar.tsx +101 -0
  101. package/src/components/nav/index.ts +1 -0
  102. package/src/components/overlay/ImageViewOverlay.tsx +88 -0
  103. package/src/components/overlay/MobileOverlay.tsx +22 -0
  104. package/src/components/overlay/index.ts +2 -0
  105. package/src/components/text/GradientText/index.tsx +16 -0
  106. package/src/components/text/GradientText/styles.css +5 -0
  107. package/src/components/text/NumberTicker.tsx +28 -0
  108. package/src/components/text/index.ts +1 -0
  109. package/src/components/theme/colorMode/DarkModeToggle.tsx +40 -0
  110. package/src/components/theme/colorMode/index.ts +1 -0
  111. package/src/components/theme/index.ts +1 -0
  112. package/src/components/view/ErrorView.tsx +13 -0
  113. package/src/components/view/RedirectView.tsx +42 -0
  114. package/src/components/view/index.ts +2 -0
  115. package/src/contexts/index.ts +1 -0
  116. package/src/contexts/theme.ts +316 -0
  117. package/src/custom.d.ts +4 -0
  118. package/src/hooks/index.ts +1 -0
  119. package/src/hooks/ui/index.ts +1 -0
  120. package/src/hooks/ui/useBorderColor.ts +4 -0
  121. package/src/store/index.ts +1 -0
  122. package/src/store/usePlayer.ts +75 -0
  123. package/src/store/useScreen.ts +22 -0
@@ -0,0 +1,30 @@
1
+ import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'
2
+ import { IconButton } from '@chakra-ui/react'
3
+ import { FC, MouseEvent } from 'react'
4
+
5
+ export const ViewButton: FC<{
6
+ onClick: (e: MouseEvent) => void
7
+ isOpen: boolean
8
+ viewText?: string
9
+ hideText?: string
10
+ color?: string
11
+ }> = ({ viewText = 'View', hideText = 'Hide', onClick, isOpen, color }) => (
12
+ <IconButton
13
+ icon={
14
+ isOpen ? <ChevronUpIcon width={5} height={5} /> : <ChevronDownIcon width={5} height={5} />
15
+ }
16
+ aria-label={isOpen ? hideText : viewText}
17
+ variant="ghost"
18
+ p="2px"
19
+ ml="auto"
20
+ size="xs"
21
+ fontSize="xs"
22
+ borderRadius="full"
23
+ _hover={{ bg: 'blackAlpha.200' }}
24
+ color={color ?? '#777'}
25
+ onClick={e => {
26
+ e.stopPropagation()
27
+ onClick(e)
28
+ }}
29
+ />
30
+ )
@@ -0,0 +1,3 @@
1
+ export * from './DeleteButton'
2
+ export * from './Toggle'
3
+ export * from './ViewButton'
@@ -0,0 +1,111 @@
1
+ import { AnyObject, WithId } from '@chem-po/core'
2
+ import { MobileFrameContextData, useDocument, useMobileFrame } from '@chem-po/react'
3
+ import { motion, MotionValue, useMotionValue, useSpring, useTransform } from 'framer-motion'
4
+ import { JSX, useEffect, useMemo, useRef } from 'react'
5
+ import { springConfig } from './constants'
6
+ import { PanelStatus } from './types'
7
+
8
+ const getContentScale = (status: PanelStatus) => {
9
+ // if (status === 'current') return 1
10
+ if (status === 'current') return 1
11
+ return 0
12
+ }
13
+ const getContentX = (status: PanelStatus, contentSize: { width: number; height: number }) => {
14
+ const { width: cWidth } = contentSize
15
+ if (status === 'current') return 0
16
+ if (status === 'next') return cWidth
17
+ if (status === 'prev') return -cWidth
18
+ return 0
19
+ }
20
+
21
+ const getContentY = (status: PanelStatus, contentSize: { width: number; height: number }) => {
22
+ const { height: cHeight } = contentSize
23
+ if (status === 'current') return 0
24
+ if (status === 'next') return cHeight * 1.1
25
+ if (status === 'prev') return -cHeight * 1.1
26
+ return 0
27
+ }
28
+
29
+ interface FeedContentPaneProps<T extends AnyObject = AnyObject> {
30
+ id: string
31
+ collectionPath: string
32
+ RenderItem: (i: WithId<T>) => JSX.Element
33
+ onItemLoad: (data: WithId<T> | null) => void
34
+ status: PanelStatus
35
+ enterStatus?: PanelStatus | null
36
+ offsetY: MotionValue<number>
37
+ }
38
+
39
+ export const FeedContentPane = <T extends AnyObject = AnyObject>({
40
+ id,
41
+ collectionPath,
42
+ onItemLoad,
43
+ RenderItem,
44
+ status,
45
+ offsetY,
46
+ enterStatus,
47
+ }: FeedContentPaneProps<T>) => {
48
+ const contentSize = useMobileFrame()
49
+ const scale = useSpring(0, springConfig)
50
+ const baseY = useMotionValue(getContentY(enterStatus ?? 'next', contentSize))
51
+ const yVal = useTransform(() => baseY.get() + offsetY.get())
52
+ const y = useSpring(yVal, springConfig)
53
+
54
+ const docPath = useMemo(() => `${collectionPath}/${id}`, [collectionPath, id])
55
+ const { data: item } = useDocument<T>(docPath, onItemLoad)
56
+
57
+ const init = useRef<{
58
+ status: PanelStatus
59
+ contentSize: MobileFrameContextData
60
+ y: number
61
+ scale: number
62
+ }>({
63
+ contentSize,
64
+ status: enterStatus ?? status,
65
+ // x: getContentX(fromStatus || status, contentSize),
66
+ y: getContentY(enterStatus ?? status, contentSize),
67
+ scale: getContentScale(enterStatus ?? status),
68
+ })
69
+
70
+ useEffect(() => {
71
+ const newScale = getContentScale(status)
72
+ const updatedX = getContentX(status, contentSize)
73
+ baseY.set(updatedX)
74
+ scale.set(newScale)
75
+ }, [status, baseY, scale, contentSize])
76
+
77
+ return (
78
+ // <AnimatePresence>
79
+ <motion.div
80
+ initial={{
81
+ x: 0,
82
+ y: init.current.y,
83
+ scale: init.current.scale,
84
+ }}
85
+ style={{
86
+ position: 'absolute',
87
+ top: 0,
88
+ left: 0,
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ touchAction: 'none',
93
+ userSelect: 'none',
94
+ height: '100%',
95
+ y,
96
+ scale,
97
+ opacity: scale,
98
+ width: '100%',
99
+ }}
100
+ exit={{
101
+ opacity: 0,
102
+ scale: 0,
103
+ // x: getContentX(fromStatus || status, contentSize),
104
+ x: 0,
105
+ y: getContentY(enterStatus === 'next' ? 'prev' : 'next', contentSize),
106
+ }}>
107
+ {item ? RenderItem(item) : null}
108
+ </motion.div>
109
+ // </AnimatePresence>
110
+ )
111
+ }
@@ -0,0 +1,200 @@
1
+ import { Center } from '@chakra-ui/react'
2
+ import { AnyObject, WithId } from '@chem-po/core'
3
+ import { useMobileFrame } from '@chem-po/react'
4
+ import { useMotionValue } from 'framer-motion'
5
+ import React, {
6
+ CSSProperties,
7
+ Dispatch,
8
+ PropsWithChildren,
9
+ useCallback,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react'
14
+ import { LoadingOverlay } from '../loading'
15
+ import { REFRESH_THRESHOLD, SWIPE_THRESHOLD } from './constants'
16
+ import { MediaFeedProvider } from './context'
17
+ import { FeedContentPane } from './FeedContentPane'
18
+ import { useMediaFeed } from './hooks'
19
+ import { MediaFeedBackground } from './MediaFeedBackground'
20
+ import { MediaFeedRefresh } from './MediaFeedRefresh'
21
+ import { MediaBackgroundRef, MediaFeedProps, PanelData, UpdatePanelsArgs } from './types'
22
+
23
+ const useUpdatePanels = (setItems: Dispatch<React.SetStateAction<Array<PanelData>>>) =>
24
+ useCallback(
25
+ (data: UpdatePanelsArgs) => {
26
+ const updated: PanelData[] = []
27
+ if (data.prev) updated.push({ status: 'prev', id: data.prev })
28
+ if (data.curr) {
29
+ updated.push({
30
+ status: 'current',
31
+ id: data.curr,
32
+ })
33
+ }
34
+ if (data.next) updated.push({ status: 'next', id: data.next })
35
+ setItems(updated)
36
+ },
37
+ [setItems],
38
+ )
39
+
40
+ export const MediaFeed = <T extends AnyObject = AnyObject>({
41
+ fetch,
42
+ collection: collectionPath,
43
+ RenderItem,
44
+ authRequired,
45
+ getBackgroundUrl,
46
+ getBackgroundValue,
47
+ limit,
48
+ defaultBackground,
49
+ swipeDisabled,
50
+ children,
51
+ }: PropsWithChildren<MediaFeedProps<T>>) => {
52
+ const { width, height } = useMobileFrame()
53
+
54
+ const contentRef = useRef<HTMLDivElement>(null)
55
+
56
+ const containerRef = useRef<HTMLDivElement>(null)
57
+ // const panels = useRef<Array<FeedContentPaneRef>>([])
58
+ const [direction, setDirection] = useState<'next' | 'prev' | null>(null)
59
+ const [panels, setPanels] = useState<Array<PanelData>>([])
60
+
61
+ const offsetY = useMotionValue(0)
62
+ const onNewData = useUpdatePanels(setPanels)
63
+
64
+ const backgroundRef = useRef<MediaBackgroundRef<T>>(null)
65
+
66
+ const { goNext, goPrev, loading, canGoNext, canGoPrev, refresh, refreshing } = useMediaFeed(
67
+ fetch,
68
+ onNewData,
69
+ limit,
70
+ authRequired,
71
+ )
72
+
73
+ const pointerDown = useRef(false)
74
+ const dragStart = useRef({ x: 0, y: 0 })
75
+
76
+ const onDragStart = useCallback(
77
+ (e: React.PointerEvent) => {
78
+ if (swipeDisabled) return
79
+ pointerDown.current = true
80
+ dragStart.current = { x: e.clientX, y: e.clientY }
81
+ const dragEndListener = (ev: PointerEvent) => {
82
+ // const oX = Math.max(-width / 4, Math.min(width / 4, ev.clientX - dragStart.current.x))
83
+ const maxY = canGoPrev ? SWIPE_THRESHOLD : REFRESH_THRESHOLD
84
+ const minY = canGoNext ? -SWIPE_THRESHOLD : -REFRESH_THRESHOLD
85
+ const oY = Math.max(minY, Math.min(maxY, ev.clientY - dragStart.current.y))
86
+ if (contentRef.current) contentRef.current.style.setProperty('pointer-events', 'auto')
87
+ if (canGoNext && oY < -(SWIPE_THRESHOLD - 10)) {
88
+ setDirection('next')
89
+ goNext()
90
+ } else if (canGoPrev && oY > SWIPE_THRESHOLD - 10) {
91
+ setDirection('prev')
92
+ goPrev()
93
+ } else if (oY > REFRESH_THRESHOLD - 10 || oY < -(REFRESH_THRESHOLD - 10)) {
94
+ refresh()
95
+ if (backgroundRef.current) backgroundRef.current.onNewData(null)
96
+ }
97
+ // else if (onSwipeUp && oY < -97) {
98
+ // onSwipeUp()
99
+ // }
100
+ offsetY.set(0)
101
+ pointerDown.current = false
102
+ window.removeEventListener('pointerup', dragEndListener)
103
+ }
104
+
105
+ window.addEventListener('pointerup', dragEndListener)
106
+ },
107
+ [offsetY, goNext, goPrev, canGoNext, canGoPrev, refresh, swipeDisabled],
108
+ )
109
+
110
+ const onDragMove = useCallback(
111
+ (e: React.PointerEvent) => {
112
+ requestAnimationFrame(() => {
113
+ if (pointerDown.current) {
114
+ const oX = Math.max(-10, Math.min(10, e.clientX - dragStart.current.x))
115
+ const maxY = canGoPrev ? SWIPE_THRESHOLD : REFRESH_THRESHOLD
116
+ const minY = canGoNext ? -SWIPE_THRESHOLD : -REFRESH_THRESHOLD
117
+ const oY = Math.max(minY, Math.min(maxY, e.clientY - dragStart.current.y))
118
+
119
+ const dist = Math.sqrt(oX ** 2 + oY ** 2)
120
+ if (dist > 10 && contentRef.current) {
121
+ contentRef.current.style.setProperty('pointer-events', 'none')
122
+ }
123
+ // offsetX.jump(oX)
124
+ offsetY.set(oY)
125
+ }
126
+ })
127
+ },
128
+ [offsetY, canGoNext, canGoPrev],
129
+ )
130
+
131
+ const containerStyle = useMemo<CSSProperties>(
132
+ () => ({
133
+ height: `${height}px`,
134
+ width: `${width}px`,
135
+ overflow: 'hidden',
136
+ pointerEvents: swipeDisabled ? 'none' : 'auto',
137
+ }),
138
+ [width, height, swipeDisabled],
139
+ )
140
+ const [curr, setCurr] = useState<WithId<T> | null>(null)
141
+
142
+ const handleItemLoad = useCallback(
143
+ (data: WithId<T> | null) => {
144
+ const isCurr = panels.find(p => p.status === 'current')?.id === data?._id
145
+ if (isCurr) setCurr(data)
146
+ },
147
+ [panels],
148
+ )
149
+
150
+ return (
151
+ <MediaFeedProvider curr={curr}>
152
+ <Center
153
+ background={defaultBackground ?? 'background.100'}
154
+ style={{ touchAction: 'none' }}
155
+ userSelect="none"
156
+ position="relative"
157
+ w="100%"
158
+ h="100%"
159
+ overflow="hidden"
160
+ onPointerDown={onDragStart}
161
+ onPointerMove={onDragMove}>
162
+ <MediaFeedBackground
163
+ item={curr}
164
+ getBackgroundValue={getBackgroundValue}
165
+ getBackgroundUrl={getBackgroundUrl}
166
+ />
167
+ <div ref={containerRef} style={containerStyle}>
168
+ <Center ref={contentRef} h="100%" w="100%">
169
+ {panels.map(panel => (
170
+ <FeedContentPane<T>
171
+ key={panel.id}
172
+ id={panel.id}
173
+ collectionPath={collectionPath}
174
+ RenderItem={RenderItem}
175
+ onItemLoad={handleItemLoad}
176
+ status={panel.status}
177
+ enterStatus={direction}
178
+ offsetY={offsetY}
179
+ />
180
+ ))}
181
+ </Center>
182
+ </div>
183
+ <MediaFeedRefresh canRefresh={!canGoPrev} refreshing={refreshing} offsetY={offsetY} />
184
+ {/* {onSwipeUp ? <MediaFeedSwipeUp offsetY={offsetY} /> : null} */}
185
+ <LoadingOverlay
186
+ inFeed
187
+ zIndex={2}
188
+ bg="transparent"
189
+ pointerEvents="none"
190
+ isLoading={loading}
191
+ />
192
+ {children ? (
193
+ <Center position="absolute" bottom="0" left="0" right="0" zIndex={3} pointerEvents="none">
194
+ {children}
195
+ </Center>
196
+ ) : null}
197
+ </Center>
198
+ </MediaFeedProvider>
199
+ )
200
+ }
@@ -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>