♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
@@ -0,0 +1,37 @@
|
||||
import { CloseButton, Flex, HStack, StackProps } from '@chakra-ui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
type VerifyEmailBannerProps = { id: string } & StackProps
|
||||
|
||||
export const Banner = ({ id, ...props }: VerifyEmailBannerProps) => {
|
||||
const [show, setShow] = useState(false)
|
||||
const localStorageKey = `banner-${id}`
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem(localStorageKey)) setShow(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleCloseClick = () => {
|
||||
localStorage.setItem(localStorageKey, 'hide')
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
if (!show) return <></>
|
||||
return (
|
||||
<HStack
|
||||
h="50px"
|
||||
bgColor="blue.400"
|
||||
color="white"
|
||||
justifyContent="center"
|
||||
align="center"
|
||||
w="full"
|
||||
{...props}
|
||||
>
|
||||
<Flex maxW="1000px" justifyContent="space-between" w="full">
|
||||
<HStack>{props.children}</HStack>
|
||||
<CloseButton rounded="full" onClick={handleCloseClick} />
|
||||
</Flex>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Text,
|
||||
Stack,
|
||||
Link,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const localStorageKey = 'typebot-20-modal'
|
||||
export const AnnoucementModal = ({ isOpen, onClose }: Props) => {
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem(localStorageKey)) setShow(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleCloseClick = () => {
|
||||
localStorage.setItem(localStorageKey, 'hide')
|
||||
setShow(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!show) return <></>
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleCloseClick} size="2xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>What's new in Typebot 2.0?</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing="6" pb="10">
|
||||
<Text>Typebo 2.0 has been launched February the 15th 🎉.</Text>
|
||||
<iframe
|
||||
width="620"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/u8FZHvlYviw"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '5px' }}
|
||||
/>
|
||||
<Text>
|
||||
Most questions are answered in this{' '}
|
||||
<Link
|
||||
href="https://docs.typebot.io"
|
||||
color="blue.500"
|
||||
textDecor="underline"
|
||||
>
|
||||
FAQ
|
||||
</Link>
|
||||
. If you have other questions, open up the bot on the bottom right
|
||||
corner. 😃
|
||||
</Text>
|
||||
<Text>Baptiste.</Text>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Text,
|
||||
HStack,
|
||||
Flex,
|
||||
SkeletonCircle,
|
||||
Button,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
HardDriveIcon,
|
||||
LogOutIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
} from '@/components/icons'
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { useUser } from '@/features/account'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { isNotDefined } from 'utils'
|
||||
import Link from 'next/link'
|
||||
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||
import { TypebotLogo } from '@/components/TypebotLogo'
|
||||
import { PlanTag } from '@/features/billing'
|
||||
import { WorkspaceSettingsModal } from '@/features/workspace'
|
||||
|
||||
export const DashboardHeader = () => {
|
||||
const { user } = useUser()
|
||||
|
||||
const { workspace, workspaces, switchWorkspace, createWorkspace } =
|
||||
useWorkspace()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
const handleLogOut = () => {
|
||||
localStorage.removeItem('workspaceId')
|
||||
signOut()
|
||||
}
|
||||
|
||||
const handleCreateNewWorkspace = () =>
|
||||
createWorkspace(user?.name ?? undefined)
|
||||
|
||||
return (
|
||||
<Flex w="full" borderBottomWidth="1px" justify="center">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
alignItems="center"
|
||||
h="16"
|
||||
maxW="1000px"
|
||||
flex="1"
|
||||
>
|
||||
<Link href="/typebots" data-testid="typebot-logo">
|
||||
<TypebotLogo w="30px" />
|
||||
</Link>
|
||||
<HStack>
|
||||
{user && workspace && (
|
||||
<WorkspaceSettingsModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
user={user}
|
||||
workspace={workspace}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
leftIcon={<SettingsIcon />}
|
||||
onClick={onOpen}
|
||||
isLoading={isNotDefined(workspace)}
|
||||
>
|
||||
Settings & Members
|
||||
</Button>
|
||||
<Menu placement="bottom-end">
|
||||
<MenuButton as={Button} variant="outline" px="2">
|
||||
<HStack>
|
||||
<SkeletonCircle
|
||||
isLoaded={workspace !== undefined}
|
||||
alignItems="center"
|
||||
display="flex"
|
||||
boxSize="20px"
|
||||
>
|
||||
<EmojiOrImageIcon
|
||||
boxSize="20px"
|
||||
icon={workspace?.icon}
|
||||
defaultIcon={HardDriveIcon}
|
||||
/>
|
||||
</SkeletonCircle>
|
||||
{workspace && (
|
||||
<>
|
||||
<Text noOfLines={1} maxW="200px">
|
||||
{workspace.name}
|
||||
</Text>
|
||||
<PlanTag plan={workspace.plan} />
|
||||
</>
|
||||
)}
|
||||
<ChevronLeftIcon transform="rotate(-90deg)" />
|
||||
</HStack>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{workspaces
|
||||
?.filter((w) => w.id !== workspace?.id)
|
||||
.map((workspace) => (
|
||||
<MenuItem
|
||||
key={workspace.id}
|
||||
onClick={() => switchWorkspace(workspace.id)}
|
||||
>
|
||||
<HStack>
|
||||
<EmojiOrImageIcon
|
||||
icon={workspace.icon}
|
||||
boxSize="16px"
|
||||
defaultIcon={HardDriveIcon}
|
||||
/>
|
||||
<Text>{workspace.name}</Text>
|
||||
<PlanTag plan={workspace.plan} />
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem onClick={handleCreateNewWorkspace} icon={<PlusIcon />}>
|
||||
New workspace
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleLogOut}
|
||||
icon={<LogOutIcon />}
|
||||
color="orange.500"
|
||||
>
|
||||
Log out
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Seo } from '@/components/Seo'
|
||||
import { useUser } from '@/features/account'
|
||||
import { upgradePlanQuery } from '@/features/billing'
|
||||
import { TypebotDndProvider, FolderContent } from '@/features/folders'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { Stack, VStack, Spinner, Text } from '@chakra-ui/react'
|
||||
import { Plan } from 'db'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { DashboardHeader } from './DashboardHeader'
|
||||
|
||||
export const DashboardPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { query } = useRouter()
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
|
||||
useEffect(() => {
|
||||
const { subscribePlan, chats, storage } = query as {
|
||||
subscribePlan: Plan | undefined
|
||||
chats: string | undefined
|
||||
storage: string | undefined
|
||||
}
|
||||
if (workspace && subscribePlan && user && workspace.plan === 'FREE') {
|
||||
setIsLoading(true)
|
||||
upgradePlanQuery({
|
||||
user,
|
||||
plan: subscribePlan,
|
||||
workspaceId: workspace.id,
|
||||
additionalChats: chats ? parseInt(chats) : 0,
|
||||
additionalStorage: storage ? parseInt(storage) : 0,
|
||||
})
|
||||
}
|
||||
}, [query, user, workspace])
|
||||
|
||||
return (
|
||||
<Stack minH="100vh">
|
||||
<Seo title="My typebots" />
|
||||
<DashboardHeader />
|
||||
<TypebotDndProvider>
|
||||
{isLoading ? (
|
||||
<VStack w="full" justifyContent="center" pt="10" spacing={6}>
|
||||
<Text>You are being redirected...</Text>
|
||||
<Spinner />
|
||||
</VStack>
|
||||
) : (
|
||||
<FolderContent folder={null} />
|
||||
)}
|
||||
</TypebotDndProvider>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
chakra,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { useUser } from '@/features/account'
|
||||
import { Answer, Typebot } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { getViewerUrl, sendRequest } from 'utils'
|
||||
import confetti from 'canvas-confetti'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { parseTypebotToPublicTypebot } from '@/features/publish'
|
||||
|
||||
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 [openedOnce, setOpenedOnce] = useState(false)
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplate()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (openedOnce) return
|
||||
const isNewUser =
|
||||
user &&
|
||||
new Date(user?.createdAt as unknown as string).toDateString() ===
|
||||
new Date().toDateString() &&
|
||||
totalTypebots === 0
|
||||
if (isNewUser) {
|
||||
onOpen()
|
||||
setOpenedOnce(true)
|
||||
}
|
||||
// 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 showToast({ title: error.name, description: error.message })
|
||||
setTypebot(data as Typebot)
|
||||
}
|
||||
|
||||
const handleNewAnswer = async (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
|
||||
apiHost={getViewerUrl({ isBuilder: true })}
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user