2
0

♻️ (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,106 @@
import { VStack, Heading, Stack, Button, useDisclosure } from '@chakra-ui/react'
import { ToolIcon, TemplateIcon, DownloadIcon } from '@/components/icons'
import { Typebot } from 'models'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { ImportTypebotFromFileButton } from './ImportTypebotFromFileButton'
import { TemplatesModal } from './TemplatesModal'
import { useWorkspace } from '@/features/workspace'
import { useUser } from '@/features/account'
import { useToast } from '@/hooks/useToast'
import { createTypebotQuery, importTypebotQuery } from '@/features/dashboard'
export const CreateNewTypebotButtons = () => {
const { workspace } = useWorkspace()
const { user } = useUser()
const router = useRouter()
const { isOpen, onOpen, onClose } = useDisclosure()
const [isLoading, setIsLoading] = useState(false)
const { showToast } = useToast()
const handleCreateSubmit = async (typebot?: Typebot) => {
if (!user || !workspace) return
setIsLoading(true)
const folderId = router.query.folderId?.toString() ?? null
const { error, data } = typebot
? await importTypebotQuery(
{
...typebot,
folderId,
workspaceId: workspace.id,
theme: {
...typebot.theme,
chat: {
...typebot.theme.chat,
hostAvatar: { isEnabled: true, url: user.image ?? undefined },
},
},
},
workspace.plan
)
: await createTypebotQuery({
folderId,
workspaceId: workspace.id,
})
if (error) showToast({ description: error.message })
if (data)
router.push({
pathname: `/typebots/${data.id}/edit`,
query:
router.query.isFirstBot === 'true'
? {
isFirstBot: 'true',
}
: {},
})
setIsLoading(false)
}
return (
<VStack maxW="600px" w="full" flex="1" pt="20" spacing={10}>
<Heading>Create a new typebot</Heading>
<Stack w="full" spacing={6}>
<Button
variant="outline"
w="full"
py="8"
fontSize="lg"
leftIcon={<ToolIcon color="blue.500" boxSize="25px" mr="2" />}
onClick={() => handleCreateSubmit()}
isLoading={isLoading}
>
Start from scratch
</Button>
<Button
variant="outline"
w="full"
py="8"
fontSize="lg"
leftIcon={<TemplateIcon color="orange.500" boxSize="25px" mr="2" />}
onClick={onOpen}
isLoading={isLoading}
>
Start from a template
</Button>
<ImportTypebotFromFileButton
variant="outline"
w="full"
py="8"
fontSize="lg"
leftIcon={<DownloadIcon color="purple.500" boxSize="25px" mr="2" />}
isLoading={isLoading}
onNewTypebot={handleCreateSubmit}
>
Import a file
</ImportTypebotFromFileButton>
</Stack>
<TemplatesModal
isOpen={isOpen}
onClose={onClose}
onTypebotChoose={handleCreateSubmit}
/>
</VStack>
)
}

View File

@ -0,0 +1,43 @@
import { useToast } from '@/hooks/useToast'
import { readFile } from '@/utils/helpers'
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
import { Typebot } from 'models'
import React, { ChangeEvent } from 'react'
type Props = {
onNewTypebot: (typebot: Typebot) => void
} & ButtonProps
export const ImportTypebotFromFileButton = ({
onNewTypebot,
...props
}: Props) => {
const { showToast } = useToast()
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target?.files) return
const file = e.target.files[0]
const fileContent = await readFile(file)
try {
onNewTypebot(JSON.parse(fileContent))
} catch (err) {
console.error(err)
showToast({ description: 'Failed to parse the file' })
}
}
return (
<>
<chakra.input
type="file"
id="file-input"
display="none"
onChange={handleInputChange}
accept=".json"
/>
<Button as="label" htmlFor="file-input" cursor="pointer" {...props}>
{props.children}
</Button>
</>
)
}

View File

@ -0,0 +1,152 @@
import {
Button,
chakra,
Divider,
Heading,
HStack,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
Stack,
Tooltip,
} from '@chakra-ui/react'
import { ExternalLinkIcon } from '@/components/icons'
import { TypebotViewer } from 'bot-engine'
import { Typebot } from 'models'
import React, { useCallback, useEffect, useState } from 'react'
import { getViewerUrl, sendRequest } from 'utils'
import { templates } from '../data'
import { TemplateProps } from '../types'
import { useToast } from '@/hooks/useToast'
import { parseTypebotToPublicTypebot } from '@/features/publish'
type Props = {
isOpen: boolean
onClose: () => void
onTypebotChoose: (typebot: Typebot) => void
}
export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
const [typebot, setTypebot] = useState<Typebot>()
const [selectedTemplate, setSelectedTemplate] = useState<TemplateProps>(
templates[0]
)
const [isLoading, setIsLoading] = useState(false)
const { showToast } = useToast()
const fetchTemplate = useCallback(
async (template: TemplateProps) => {
setSelectedTemplate(template)
const { data, error } = await sendRequest(
`/templates/${template.fileName}`
)
if (error)
return showToast({ title: error.name, description: error.message })
setTypebot(data as Typebot)
},
[showToast]
)
useEffect(() => {
fetchTemplate(templates[0])
}, [fetchTemplate])
const onUseThisTemplateClick = () => {
if (!typebot) return
onTypebotChoose(typebot)
setIsLoading(true)
}
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="6xl"
blockScrollOnMount={false}
>
<ModalOverlay />
<ModalContent h="85vh">
<ModalBody as={HStack} p="0">
<Stack w="full" h="full">
<Heading pl="10" pt="4" fontSize="2xl">
{selectedTemplate.emoji}{' '}
<chakra.span ml="2">{selectedTemplate.name}</chakra.span>
</Heading>
{typebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={parseTypebotToPublicTypebot(typebot)}
key={typebot.id}
/>
)}
</Stack>
<Stack
h="full"
py="6"
w="300px"
px="4"
borderLeftWidth={1}
justify="space-between"
>
<Stack spacing={4}>
<Button
colorScheme="blue"
onClick={onUseThisTemplateClick}
isLoading={isLoading}
>
Use this template
</Button>
<Divider />
<Stack>
{templates.map((template) => (
<Tooltip
key={template.name}
isDisabled={!template.isComingSoon}
label="Coming soon!"
>
<span>
<Button
onClick={() => fetchTemplate(template)}
w="full"
variant={
selectedTemplate.name === template.name
? 'solid'
: 'ghost'
}
isDisabled={template.isComingSoon}
>
{template.emoji}{' '}
<chakra.span minW="200px" textAlign="left" ml="3">
{template.name}
</chakra.span>
</Button>
</span>
</Tooltip>
))}
</Stack>
</Stack>
<Stack>
<Divider />
<Tooltip label="Coming soon!" placement="top">
<span>
<Button
w="full"
variant="ghost"
isDisabled
leftIcon={<ExternalLinkIcon />}
>
Community templates
</Button>
</span>
</Tooltip>
</Stack>
</Stack>
</ModalBody>
</ModalContent>
</Modal>
)
}

View File

@ -0,0 +1,12 @@
import { Seo } from '@/components/Seo'
import { DashboardHeader } from '@/features/dashboard'
import { VStack } from '@chakra-ui/react'
import { CreateNewTypebotButtons } from './CreateNewTypebotButtons'
export const TemplatesPage = () => (
<VStack>
<Seo title="Templates" />
<DashboardHeader />
<CreateNewTypebotButtons />
</VStack>
)

View File

@ -0,0 +1,30 @@
import { TemplateProps } from './types'
export const templates: TemplateProps[] = [
{ name: 'Lead Generation', emoji: '🤝', fileName: 'lead-gen.json' },
{ name: 'Customer Support', emoji: '😍', fileName: 'customer-support.json' },
{ name: 'Quiz', emoji: '🕹️', fileName: 'quiz.json' },
{ name: 'Lead Scoring', emoji: '🏆', fileName: 'lead-scoring.json' },
{
name: 'Digital Product Payment',
emoji: '🖼️',
fileName: 'digital-product-payment.json',
},
{
name: 'FAQ',
emoji: '💬',
fileName: 'faq.json',
},
{
name: 'Conversational Resume',
emoji: '👨‍💼',
fileName: 'customer-support.json',
isComingSoon: true,
},
{
name: 'User Onboarding',
emoji: '🧑‍🚀',
fileName: 'customer-support.json',
isComingSoon: true,
},
]

View File

@ -0,0 +1 @@
export { TemplatesPage } from './components/TemplatesPage'

View File

@ -0,0 +1,35 @@
import { getTestAsset } from '@/test/utils/playwright'
import test, { expect } from '@playwright/test'
import { typebotViewer } from 'utils/playwright/testHelpers'
test.describe.parallel('Templates page', () => {
test('From scratch should create a blank typebot', async ({ page }) => {
await page.goto('/typebots/create')
await expect(
page.locator('button >> text="Settings & Members"')
).toBeEnabled()
await page.click('text=Start from scratch')
await expect(page).toHaveURL(new RegExp(`/edit`))
})
test('From file should import correctly', async ({ page }) => {
await page.goto('/typebots/create')
await page.waitForTimeout(1000)
await page.setInputFiles(
'input[type="file"]',
getTestAsset('typebots/singleChoiceTarget.json')
)
await expect(page).toHaveURL(new RegExp(`/edit`))
})
test('Templates should be previewable and usable', async ({ page }) => {
await page.goto('/typebots/create')
await page.click('text=Start from a template')
await page.click('text=Customer Support')
await expect(
typebotViewer(page).locator('text=How can I help you?')
).toBeVisible()
await page.click('text=Use this template')
await expect(page).toHaveURL(new RegExp(`/edit`))
})
})

View File

@ -0,0 +1,6 @@
export type TemplateProps = {
name: string
emoji: string
fileName: string
isComingSoon?: boolean
}