@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,178 @@
1
+ import { DeleteIcon } from '@chakra-ui/icons'
2
+ import {
3
+ AlertDialog,
4
+ AlertDialogBody,
5
+ AlertDialogContent,
6
+ AlertDialogOverlay,
7
+ Button,
8
+ Flex,
9
+ HStack,
10
+ IconButton,
11
+ IconButtonProps,
12
+ Text,
13
+ useToast,
14
+ VStack,
15
+ } from '@chakra-ui/react'
16
+ import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
17
+
18
+ const defaultAlertBodyText = "Are you sure? You can't undo this action afterwards."
19
+ export const DeleteConfirmAlert = ({
20
+ confirmActive,
21
+ onCancel,
22
+ onConfirm,
23
+ body = defaultAlertBodyText,
24
+ actionName = 'Delete',
25
+ actionLoading,
26
+ itemName,
27
+ }: {
28
+ confirmActive: boolean
29
+ onCancel: () => void
30
+ onConfirm: () => void
31
+ body?: string | ReactNode
32
+ actionLoading?: boolean
33
+ actionName?: string
34
+ itemName: string
35
+ }) => {
36
+ const cancelRef = useRef<HTMLButtonElement>(null)
37
+ return (
38
+ <AlertDialog
39
+ isCentered
40
+ isOpen={confirmActive}
41
+ leastDestructiveRef={cancelRef}
42
+ onClose={() => onCancel()}>
43
+ <AlertDialogOverlay>
44
+ <AlertDialogContent gap={0}>
45
+ <AlertDialogBody>
46
+ <VStack align="flex-start" py={1}>
47
+ <Text fontSize="lg" fontWeight={500}>
48
+ {actionName} {itemName}?
49
+ </Text>
50
+ {typeof body === 'string' ? <Text fontSize="md">{body}</Text> : body}
51
+ <HStack justify="flex-end" w="100%">
52
+ <Button size="sm" ref={cancelRef} onClick={onCancel}>
53
+ Cancel
54
+ </Button>
55
+ <Button
56
+ size="sm"
57
+ isLoading={actionLoading}
58
+ color="white"
59
+ textShadow="1px 1px 3px #00000077"
60
+ bg="red.500"
61
+ _dark={{
62
+ bg: 'red.500',
63
+ }}
64
+ onClick={onConfirm}>
65
+ {actionName}
66
+ </Button>
67
+ </HStack>
68
+ </VStack>
69
+ </AlertDialogBody>
70
+ </AlertDialogContent>
71
+ </AlertDialogOverlay>
72
+ </AlertDialog>
73
+ )
74
+ }
75
+
76
+ export const DeleteButton: React.FC<
77
+ Partial<IconButtonProps> & {
78
+ onDelete: (() => Promise<any>) | (() => void)
79
+ itemName: string
80
+ actionName?: string
81
+ noConfirm?: boolean
82
+ text?: string
83
+ alertBody?: string | ReactNode
84
+ }
85
+ > = ({ onDelete, itemName, noConfirm, alertBody, text, actionName = 'Delete', ...props }) => {
86
+ const [deleteLoading, setDeleteLoading] = useState(false)
87
+ const [confirmActive, setConfirmActive] = useState(false)
88
+ const confirmTimer = useRef<any>()
89
+ const isMounted = useRef(true)
90
+ useEffect(() => {
91
+ isMounted.current = true
92
+
93
+ return () => {
94
+ isMounted.current = false
95
+ }
96
+ }, [])
97
+
98
+ const toast = useToast()
99
+ const handleDelete = useCallback(async () => {
100
+ if (confirmTimer.current) clearTimeout(confirmTimer.current)
101
+ setConfirmActive(false)
102
+ if (onDelete) {
103
+ setDeleteLoading(true)
104
+ try {
105
+ await onDelete()
106
+ } catch (err: any) {
107
+ console.error(err)
108
+ toast({
109
+ title: 'Error',
110
+ description: err.message ?? 'An error occurred',
111
+ status: 'error',
112
+ duration: 5000,
113
+ isClosable: true,
114
+ })
115
+ }
116
+ if (isMounted.current) {
117
+ setDeleteLoading(false)
118
+ }
119
+ } else {
120
+ console.error('No delete function')
121
+ }
122
+ }, [onDelete, toast])
123
+
124
+ return (
125
+ <>
126
+ {text ? (
127
+ <Button
128
+ variant="ghost"
129
+ size={props.size ?? 'md'}
130
+ aria-label="delete"
131
+ _hover={{ color: 'red.600', bg: 'red.200' }}
132
+ color="red.600"
133
+ onClick={e => {
134
+ e.stopPropagation()
135
+ if (noConfirm) handleDelete()
136
+ else setConfirmActive(true)
137
+ }}
138
+ isLoading={deleteLoading}
139
+ {...props}>
140
+ <Flex align="center" gap={1}>
141
+ <DeleteIcon />
142
+ <Text>{text}</Text>
143
+ </Flex>
144
+ </Button>
145
+ ) : (
146
+ <IconButton
147
+ variant="ghost"
148
+ size={props.size ?? 'sm'}
149
+ aria-label={actionName}
150
+ _hover={{ color: 'red.600', bg: 'red.200' }}
151
+ color="red.500"
152
+ borderRadius="full"
153
+ icon={<DeleteIcon />}
154
+ onClick={e => {
155
+ e.stopPropagation()
156
+ if (noConfirm) handleDelete()
157
+ else setConfirmActive(true)
158
+ }}
159
+ isLoading={deleteLoading}
160
+ {...props}
161
+ />
162
+ )}
163
+ {noConfirm ? null : (
164
+ <DeleteConfirmAlert
165
+ confirmActive={confirmActive}
166
+ onCancel={() => setConfirmActive(false)}
167
+ actionLoading={deleteLoading}
168
+ onConfirm={() => {
169
+ handleDelete()
170
+ }}
171
+ actionName={actionName}
172
+ body={alertBody}
173
+ itemName={itemName}
174
+ />
175
+ )}
176
+ </>
177
+ )
178
+ }
@@ -0,0 +1,88 @@
1
+ import { Box, Flex, IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react'
2
+ import { JSX } from 'react'
3
+ import { useBorderColor } from '../../hooks'
4
+
5
+ export interface ToggleOption<Option extends string = string> {
6
+ id: Option
7
+ label: string
8
+ disabledMessage?: string
9
+ Render: (selected: boolean) => JSX.Element
10
+ }
11
+ export interface ToggleProps<Option extends string> {
12
+ options: ToggleOption<Option>[]
13
+ value: Option
14
+ size?: number
15
+ onChange: (value: Option) => Promise<void> | void
16
+ }
17
+
18
+ const ToggleButton = <Option extends string>({
19
+ selected,
20
+ option: { Render, label, disabledMessage },
21
+ size,
22
+ ...props
23
+ }: Omit<IconButtonProps, 'children' | 'aria-label' | 'size'> & {
24
+ option: ToggleOption<Option>
25
+ size: number
26
+ selected: boolean
27
+ }) => {
28
+ const body = (
29
+ <Box>
30
+ <IconButton
31
+ variant="unstyled"
32
+ w={`${size}px`}
33
+ h={`${size}px`}
34
+ minW={0}
35
+ borderRadius={0}
36
+ outline="none"
37
+ display="flex"
38
+ alignItems="center"
39
+ justifyContent="center"
40
+ opacity={!disabledMessage ? 1 : 0.6}
41
+ filter={!disabledMessage ? 'none' : 'grayscale(1)'}
42
+ bg={selected ? 'whiteAlpha.500' : 'blackAlpha.100'}
43
+ _dark={{ bg: selected ? 'blackAlpha.500' : 'whiteAlpha.200' }}
44
+ icon={Render(selected)}
45
+ pointerEvents={selected ? 'none' : 'auto'}
46
+ aria-label={label}
47
+ {...props}
48
+ />
49
+ </Box>
50
+ )
51
+
52
+ return (
53
+ <Tooltip label={disabledMessage ?? label} aria-label={label}>
54
+ {body}
55
+ </Tooltip>
56
+ )
57
+ }
58
+
59
+ export const Toggle = <Option extends string>({
60
+ value,
61
+ onChange,
62
+ size = 28,
63
+ options,
64
+ }: ToggleProps<Option>) => {
65
+ const borderColor = useBorderColor()
66
+ return (
67
+ <Flex
68
+ bg="whiteAlpha.300"
69
+ _dark={{ bg: 'blackAlpha.300' }}
70
+ border={`1px solid ${borderColor}`}
71
+ borderRadius={3}>
72
+ {options.map((option, i) => (
73
+ <ToggleButton<Option>
74
+ key={option.id}
75
+ onClick={() => {
76
+ onChange(option.id)
77
+ }}
78
+ option={option}
79
+ size={size}
80
+ borderLeftRadius={i ? 0 : 3}
81
+ borderRightRadius={i === options.length - 1 ? 3 : 0}
82
+ selected={option.id === value}
83
+ borderLeft={i ? `1px solid ${borderColor}` : 'none'}
84
+ />
85
+ ))}
86
+ </Flex>
87
+ )
88
+ }
@@ -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
+ }