sanity-plugin-mux-input 2.1.1 → 2.2.0

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 (66) hide show
  1. package/lib/index.cjs +4037 -4
  2. package/lib/index.cjs.map +1 -1
  3. package/lib/index.d.ts +14 -1
  4. package/lib/index.js +4026 -2
  5. package/lib/index.js.map +1 -1
  6. package/package.json +27 -28
  7. package/src/actions/assets.ts +30 -2
  8. package/src/components/ConfigureApi.tsx +9 -1
  9. package/src/components/FormField.tsx +8 -10
  10. package/src/components/IconInfo.tsx +23 -0
  11. package/src/components/Input.styled.tsx +0 -8
  12. package/src/components/Input.tsx +4 -3
  13. package/src/components/InputBrowser.tsx +1 -8
  14. package/src/components/Player.styled.tsx +5 -144
  15. package/src/components/Player.tsx +23 -109
  16. package/src/components/PlayerActionsMenu.tsx +0 -4
  17. package/src/components/SelectAsset.tsx +18 -58
  18. package/src/components/SelectSortOptions.tsx +45 -0
  19. package/src/components/SpinnerBox.tsx +17 -0
  20. package/src/components/StudioTool.tsx +20 -0
  21. package/src/components/VideoDetails/DeleteDialog.tsx +156 -0
  22. package/src/components/VideoDetails/VideoDetails.tsx +298 -0
  23. package/src/components/VideoDetails/VideoReferences.tsx +70 -0
  24. package/src/components/VideoDetails/useVideoDetails.ts +85 -0
  25. package/src/components/VideoInBrowser.tsx +183 -0
  26. package/src/components/VideoMetadata.tsx +43 -0
  27. package/src/components/VideoPlayer.tsx +69 -0
  28. package/src/components/VideoThumbnail.tsx +106 -0
  29. package/src/components/VideosBrowser.tsx +83 -0
  30. package/src/components/__legacy__Uploader.tsx +2 -9
  31. package/src/components/documentPreview/DocumentPreview.tsx +107 -0
  32. package/src/components/documentPreview/DraftStatus.tsx +34 -0
  33. package/src/components/documentPreview/MissingSchemaType.tsx +33 -0
  34. package/src/components/documentPreview/PaneItemPreview.tsx +71 -0
  35. package/src/components/documentPreview/PublishedStatus.tsx +35 -0
  36. package/src/components/documentPreview/TimeAgo.tsx +13 -0
  37. package/src/components/documentPreview/paneItemTypes.ts +7 -0
  38. package/src/components/icons/Resolution.tsx +12 -0
  39. package/src/components/icons/StopWatch.tsx +20 -0
  40. package/src/components/icons/ToolIcon.tsx +21 -0
  41. package/src/hooks/useAssets.ts +61 -0
  42. package/src/hooks/useCancelUpload.ts +2 -2
  43. package/src/hooks/useClient.ts +3 -1
  44. package/src/hooks/useDocReferences.ts +21 -0
  45. package/src/hooks/useInView.ts +45 -0
  46. package/src/index.ts +2 -0
  47. package/src/plugin.tsx +1 -1
  48. package/src/util/constants.ts +7 -0
  49. package/src/util/createSearchFilter.ts +78 -0
  50. package/src/util/formatSeconds.ts +22 -0
  51. package/src/util/getAnimatedPosterSrc.ts +1 -1
  52. package/src/util/getPlaybackId.ts +1 -1
  53. package/src/util/getPlaybackPolicy.ts +1 -1
  54. package/src/util/getVideoMetadata.ts +18 -0
  55. package/src/util/types.ts +16 -1
  56. package/lib/_chunks/Player-547f8e2a.cjs +0 -474
  57. package/lib/_chunks/Player-547f8e2a.cjs.map +0 -1
  58. package/lib/_chunks/Player-bfdb96f6.js +0 -465
  59. package/lib/_chunks/Player-bfdb96f6.js.map +0 -1
  60. package/lib/_chunks/index-39e38243.cjs +0 -3251
  61. package/lib/_chunks/index-39e38243.cjs.map +0 -1
  62. package/lib/_chunks/index-71899191.js +0 -3229
  63. package/lib/_chunks/index-71899191.js.map +0 -1
  64. package/src/components/EditThumbnailDialog.tsx +0 -74
  65. package/src/components/VideoSource.styled.tsx +0 -235
  66. package/src/components/VideoSource.tsx +0 -318
@@ -1,73 +1,33 @@
1
- import React, {useCallback, useEffect, useRef, useState} from 'react'
2
- import {PatchEvent, set, setIfMissing} from 'sanity'
1
+ import React, {useCallback} from 'react'
2
+ import {PatchEvent, set, setIfMissing, unset} from 'sanity'
3
3
 
4
- import {useClient} from '../hooks/useClient'
5
4
  import type {SetDialogState} from '../hooks/useDialogState'
6
5
  import type {MuxInputProps, VideoAssetDocument} from '../util/types'
7
- import VideoSource, {type Props as VideoSourceProps} from './VideoSource'
8
-
9
- const PER_PAGE = 200
10
-
11
- function createQuery(start = 0, end = PER_PAGE) {
12
- return /* groq */ `*[_type == "mux.videoAsset"] | order(_updatedAt desc) [${start}...${end}]`
13
- }
6
+ import VideosBrowser, {type VideosBrowserProps} from './VideosBrowser'
14
7
 
15
8
  export interface Props extends Pick<MuxInputProps, 'onChange'> {
16
9
  asset?: VideoAssetDocument | null | undefined
17
10
  setDialogState: SetDialogState
18
11
  }
19
12
 
20
- export default function SelectAssets({asset, onChange, setDialogState}: Props) {
21
- const client = useClient()
22
- const pageNoRef = useRef(0)
23
- const [isLastPage, setLastPage] = useState(false)
24
- const [isLoading, setLoading] = useState(false)
25
- const [assets, setAssets] = useState<VideoAssetDocument[]>([])
26
-
27
- const fetchPage = useCallback(
28
- (pageNo: number) => {
29
- const start = pageNo * PER_PAGE
30
- const end = start + PER_PAGE
31
- setLoading(true)
32
- return client
33
- .fetch(createQuery(start, end))
34
- .then((result: VideoAssetDocument[]) => {
35
- setLastPage(result.length < PER_PAGE)
36
- setAssets((prev) => prev.concat(result))
37
- })
38
- .finally(() => setLoading(false))
39
- },
40
- [client]
41
- )
42
- const handleSelect = useCallback<VideoSourceProps['onSelect']>(
43
- (id) => {
44
- const selected = assets.find((doc) => doc._id === id)
45
- if (!selected) {
46
- throw new TypeError(`Failed to find video asset with id: ${id}`)
13
+ export default function SelectAssets({asset: selectedAsset, onChange, setDialogState}: Props) {
14
+ const handleSelect = useCallback<Required<VideosBrowserProps>['onSelect']>(
15
+ (chosenAsset) => {
16
+ if (!chosenAsset?._id) {
17
+ onChange(PatchEvent.from([unset(['asset'])]))
18
+ }
19
+ if (chosenAsset._id !== selectedAsset?._id) {
20
+ onChange(
21
+ PatchEvent.from([
22
+ setIfMissing({asset: {}}),
23
+ set({_type: 'reference', _weak: true, _ref: chosenAsset._id}, ['asset']),
24
+ ])
25
+ )
47
26
  }
48
- onChange(
49
- PatchEvent.from([
50
- setIfMissing({asset: {}}),
51
- set({_type: 'reference', _weak: true, _ref: selected._id}, ['asset']),
52
- ])
53
- )
54
27
  setDialogState(false)
55
28
  },
56
- [assets, onChange, setDialogState]
29
+ [onChange, setDialogState, selectedAsset]
57
30
  )
58
- const handleLoadMore = useCallback<VideoSourceProps['onLoadMore']>(() => {
59
- fetchPage(++pageNoRef.current)
60
- }, [fetchPage])
61
31
 
62
- useEffect(() => void fetchPage(pageNoRef.current), [fetchPage])
63
-
64
- return (
65
- <VideoSource
66
- onSelect={handleSelect}
67
- assets={assets}
68
- isLastPage={isLastPage}
69
- isLoading={isLoading}
70
- onLoadMore={handleLoadMore}
71
- />
72
- )
32
+ return <VideosBrowser onSelect={handleSelect} />
73
33
  }
@@ -0,0 +1,45 @@
1
+ import {SortIcon} from '@sanity/icons'
2
+ import {Button, Menu, MenuButton, MenuItem, PopoverProps} from '@sanity/ui'
3
+ import React, {useId} from 'react'
4
+
5
+ import {ASSET_SORT_OPTIONS, SortOption} from '../hooks/useAssets'
6
+
7
+ export const CONTEXT_MENU_POPOVER_PROPS: PopoverProps = {
8
+ constrainSize: true,
9
+ placement: 'bottom',
10
+ portal: true,
11
+ width: 0,
12
+ }
13
+
14
+ /**
15
+ * @sanity/ui components adapted from:
16
+ * https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/desk/components/pane/PaneContextMenuButton.tsx#L19
17
+ */
18
+ export function SelectSortOptions(props: {sort: SortOption; setSort: (s: SortOption) => void}) {
19
+ const id = useId()
20
+
21
+ return (
22
+ <MenuButton
23
+ button={
24
+ <Button text="Sort" icon={SortIcon} mode="bleed" padding={3} style={{cursor: 'pointer'}} />
25
+ }
26
+ id={id}
27
+ menu={
28
+ <Menu>
29
+ {Object.entries(ASSET_SORT_OPTIONS).map(([type, {label}]) => (
30
+ <MenuItem
31
+ key={type}
32
+ data-as="button"
33
+ onClick={() => props.setSort(type as SortOption)}
34
+ padding={3}
35
+ tone="default"
36
+ text={label}
37
+ pressed={type === props.sort}
38
+ />
39
+ ))}
40
+ </Menu>
41
+ }
42
+ popover={CONTEXT_MENU_POPOVER_PROPS}
43
+ />
44
+ )
45
+ }
@@ -0,0 +1,17 @@
1
+ import {Box, Spinner} from '@sanity/ui'
2
+ import React from 'react'
3
+
4
+ const SpinnerBox: React.FC = () => (
5
+ <Box
6
+ style={{
7
+ display: 'flex',
8
+ alignItems: 'center',
9
+ justifyContent: 'center',
10
+ minHeight: '150px',
11
+ }}
12
+ >
13
+ <Spinner />
14
+ </Box>
15
+ )
16
+
17
+ export default SpinnerBox
@@ -0,0 +1,20 @@
1
+ import React from 'react'
2
+ import {Tool} from 'sanity'
3
+
4
+ import {Config} from '../util/types'
5
+ import ToolIcon from './icons/ToolIcon'
6
+ import VideosBrowser from './VideosBrowser'
7
+
8
+ const StudioTool: React.FC<Config> = () => {
9
+ return <VideosBrowser />
10
+ }
11
+
12
+ export default function createStudioTool(config: Config): Tool {
13
+ const toolConfig = typeof config.tool === 'object' ? config.tool : {}
14
+ return {
15
+ name: 'mux',
16
+ title: toolConfig.title || 'Videos',
17
+ component: (props: any) => <StudioTool {...config} {...props} />,
18
+ icon: toolConfig.icon || ToolIcon,
19
+ }
20
+ }
@@ -0,0 +1,156 @@
1
+ import {TrashIcon} from '@sanity/icons'
2
+ import {Button, Card, Checkbox, Dialog, Flex, Heading, Stack, Text, useToast} from '@sanity/ui'
3
+ import React, {useEffect, useState} from 'react'
4
+ import {SanityDocument} from 'sanity'
5
+
6
+ import {deleteAsset} from '../../actions/assets'
7
+ import {useClient} from '../../hooks/useClient'
8
+ import {DIALOGS_Z_INDEX} from '../../util/constants'
9
+ import {PluginPlacement, VideoAssetDocument} from '../../util/types'
10
+ import SpinnerBox from '../SpinnerBox'
11
+ import FileReferences from './VideoReferences'
12
+
13
+ export default function DeleteDialog({
14
+ asset,
15
+ references,
16
+ referencesLoading,
17
+ cancelDelete,
18
+ placement,
19
+ succeededDeleting,
20
+ }: {
21
+ asset: VideoAssetDocument
22
+ placement: PluginPlacement
23
+ references?: SanityDocument[]
24
+ referencesLoading: boolean
25
+ cancelDelete: () => void
26
+ succeededDeleting: () => void
27
+ }) {
28
+ const client = useClient()
29
+ const [state, setState] = useState<
30
+ 'processing_deletion' | 'checkingReferences' | 'error_deleting' | 'cantDelete' | 'confirm'
31
+ >('checkingReferences')
32
+ const [deleteOnMux, setDeleteOnMux] = useState(true)
33
+ const toast = useToast()
34
+
35
+ useEffect(() => {
36
+ if (state !== 'checkingReferences' || referencesLoading) return
37
+
38
+ setState(references?.length ? 'cantDelete' : 'confirm')
39
+ }, [state, references, referencesLoading])
40
+
41
+ async function confirmDelete() {
42
+ if (state !== 'confirm') return
43
+
44
+ setState('processing_deletion')
45
+ const worked = await deleteAsset({client, asset, deleteOnMux})
46
+ if (worked === true) {
47
+ toast.push({title: 'Successfully deleted video', status: 'success'})
48
+ succeededDeleting()
49
+ } else if (worked === 'failed-mux') {
50
+ toast.push({
51
+ title: 'Deleted video in Sanity',
52
+ description: "But it wasn't deleted in Mux",
53
+ status: 'warning',
54
+ })
55
+ succeededDeleting()
56
+ } else {
57
+ toast.push({title: 'Failed deleting video', status: 'error'})
58
+
59
+ setState('error_deleting')
60
+ }
61
+ }
62
+
63
+ return (
64
+ <Dialog
65
+ header={'Delete file'}
66
+ zOffset={DIALOGS_Z_INDEX}
67
+ id="deleting-file-details-dialog"
68
+ onClose={cancelDelete}
69
+ onClickOutside={cancelDelete}
70
+ width={1}
71
+ position="fixed"
72
+ footer={
73
+ <Card padding={3}>
74
+ <Flex justify="space-between" align="center">
75
+ <Button
76
+ icon={TrashIcon}
77
+ fontSize={2}
78
+ padding={3}
79
+ text="Delete file"
80
+ tone="critical"
81
+ onClick={confirmDelete}
82
+ disabled={['processing_deletion', 'checkingReferences', 'cantDelete'].some(
83
+ (s) => s === state
84
+ )}
85
+ />
86
+ </Flex>
87
+ </Card>
88
+ }
89
+ >
90
+ <Card
91
+ padding={5}
92
+ style={{
93
+ minHeight: '150px',
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ justifyContent: 'center',
97
+ }}
98
+ >
99
+ <Stack space={3}>
100
+ {state === 'checkingReferences' && (
101
+ <>
102
+ <Heading size={2}>Checking if file can be deleted</Heading>
103
+ <SpinnerBox />
104
+ </>
105
+ )}
106
+ {state === 'cantDelete' && (
107
+ <>
108
+ <Heading size={2}>Video can't be deleted</Heading>
109
+ <Text size={2} style={{marginBottom: '2rem'}}>
110
+ There are {references?.length} document{references && references.length > 0 && 's'}{' '}
111
+ pointing to this file. Remove their references to this file or delete them before
112
+ proceeding.
113
+ </Text>
114
+ <FileReferences
115
+ references={references}
116
+ isLoaded={!referencesLoading}
117
+ placement={placement}
118
+ />
119
+ </>
120
+ )}
121
+ {state === 'confirm' && (
122
+ <>
123
+ <Heading size={2}>Are you sure you want to delete this file?</Heading>
124
+ <Text size={2}>This action is irreversible</Text>
125
+ <Stack space={4} marginTop={4}>
126
+ <Flex align="center" as="label">
127
+ <Checkbox
128
+ checked={deleteOnMux}
129
+ onChange={() => setDeleteOnMux((prev) => !prev)}
130
+ />
131
+ <Text style={{margin: '0 10px'}}>Delete asset on Mux</Text>
132
+ </Flex>
133
+ <Flex align="center" as="label">
134
+ <Checkbox disabled checked />
135
+ <Text style={{margin: '0 10px'}}>Delete video from dataset</Text>
136
+ </Flex>
137
+ </Stack>
138
+ </>
139
+ )}
140
+ {state === 'processing_deletion' && (
141
+ <>
142
+ <Heading size={2}>Deleting file...</Heading>
143
+ <SpinnerBox />
144
+ </>
145
+ )}
146
+ {state === 'error_deleting' && (
147
+ <>
148
+ <Heading size={2}>Something went wrong!</Heading>
149
+ <Text size={2}>Try deleting the file again by clicking the button below</Text>
150
+ </>
151
+ )}
152
+ </Stack>
153
+ </Card>
154
+ </Dialog>
155
+ )
156
+ }
@@ -0,0 +1,298 @@
1
+ import {
2
+ CalendarIcon,
3
+ CheckmarkIcon,
4
+ ClockIcon,
5
+ CropIcon,
6
+ EditIcon,
7
+ ErrorOutlineIcon,
8
+ RevertIcon,
9
+ SearchIcon,
10
+ TrashIcon,
11
+ } from '@sanity/icons'
12
+ import {
13
+ Button,
14
+ Card,
15
+ Dialog,
16
+ Flex,
17
+ Heading,
18
+ Spinner,
19
+ Stack,
20
+ Tab,
21
+ TabList,
22
+ TabPanel,
23
+ Text,
24
+ TextInput,
25
+ } from '@sanity/ui'
26
+ import React, {useEffect, useState} from 'react'
27
+
28
+ import {DIALOGS_Z_INDEX} from '../../util/constants'
29
+ import FormField from '../FormField'
30
+ import IconInfo from '../IconInfo'
31
+ import {ResolutionIcon} from '../icons/Resolution'
32
+ import {StopWatchIcon} from '../icons/StopWatch'
33
+ import VideoPlayer from '../VideoPlayer'
34
+ import DeleteDialog from './DeleteDialog'
35
+ import useFileDetails, {FileDetailsProps} from './useVideoDetails'
36
+ import FileReferences from './VideoReferences'
37
+
38
+ const AssetInput: React.FC<{
39
+ label: string
40
+ description?: string
41
+ placeholder?: string
42
+ value: string
43
+ onInput: (e: React.FormEvent<HTMLInputElement>) => void
44
+ disabled?: boolean
45
+ }> = (props) => (
46
+ <FormField title={props.label} description={props.description} inputId={props.label}>
47
+ <TextInput
48
+ id={props.label}
49
+ value={props.value}
50
+ placeholder={props.placeholder}
51
+ onInput={props.onInput}
52
+ disabled={props.disabled}
53
+ />
54
+ </FormField>
55
+ )
56
+
57
+ const VideoDetails: React.FC<FileDetailsProps> = (props) => {
58
+ const [tab, setTab] = useState<'details' | 'references'>('details')
59
+ const {
60
+ displayInfo,
61
+ filename,
62
+ modified,
63
+ references,
64
+ referencesLoading,
65
+ setFilename,
66
+ state,
67
+ setState,
68
+ handleClose,
69
+ confirmClose,
70
+ saveChanges,
71
+ } = useFileDetails(props)
72
+
73
+ const isSaving = state === 'saving'
74
+
75
+ // Avoid layout shifts in large screens' 2-column dialog by setting their `minHeight` to the container's
76
+ const [containerHeight, setContainerHeight] = useState<number | null>(null)
77
+ const contentsRef = React.useRef<HTMLDivElement>(null)
78
+ useEffect(() => {
79
+ if (!contentsRef.current || !('getBoundingClientRect' in contentsRef.current)) return
80
+
81
+ setContainerHeight(contentsRef.current.getBoundingClientRect().height)
82
+ }, [])
83
+
84
+ return (
85
+ <Dialog
86
+ header={displayInfo.title}
87
+ zOffset={DIALOGS_Z_INDEX}
88
+ id="file-details-dialog"
89
+ onClose={handleClose}
90
+ onClickOutside={handleClose}
91
+ width={2}
92
+ style={{minHeight: '50vh'}}
93
+ position="fixed"
94
+ footer={
95
+ <Card padding={3}>
96
+ <Flex justify="space-between" align="center">
97
+ <Button
98
+ icon={TrashIcon}
99
+ fontSize={2}
100
+ padding={3}
101
+ mode="bleed"
102
+ text="Delete"
103
+ tone="critical"
104
+ onClick={() => setState('deleting')}
105
+ disabled={isSaving}
106
+ />
107
+ {modified && (
108
+ <Button
109
+ icon={CheckmarkIcon}
110
+ fontSize={2}
111
+ padding={3}
112
+ mode="ghost"
113
+ text="Save and close"
114
+ tone="positive"
115
+ onClick={saveChanges}
116
+ iconRight={isSaving && Spinner}
117
+ disabled={isSaving}
118
+ />
119
+ )}
120
+ </Flex>
121
+ </Card>
122
+ }
123
+ >
124
+ {/* DELETION DIALOG */}
125
+ {state === 'deleting' && (
126
+ <DeleteDialog
127
+ asset={props.asset}
128
+ cancelDelete={() => setState('idle')}
129
+ placement={props.placement}
130
+ referencesLoading={referencesLoading}
131
+ references={references}
132
+ succeededDeleting={() => {
133
+ props.closeDialog()
134
+ }}
135
+ />
136
+ )}
137
+
138
+ {/* CONFIRM CLOSING DIALOG */}
139
+ {state === 'closing' && (
140
+ <Dialog
141
+ header={'You have unsaved changes'}
142
+ zOffset={DIALOGS_Z_INDEX}
143
+ id="closing-file-details-dialog"
144
+ onClose={() => confirmClose(false)}
145
+ onClickOutside={() => confirmClose(false)}
146
+ width={1}
147
+ position="fixed"
148
+ footer={
149
+ <Card padding={3}>
150
+ <Flex justify="space-between" align="center">
151
+ <Button
152
+ icon={ErrorOutlineIcon}
153
+ fontSize={2}
154
+ padding={3}
155
+ text="Discard changes"
156
+ tone="critical"
157
+ onClick={() => confirmClose(true)}
158
+ />
159
+ {modified && (
160
+ <Button
161
+ icon={RevertIcon}
162
+ fontSize={2}
163
+ padding={3}
164
+ mode="ghost"
165
+ text="Keep editing"
166
+ tone="primary"
167
+ onClick={() => confirmClose(false)}
168
+ />
169
+ )}
170
+ </Flex>
171
+ </Card>
172
+ }
173
+ >
174
+ <Card padding={5}>
175
+ <Stack style={{textAlign: 'center'}} space={3}>
176
+ <Heading size={2}>Unsaved changes will be lost</Heading>
177
+ <Text size={2}>Are you sure you want to discard them?</Text>
178
+ </Stack>
179
+ </Card>
180
+ </Dialog>
181
+ )}
182
+ <Card
183
+ padding={4}
184
+ sizing="border"
185
+ style={{
186
+ containerType: 'inline-size',
187
+ }}
188
+ >
189
+ <Flex
190
+ sizing="border"
191
+ gap={4}
192
+ direction={['column', 'column', 'row']}
193
+ align="flex-start"
194
+ ref={contentsRef}
195
+ style={
196
+ typeof containerHeight === 'number'
197
+ ? {
198
+ minHeight: containerHeight,
199
+ }
200
+ : undefined
201
+ }
202
+ >
203
+ <Stack space={4} flex={1} sizing="border">
204
+ <VideoPlayer asset={props.asset} autoPlay={props.asset.autoPlay || false} />
205
+ </Stack>
206
+ <Stack space={4} flex={1} sizing="border">
207
+ <TabList space={2}>
208
+ <Tab
209
+ aria-controls="details-panel"
210
+ icon={EditIcon}
211
+ id="details-tab"
212
+ label="Details"
213
+ onClick={() => setTab('details')}
214
+ selected={tab === 'details'}
215
+ />
216
+ {references && references.length > 0 && (
217
+ <Tab
218
+ aria-controls="references-panel"
219
+ icon={SearchIcon}
220
+ id="references-tab"
221
+ label={`Used by (${references.length})`}
222
+ onClick={() => setTab('references')}
223
+ selected={tab === 'references'}
224
+ />
225
+ )}
226
+ </TabList>
227
+ <TabPanel aria-labelledby="details-tab" id="details-panel" hidden={tab !== 'details'}>
228
+ <Stack space={4}>
229
+ <AssetInput
230
+ label="File name"
231
+ description="Not visible to users. Useful for finding files later."
232
+ value={filename || ''}
233
+ onInput={(e) => setFilename(e.currentTarget.value)}
234
+ disabled={state !== 'idle'}
235
+ />
236
+ <Stack space={3}>
237
+ {displayInfo?.duration && (
238
+ <IconInfo
239
+ text={`Duration: ${displayInfo.duration}`}
240
+ icon={ClockIcon}
241
+ size={2}
242
+ />
243
+ )}
244
+ {displayInfo?.max_stored_resolution && (
245
+ <IconInfo
246
+ text={`Max Resolution: ${displayInfo.max_stored_resolution}`}
247
+ icon={ResolutionIcon}
248
+ size={2}
249
+ />
250
+ )}
251
+ {displayInfo?.max_stored_frame_rate && (
252
+ <IconInfo
253
+ text={`Frame rate: ${displayInfo.max_stored_frame_rate}`}
254
+ icon={StopWatchIcon}
255
+ size={2}
256
+ />
257
+ )}
258
+ {displayInfo?.aspect_ratio && (
259
+ <IconInfo
260
+ text={`Aspect Ratio: ${displayInfo.aspect_ratio}`}
261
+ icon={CropIcon}
262
+ size={2}
263
+ />
264
+ )}
265
+ <IconInfo
266
+ text={`Uploaded on: ${displayInfo.createdAt.toLocaleDateString('en', {
267
+ year: 'numeric',
268
+ month: '2-digit',
269
+ day: '2-digit',
270
+ hour: '2-digit',
271
+ minute: '2-digit',
272
+ hour12: true,
273
+ })}`}
274
+ icon={CalendarIcon}
275
+ size={2}
276
+ />
277
+ </Stack>
278
+ </Stack>
279
+ </TabPanel>
280
+ <TabPanel
281
+ aria-labelledby="references-tab"
282
+ id="references-panel"
283
+ hidden={tab !== 'references'}
284
+ >
285
+ <FileReferences
286
+ references={references}
287
+ isLoaded={!referencesLoading}
288
+ placement={props.placement}
289
+ />
290
+ </TabPanel>
291
+ </Stack>
292
+ </Flex>
293
+ </Card>
294
+ </Dialog>
295
+ )
296
+ }
297
+
298
+ export default VideoDetails