feat(bubbles): ✨ Add video bubble
This commit is contained in:
@ -267,3 +267,16 @@ export const ExternalLinkIcon = (props: IconProps) => (
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const FilmIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect>
|
||||
<line x1="7" y1="2" x2="7" y2="22"></line>
|
||||
<line x1="17" y1="2" x2="17" y2="22"></line>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<line x1="2" y1="7" x2="7" y2="7"></line>
|
||||
<line x1="2" y1="17" x2="7" y2="17"></line>
|
||||
<line x1="17" y1="17" x2="22" y2="17"></line>
|
||||
<line x1="17" y1="7" x2="22" y2="7"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
EditIcon,
|
||||
EmailIcon,
|
||||
ExternalLinkIcon,
|
||||
FilmIcon,
|
||||
FilterIcon,
|
||||
FlagIcon,
|
||||
GlobeIcon,
|
||||
@ -32,6 +33,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
return <ChatIcon {...props} />
|
||||
case BubbleStepType.IMAGE:
|
||||
return <ImageIcon {...props} />
|
||||
case BubbleStepType.VIDEO:
|
||||
return <FilmIcon {...props} />
|
||||
case InputStepType.TEXT:
|
||||
return <TextIcon {...props} />
|
||||
case InputStepType.NUMBER:
|
||||
|
@ -13,54 +13,43 @@ type Props = { type: StepType }
|
||||
export const StepTypeLabel = ({ type }: Props) => {
|
||||
switch (type) {
|
||||
case BubbleStepType.TEXT:
|
||||
case InputStepType.TEXT: {
|
||||
case InputStepType.TEXT:
|
||||
return <Text>Text</Text>
|
||||
}
|
||||
case BubbleStepType.IMAGE:
|
||||
return <Text>Image</Text>
|
||||
case InputStepType.NUMBER: {
|
||||
case BubbleStepType.VIDEO:
|
||||
return <Text>Video</Text>
|
||||
case InputStepType.NUMBER:
|
||||
return <Text>Number</Text>
|
||||
}
|
||||
case InputStepType.EMAIL: {
|
||||
case InputStepType.EMAIL:
|
||||
return <Text>Email</Text>
|
||||
}
|
||||
case InputStepType.URL: {
|
||||
case InputStepType.URL:
|
||||
return <Text>Website</Text>
|
||||
}
|
||||
case InputStepType.DATE: {
|
||||
case InputStepType.DATE:
|
||||
return <Text>Date</Text>
|
||||
}
|
||||
case InputStepType.PHONE: {
|
||||
case InputStepType.PHONE:
|
||||
return <Text>Phone</Text>
|
||||
}
|
||||
case InputStepType.CHOICE: {
|
||||
case InputStepType.CHOICE:
|
||||
return <Text>Button</Text>
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
return <Text>Set variable</Text>
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
case LogicStepType.CONDITION:
|
||||
return <Text>Condition</Text>
|
||||
}
|
||||
case LogicStepType.REDIRECT: {
|
||||
case LogicStepType.REDIRECT:
|
||||
return <Text>Redirect</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return (
|
||||
<Tooltip label="Google Sheets">
|
||||
<Text>Sheets</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
return (
|
||||
<Tooltip label="Google Analytics">
|
||||
<Text>Analytics</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,17 @@ import {
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
} from '@chakra-ui/react'
|
||||
import { ImagePopoverContent } from 'components/shared/ImageUploadContent'
|
||||
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepContent,
|
||||
BubbleStepType,
|
||||
ImageBubbleContent,
|
||||
ImageBubbleStep,
|
||||
TextBubbleStep,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
import { VideoUploadContent } from './VideoUploadContent'
|
||||
|
||||
type Props = {
|
||||
step: Exclude<BubbleStep, TextBubbleStep>
|
||||
@ -25,7 +26,7 @@ export const ContentPopover = ({ step }: Props) => {
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<PopoverContent onMouseDown={handleMouseDown} w="500px">
|
||||
<PopoverContent onMouseDown={handleMouseDown}>
|
||||
<PopoverArrow />
|
||||
<PopoverBody ref={ref} shadow="lg">
|
||||
<StepContent step={step} />
|
||||
@ -37,16 +38,24 @@ export const ContentPopover = ({ step }: Props) => {
|
||||
|
||||
export const StepContent = ({ step }: Props) => {
|
||||
const { updateStep } = useTypebot()
|
||||
const handleContentChange = (content: ImageBubbleContent) =>
|
||||
|
||||
const handleContentChange = (content: BubbleStepContent) =>
|
||||
updateStep(step.id, { content } as Partial<ImageBubbleStep>)
|
||||
|
||||
const handleNewImageSubmit = (url: string) => handleContentChange({ url })
|
||||
switch (step.type) {
|
||||
case BubbleStepType.IMAGE: {
|
||||
return (
|
||||
<ImagePopoverContent
|
||||
url={step.content?.url}
|
||||
onSubmit={handleNewImageSubmit}
|
||||
<ImageUploadContent
|
||||
content={step.content}
|
||||
onSubmit={handleContentChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.VIDEO: {
|
||||
return (
|
||||
<VideoUploadContent
|
||||
content={step.content}
|
||||
onSubmit={handleContentChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
|
||||
import { VideoBubbleContent, VideoBubbleContentType } from 'models'
|
||||
import urlParser from 'js-video-url-parser/lib/base'
|
||||
import 'js-video-url-parser/lib/provider/vimeo'
|
||||
import 'js-video-url-parser/lib/provider/youtube'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
content?: VideoBubbleContent
|
||||
onSubmit: (content: VideoBubbleContent) => void
|
||||
}
|
||||
|
||||
export const VideoUploadContent = ({ content, onSubmit }: Props) => {
|
||||
const handleUrlChange = (url: string) => {
|
||||
const info = urlParser.parse(url)
|
||||
return isDefined(info) && info.provider && info.id
|
||||
? onSubmit({
|
||||
type: info.provider as VideoBubbleContentType,
|
||||
url,
|
||||
id: info.id,
|
||||
})
|
||||
: onSubmit({ type: VideoBubbleContentType.URL, url })
|
||||
}
|
||||
return (
|
||||
<Stack p="2">
|
||||
<InputWithVariableButton
|
||||
placeholder="Paste the video link..."
|
||||
initialValue={content?.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
delay={100}
|
||||
/>
|
||||
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||
Works with Youtube, Vimeo and others
|
||||
</Text>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -9,6 +9,8 @@ import {
|
||||
SetVariableStep,
|
||||
ConditionStep,
|
||||
IntegrationStepType,
|
||||
VideoBubbleStep,
|
||||
VideoBubbleContentType,
|
||||
} from 'models'
|
||||
import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
|
||||
import { SourceEndpoint } from './SourceEndpoint'
|
||||
@ -48,6 +50,9 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.VIDEO: {
|
||||
return <VideoStepNodeContent step={step} />
|
||||
}
|
||||
case InputStepType.TEXT: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
@ -182,3 +187,52 @@ const ConditionNodeContent = ({ step }: { step: ConditionStep }) => {
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const VideoStepNodeContent = ({ step }: { step: VideoBubbleStep }) => {
|
||||
if (!step.content?.url || !step.content.type)
|
||||
return <Text color="gray.500">Click to edit...</Text>
|
||||
switch (step.content.type) {
|
||||
case VideoBubbleContentType.URL:
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<video
|
||||
key={step.content.url}
|
||||
controls
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<source src={step.content.url} />
|
||||
</video>
|
||||
</Box>
|
||||
)
|
||||
case VideoBubbleContentType.VIMEO:
|
||||
case VideoBubbleContentType.YOUTUBE: {
|
||||
const baseUrl =
|
||||
step.content.type === VideoBubbleContentType.VIMEO
|
||||
? 'https://player.vimeo.com/video'
|
||||
: 'https://www.youtube.com/embed'
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<iframe
|
||||
src={`${baseUrl}/${step.content.id}`}
|
||||
allowFullScreen
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,17 +4,19 @@ import { SearchContextManager } from '@giphy/react-components'
|
||||
import { UploadButton } from '../buttons/UploadButton'
|
||||
import { GiphySearch } from './GiphySearch'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ImageBubbleContent } from 'models'
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
onSubmit: (url: string) => void
|
||||
content?: ImageBubbleContent
|
||||
onSubmit: (content: ImageBubbleContent) => void
|
||||
}
|
||||
|
||||
export const ImageUploadContent = ({ url, onSubmit }: Props) => {
|
||||
export const ImageUploadContent = ({ content, onSubmit }: Props) => {
|
||||
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
|
||||
'upload'
|
||||
)
|
||||
|
||||
const handleSubmit = (url: string) => onSubmit({ url })
|
||||
return (
|
||||
<Stack>
|
||||
<HStack>
|
||||
@ -43,7 +45,11 @@ export const ImageUploadContent = ({ url, onSubmit }: Props) => {
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<BodyContent tab={currentTab} onSubmit={onSubmit} url={url} />
|
||||
<BodyContent
|
||||
tab={currentTab}
|
||||
onSubmit={handleSubmit}
|
||||
url={content?.url}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { ImageUploadContent as ImagePopoverContent } from './ImageUploadContent'
|
||||
export { ImageUploadContent } from './ImageUploadContent'
|
||||
|
116
apps/builder/cypress/tests/bubbles/video.ts
Normal file
116
apps/builder/cypress/tests/bubbles/video.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
import { BubbleStepType, Step, VideoBubbleContentType } from 'models'
|
||||
|
||||
const videoSrc =
|
||||
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
|
||||
const youtubeVideoSrc = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
const vimeoVideoSrc = 'https://vimeo.com/649301125'
|
||||
|
||||
describe('Video bubbles', () => {
|
||||
afterEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
})
|
||||
})
|
||||
describe('Content settings', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
it('upload image file correctly', () => {
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByText('Click to edit...').click()
|
||||
cy.findByPlaceholderText('Paste the video link...').type(videoSrc, {
|
||||
waitForAnimations: false,
|
||||
})
|
||||
cy.get('video > source').should('have.attr', 'src').should('eq', videoSrc)
|
||||
|
||||
cy.findByPlaceholderText('Paste the video link...')
|
||||
.clear()
|
||||
.type(youtubeVideoSrc, {
|
||||
waitForAnimations: false,
|
||||
})
|
||||
cy.get('iframe')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://www.youtube.com/embed/dQw4w9WgXcQ')
|
||||
|
||||
cy.findByPlaceholderText('Paste the video link...')
|
||||
.clear()
|
||||
.type(vimeoVideoSrc, {
|
||||
waitForAnimations: false,
|
||||
})
|
||||
cy.get('iframe')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://player.vimeo.com/video/649301125')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Preview', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
it('should display video correctly', () => {
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
content: {
|
||||
type: VideoBubbleContentType.URL,
|
||||
url: videoSrc,
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.get('video > source')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', videoSrc)
|
||||
})
|
||||
|
||||
it('should display youtube iframe correctly', () => {
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
content: {
|
||||
type: VideoBubbleContentType.YOUTUBE,
|
||||
url: youtubeVideoSrc,
|
||||
id: 'dQw4w9WgXcQ',
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.get('iframe')
|
||||
.first()
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://www.youtube.com/embed/dQw4w9WgXcQ')
|
||||
})
|
||||
|
||||
it('should display vimeo iframe correctly', () => {
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
content: {
|
||||
type: VideoBubbleContentType.VIMEO,
|
||||
url: vimeoVideoSrc,
|
||||
id: '649301125',
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.get('iframe')
|
||||
.first()
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://player.vimeo.com/video/649301125')
|
||||
})
|
||||
})
|
||||
})
|
@ -38,6 +38,7 @@
|
||||
"google-spreadsheet": "^3.2.0",
|
||||
"htmlparser2": "^7.2.0",
|
||||
"immer": "^9.0.7",
|
||||
"js-video-url-parser": "^0.5.1",
|
||||
"kbar": "^0.1.0-beta.24",
|
||||
"micro": "^9.3.4",
|
||||
"micro-cors": "^0.1.1",
|
||||
|
Reference in New Issue
Block a user