🚸 (onboarding) Introduce new onboarding floating videos mechanism
This commit is contained in:
@ -678,3 +678,20 @@ export const BracesIcon = (props: IconProps) => (
|
||||
<path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" />
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const VideoPopoverIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 21 21" {...featherIconsBaseProps} {...props}>
|
||||
<path
|
||||
d="M6.9 18.0079C8.80858 18.9869 11.0041 19.2521 13.0909 18.7556C15.1777 18.2592 17.0186 17.0337 18.2818 15.3C19.545 13.5664 20.1474 11.4386 19.9806 9.30002C19.8137 7.16147 18.8886 5.15283 17.3718 3.63605C15.855 2.11928 13.8464 1.19411 11.7078 1.02728C9.56929 0.860441 7.44147 1.46291 5.70782 2.72611C3.97417 3.98931 2.74869 5.83017 2.25222 7.91697C1.75575 10.0038 2.02094 12.1993 3 14.1079L1 20.0079L6.9 18.0079Z"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 6L15 10L9 14V6Z"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -18,27 +18,19 @@ import {
|
||||
SettingsIcon,
|
||||
} from '@/components/icons'
|
||||
import { useTypebot } from '../providers/TypebotProvider'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { EditorSettingsModal } from './EditorSettingsModal'
|
||||
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { useUser } from '@/features/account/hooks/useUser'
|
||||
import { useRouter } from 'next/router'
|
||||
import { RightPanel, useEditor } from '../providers/EditorProvider'
|
||||
|
||||
export const BoardMenuButton = (props: StackProps) => {
|
||||
const { query } = useRouter()
|
||||
const { typebot, currentUserMode } = useTypebot()
|
||||
const { user } = useUser()
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { t } = useTranslate()
|
||||
const { setRightPanel } = useEditor()
|
||||
|
||||
useEffect(() => {
|
||||
if (user && !user.graphNavigation && !query.isFirstBot) onOpen()
|
||||
}, [onOpen, query.isFirstBot, user, user?.graphNavigation])
|
||||
|
||||
const downloadFlow = () => {
|
||||
assert(typebot)
|
||||
setIsDownloading(true)
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
import { useTypebot } from '../providers/TypebotProvider'
|
||||
import { BlocksSideBar } from './BlocksSideBar'
|
||||
import { BoardMenuButton } from './BoardMenuButton'
|
||||
import { GettingStartedModal } from './GettingStartedModal'
|
||||
import { PreviewDrawer } from '@/features/preview/components/PreviewDrawer'
|
||||
import { TypebotHeader } from './TypebotHeader'
|
||||
import { Graph } from '@/features/graph/components/Graph'
|
||||
@ -19,6 +18,7 @@ import { TypebotNotFoundPage } from './TypebotNotFoundPage'
|
||||
import { SuspectedTypebotBanner } from './SuspectedTypebotBanner'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { VariablesDrawer } from '@/features/preview/components/VariablesDrawer'
|
||||
import { VideoOnboardingFloatingWindow } from '@/features/onboarding/components/VideoOnboardingFloatingWindow'
|
||||
|
||||
export const EditorPage = () => {
|
||||
const { typebot, currentUserMode, is404 } = useTypebot()
|
||||
@ -32,11 +32,12 @@ export const EditorPage = () => {
|
||||
const isSuspicious = typebot?.riskLevel === 100 && !workspace?.isVerified
|
||||
|
||||
if (is404) return <TypebotNotFoundPage />
|
||||
|
||||
return (
|
||||
<EditorProvider>
|
||||
<Seo title={typebot?.name ? `${typebot.name} | Editor` : 'Editor'} />
|
||||
<Flex overflow="clip" h="100vh" flexDir="column" id="editor-container">
|
||||
<GettingStartedModal />
|
||||
<VideoOnboardingFloatingWindow type="editor" />
|
||||
{isSuspicious && <SuspectedTypebotBanner typebotId={typebot.id} />}
|
||||
<TypebotHeader />
|
||||
<Flex
|
||||
|
@ -1,174 +0,0 @@
|
||||
import {
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
Heading,
|
||||
List,
|
||||
ListItem,
|
||||
Text,
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
HStack,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
|
||||
export const GettingStartedModal = () => {
|
||||
const { t } = useTranslate()
|
||||
const { query } = useRouter()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isFirstBot) onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing="8" py="10">
|
||||
<Stack spacing={4}>
|
||||
<Heading fontSize="xl">
|
||||
{t('editor.gettingStartedModal.editorBasics.heading')}
|
||||
</Heading>
|
||||
<List spacing={4}>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
fontSize="13px"
|
||||
>
|
||||
1
|
||||
</Flex>
|
||||
<Text>
|
||||
{t('editor.gettingStartedModal.editorBasics.list.one.label')}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
fontSize="13px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
>
|
||||
2
|
||||
</Flex>
|
||||
<Text>
|
||||
{t('editor.gettingStartedModal.editorBasics.list.two.label')}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
fontSize="13px"
|
||||
>
|
||||
3
|
||||
</Flex>
|
||||
<Text>
|
||||
{t(
|
||||
'editor.gettingStartedModal.editorBasics.list.three.label'
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
fontSize="13px"
|
||||
>
|
||||
4
|
||||
</Flex>
|
||||
<Text>
|
||||
{t('editor.gettingStartedModal.editorBasics.list.four.label')}
|
||||
</Text>
|
||||
</HStack>
|
||||
</List>
|
||||
</Stack>
|
||||
|
||||
<Text>{t('editor.gettingStartedModal.editorBasics.list.label')}</Text>
|
||||
<Stack spacing={4}>
|
||||
<Heading fontSize="xl">
|
||||
{t('editor.gettingStartedModal.seeAction.label')} ({`<`}{' '}
|
||||
{t('editor.gettingStartedModal.seeAction.time')})
|
||||
</Heading>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/jp3ggg_42-M"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '0.5rem', border: 'none' }}
|
||||
/>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
{t('editor.gettingStartedModal.seeAction.item.label')}
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={10} as={Stack} spacing="10">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/6BudIC4GYNk"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '0.5rem', border: 'none' }}
|
||||
/>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/ZuyDwFLRbfQ"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '0.5rem', border: 'none' }}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -60,7 +60,7 @@ export const TypebotHeader = () => {
|
||||
justify="center"
|
||||
align="center"
|
||||
h={`${headerHeight}px`}
|
||||
zIndex={100}
|
||||
zIndex={1}
|
||||
pos="relative"
|
||||
bgColor={headerBgColor}
|
||||
flexShrink={0}
|
||||
|
@ -8,9 +8,8 @@ import { useTypebotDnd } from '../TypebotDndProvider'
|
||||
|
||||
export const CreateBotButton = ({
|
||||
folderId,
|
||||
isFirstBot,
|
||||
...props
|
||||
}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
|
||||
}: { folderId?: string } & ButtonProps) => {
|
||||
const { t } = useTranslate()
|
||||
const router = useRouter()
|
||||
const { draggedTypebot } = useTypebotDnd()
|
||||
@ -18,7 +17,6 @@ export const CreateBotButton = ({
|
||||
const handleClick = () =>
|
||||
router.push(
|
||||
`/typebots/create?${stringify({
|
||||
isFirstBot: !isFirstBot ? undefined : isFirstBot,
|
||||
folderId,
|
||||
})}`
|
||||
)
|
||||
|
@ -187,7 +187,6 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
<CreateBotButton
|
||||
folderId={folder?.id}
|
||||
isLoading={isTypebotLoading}
|
||||
isFirstBot={typebots?.length === 0 && folder === null}
|
||||
/>
|
||||
)}
|
||||
{isFolderLoading && <ButtonSkeleton />}
|
||||
|
@ -8,17 +8,26 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { BlockWithOptions } from '@typebot.io/schemas'
|
||||
import { getHelpDocUrl } from '@/features/graph/helpers/getHelpDocUrl'
|
||||
import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { VideoOnboardingPopover } from '@/features/onboarding/components/VideoOnboardingPopover'
|
||||
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
|
||||
|
||||
type Props = {
|
||||
blockType: BlockWithOptions['type']
|
||||
blockDef?: (typeof forgedBlocks)[keyof typeof forgedBlocks]
|
||||
isVideoOnboardingItemDisplayed: boolean
|
||||
onExpandClick: () => void
|
||||
onVideoOnboardingClick: () => void
|
||||
}
|
||||
|
||||
export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
||||
export const SettingsHoverBar = ({
|
||||
blockType,
|
||||
blockDef,
|
||||
isVideoOnboardingItemDisplayed,
|
||||
onExpandClick,
|
||||
onVideoOnboardingClick,
|
||||
}: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const { blockDef } = useForgedBlock(blockType)
|
||||
const helpDocUrl = getHelpDocUrl(blockType, blockDef)
|
||||
return (
|
||||
<HStack
|
||||
@ -43,6 +52,10 @@ export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
||||
as={Link}
|
||||
leftIcon={<BuoyIcon />}
|
||||
borderLeftRadius="none"
|
||||
borderRightRadius={
|
||||
isVideoOnboardingItemDisplayed ? 'none' : undefined
|
||||
}
|
||||
borderRightWidth={isVideoOnboardingItemDisplayed ? '1px' : undefined}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
href={helpDocUrl}
|
||||
@ -51,6 +64,13 @@ export const SettingsHoverBar = ({ blockType, onExpandClick }: Props) => {
|
||||
{t('help')}
|
||||
</Button>
|
||||
)}
|
||||
{isVideoOnboardingItemDisplayed && (
|
||||
<VideoOnboardingPopover.TriggerIconButton
|
||||
onClick={onVideoOnboardingClick}
|
||||
size="xs"
|
||||
borderLeftRadius="none"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
@ -45,6 +45,9 @@ import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integr
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { ForgedBlockSettings } from '../../../../forge/components/ForgedBlockSettings'
|
||||
import { OpenAISettings } from '@/features/blocks/integrations/openai/components/OpenAISettings'
|
||||
import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
|
||||
import { VideoOnboardingPopover } from '@/features/onboarding/components/VideoOnboardingPopover'
|
||||
import { hasOnboardingVideo } from '@/features/onboarding/helpers/hasOnboardingVideo'
|
||||
|
||||
type Props = {
|
||||
block: BlockWithOptions
|
||||
@ -56,6 +59,7 @@ type Props = {
|
||||
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
const arrowColor = useColorModeValue('white', 'gray.800')
|
||||
const { blockDef } = useForgedBlock(props.block.type)
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||
|
||||
@ -63,10 +67,17 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
useEventListener('wheel', handleMouseWheel, ref.current)
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
||||
<PopoverArrow bgColor={arrowColor} />
|
||||
|
||||
<VideoOnboardingPopover.Root
|
||||
type={props.block.type}
|
||||
blockDef={blockDef}
|
||||
>
|
||||
{({ onToggle }) => (
|
||||
<PopoverBody
|
||||
py="3"
|
||||
overflowY="auto"
|
||||
@ -89,13 +100,21 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
<SlideFade in={isHovering} unmountOnExit>
|
||||
<SettingsHoverBar
|
||||
onExpandClick={onExpandClick}
|
||||
onVideoOnboardingClick={onToggle}
|
||||
blockType={props.block.type}
|
||||
blockDef={blockDef}
|
||||
isVideoOnboardingItemDisplayed={hasOnboardingVideo({
|
||||
blockType: props.block.type,
|
||||
blockDef,
|
||||
})}
|
||||
/>
|
||||
</SlideFade>
|
||||
</Flex>
|
||||
<BlockSettings {...props} />
|
||||
</Stack>
|
||||
</PopoverBody>
|
||||
)}
|
||||
</VideoOnboardingPopover.Root>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
)
|
||||
|
@ -102,7 +102,7 @@ export const OnboardingPage = () => {
|
||||
setTimeout(() => {
|
||||
replace({
|
||||
pathname: '/typebots',
|
||||
query: { ...query, isFirstBot: true },
|
||||
query: { ...query },
|
||||
})
|
||||
}, 2000)
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import { CloseIcon } from '@/components/icons'
|
||||
import { useUser } from '@/features/account/hooks/useUser'
|
||||
import {
|
||||
IconButton,
|
||||
SlideFade,
|
||||
Flex,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
import { useOnboardingDisclosure } from '../hooks/useOnboardingDisclosure'
|
||||
import { onboardingVideos } from '../data'
|
||||
import { YoutubeIframe } from './YoutubeIframe'
|
||||
|
||||
type Props = {
|
||||
type: keyof typeof onboardingVideos
|
||||
}
|
||||
|
||||
export const VideoOnboardingFloatingWindow = ({ type }: Props) => {
|
||||
const { user, updateUser } = useUser()
|
||||
const { isOpen, onClose } = useOnboardingDisclosure({
|
||||
key: type,
|
||||
user,
|
||||
updateUser,
|
||||
defaultOpenDelay: 1000,
|
||||
blockDef: undefined,
|
||||
})
|
||||
const bgColor = useColorModeValue('white', 'gray.900')
|
||||
const closeButtonColorScheme = useColorModeValue('blackAlpha', 'gray')
|
||||
|
||||
if (!onboardingVideos[type]) return null
|
||||
|
||||
return (
|
||||
<SlideFade
|
||||
in={isOpen}
|
||||
offsetY="20px"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '18px',
|
||||
right: '18px',
|
||||
zIndex: 42,
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<Flex
|
||||
p="5"
|
||||
bgColor={bgColor}
|
||||
borderWidth="1px"
|
||||
shadow="xl"
|
||||
rounded="md"
|
||||
aspectRatio="1.5"
|
||||
w="600px"
|
||||
>
|
||||
<YoutubeIframe id={onboardingVideos[type]!.youtubeId} />
|
||||
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
aria-label={'Close'}
|
||||
pos="absolute"
|
||||
top="-3"
|
||||
right="-3"
|
||||
colorScheme={closeButtonColorScheme}
|
||||
size="sm"
|
||||
rounded="full"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Flex>
|
||||
</SlideFade>
|
||||
)
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import { CloseIcon, VideoPopoverIcon } from '@/components/icons'
|
||||
import {
|
||||
PopoverContent,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
IconButtonProps,
|
||||
} from '@chakra-ui/react'
|
||||
import { useOnboardingDisclosure } from '../hooks/useOnboardingDisclosure'
|
||||
import { onboardingVideos } from '../data'
|
||||
import { useUser } from '@/features/account/hooks/useUser'
|
||||
import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
|
||||
import { YoutubeIframe } from './YoutubeIframe'
|
||||
|
||||
type Props = {
|
||||
type: keyof typeof onboardingVideos
|
||||
defaultIsOpen?: boolean
|
||||
blockDef?: ForgedBlockDefinition
|
||||
children: ({ onToggle }: { onToggle: () => void }) => JSX.Element
|
||||
}
|
||||
|
||||
const Root = ({ type, blockDef, children }: Props) => {
|
||||
const { user, updateUser } = useUser()
|
||||
const youtubeId =
|
||||
onboardingVideos[type]?.youtubeId ?? blockDef?.onboarding?.youtubeId
|
||||
const { isOpen, onClose, onToggle } = useOnboardingDisclosure({
|
||||
key: type,
|
||||
updateUser,
|
||||
user,
|
||||
blockDef,
|
||||
})
|
||||
|
||||
if (!youtubeId) return children({ onToggle })
|
||||
|
||||
return (
|
||||
<Popover isOpen={isOpen} placement="right" isLazy>
|
||||
<PopoverTrigger>{children({ onToggle })}</PopoverTrigger>
|
||||
|
||||
<PopoverContent aspectRatio="1.5" width="640px">
|
||||
<PopoverArrow />
|
||||
<PopoverBody h="full" p="5">
|
||||
<YoutubeIframe id={youtubeId} />
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
aria-label={'Close'}
|
||||
pos="absolute"
|
||||
top="-3"
|
||||
right="-3"
|
||||
colorScheme="blackAlpha"
|
||||
size="sm"
|
||||
rounded="full"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const TriggerIconButton = (props: Omit<IconButtonProps, 'aria-label'>) => (
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={<VideoPopoverIcon />}
|
||||
aria-label={'Open Bubbles help video'}
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export const VideoOnboardingPopover = {
|
||||
Root,
|
||||
TriggerIconButton,
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import { chakra } from '@chakra-ui/react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
declare const window: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
YT:
|
||||
| undefined
|
||||
| {
|
||||
Player: new (
|
||||
id: string,
|
||||
options: { events: { onReady: () => void } }
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) => any
|
||||
loaded: boolean
|
||||
}
|
||||
onYouTubeIframeAPIReady: () => void
|
||||
}
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const YoutubeIframe = ({ id }: Props) => {
|
||||
const initPlayer = useCallback(() => {
|
||||
if (!window.YT) return
|
||||
const player = new window.YT.Player(id, {
|
||||
events: {
|
||||
onReady: () => {
|
||||
player.setPlaybackRate(1.2)
|
||||
player.setPlaybackQuality('')
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
if (window.YT?.loaded) initPlayer()
|
||||
initYoutubeIframeApi()
|
||||
window.onYouTubeIframeAPIReady = initPlayer
|
||||
}, [initPlayer])
|
||||
|
||||
return (
|
||||
<chakra.iframe
|
||||
id={id}
|
||||
src={`https://www.youtube.com/embed/${id}?autoplay=1&enablejsapi=1`}
|
||||
allowFullScreen
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
boxSize="full"
|
||||
rounded="md"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const initYoutubeIframeApi = () => {
|
||||
if (document.getElementById('youtube-iframe-api')) return
|
||||
const tag = document.createElement('script')
|
||||
tag.src = 'https://www.youtube.com/iframe_api'
|
||||
tag.id = 'youtube-iframe-api'
|
||||
document.head.appendChild(tag)
|
||||
}
|
32
apps/builder/src/features/onboarding/data.ts
Normal file
32
apps/builder/src/features/onboarding/data.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Block } from '@typebot.io/schemas'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
|
||||
type Feature = 'editor' | Block['type']
|
||||
|
||||
export const onboardingVideos: Partial<
|
||||
Record<
|
||||
Feature,
|
||||
| {
|
||||
key: string
|
||||
youtubeId: string
|
||||
deployedAt: Date
|
||||
}
|
||||
| undefined
|
||||
>
|
||||
> = {
|
||||
editor: {
|
||||
key: 'editor',
|
||||
youtubeId: 'jp3ggg_42-M',
|
||||
deployedAt: new Date('2024-06-04'),
|
||||
},
|
||||
[IntegrationBlockType.ZAPIER]: {
|
||||
key: IntegrationBlockType.ZAPIER,
|
||||
youtubeId: '2ZskGItI_Zo',
|
||||
deployedAt: new Date('2024-06-04'),
|
||||
},
|
||||
[IntegrationBlockType.MAKE_COM]: {
|
||||
key: IntegrationBlockType.MAKE_COM,
|
||||
youtubeId: 'V-y1Orys_kY',
|
||||
deployedAt: new Date('2024-06-04'),
|
||||
},
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
|
||||
import { Block } from '@typebot.io/schemas'
|
||||
import { onboardingVideos } from '../data'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
|
||||
type Props = {
|
||||
blockType: Block['type']
|
||||
blockDef?: ForgedBlockDefinition
|
||||
}
|
||||
export const hasOnboardingVideo = ({ blockType, blockDef }: Props) =>
|
||||
isDefined(
|
||||
blockDef?.onboarding?.youtubeId ?? onboardingVideos[blockType]?.youtubeId
|
||||
)
|
@ -0,0 +1,65 @@
|
||||
import { useDisclosure } from '@chakra-ui/react'
|
||||
import { onboardingVideos } from '../data'
|
||||
import { User } from '@typebot.io/schemas'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
|
||||
|
||||
type Props = {
|
||||
key?: keyof typeof onboardingVideos
|
||||
updateUser: (data: Partial<User>) => void
|
||||
user?: Pick<User, 'createdAt' | 'displayedInAppNotifications'>
|
||||
defaultOpenDelay?: number
|
||||
blockDef: ForgedBlockDefinition | undefined
|
||||
}
|
||||
|
||||
export const useOnboardingDisclosure = ({
|
||||
key,
|
||||
updateUser,
|
||||
user,
|
||||
defaultOpenDelay,
|
||||
blockDef,
|
||||
}: Props) => {
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const { isOpen, onOpen, onClose, onToggle } = useDisclosure({
|
||||
onOpen: () => {
|
||||
if (!user || !key || user.displayedInAppNotifications?.[key]) return
|
||||
updateUser({
|
||||
displayedInAppNotifications: {
|
||||
...user.displayedInAppNotifications,
|
||||
[key]: true,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized || !user?.createdAt || !key) return
|
||||
setIsInitialized(true)
|
||||
if (
|
||||
key &&
|
||||
new Date(user.createdAt) >=
|
||||
(onboardingVideos[key]
|
||||
? onboardingVideos[key]!.deployedAt
|
||||
: blockDef?.onboarding?.deployedAt ?? new Date()) &&
|
||||
user.displayedInAppNotifications?.[key] === undefined
|
||||
) {
|
||||
if (defaultOpenDelay) {
|
||||
setTimeout(() => {
|
||||
onOpen()
|
||||
}, defaultOpenDelay)
|
||||
} else {
|
||||
onOpen()
|
||||
}
|
||||
}
|
||||
}, [
|
||||
blockDef?.onboarding?.deployedAt,
|
||||
defaultOpenDelay,
|
||||
isInitialized,
|
||||
key,
|
||||
onOpen,
|
||||
user?.createdAt,
|
||||
user?.displayedInAppNotifications,
|
||||
])
|
||||
|
||||
return { isOpen, onClose, onToggle }
|
||||
}
|
@ -42,12 +42,6 @@ export const CreateNewTypebotButtons = () => {
|
||||
onSuccess: (data) => {
|
||||
router.push({
|
||||
pathname: `/typebots/${data.typebot.id}/edit`,
|
||||
query:
|
||||
router.query.isFirstBot === 'true'
|
||||
? {
|
||||
isFirstBot: 'true',
|
||||
}
|
||||
: {},
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
@ -68,12 +62,6 @@ export const CreateNewTypebotButtons = () => {
|
||||
onSuccess: (data) => {
|
||||
router.push({
|
||||
pathname: `/typebots/${data.typebot.id}/edit`,
|
||||
query:
|
||||
router.query.isFirstBot === 'true'
|
||||
? {
|
||||
isFirstBot: 'true',
|
||||
}
|
||||
: {},
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OnboardingPage } from '@/features/auth/components/OnboardingPage'
|
||||
import { OnboardingPage } from '@/features/onboarding/components/OnboardingPage'
|
||||
|
||||
export default function Page() {
|
||||
return <OnboardingPage />
|
||||
|
@ -136,6 +136,10 @@ export type BlockDefinition<
|
||||
LightLogo: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||
DarkLogo?: (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||
docsUrl?: string
|
||||
onboarding?: {
|
||||
deployedAt: Date
|
||||
youtubeId: string
|
||||
}
|
||||
auth?: Auth
|
||||
options?: Options | undefined
|
||||
fetchers?: FetcherDefinition<Auth, Options>[]
|
||||
|
Reference in New Issue
Block a user