✨ Implement Pexels videos option to media popover (#1636)
Closes #1575 Note: Need to create a new environment variable named `NEXT_PUBLIC_PEXELS_API_KEY` to store the API Key obtained from Pexels! https://github.com/user-attachments/assets/4250f799-0bd7-48e9-b9a8-4bc188ad7704 --------- Co-authored-by: Baptiste Arnaud <baptiste.arnaud95@gmail.com> Co-authored-by: younesbenallal <younes.benallal.06@gmail.com>
This commit is contained in:
@ -82,6 +82,7 @@
|
||||
"nprogress": "0.2.0",
|
||||
"openai": "4.47.1",
|
||||
"papaparse": "5.4.1",
|
||||
"pexels": "^1.4.0",
|
||||
"prettier": "2.8.8",
|
||||
"qs": "6.11.2",
|
||||
"react": "18.2.0",
|
||||
@ -113,6 +114,7 @@
|
||||
"@typebot.io/schemas": "workspace:*",
|
||||
"@typebot.io/telemetry": "workspace:*",
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"@typebot.io/variables": "workspace:*",
|
||||
"@types/canvas-confetti": "1.6.0",
|
||||
"@types/jsonwebtoken": "9.0.2",
|
||||
"@types/micro-cors": "0.1.3",
|
||||
@ -131,7 +133,6 @@
|
||||
"next-runtime-env": "1.6.2",
|
||||
"superjson": "1.12.4",
|
||||
"typescript": "5.4.5",
|
||||
"zod": "3.22.4",
|
||||
"@typebot.io/variables": "workspace:*"
|
||||
"zod": "3.22.4"
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => {
|
||||
placeholder="Search..."
|
||||
onChange={setInputValue}
|
||||
withVariableButton={false}
|
||||
width="full"
|
||||
/>
|
||||
<GiphyLogo w="100px" />
|
||||
</Flex>
|
||||
|
254
apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx
Normal file
254
apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Box,
|
||||
Flex,
|
||||
Grid,
|
||||
GridItem,
|
||||
HStack,
|
||||
Image,
|
||||
Link,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createClient, Video, ErrorResponse, Videos } from 'pexels'
|
||||
import { TextInput } from '../inputs'
|
||||
import { TextLink } from '../TextLink'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { PexelsLogo } from '../logos/PexelsLogo'
|
||||
|
||||
const client = createClient(env.NEXT_PUBLIC_PEXELS_API_KEY ?? 'dummy')
|
||||
|
||||
type Props = {
|
||||
videoSize: 'large' | 'medium' | 'small'
|
||||
onVideoSelect: (videoUrl: string) => void
|
||||
}
|
||||
|
||||
export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => {
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [videos, setVideos] = useState<Video[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const scrollContainer = useRef<HTMLDivElement>(null)
|
||||
const bottomAnchor = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [nextPage, setNextPage] = useState(0)
|
||||
|
||||
const fetchNewVideos = useCallback(async (query: string, page: number) => {
|
||||
if (query === '') getInitialVideos()
|
||||
if (query.length <= 2) {
|
||||
setNextPage(0)
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setIsFetching(true)
|
||||
try {
|
||||
const result = await client.videos.search({
|
||||
query,
|
||||
per_page: 24,
|
||||
size: videoSize,
|
||||
page,
|
||||
})
|
||||
if ((result as ErrorResponse).error)
|
||||
setError((result as ErrorResponse).error)
|
||||
if (isDefined((result as Videos).videos)) {
|
||||
if (page === 0) setVideos((result as Videos).videos)
|
||||
else
|
||||
setVideos((videos) => [
|
||||
...videos,
|
||||
...((result as Videos)?.videos ?? []),
|
||||
])
|
||||
setNextPage((page) => page + 1)
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && typeof err === 'object' && 'message' in err)
|
||||
setError(err.message as string)
|
||||
setError('Something went wrong')
|
||||
}
|
||||
setIsFetching(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!bottomAnchor.current) return
|
||||
const observer = new IntersectionObserver(
|
||||
(entities: IntersectionObserverEntry[]) => {
|
||||
const target = entities[0]
|
||||
if (target.isIntersecting) fetchNewVideos(searchQuery, nextPage + 1)
|
||||
},
|
||||
{
|
||||
root: scrollContainer.current,
|
||||
}
|
||||
)
|
||||
if (bottomAnchor.current && nextPage > 0)
|
||||
observer.observe(bottomAnchor.current)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [fetchNewVideos, nextPage, searchQuery])
|
||||
|
||||
const getInitialVideos = async () => {
|
||||
setError(null)
|
||||
setIsFetching(true)
|
||||
client.videos
|
||||
.popular({
|
||||
per_page: 24,
|
||||
size: videoSize,
|
||||
})
|
||||
.then((res) => {
|
||||
if ((res as ErrorResponse).error) {
|
||||
setError((res as ErrorResponse).error)
|
||||
}
|
||||
setVideos((res as Videos).videos)
|
||||
setIsFetching(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && typeof err === 'object' && 'message' in err)
|
||||
setError(err.message as string)
|
||||
setError('Something went wrong')
|
||||
setIsFetching(false)
|
||||
})
|
||||
}
|
||||
|
||||
const selectVideo = (video: Video) => {
|
||||
const videoUrl = video.video_files[0].link
|
||||
if (isDefined(videoUrl)) onVideoSelect(videoUrl)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!env.NEXT_PUBLIC_PEXELS_API_KEY) return
|
||||
getInitialVideos()
|
||||
}, [])
|
||||
|
||||
if (!env.NEXT_PUBLIC_PEXELS_API_KEY)
|
||||
return <Text>NEXT_PUBLIC_PEXELS_API_KEY is missing in environment</Text>
|
||||
|
||||
return (
|
||||
<Stack spacing={4} pt="2">
|
||||
<HStack align="center">
|
||||
<TextInput
|
||||
autoFocus
|
||||
placeholder="Search..."
|
||||
onChange={(query) => {
|
||||
setSearchQuery(query)
|
||||
fetchNewVideos(query, 0)
|
||||
}}
|
||||
withVariableButton={false}
|
||||
debounceTimeout={500}
|
||||
forceDebounce
|
||||
width="full"
|
||||
/>
|
||||
<Link isExternal href={`https://www.pexels.com`}>
|
||||
<PexelsLogo width="100px" height="40px" />
|
||||
</Link>
|
||||
</HStack>
|
||||
{isDefined(error) && (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<Stack overflowY="auto" maxH="400px" ref={scrollContainer}>
|
||||
{videos.length > 0 && (
|
||||
<Grid templateColumns="repeat(3, 1fr)" columnGap={2} rowGap={3}>
|
||||
{videos.map((video, index) => (
|
||||
<GridItem
|
||||
as={Stack}
|
||||
key={video.id}
|
||||
boxSize="100%"
|
||||
spacing="0"
|
||||
ref={index === videos.length - 1 ? bottomAnchor : undefined}
|
||||
>
|
||||
<PexelsVideo video={video} onClick={() => selectVideo(video)} />
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
{isFetching && (
|
||||
<Flex justifyContent="center" py="4">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
type PexelsVideoProps = {
|
||||
video: Video
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const PexelsVideo = ({ video, onClick }: PexelsVideoProps) => {
|
||||
const { user, url, video_pictures } = video
|
||||
const [isImageHovered, setIsImageHovered] = useState(false)
|
||||
const [thumbnailImage, setThumbnailImage] = useState(
|
||||
video_pictures[0].picture
|
||||
)
|
||||
const [imageIndex, setImageIndex] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timer
|
||||
|
||||
if (isImageHovered && video_pictures.length > 0) {
|
||||
interval = setInterval(() => {
|
||||
setImageIndex((prevIndex) => (prevIndex + 1) % video_pictures.length)
|
||||
setThumbnailImage(video_pictures[imageIndex].picture)
|
||||
}, 200)
|
||||
} else {
|
||||
setThumbnailImage(video_pictures[0].picture)
|
||||
setImageIndex(1)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
}, [isImageHovered, imageIndex, video_pictures])
|
||||
|
||||
return (
|
||||
<Box
|
||||
pos="relative"
|
||||
onMouseEnter={() => setIsImageHovered(true)}
|
||||
onMouseLeave={() => setIsImageHovered(false)}
|
||||
h="full"
|
||||
>
|
||||
{
|
||||
<Image
|
||||
objectFit="cover"
|
||||
src={thumbnailImage}
|
||||
alt={`Pexels Video ${video.id}`}
|
||||
onClick={onClick}
|
||||
rounded="md"
|
||||
h="100%"
|
||||
aspectRatio={4 / 3}
|
||||
cursor="pointer"
|
||||
/>
|
||||
}
|
||||
<Box
|
||||
pos="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
bgColor="rgba(0,0,0,.5)"
|
||||
px="2"
|
||||
rounded="md"
|
||||
opacity={isImageHovered ? 1 : 0}
|
||||
transition="opacity .2s ease-in-out"
|
||||
>
|
||||
<TextLink
|
||||
fontSize="xs"
|
||||
isExternal
|
||||
href={url}
|
||||
noOfLines={1}
|
||||
color="white"
|
||||
>
|
||||
{user.name}
|
||||
</TextLink>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { VideoBubbleBlock } from '@typebot.io/schemas'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||
import { SwitchWithLabel } from '../inputs/SwitchWithLabel'
|
||||
|
||||
export const VideoLinkEmbedContent = ({
|
||||
content,
|
||||
updateUrl,
|
||||
onSubmit,
|
||||
}: {
|
||||
content?: VideoBubbleBlock['content']
|
||||
updateUrl: (url: string) => void
|
||||
onSubmit: (content: VideoBubbleBlock['content']) => void
|
||||
}) => {
|
||||
const { t } = useTranslate()
|
||||
|
||||
const updateAspectRatio = (aspectRatio?: string) => {
|
||||
return onSubmit({
|
||||
...content,
|
||||
aspectRatio,
|
||||
})
|
||||
}
|
||||
|
||||
const updateMaxWidth = (maxWidth?: string) => {
|
||||
return onSubmit({
|
||||
...content,
|
||||
maxWidth,
|
||||
})
|
||||
}
|
||||
|
||||
const updateAutoPlay = (isAutoplayEnabled: boolean) => {
|
||||
return onSubmit({ ...content, isAutoplayEnabled })
|
||||
}
|
||||
|
||||
const updateControlsDisplay = (areControlsDisplayed: boolean) => {
|
||||
if (areControlsDisplayed === false) {
|
||||
// Make sure autoplay is enabled when video controls are disabled
|
||||
return onSubmit({
|
||||
...content,
|
||||
isAutoplayEnabled: true,
|
||||
areControlsDisplayed,
|
||||
})
|
||||
}
|
||||
return onSubmit({ ...content, areControlsDisplayed })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack py="2">
|
||||
<TextInput
|
||||
placeholder={t('video.urlInput.placeholder')}
|
||||
defaultValue={content?.url ?? ''}
|
||||
onChange={updateUrl}
|
||||
/>
|
||||
<Text fontSize="xs" color="gray.400" textAlign="center">
|
||||
{t('video.urlInput.helperText')}
|
||||
</Text>
|
||||
</Stack>
|
||||
{content?.url && (
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t('video.aspectRatioInput.label')}
|
||||
moreInfoTooltip={t('video.aspectRatioInput.moreInfoTooltip')}
|
||||
defaultValue={
|
||||
content?.aspectRatio ?? defaultVideoBubbleContent.aspectRatio
|
||||
}
|
||||
onChange={updateAspectRatio}
|
||||
direction="row"
|
||||
/>
|
||||
<TextInput
|
||||
label={t('video.maxWidthInput.label')}
|
||||
moreInfoTooltip={t('video.maxWidthInput.moreInfoTooltip')}
|
||||
defaultValue={
|
||||
content?.maxWidth ?? defaultVideoBubbleContent.maxWidth
|
||||
}
|
||||
onChange={updateMaxWidth}
|
||||
direction="row"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{content?.url && content?.type === 'url' && (
|
||||
<Stack>
|
||||
<SwitchWithLabel
|
||||
label={'Display controls'}
|
||||
initialValue={
|
||||
content?.areControlsDisplayed ??
|
||||
defaultVideoBubbleContent.areControlsDisplayed
|
||||
}
|
||||
onCheckChange={updateControlsDisplay}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label={t('editor.blocks.bubbles.audio.settings.autoplay.label')}
|
||||
initialValue={
|
||||
content?.isAutoplayEnabled ??
|
||||
defaultVideoBubbleContent.isAutoplayEnabled
|
||||
}
|
||||
isChecked={
|
||||
content?.isAutoplayEnabled ??
|
||||
defaultVideoBubbleContent.isAutoplayEnabled
|
||||
}
|
||||
isDisabled={content?.areControlsDisplayed === false}
|
||||
onCheckChange={() => updateAutoPlay(!content.isAutoplayEnabled)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { IconProps, Icon } from '@chakra-ui/react'
|
||||
import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react'
|
||||
|
||||
export const GiphyLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 163.79999999999998 35" {...props}>
|
||||
@ -15,7 +15,7 @@ export const GiphyLogo = (props: IconProps) => (
|
||||
<path d="M16 0v4h-4" fill="#999131" />
|
||||
<path
|
||||
d="M59.1 12c-2-1.9-4.4-2.4-6.2-2.4-4.4 0-7.3 2.6-7.3 8 0 3.5 1.8 7.8 7.3 7.8 1.4 0 3.7-.3 5.2-1.4v-3.5h-6.9v-6h13.3v12.1c-1.7 3.5-6.4 5.3-11.7 5.3-10.7 0-14.8-7.2-14.8-14.3S42.7 3.2 52.9 3.2c3.8 0 7.1.8 10.7 4.4zm9.1 19.2V4h7.6v27.2zm20.1-7.4v7.3h-7.7V4h13.2c7.3 0 10.9 4.6 10.9 9.9 0 5.6-3.6 9.9-10.9 9.9zm0-6.5h5.5c2.1 0 3.2-1.6 3.2-3.3 0-1.8-1.1-3.4-3.2-3.4h-5.5zM125 31.2V20.9h-9.8v10.3h-7.7V4h7.7v10.3h9.8V4h7.6v27.2zm24.2-17.9l5.9-9.3h8.7v.3l-10.8 16v10.8h-7.7V20.3L135 4.3V4h8.7z"
|
||||
fill="#000"
|
||||
fill={useColorModeValue('#000', '#fff')}
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
|
25
apps/builder/src/components/logos/PexelsLogo.tsx
Normal file
25
apps/builder/src/components/logos/PexelsLogo.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react'
|
||||
|
||||
export const PexelsLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 130.318 50" {...props}>
|
||||
<g transform="translate(-3894 2762)">
|
||||
<rect
|
||||
width="50"
|
||||
height="50"
|
||||
rx="8"
|
||||
transform="translate(3894 -2762)"
|
||||
fill="#07a081"
|
||||
/>
|
||||
<path
|
||||
d="M32.671,44.73h7.091V37.935H41.9a5.657,5.657,0,1,0,0-11.314H32.671Zm10.763,3.622H29V23H41.9a9.271,9.271,0,0,1,1.53,18.435Z"
|
||||
transform="translate(3880 -2773)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M1.694,0h2.6V-6.16H7.656a6.579,6.579,0,0,0,2.915-.616,4.639,4.639,0,0,0,1.969-1.76,5.1,5.1,0,0,0,.7-2.728,5.146,5.146,0,0,0-.7-2.75,4.639,4.639,0,0,0-1.969-1.76,6.579,6.579,0,0,0-2.915-.616H1.694Zm2.6-8.47v-5.61H7.722a3.03,3.03,0,0,1,2.134.748,2.641,2.641,0,0,1,.814,2.046A2.684,2.684,0,0,1,9.856-9.24a2.978,2.978,0,0,1-2.134.77ZM20.372.264a5.925,5.925,0,0,0,3.179-.836,4.64,4.64,0,0,0,1.9-2.112l-2.024-.99a3.73,3.73,0,0,1-1.2,1.243,3.29,3.29,0,0,1-1.837.5A3.458,3.458,0,0,1,18-2.827a3.433,3.433,0,0,1-1.1-2.409H25.74a3.34,3.34,0,0,0,.088-.572q.022-.308.022-.594a6.154,6.154,0,0,0-.671-2.849,5.361,5.361,0,0,0-1.936-2.112,5.61,5.61,0,0,0-3.069-.8,5.7,5.7,0,0,0-3,.8,5.773,5.773,0,0,0-2.1,2.2,6.476,6.476,0,0,0-.77,3.179A6.482,6.482,0,0,0,15.081-2.8,5.9,5.9,0,0,0,17.226-.561,5.958,5.958,0,0,0,20.372.264Zm-.2-10.34a3,3,0,0,1,2.112.792,2.9,2.9,0,0,1,.924,2.068H16.94a3.313,3.313,0,0,1,1.122-2.112A3.208,3.208,0,0,1,20.174-10.076ZM26.422,0h2.926l2.706-3.982L34.738,0h2.926L33.506-5.962l4.18-5.94H34.76L32.054-7.964,29.348-11.9H26.422l4.158,5.94ZM44.088.264a5.925,5.925,0,0,0,3.179-.836,4.64,4.64,0,0,0,1.9-2.112l-2.024-.99a3.73,3.73,0,0,1-1.2,1.243,3.29,3.29,0,0,1-1.837.5,3.458,3.458,0,0,1-2.4-.891,3.433,3.433,0,0,1-1.1-2.409h8.844a3.34,3.34,0,0,0,.088-.572q.022-.308.022-.594A6.154,6.154,0,0,0,48.9-9.251a5.361,5.361,0,0,0-1.936-2.112,5.61,5.61,0,0,0-3.069-.8,5.7,5.7,0,0,0-3,.8,5.773,5.773,0,0,0-2.1,2.2,6.476,6.476,0,0,0-.77,3.179A6.482,6.482,0,0,0,38.8-2.8,5.9,5.9,0,0,0,40.942-.561,5.958,5.958,0,0,0,44.088.264Zm-.2-10.34A3,3,0,0,1,46-9.284a2.9,2.9,0,0,1,.924,2.068h-6.27a3.313,3.313,0,0,1,1.122-2.112A3.208,3.208,0,0,1,43.89-10.076ZM51.546,0h2.486V-16.654H51.546ZM60.9.264a5.6,5.6,0,0,0,2.321-.451,3.635,3.635,0,0,0,1.551-1.254,3.21,3.21,0,0,0,.55-1.859,3.088,3.088,0,0,0-.792-2.123A4.635,4.635,0,0,0,62.26-6.732L60.324-7.3a4.436,4.436,0,0,1-1.034-.484,1.023,1.023,0,0,1-.484-.924,1.212,1.212,0,0,1,.484-1.012,2.068,2.068,0,0,1,1.3-.374,3.005,3.005,0,0,1,1.705.506A2.944,2.944,0,0,1,63.4-8.228l1.914-.9a4.344,4.344,0,0,0-1.8-2.233,5.337,5.337,0,0,0-2.9-.8,5.1,5.1,0,0,0-2.178.451,3.7,3.7,0,0,0-1.518,1.243,3.2,3.2,0,0,0-.55,1.87,3.1,3.1,0,0,0,.759,2.09,4.624,4.624,0,0,0,2.3,1.32l1.87.528a3.923,3.923,0,0,1,1.078.473,1.057,1.057,0,0,1,.506.957,1.259,1.259,0,0,1-.55,1.078,2.391,2.391,0,0,1-1.43.4,3.2,3.2,0,0,1-1.881-.594A4.049,4.049,0,0,1,57.684-3.96l-1.914.9a4.774,4.774,0,0,0,1.925,2.42A5.7,5.7,0,0,0,60.9.264Z"
|
||||
transform="translate(3959 -2728)"
|
||||
fill={useColorModeValue('#000', '#fff')}
|
||||
/>
|
||||
</g>
|
||||
</Icon>
|
||||
)
|
@ -1,18 +1,46 @@
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { Button, HStack, Stack } from '@chakra-ui/react'
|
||||
import { VideoBubbleBlock } from '@typebot.io/schemas'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { parseVideoUrl } from '@typebot.io/schemas/features/blocks/bubbles/video/helpers'
|
||||
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { useState } from 'react'
|
||||
import { PexelsPicker } from '@/components/VideoUploadContent/PexelsPicker'
|
||||
import { VideoLinkEmbedContent } from '@/components/VideoUploadContent/VideoLinkEmbedContent'
|
||||
|
||||
type Tabs = 'link' | 'pexels'
|
||||
|
||||
type Props = {
|
||||
content?: VideoBubbleBlock['content']
|
||||
onSubmit: (content: VideoBubbleBlock['content']) => void
|
||||
}
|
||||
initialTab?: Tabs
|
||||
} & (
|
||||
| {
|
||||
includedTabs?: Tabs[]
|
||||
}
|
||||
| {
|
||||
excludedTabs?: Tabs[]
|
||||
}
|
||||
)
|
||||
|
||||
const defaultDisplayedTabs: Tabs[] = ['link', 'pexels']
|
||||
|
||||
export const VideoUploadContent = ({
|
||||
content,
|
||||
onSubmit,
|
||||
initialTab,
|
||||
...props
|
||||
}: Props) => {
|
||||
const includedTabs =
|
||||
'includedTabs' in props
|
||||
? props.includedTabs ?? defaultDisplayedTabs
|
||||
: defaultDisplayedTabs
|
||||
const excludedTabs = 'excludedTabs' in props ? props.excludedTabs ?? [] : []
|
||||
const displayedTabs = defaultDisplayedTabs.filter(
|
||||
(tab) => !excludedTabs.includes(tab) && includedTabs.includes(tab)
|
||||
)
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<Tabs>(
|
||||
initialTab ?? displayedTabs[0]
|
||||
)
|
||||
|
||||
export const VideoUploadContent = ({ content, onSubmit }: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const updateUrl = (url: string) => {
|
||||
const {
|
||||
type,
|
||||
@ -20,6 +48,10 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
|
||||
id,
|
||||
videoSizeSuggestion,
|
||||
} = parseVideoUrl(url)
|
||||
if (currentTab !== 'link') {
|
||||
// Allow user to update video settings after selection
|
||||
setCurrentTab('link')
|
||||
}
|
||||
return onSubmit({
|
||||
...content,
|
||||
type,
|
||||
@ -30,94 +62,40 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
const updateAspectRatio = (aspectRatio?: string) => {
|
||||
return onSubmit({
|
||||
...content,
|
||||
aspectRatio,
|
||||
})
|
||||
}
|
||||
|
||||
const updateMaxWidth = (maxWidth?: string) => {
|
||||
return onSubmit({
|
||||
...content,
|
||||
maxWidth,
|
||||
})
|
||||
}
|
||||
|
||||
const updateAutoPlay = (isAutoplayEnabled: boolean) => {
|
||||
return onSubmit({ ...content, isAutoplayEnabled })
|
||||
}
|
||||
|
||||
const updateControlsDisplay = (areControlsDisplayed: boolean) => {
|
||||
if (areControlsDisplayed === false) {
|
||||
// Make sure autoplay is enabled when video controls are disabled
|
||||
return onSubmit({
|
||||
...content,
|
||||
isAutoplayEnabled: true,
|
||||
areControlsDisplayed,
|
||||
})
|
||||
}
|
||||
return onSubmit({ ...content, areControlsDisplayed })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack p="2" spacing={4}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
placeholder={t('video.urlInput.placeholder')}
|
||||
defaultValue={content?.url ?? ''}
|
||||
onChange={updateUrl}
|
||||
/>
|
||||
<Text fontSize="xs" color="gray.400" textAlign="center">
|
||||
{t('video.urlInput.helperText')}
|
||||
</Text>
|
||||
</Stack>
|
||||
{content?.url && (
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t('video.aspectRatioInput.label')}
|
||||
moreInfoTooltip={t('video.aspectRatioInput.moreInfoTooltip')}
|
||||
defaultValue={
|
||||
content?.aspectRatio ?? defaultVideoBubbleContent.aspectRatio
|
||||
}
|
||||
onChange={updateAspectRatio}
|
||||
direction="row"
|
||||
/>
|
||||
<TextInput
|
||||
label={t('video.maxWidthInput.label')}
|
||||
moreInfoTooltip={t('video.maxWidthInput.moreInfoTooltip')}
|
||||
defaultValue={
|
||||
content?.maxWidth ?? defaultVideoBubbleContent.maxWidth
|
||||
}
|
||||
onChange={updateMaxWidth}
|
||||
direction="row"
|
||||
/>
|
||||
</Stack>
|
||||
<HStack>
|
||||
{displayedTabs.includes('link') && (
|
||||
<Button
|
||||
variant={currentTab === 'link' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('link')}
|
||||
size="sm"
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
)}
|
||||
{content?.url && content?.type === 'url' && (
|
||||
<Stack>
|
||||
<SwitchWithLabel
|
||||
label={'Display controls'}
|
||||
initialValue={
|
||||
content?.areControlsDisplayed ??
|
||||
defaultVideoBubbleContent.areControlsDisplayed
|
||||
}
|
||||
onCheckChange={updateControlsDisplay}
|
||||
{displayedTabs.includes('pexels') && (
|
||||
<Button
|
||||
variant={currentTab === 'pexels' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('pexels')}
|
||||
size="sm"
|
||||
>
|
||||
Pexels
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* Body content to be displayed below conditionally based on currentTab */}
|
||||
{currentTab === 'link' && (
|
||||
<VideoLinkEmbedContent
|
||||
content={content}
|
||||
updateUrl={updateUrl}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label={t('editor.blocks.bubbles.audio.settings.autoplay.label')}
|
||||
initialValue={
|
||||
content?.isAutoplayEnabled ??
|
||||
defaultVideoBubbleContent.isAutoplayEnabled
|
||||
}
|
||||
isChecked={
|
||||
content?.isAutoplayEnabled ??
|
||||
defaultVideoBubbleContent.isAutoplayEnabled
|
||||
}
|
||||
isDisabled={content?.areControlsDisplayed === false}
|
||||
onCheckChange={() => updateAutoPlay(!content.isAutoplayEnabled)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{currentTab === 'pexels' && (
|
||||
<PexelsPicker videoSize="medium" onVideoSelect={updateUrl} />
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
|
@ -235,6 +235,14 @@ Used to search for images. You can create an Unsplash app [here](https://unsplas
|
||||
| NEXT_PUBLIC_UNSPLASH_APP_NAME | | Unsplash App name |
|
||||
| NEXT_PUBLIC_UNSPLASH_ACCESS_KEY | | Unsplash API key |
|
||||
|
||||
## Pexels (video picker)
|
||||
|
||||
Used to search for videos. You can create a Pexels app [here](https://www.pexels.com/api/key/)
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| -------------------------- | ------- | -------------- |
|
||||
| NEXT_PUBLIC_PEXELS_API_KEY | | Pexels API key |
|
||||
|
||||
## Tolgee (i18n contribution dev tool)
|
||||
|
||||
<Note>
|
||||
|
13
packages/env/env.ts
vendored
13
packages/env/env.ts
vendored
@ -322,6 +322,17 @@ const unsplashEnv = {
|
||||
},
|
||||
}
|
||||
|
||||
const pexelsEnv = {
|
||||
client: {
|
||||
NEXT_PUBLIC_PEXELS_API_KEY: z.string().min(1).optional(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
NEXT_PUBLIC_PEXELS_API_KEY: getRuntimeVariable(
|
||||
'NEXT_PUBLIC_PEXELS_API_KEY'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const whatsAppEnv = {
|
||||
server: {
|
||||
META_SYSTEM_USER_TOKEN: z.string().min(1).optional(),
|
||||
@ -440,6 +451,7 @@ export const env = createEnv({
|
||||
...giphyEnv.client,
|
||||
...vercelEnv.client,
|
||||
...unsplashEnv.client,
|
||||
...pexelsEnv.client,
|
||||
...sentryEnv.client,
|
||||
...posthogEnv.client,
|
||||
...tolgeeEnv.client,
|
||||
@ -452,6 +464,7 @@ export const env = createEnv({
|
||||
...giphyEnv.runtimeEnv,
|
||||
...vercelEnv.runtimeEnv,
|
||||
...unsplashEnv.runtimeEnv,
|
||||
...pexelsEnv.runtimeEnv,
|
||||
...sentryEnv.runtimeEnv,
|
||||
...posthogEnv.runtimeEnv,
|
||||
...tolgeeEnv.runtimeEnv,
|
||||
|
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@ -238,6 +238,9 @@ importers:
|
||||
papaparse:
|
||||
specifier: 5.4.1
|
||||
version: 5.4.1
|
||||
pexels:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
prettier:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8
|
||||
@ -969,7 +972,7 @@ importers:
|
||||
version: 2.8.8
|
||||
ts-jest:
|
||||
specifier: 29.0.5
|
||||
version: 29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
|
||||
version: 29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(esbuild@0.15.18)(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
|
||||
tsup:
|
||||
specifier: 6.5.0
|
||||
version: 6.5.0(@swc/core@1.3.101)(postcss@8.4.35)(typescript@5.4.5)
|
||||
@ -4083,6 +4086,7 @@ packages:
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
deprecated: Use @eslint/config-array instead
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||
@ -4090,6 +4094,7 @@ packages:
|
||||
|
||||
'@humanwhocodes/object-schema@2.0.2':
|
||||
resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@img/sharp-darwin-arm64@0.33.2':
|
||||
resolution: {integrity: sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==}
|
||||
@ -8489,6 +8494,7 @@ packages:
|
||||
|
||||
glob@7.1.7:
|
||||
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
@ -8497,6 +8503,7 @@ packages:
|
||||
glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
@ -8789,6 +8796,7 @@ packages:
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
@ -10607,6 +10615,9 @@ packages:
|
||||
periscopic@3.1.0:
|
||||
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
||||
|
||||
pexels@1.4.0:
|
||||
resolution: {integrity: sha512-akpLySokCtw9JHGx7yMavOIAHGVP5721rLUONR/cFKjWkLjUXsHrJ5jndMKss9mx7AEMZRXs7loxEb+vLJf6kA==}
|
||||
|
||||
picocolors@1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
|
||||
@ -11417,6 +11428,7 @@ packages:
|
||||
|
||||
rimraf@3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-postcss@4.0.2:
|
||||
@ -24186,6 +24198,12 @@ snapshots:
|
||||
estree-walker: 3.0.3
|
||||
is-reference: 3.0.2
|
||||
|
||||
pexels@1.4.0:
|
||||
dependencies:
|
||||
isomorphic-fetch: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
picocolors@1.0.0: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
@ -26026,7 +26044,7 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-jest@29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5):
|
||||
ts-jest@29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(esbuild@0.15.18)(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
@ -26042,6 +26060,7 @@ snapshots:
|
||||
'@babel/core': 7.22.9
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.22.9)
|
||||
esbuild: 0.15.18
|
||||
|
||||
tsconfck@3.0.3(typescript@5.4.5):
|
||||
optionalDependencies:
|
||||
|
Reference in New Issue
Block a user