feat: ✨ Add new onboarding flow
This commit is contained in:
@@ -122,7 +122,11 @@ export const PersonalInfoForm = () => {
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<Flex justifyContent="flex-end">
|
||||
<Button colorScheme="blue" onClick={saveUser} isLoading={isSaving}>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={() => saveUser()}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { ButtonSkeleton, FolderButton } from './FolderContent/FolderButton'
|
||||
import { SharedTypebotsButton } from './FolderContent/SharedTypebotsButton'
|
||||
import { TypebotButton } from './FolderContent/TypebotButton'
|
||||
import { TypebotCardOverlay } from './FolderContent/TypebotButtonOverlay'
|
||||
import { OnboardingModal } from './OnboardingModal'
|
||||
|
||||
type Props = { folder: DashboardFolder | null }
|
||||
|
||||
@@ -163,6 +164,9 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
|
||||
return (
|
||||
<Flex w="full" flex="1" justify="center">
|
||||
{typebots && user && folder === null && (
|
||||
<OnboardingModal totalTypebots={typebots.length} />
|
||||
)}
|
||||
<Stack w="1000px" spacing={6}>
|
||||
<Skeleton isLoaded={folder?.name !== undefined}>
|
||||
<Heading as="h1">{folder?.name}</Heading>
|
||||
@@ -179,6 +183,7 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
<CreateBotButton
|
||||
folderId={folder?.id}
|
||||
isLoading={isTypebotLoading}
|
||||
isFirstBot={typebots?.length === 0 && folder === null}
|
||||
/>
|
||||
{totalSharedTypebots > 0 && <SharedTypebotsButton />}
|
||||
{isFolderLoading && <ButtonSkeleton />}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Button, ButtonProps, Text, VStack } from '@chakra-ui/react'
|
||||
import { PlusIcon } from 'assets/icons'
|
||||
import { useRouter } from 'next/router'
|
||||
import { stringify } from 'qs'
|
||||
import React from 'react'
|
||||
|
||||
export const CreateBotButton = ({
|
||||
folderId,
|
||||
isFirstBot,
|
||||
...props
|
||||
}: { folderId?: string } & ButtonProps) => {
|
||||
}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
|
||||
const router = useRouter()
|
||||
|
||||
const handleClick = () =>
|
||||
folderId
|
||||
? router.push(`/typebots/create?folderId=${folderId}`)
|
||||
: router.push('/typebots/create')
|
||||
router.push(
|
||||
`/typebots/create?${stringify({
|
||||
isFirstBot: !isFirstBot ? undefined : isFirstBot,
|
||||
folderId,
|
||||
})}`
|
||||
)
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
176
apps/builder/components/dashboard/OnboardingModal.tsx
Normal file
176
apps/builder/components/dashboard/OnboardingModal.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
chakra,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { Answer, Typebot } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
import { sendRequest } from 'utils'
|
||||
import confetti from 'canvas-confetti'
|
||||
|
||||
type Props = { totalTypebots: number }
|
||||
|
||||
export const OnboardingModal = ({ totalTypebots }: Props) => {
|
||||
const { user, saveUser } = useUser()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [typebot, setTypebot] = useState<Typebot>()
|
||||
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
|
||||
const confettiCanon = useRef<confetti.CreateTypes>()
|
||||
const [chosenCategories, setChosenCategories] = useState<string[]>([])
|
||||
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplate()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const isNewUser =
|
||||
user &&
|
||||
new Date(user?.createdAt as unknown as string).toDateString() ===
|
||||
new Date().toDateString() &&
|
||||
totalTypebots === 0
|
||||
if (isNewUser) onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
initConfettis()
|
||||
return () => {
|
||||
window.removeEventListener('message', handleIncomingMessage)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [confettiCanvaContainer.current])
|
||||
|
||||
const initConfettis = () => {
|
||||
if (!confettiCanvaContainer.current || confettiCanon.current) return
|
||||
confettiCanon.current = confetti.create(confettiCanvaContainer.current, {
|
||||
resize: true,
|
||||
useWorker: true,
|
||||
})
|
||||
window.addEventListener('message', handleIncomingMessage)
|
||||
}
|
||||
|
||||
const handleIncomingMessage = (message: MessageEvent) => {
|
||||
if (message.data.from === 'typebot') {
|
||||
if (message.data.action === 'shootConfettis' && confettiCanon.current)
|
||||
shootConfettis(confettiCanon.current)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplate = async () => {
|
||||
const { data, error } = await sendRequest(`/bots/onboarding.json`)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
setTypebot(data as Typebot)
|
||||
}
|
||||
|
||||
const handleNewAnswer = (answer: Answer) => {
|
||||
const isName = answer.variableId === 'cl126f4hf000i2e6d8zvzc3t1'
|
||||
const isCompany = answer.variableId === 'cl126jqww000w2e6dq9yv4ifq'
|
||||
const isCategories = answer.variableId === 'cl126mo3t001b2e6dvyi16bkd'
|
||||
const isOtherCategories = answer.variableId === 'cl126q38p001q2e6d0hj23f6b'
|
||||
if (isName) saveUser({ name: answer.content })
|
||||
if (isCompany) saveUser({ company: answer.content })
|
||||
if (isCategories) {
|
||||
const onboardingCategories = answer.content.split(', ')
|
||||
saveUser({ onboardingCategories })
|
||||
setChosenCategories(onboardingCategories)
|
||||
}
|
||||
if (isOtherCategories)
|
||||
saveUser({
|
||||
onboardingCategories: [...chosenCategories, answer.content],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="3xl"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
blockScrollOnMount={false}
|
||||
>
|
||||
<chakra.canvas
|
||||
ref={confettiCanvaContainer}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
w="full"
|
||||
h="full"
|
||||
zIndex={9999}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<ModalOverlay />
|
||||
<ModalContent h="85vh">
|
||||
<ModalBody>
|
||||
{typebot && (
|
||||
<TypebotViewer
|
||||
typebot={parseTypebotToPublicTypebot(typebot)}
|
||||
predefinedVariables={{
|
||||
Name: user?.name?.split(' ')[0] ?? undefined,
|
||||
}}
|
||||
onNewAnswer={handleNewAnswer}
|
||||
/>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const shootConfettis = (confettiCanon: confetti.CreateTypes) => {
|
||||
const count = 200
|
||||
const defaults = {
|
||||
origin: { y: 0.7 },
|
||||
}
|
||||
|
||||
const fire = (
|
||||
particleRatio: number,
|
||||
opts: {
|
||||
spread: number
|
||||
startVelocity?: number
|
||||
decay?: number
|
||||
scalar?: number
|
||||
}
|
||||
) => {
|
||||
confettiCanon(
|
||||
Object.assign({}, defaults, opts, {
|
||||
particleCount: Math.floor(count * particleRatio),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fire(0.25, {
|
||||
spread: 26,
|
||||
startVelocity: 55,
|
||||
})
|
||||
fire(0.2, {
|
||||
spread: 60,
|
||||
})
|
||||
fire(0.35, {
|
||||
spread: 100,
|
||||
decay: 0.91,
|
||||
scalar: 0.8,
|
||||
})
|
||||
fire(0.1, {
|
||||
spread: 120,
|
||||
startVelocity: 25,
|
||||
decay: 0.92,
|
||||
scalar: 1.2,
|
||||
})
|
||||
fire(0.1, {
|
||||
spread: 120,
|
||||
startVelocity: 45,
|
||||
})
|
||||
}
|
||||
@@ -59,10 +59,8 @@ export const PreviewDrawer = () => {
|
||||
setRightPanel(undefined)
|
||||
}
|
||||
|
||||
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => {
|
||||
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
|
||||
toast(log as UseToastOptions)
|
||||
console.log(log.details)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -139,4 +139,4 @@ export const parseInitBubbleCode = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const typebotJsHtml = `<script src="https://unpkg.com/typebot-js@2.2.0"></script>`
|
||||
export const typebotJsHtml = `<script src="https://unpkg.com/typebot-js@2.2.1"></script>`
|
||||
|
||||
@@ -80,7 +80,6 @@ export const WebhookSettings = ({
|
||||
|
||||
return () => {
|
||||
setLocalWebhook((localWebhook) => {
|
||||
console.log(localWebhook)
|
||||
if (!localWebhook) return
|
||||
updateWebhook(webhookId, localWebhook).then()
|
||||
return localWebhook
|
||||
|
||||
@@ -17,7 +17,12 @@ export const SupportBubble = () => {
|
||||
process.env.NEXT_PUBLIC_VIEWER_URL
|
||||
}/typebot-support`,
|
||||
backgroundColor: '#ffffff',
|
||||
button: { color: '#0042DA' },
|
||||
button: {
|
||||
color: '#0042DA',
|
||||
iconUrl:
|
||||
'https://user-images.githubusercontent.com/16015833/159536717-35bb78f8-f659-49f2-ad7f-00172be69cfb.svg',
|
||||
iconStyle: 'border-radius: 0; width: 50%',
|
||||
},
|
||||
hiddenVariables: {
|
||||
'User ID': user?.id,
|
||||
Name: user?.name ?? undefined,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { PublishButton } from '../buttons/PublishButton'
|
||||
import { CollaborationMenuButton } from './CollaborationMenuButton'
|
||||
import { EditableTypebotName } from './EditableTypebotName'
|
||||
@@ -21,6 +22,7 @@ export const headerHeight = 56
|
||||
|
||||
export const TypebotHeader = () => {
|
||||
const router = useRouter()
|
||||
const { rightPanel } = useEditor()
|
||||
const {
|
||||
typebot,
|
||||
updateOnBothTypebots,
|
||||
@@ -154,7 +156,7 @@ export const TypebotHeader = () => {
|
||||
|
||||
<HStack right="40px" pos="absolute">
|
||||
<CollaborationMenuButton />
|
||||
{router.pathname.includes('/edit') && (
|
||||
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
||||
<Button onClick={handlePreviewClick}>Preview</Button>
|
||||
)}
|
||||
<PublishButton />
|
||||
|
||||
@@ -12,10 +12,12 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import { timeSince } from 'services/utils'
|
||||
import { isNotDefined } from 'utils'
|
||||
|
||||
export const PublishButton = () => {
|
||||
const { push, query } = useRouter()
|
||||
const {
|
||||
isPublishing,
|
||||
isPublished,
|
||||
@@ -24,6 +26,11 @@ export const PublishButton = () => {
|
||||
restorePublishedTypebot,
|
||||
} = useTypebot()
|
||||
|
||||
const handlePublishClick = () => {
|
||||
publishTypebot()
|
||||
if (!publishedTypebot) push(`/typebots/${query.typebotId}/share`)
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack spacing="1px">
|
||||
<Tooltip
|
||||
@@ -47,7 +54,7 @@ export const PublishButton = () => {
|
||||
colorScheme="blue"
|
||||
isLoading={isPublishing}
|
||||
isDisabled={isPublished}
|
||||
onClick={publishTypebot}
|
||||
onClick={handlePublishClick}
|
||||
borderRightRadius={publishedTypebot && !isPublished ? 0 : undefined}
|
||||
>
|
||||
{isPublished ? 'Published' : 'Publish'}
|
||||
|
||||
Reference in New Issue
Block a user