@cliff-studio/sanity-plugin-bunny-input 1.0.9 → 1.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.
- package/dist/_chunks-es/index.js +441 -50
- package/dist/_chunks-es/index.js.map +1 -1
- package/package.json +2 -2
- package/src/api.js +42 -0
- package/src/components/BunnyBrowserModal.jsx +254 -0
- package/src/components/BunnyInput.jsx +122 -48
- package/src/components/index.js +1 -0
- package/src/schema.js +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cliff-studio/sanity-plugin-bunny-input",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Sanity Studio plugin for uploading videos to Bunny Stream",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Cliff Studio",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"prepublishOnly": "npm run build"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"sanity": "^3.0.0 || ^5.0.0",
|
|
31
|
+
"sanity": "^3.0.0 || ^5.0.0 || ^6.0.0",
|
|
32
32
|
"react": "^18.0.0 || ^19.0.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
package/src/api.js
CHANGED
|
@@ -250,6 +250,48 @@ export async function getVideo(config, videoId) {
|
|
|
250
250
|
return response.json()
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* List videos in a collection (or entire library if no collectionId)
|
|
255
|
+
* @param {Object} config
|
|
256
|
+
* @param {Object} options
|
|
257
|
+
* @param {string} [options.collectionId]
|
|
258
|
+
* @param {string} [options.search]
|
|
259
|
+
* @param {number} [options.page]
|
|
260
|
+
* @param {number} [options.itemsPerPage]
|
|
261
|
+
* @returns {Promise<{items: Array, totalItems: number, currentPage: number, itemsPerPage: number}>}
|
|
262
|
+
*/
|
|
263
|
+
export async function listVideos(config, {collectionId, search = '', page = 1, itemsPerPage = 50} = {}) {
|
|
264
|
+
const params = new URLSearchParams({
|
|
265
|
+
page: String(page),
|
|
266
|
+
itemsPerPage: String(itemsPerPage),
|
|
267
|
+
orderBy: 'date',
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
if (collectionId) params.set('collection', collectionId)
|
|
271
|
+
if (search) params.set('search', search)
|
|
272
|
+
|
|
273
|
+
const url = config.proxyEndpoint
|
|
274
|
+
? `${config.proxyEndpoint}/videos?${params}`
|
|
275
|
+
: `${BUNNY_API_BASE}/library/${config.libraryId}/videos?${params}`
|
|
276
|
+
|
|
277
|
+
const response = await fetch(url, {
|
|
278
|
+
headers: createHeaders(config, false, !config.proxyEndpoint || Boolean(config.apiKey)),
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
const error = await response.text()
|
|
283
|
+
throw new Error(`Failed to list videos: ${error}`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const payload = await response.json()
|
|
287
|
+
return {
|
|
288
|
+
items: payload.items || [],
|
|
289
|
+
totalItems: payload.totalItems ?? 0,
|
|
290
|
+
currentPage: payload.currentPage ?? page,
|
|
291
|
+
itemsPerPage: payload.itemsPerPage ?? itemsPerPage,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
253
295
|
/**
|
|
254
296
|
* Delete a video from Bunny Stream
|
|
255
297
|
* @param {Object} config
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
2
|
+
import {Box, Button, Card, Dialog, Flex, Grid, Spinner, Stack, Text, TextInput} from '@sanity/ui'
|
|
3
|
+
import {SearchIcon} from '@sanity/icons'
|
|
4
|
+
import {listVideos, getThumbnailUrl, getPlaybackUrl, getMp4Url, mapBunnyStatus, getOrCreateCollection} from '../api'
|
|
5
|
+
|
|
6
|
+
const ITEMS_PER_PAGE = 48
|
|
7
|
+
|
|
8
|
+
export function BunnyBrowserModal({config, collectionId: collectionIdProp, onSelect, onClose}) {
|
|
9
|
+
const [videos, setVideos] = useState([])
|
|
10
|
+
const [search, setSearch] = useState('')
|
|
11
|
+
const [page, setPage] = useState(1)
|
|
12
|
+
const [totalItems, setTotalItems] = useState(0)
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
14
|
+
const [error, setError] = useState(null)
|
|
15
|
+
// Resolved collection ID (may come from prop or be looked up from collectionName)
|
|
16
|
+
const [resolvedCollectionId, setResolvedCollectionId] = useState(collectionIdProp || null)
|
|
17
|
+
const searchTimeoutRef = useRef(null)
|
|
18
|
+
|
|
19
|
+
const fetchVideos = useCallback(
|
|
20
|
+
async (searchTerm, pageNum, collId) => {
|
|
21
|
+
setIsLoading(true)
|
|
22
|
+
setError(null)
|
|
23
|
+
try {
|
|
24
|
+
const result = await listVideos(config, {
|
|
25
|
+
collectionId: collId,
|
|
26
|
+
search: searchTerm,
|
|
27
|
+
page: pageNum,
|
|
28
|
+
itemsPerPage: ITEMS_PER_PAGE,
|
|
29
|
+
})
|
|
30
|
+
setVideos(result.items)
|
|
31
|
+
setTotalItems(result.totalItems)
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(err instanceof Error ? err.message : 'Failed to load videos')
|
|
34
|
+
} finally {
|
|
35
|
+
setIsLoading(false)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
[config]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Resolve collectionId from collectionName if needed, then fetch
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
async function init() {
|
|
44
|
+
let collId = collectionIdProp || null
|
|
45
|
+
|
|
46
|
+
if (!collId && config.collectionName) {
|
|
47
|
+
try {
|
|
48
|
+
const collection = await getOrCreateCollection(config, config.collectionName)
|
|
49
|
+
collId = collection.guid
|
|
50
|
+
setResolvedCollectionId(collId)
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// Fall back to browsing entire library if collection lookup fails
|
|
53
|
+
console.warn('Could not resolve collection, browsing full library:', err)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fetchVideos('', 1, collId)
|
|
58
|
+
}
|
|
59
|
+
init()
|
|
60
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
61
|
+
|
|
62
|
+
const handleSearchChange = useCallback(
|
|
63
|
+
(e) => {
|
|
64
|
+
const value = e.target.value
|
|
65
|
+
setSearch(value)
|
|
66
|
+
setPage(1)
|
|
67
|
+
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current)
|
|
68
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
69
|
+
fetchVideos(value, 1, resolvedCollectionId)
|
|
70
|
+
}, 400)
|
|
71
|
+
},
|
|
72
|
+
[fetchVideos, resolvedCollectionId]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const handlePageChange = useCallback(
|
|
76
|
+
(newPage) => {
|
|
77
|
+
setPage(newPage)
|
|
78
|
+
fetchVideos(search, newPage, resolvedCollectionId)
|
|
79
|
+
},
|
|
80
|
+
[fetchVideos, search, resolvedCollectionId]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const handleSelect = useCallback(
|
|
84
|
+
(video) => {
|
|
85
|
+
const videoStatus = mapBunnyStatus(video.status)
|
|
86
|
+
if (videoStatus !== 'ready') return
|
|
87
|
+
|
|
88
|
+
const width = Number(video.width)
|
|
89
|
+
const height = Number(video.height)
|
|
90
|
+
const hasDims = Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0
|
|
91
|
+
const ratio = hasDims ? Number((width / height).toFixed(6)) : null
|
|
92
|
+
const orientation = hasDims
|
|
93
|
+
? width > height
|
|
94
|
+
? 'landscape'
|
|
95
|
+
: width < height
|
|
96
|
+
? 'portrait'
|
|
97
|
+
: 'square'
|
|
98
|
+
: null
|
|
99
|
+
|
|
100
|
+
onSelect({
|
|
101
|
+
_type: 'bunnyVideo',
|
|
102
|
+
videoId: video.guid,
|
|
103
|
+
libraryId: String(video.videoLibraryId),
|
|
104
|
+
title: video.title,
|
|
105
|
+
status: 'ready',
|
|
106
|
+
duration: video.length,
|
|
107
|
+
width: hasDims ? width : null,
|
|
108
|
+
height: hasDims ? height : null,
|
|
109
|
+
ratio,
|
|
110
|
+
orientation,
|
|
111
|
+
thumbnailUrl: getThumbnailUrl(config.cdnHostname, video.guid),
|
|
112
|
+
playbackUrl: getPlaybackUrl(config.cdnHostname, video.guid),
|
|
113
|
+
mp4Url: getMp4Url(config.cdnHostname, video.guid, video.availableResolutions),
|
|
114
|
+
...(resolvedCollectionId ? {collectionId: resolvedCollectionId} : {}),
|
|
115
|
+
...(video.collectionId ? {collectionId: video.collectionId} : {}),
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
[config, resolvedCollectionId, onSelect]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Dialog
|
|
125
|
+
header="Browse videos"
|
|
126
|
+
id="bunny-browser-modal"
|
|
127
|
+
onClose={onClose}
|
|
128
|
+
width={3}
|
|
129
|
+
>
|
|
130
|
+
<Box padding={4}>
|
|
131
|
+
<Box marginBottom={4}>
|
|
132
|
+
<TextInput
|
|
133
|
+
icon={SearchIcon}
|
|
134
|
+
placeholder="Search videos…"
|
|
135
|
+
value={search}
|
|
136
|
+
onChange={handleSearchChange}
|
|
137
|
+
/>
|
|
138
|
+
</Box>
|
|
139
|
+
|
|
140
|
+
{error && (
|
|
141
|
+
<Card padding={3} radius={2} tone="critical" marginBottom={4}>
|
|
142
|
+
<Text size={1}>{error}</Text>
|
|
143
|
+
</Card>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{isLoading ? (
|
|
147
|
+
<Flex align="center" justify="center" padding={6}>
|
|
148
|
+
<Spinner />
|
|
149
|
+
</Flex>
|
|
150
|
+
) : videos.length === 0 ? (
|
|
151
|
+
<Flex align="center" justify="center" padding={6}>
|
|
152
|
+
<Text muted size={1}>
|
|
153
|
+
{search ? 'No videos found matching your search' : 'No videos in this collection'}
|
|
154
|
+
</Text>
|
|
155
|
+
</Flex>
|
|
156
|
+
) : (
|
|
157
|
+
<>
|
|
158
|
+
<Grid columns={[2, 2, 3, 4]} gap={3}>
|
|
159
|
+
{videos.map((video) => {
|
|
160
|
+
const status = mapBunnyStatus(video.status)
|
|
161
|
+
const isReady = status === 'ready'
|
|
162
|
+
const thumbUrl = getThumbnailUrl(config.cdnHostname, video.guid)
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Card
|
|
166
|
+
key={video.guid}
|
|
167
|
+
radius={2}
|
|
168
|
+
shadow={1}
|
|
169
|
+
style={{
|
|
170
|
+
cursor: isReady ? 'pointer' : 'not-allowed',
|
|
171
|
+
opacity: isReady ? 1 : 0.5,
|
|
172
|
+
}}
|
|
173
|
+
onClick={() => isReady && handleSelect(video)}
|
|
174
|
+
>
|
|
175
|
+
<Box
|
|
176
|
+
style={{
|
|
177
|
+
position: 'relative',
|
|
178
|
+
paddingBottom: '56.25%',
|
|
179
|
+
backgroundColor: '#111',
|
|
180
|
+
borderRadius: '2px',
|
|
181
|
+
overflow: 'hidden',
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<img
|
|
185
|
+
src={thumbUrl}
|
|
186
|
+
alt={video.title || 'Video thumbnail'}
|
|
187
|
+
style={{
|
|
188
|
+
position: 'absolute',
|
|
189
|
+
top: 0,
|
|
190
|
+
left: 0,
|
|
191
|
+
width: '100%',
|
|
192
|
+
height: '100%',
|
|
193
|
+
objectFit: 'cover',
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
{!isReady && (
|
|
197
|
+
<Flex
|
|
198
|
+
align="center"
|
|
199
|
+
justify="center"
|
|
200
|
+
style={{
|
|
201
|
+
position: 'absolute',
|
|
202
|
+
inset: 0,
|
|
203
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<Text size={0} style={{color: '#fff'}}>
|
|
207
|
+
{status === 'processing' ? 'Processing…' : status}
|
|
208
|
+
</Text>
|
|
209
|
+
</Flex>
|
|
210
|
+
)}
|
|
211
|
+
</Box>
|
|
212
|
+
<Box padding={3}>
|
|
213
|
+
<Stack space={2}>
|
|
214
|
+
<Text size={1} weight="semibold">
|
|
215
|
+
{video.title || 'Untitled'}
|
|
216
|
+
</Text>
|
|
217
|
+
{video.length > 0 && (
|
|
218
|
+
<Text size={1} muted>
|
|
219
|
+
{Math.floor(video.length / 60)}:
|
|
220
|
+
{String(Math.floor(video.length % 60)).padStart(2, '0')}
|
|
221
|
+
</Text>
|
|
222
|
+
)}
|
|
223
|
+
</Stack>
|
|
224
|
+
</Box>
|
|
225
|
+
</Card>
|
|
226
|
+
)
|
|
227
|
+
})}
|
|
228
|
+
</Grid>
|
|
229
|
+
|
|
230
|
+
{totalPages > 1 && (
|
|
231
|
+
<Flex align="center" justify="center" gap={2} marginTop={4}>
|
|
232
|
+
<Button
|
|
233
|
+
text="Previous"
|
|
234
|
+
mode="ghost"
|
|
235
|
+
disabled={page <= 1}
|
|
236
|
+
onClick={() => handlePageChange(page - 1)}
|
|
237
|
+
/>
|
|
238
|
+
<Text size={1} muted>
|
|
239
|
+
Page {page} of {totalPages}
|
|
240
|
+
</Text>
|
|
241
|
+
<Button
|
|
242
|
+
text="Next"
|
|
243
|
+
mode="ghost"
|
|
244
|
+
disabled={page >= totalPages}
|
|
245
|
+
onClick={() => handlePageChange(page + 1)}
|
|
246
|
+
/>
|
|
247
|
+
</Flex>
|
|
248
|
+
)}
|
|
249
|
+
</>
|
|
250
|
+
)}
|
|
251
|
+
</Box>
|
|
252
|
+
</Dialog>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
2
2
|
import {set, unset} from 'sanity'
|
|
3
3
|
import {Box, Button, Card, Flex, Spinner, Stack, Text} from '@sanity/ui'
|
|
4
|
+
import {UploadIcon, SearchIcon, VideoIcon} from '@sanity/icons'
|
|
4
5
|
import {
|
|
5
6
|
createVideo,
|
|
6
7
|
uploadVideo,
|
|
@@ -12,12 +13,14 @@ import {
|
|
|
12
13
|
getPlaybackUrl,
|
|
13
14
|
getMp4Url,
|
|
14
15
|
} from '../api'
|
|
16
|
+
import {BunnyBrowserModal} from './BunnyBrowserModal'
|
|
15
17
|
|
|
16
18
|
export function BunnyInput(props) {
|
|
17
19
|
const {value, onChange, config} = props
|
|
18
20
|
const [uploadProgress, setUploadProgress] = useState(0)
|
|
19
21
|
const [isUploading, setIsUploading] = useState(false)
|
|
20
22
|
const [error, setError] = useState(null)
|
|
23
|
+
const [isBrowserOpen, setIsBrowserOpen] = useState(false)
|
|
21
24
|
const fileInputRef = useRef(null)
|
|
22
25
|
const pollIntervalRef = useRef(null)
|
|
23
26
|
|
|
@@ -162,6 +165,14 @@ export function BunnyInput(props) {
|
|
|
162
165
|
[handleFileSelect]
|
|
163
166
|
)
|
|
164
167
|
|
|
168
|
+
const handleBrowseSelect = useCallback(
|
|
169
|
+
(videoData) => {
|
|
170
|
+
setIsBrowserOpen(false)
|
|
171
|
+
onChange(set(videoData))
|
|
172
|
+
},
|
|
173
|
+
[onChange]
|
|
174
|
+
)
|
|
175
|
+
|
|
165
176
|
const handleRemove = useCallback(async () => {
|
|
166
177
|
if (value?.videoId) {
|
|
167
178
|
try {
|
|
@@ -177,48 +188,65 @@ export function BunnyInput(props) {
|
|
|
177
188
|
// Render video preview if we have one
|
|
178
189
|
if (value?.videoId && value?.status === 'ready') {
|
|
179
190
|
return (
|
|
180
|
-
|
|
181
|
-
<
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
</Box>
|
|
206
|
-
<Flex justify="space-between" align="center">
|
|
207
|
-
<Stack space={2}>
|
|
208
|
-
<Text size={1} weight="semibold">
|
|
209
|
-
{value.title}
|
|
210
|
-
</Text>
|
|
211
|
-
{value.duration && (
|
|
212
|
-
<Text size={1} muted>
|
|
213
|
-
{Math.floor(value.duration / 60)}:
|
|
214
|
-
{String(Math.floor(value.duration % 60)).padStart(2, '0')}
|
|
215
|
-
</Text>
|
|
191
|
+
<>
|
|
192
|
+
<Card padding={3} radius={2} shadow={1}>
|
|
193
|
+
<Stack space={3}>
|
|
194
|
+
<Box
|
|
195
|
+
style={{
|
|
196
|
+
position: 'relative',
|
|
197
|
+
paddingBottom: '56.25%',
|
|
198
|
+
backgroundColor: '#000',
|
|
199
|
+
borderRadius: '4px',
|
|
200
|
+
overflow: 'hidden',
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
{value.thumbnailUrl && (
|
|
204
|
+
<img
|
|
205
|
+
src={value.thumbnailUrl}
|
|
206
|
+
alt={value.title || 'Video thumbnail'}
|
|
207
|
+
style={{
|
|
208
|
+
position: 'absolute',
|
|
209
|
+
top: 0,
|
|
210
|
+
left: 0,
|
|
211
|
+
width: '100%',
|
|
212
|
+
height: '100%',
|
|
213
|
+
objectFit: 'contain',
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
216
|
)}
|
|
217
|
-
</
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
</Box>
|
|
218
|
+
<Flex justify="space-between" align="center">
|
|
219
|
+
<Stack space={2}>
|
|
220
|
+
<Text size={1} weight="semibold">
|
|
221
|
+
{value.title}
|
|
222
|
+
</Text>
|
|
223
|
+
{value.duration && (
|
|
224
|
+
<Text size={1} muted>
|
|
225
|
+
{Math.floor(value.duration / 60)}:
|
|
226
|
+
{String(Math.floor(value.duration % 60)).padStart(2, '0')}
|
|
227
|
+
</Text>
|
|
228
|
+
)}
|
|
229
|
+
</Stack>
|
|
230
|
+
<Flex gap={2}>
|
|
231
|
+
<Button
|
|
232
|
+
text="Browse"
|
|
233
|
+
mode="ghost"
|
|
234
|
+
onClick={() => setIsBrowserOpen(true)}
|
|
235
|
+
/>
|
|
236
|
+
<Button text="Remove" tone="critical" mode="ghost" onClick={handleRemove} />
|
|
237
|
+
</Flex>
|
|
238
|
+
</Flex>
|
|
239
|
+
</Stack>
|
|
240
|
+
</Card>
|
|
241
|
+
{isBrowserOpen && (
|
|
242
|
+
<BunnyBrowserModal
|
|
243
|
+
config={config}
|
|
244
|
+
collectionId={value.collectionId || config.collectionId}
|
|
245
|
+
onSelect={handleBrowseSelect}
|
|
246
|
+
onClose={() => setIsBrowserOpen(false)}
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
</>
|
|
222
250
|
)
|
|
223
251
|
}
|
|
224
252
|
|
|
@@ -285,13 +313,59 @@ export function BunnyInput(props) {
|
|
|
285
313
|
)
|
|
286
314
|
}
|
|
287
315
|
|
|
288
|
-
// Render file input
|
|
316
|
+
// Render file input + browse option
|
|
289
317
|
return (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
318
|
+
<>
|
|
319
|
+
<div style={{padding: 1}}>
|
|
320
|
+
<Card tone="inherit" border paddingX={3} paddingY={2} radius={2}>
|
|
321
|
+
<Flex align="center" gap={4} justify="space-between">
|
|
322
|
+
<Flex flex={1} align="center" gap={3}>
|
|
323
|
+
<Text size={1} muted><VideoIcon /></Text>
|
|
324
|
+
<Text size={1} muted>Drag or paste video here</Text>
|
|
325
|
+
</Flex>
|
|
326
|
+
<Flex align="center" gap={1}>
|
|
327
|
+
<label style={{display: 'contents'}}>
|
|
328
|
+
<Button
|
|
329
|
+
as="span"
|
|
330
|
+
icon={UploadIcon}
|
|
331
|
+
text="Upload"
|
|
332
|
+
mode="bleed"
|
|
333
|
+
padding={2}
|
|
334
|
+
style={{cursor: 'pointer'}}
|
|
335
|
+
/>
|
|
336
|
+
<input
|
|
337
|
+
ref={fileInputRef}
|
|
338
|
+
type="file"
|
|
339
|
+
accept="video/*"
|
|
340
|
+
onChange={handleInputChange}
|
|
341
|
+
style={{
|
|
342
|
+
position: 'absolute',
|
|
343
|
+
width: '1px',
|
|
344
|
+
height: '1px',
|
|
345
|
+
overflow: 'hidden',
|
|
346
|
+
opacity: 0,
|
|
347
|
+
}}
|
|
348
|
+
/>
|
|
349
|
+
</label>
|
|
350
|
+
<Button
|
|
351
|
+
icon={SearchIcon}
|
|
352
|
+
text="Select"
|
|
353
|
+
mode="bleed"
|
|
354
|
+
padding={2}
|
|
355
|
+
onClick={() => setIsBrowserOpen(true)}
|
|
356
|
+
/>
|
|
357
|
+
</Flex>
|
|
358
|
+
</Flex>
|
|
359
|
+
</Card>
|
|
360
|
+
</div>
|
|
361
|
+
{isBrowserOpen && (
|
|
362
|
+
<BunnyBrowserModal
|
|
363
|
+
config={config}
|
|
364
|
+
collectionId={config.collectionId}
|
|
365
|
+
onSelect={handleBrowseSelect}
|
|
366
|
+
onClose={() => setIsBrowserOpen(false)}
|
|
367
|
+
/>
|
|
368
|
+
)}
|
|
369
|
+
</>
|
|
296
370
|
)
|
|
297
371
|
}
|
package/src/components/index.js
CHANGED
package/src/schema.js
CHANGED
|
@@ -69,6 +69,19 @@ export const createBunnyVideoSchema = (config) => {
|
|
|
69
69
|
name: 'ratio',
|
|
70
70
|
readOnly: true,
|
|
71
71
|
}),
|
|
72
|
+
defineField({
|
|
73
|
+
type: 'string',
|
|
74
|
+
title: 'Orientation',
|
|
75
|
+
name: 'orientation',
|
|
76
|
+
options: {
|
|
77
|
+
list: [
|
|
78
|
+
{title: 'Landscape', value: 'landscape'},
|
|
79
|
+
{title: 'Portrait', value: 'portrait'},
|
|
80
|
+
{title: 'Square', value: 'square'},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
readOnly: true,
|
|
84
|
+
}),
|
|
72
85
|
defineField({
|
|
73
86
|
type: 'string',
|
|
74
87
|
title: 'Thumbnail URL',
|