♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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,
})
}