🚸 New dedicated onboarding page

This commit is contained in:
Baptiste Arnaud
2023-07-25 16:12:40 +02:00
parent 283c55c1a4
commit 43555c171e
15 changed files with 263 additions and 183 deletions

View File

@@ -10,3 +10,4 @@ NEXT_PUBLIC_E2E_TEST=
NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME=
NEXT_PUBLIC_UNSPLASH_APP_NAME=
NEXT_PUBLIC_UNSPLASH_ACCESS_KEY=
NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID=

View File

@@ -628,3 +628,10 @@ export const BookIcon = (props: IconProps) => (
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</Icon>
)
export const ChevronLastIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="m7 18 6-6-6-6" />
<path d="M17 6v12" />
</Icon>
)

View File

@@ -0,0 +1,183 @@
import { ChevronLastIcon } from '@/components/icons'
import {
Button,
Flex,
HStack,
StackProps,
VStack,
chakra,
useColorModeValue,
} from '@chakra-ui/react'
import { Standard } from '@typebot.io/nextjs'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import confetti from 'canvas-confetti'
import { useUser } from '@/features/account/hooks/useUser'
import { env, isEmpty } from '@typebot.io/lib'
import { useI18n } from '@/locales'
const totalSteps = 5
export const OnboardingPage = () => {
const t = useI18n()
const { push, replace } = useRouter()
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
const confettiCanon = useRef<confetti.CreateTypes>()
const { user, updateUser } = useUser()
const [currentStep, setCurrentStep] = useState<number>(user?.name ? 2 : 1)
const isNewUser =
user &&
new Date(user.createdAt as unknown as string).toDateString() ===
new Date().toDateString()
useEffect(() => {
initConfettis()
})
useEffect(() => {
if (!user?.createdAt) return
if (isNewUser === false || isEmpty(env('ONBOARDING_TYPEBOT_ID')))
replace('/typebots')
}, [isNewUser, replace, user?.createdAt])
const initConfettis = () => {
if (!confettiCanvaContainer.current || confettiCanon.current) return
confettiCanon.current = confetti.create(confettiCanvaContainer.current, {
resize: true,
useWorker: true,
})
}
const updateUserInfo = async (answer: {
message: string
blockId: string
}) => {
const isOtherItem = [
'cl126pv7n001o2e6dajltc4qz',
'saw904bfzgspmt0l24achtiy',
].includes(answer.blockId)
if (isOtherItem) return
const isName = answer.blockId === 'cl126820m000g2e6dfleq78bt'
const isCompany = answer.blockId === 'cl126jioz000v2e6dwrk1f2cb'
const isCategories = answer.blockId === 'cl126lb8v00142e6duv5qe08l'
if (isName) updateUser({ name: answer.message })
if (isCategories) {
const onboardingCategories = answer.message.split(', ')
updateUser({ onboardingCategories })
}
if (isCompany) {
updateUser({ company: answer.message })
if (confettiCanon.current) shootConfettis(confettiCanon.current)
}
setCurrentStep((prev) => prev + 1)
}
if (!isNewUser) return null
return (
<VStack h="100vh" flexDir="column" justifyContent="center" spacing="10">
<Button
rightIcon={<ChevronLastIcon />}
pos="fixed"
top="5"
right="5"
variant="ghost"
size="sm"
onClick={() => push('/typebots')}
>
{t('skip')}
</Button>
<Dots currentStep={currentStep} pos="fixed" top="9" />
<Flex w="full" maxW="800px" h="full" maxH="70vh" rounded="lg">
<Standard
typebot={env('ONBOARDING_TYPEBOT_ID')}
style={{ borderRadius: '1rem' }}
prefilledVariables={{ Name: user?.name, Email: user?.email }}
onEnd={() => {
setTimeout(() => {
push('/typebots/create', { query: { isFirstBot: true } })
}, 2000)
}}
onAnswer={updateUserInfo}
/>
</Flex>
<chakra.canvas
ref={confettiCanvaContainer}
pos="fixed"
top="0"
left="0"
w="full"
h="full"
zIndex={9999}
pointerEvents="none"
/>
</VStack>
)
}
const Dots = ({
currentStep,
...props
}: { currentStep: number } & StackProps) => {
const highlightedBgColor = useColorModeValue('gray.500', 'gray.100')
const baseBgColor = useColorModeValue('gray.200', 'gray.700')
return (
<HStack spacing="10" {...props}>
{Array.from({ length: totalSteps }).map((_, index) => (
<chakra.div
key={index}
boxSize={'2'}
bgColor={currentStep === index + 1 ? highlightedBgColor : baseBgColor}
rounded="full"
transition="background-color 0.2s ease"
/>
))}
</HStack>
)
}
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,
})
}

View File

@@ -48,6 +48,22 @@ export const SignInPage = ({ type }: Props) => {
</Text>
)}
<SignInForm defaultEmail={query.g?.toString()} />
{type === 'signup' ? (
<Text fontSize="sm" maxW="400px" textAlign="center">
{scopedT('register.aggreeToTerms', {
termsOfService: (
<TextLink href={'https://typebot.io/terms-of-service'}>
{scopedT('register.termsOfService')}
</TextLink>
),
privacyPolicy: (
<TextLink href={'https://typebot.io/privacy-policies'}>
{scopedT('register.privacyPolicy')}
</TextLink>
),
})}
</Text>
) : null}
</VStack>
)
}

View File

@@ -42,7 +42,6 @@ const parseVideoUrl = (
return { type: VideoBubbleContentType.VIMEO, url, id }
}
if (youtubeRegex.test(url)) {
console.log(url.match(youtubeRegex)?.at(2))
const id = url.match(youtubeRegex)?.at(2)
if (!id) return { type: VideoBubbleContentType.URL, url }
return { type: VideoBubbleContentType.YOUTUBE, url, id }

View File

@@ -1,142 +0,0 @@
import { chakra, useColorModeValue } from '@chakra-ui/react'
import { Popup } from '@typebot.io/nextjs'
import { useUser } from '@/features/account/hooks/useUser'
import React, { useEffect, useRef, useState } from 'react'
import confetti from 'canvas-confetti'
import { useRouter } from 'next/router'
type Props = { totalTypebots: number }
export const OnboardingModal = ({ totalTypebots }: Props) => {
const { push } = useRouter()
const backgroundColor = useColorModeValue('white', '#171923')
const { user, updateUser } = useUser()
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
const confettiCanon = useRef<confetti.CreateTypes>()
const [chosenCategories, setChosenCategories] = useState<string[]>([])
const isNewUser =
user &&
new Date(user?.createdAt as unknown as string).toDateString() ===
new Date().toDateString() &&
totalTypebots === 0
useEffect(() => {
initConfettis()
}, [])
const initConfettis = () => {
if (!confettiCanvaContainer.current || confettiCanon.current) return
confettiCanon.current = confetti.create(confettiCanvaContainer.current, {
resize: true,
useWorker: true,
})
}
const handleBotEnd = () => {
setTimeout(() => {
push('/typebots/create', { query: { isFirstBot: true } })
}, 2000)
}
const handleNewAnswer = async (answer: {
message: string
blockId: string
}) => {
const isName = answer.blockId === 'cl126820m000g2e6dfleq78bt'
const isCompany = answer.blockId === 'cl126jioz000v2e6dwrk1f2cb'
const isCategories = answer.blockId === 'cl126lb8v00142e6duv5qe08l'
const isOtherCategories = answer.blockId === 'cl126pv7n001o2e6dajltc4qz'
if (isName) updateUser({ name: answer.message })
if (isCompany) {
updateUser({ company: answer.message })
if (confettiCanon.current) shootConfettis(confettiCanon.current)
}
if (isCategories) {
const onboardingCategories = answer.message.split(', ')
updateUser({ onboardingCategories })
setChosenCategories(onboardingCategories)
}
if (isOtherCategories)
updateUser({
onboardingCategories: [...chosenCategories, answer.message],
})
}
return (
<>
<chakra.canvas
ref={confettiCanvaContainer}
pos="fixed"
top="0"
left="0"
w="full"
h="full"
zIndex={9999}
pointerEvents="none"
/>
{user?.email && (
<Popup
typebot="onboarding-typebot"
prefilledVariables={{
Name: user.name?.split(' ')[0] ?? undefined,
Email: user.email ?? undefined,
}}
theme={{
backgroundColor,
zIndex: 100,
}}
defaultOpen={isNewUser}
onAnswer={handleNewAnswer}
onEnd={handleBotEnd}
/>
)}
</>
)
}
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,
})
}

View File

@@ -12,7 +12,6 @@ import {
import { useTypebotDnd } from '../TypebotDndProvider'
import React, { useState } from 'react'
import { BackButton } from './BackButton'
import { OnboardingModal } from '../../dashboard/components/OnboardingModal'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { useFolders } from '../hooks/useFolders'
@@ -26,7 +25,6 @@ import { TypebotCardOverlay } from './TypebotButtonOverlay'
import { useI18n } from '@/locales'
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
import { TypebotInDashboard } from '@/features/dashboard/types'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
type Props = { folder: DashboardFolder | null }
@@ -161,9 +159,6 @@ export const FolderContent = ({ folder }: Props) => {
return (
<Flex w="full" flex="1" justify="center">
{typebots && isCloudProdInstance && (
<OnboardingModal totalTypebots={typebots.length} />
)}
<Stack w="1000px" spacing={6}>
<Skeleton isLoaded={folder?.name !== undefined}>
<Heading as="h1">{folder?.name}</Heading>

View File

@@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Entfernen',
pending: 'Ausstehend',
skip: 'Überspringen',
'folders.createFolderButton.label': 'Ordner erstellen',
'folders.createTypebotButton.label': 'Typebot erstellen',
'folders.folderButton.deleteConfirmationMessage':
@@ -77,6 +78,10 @@ export default {
'auth.register.alreadyHaveAccountLabel.preLink':
'Bereits ein Konto vorhanden?',
'auth.register.alreadyHaveAccountLabel.link': 'Anmelden',
'auth.register.aggreeToTerms':
'Durch die Registrierung stimmst du unseren {termsOfService} und {privacyPolicy} zu.',
'auth.register.termsOfService': 'Nutzungsbedingungen',
'auth.register.privacyPolicy': 'Datenschutzrichtlinie',
'auth.error.default': 'Versuche, dich mit einem anderen Konto anzumelden.',
'auth.error.email':
'E-Mail nicht gefunden. Versuche, dich mit einem anderen Anbieter anzumelden.',

View File

@@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Remove',
pending: 'Pending',
skip: 'Skip',
'folders.createFolderButton.label': 'Create a folder',
'folders.createTypebotButton.label': 'Create a typebot',
'folders.folderButton.deleteConfirmationMessage':
@@ -75,6 +76,10 @@ export default {
'auth.register.heading': 'Create an account',
'auth.register.alreadyHaveAccountLabel.preLink': 'Already have an account?',
'auth.register.alreadyHaveAccountLabel.link': 'Sign in',
'auth.register.aggreeToTerms':
'By signing up, you agree to our {termsOfService} and {privacyPolicy}.',
'auth.register.termsOfService': 'terms of service',
'auth.register.privacyPolicy': 'privacy policy',
'auth.error.default': 'Try signing with a different account.',
'auth.error.email': 'Email not found. Try signing with a different provider.',
'auth.error.oauthNotLinked':

View File

@@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Retirer',
pending: 'En attente',
skip: 'Passer',
'folders.createFolderButton.label': 'Créer un dossier',
'folders.createTypebotButton.label': 'Créer un typebot',
'folders.folderButton.deleteConfirmationMessage':
@@ -75,6 +76,10 @@ export default {
'auth.register.heading': 'Créer un compte',
'auth.register.alreadyHaveAccountLabel.preLink': 'Tu as déjà un compte?',
'auth.register.alreadyHaveAccountLabel.link': 'Se connecter',
'auth.register.aggreeToTerms':
'En vous inscrivant, vous acceptez nos {termsOfService} et {privacyPolicy}.',
'auth.register.termsOfService': "conditions d'utilisation",
'auth.register.privacyPolicy': 'politique de confidentialité',
'auth.error.default': 'Essaye de te connecter avec un compte différent.',
'auth.error.email':
'Email non trouvé. Essaye de te connecter avec un fournisseur différent.',

View File

@@ -12,6 +12,7 @@ export default {
downgrade: 'Downgrade',
remove: 'Remover',
pending: 'Pendente',
skip: 'Pular',
'folders.createFolderButton.label': 'Criar uma pasta',
'folders.createTypebotButton.label': 'Criar um typebot',
'folders.folderButton.deleteConfirmationMessage':
@@ -76,6 +77,10 @@ export default {
'auth.register.heading': 'Criar uma conta',
'auth.register.alreadyHaveAccountLabel.preLink': 'Já tem uma conta?',
'auth.register.alreadyHaveAccountLabel.link': 'Entrar',
'auth.register.aggreeToTerms':
'Ao se cadastrar, você concorda com nossos {termsOfService} e {privacyPolicy}.',
'auth.register.termsOfService': 'termos de serviço',
'auth.register.privacyPolicy': 'política de privacidade',
'auth.error.default': 'Tente entrar com uma conta diferente.',
'auth.error.email':
'E-mail não encontrado. Tente entrar com um provedor diferente.',

View File

@@ -121,8 +121,8 @@ if (isNotEmpty(process.env.CUSTOM_OAUTH_WELL_KNOWN_URL)) {
type: 'oauth',
authorization: {
params: {
scope: process.env.CUSTOM_OAUTH_SCOPE ?? 'openid profile email'
}
scope: process.env.CUSTOM_OAUTH_SCOPE ?? 'openid profile email',
},
},
clientId: process.env.CUSTOM_OAUTH_CLIENT_ID,
clientSecret: process.env.CUSTOM_OAUTH_CLIENT_SECRET,
@@ -156,6 +156,9 @@ export const authOptions: AuthOptions = {
},
pages: {
signIn: '/signin',
newUser: process.env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID
? '/onboarding'
: undefined,
},
callbacks: {
session: async ({ session, user }) => {

View File

@@ -0,0 +1,5 @@
import { OnboardingPage } from '@/features/auth/components/OnboardingPage'
export default function Page() {
return <OnboardingPage />
}