2
0

🦴 Add theme page backbone

This commit is contained in:
Baptiste Arnaud
2021-12-23 09:37:42 +01:00
parent 6ee0647384
commit 30ddb143b4
35 changed files with 784 additions and 87 deletions

View File

@ -161,3 +161,27 @@ export const TrashIcon = (props: IconProps) => (
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</Icon>
)
export const LayoutIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line>
<line x1="9" y1="21" x2="9" y2="9"></line>
</Icon>
)
export const CodeIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</Icon>
)
export const PencilIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="M12 19l7-7 3 3-7 7-3-3z"></path>
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path>
<path d="M2 2l7.586 7.586"></path>
<circle cx="11" cy="11" r="2"></circle>
</Icon>
)

View File

@ -2,7 +2,6 @@ import { Flex } from '@chakra-ui/react'
import React from 'react'
import Graph from './graph/Graph'
import { DndContext } from 'contexts/DndContext'
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
import { StepTypesList } from './StepTypesList'
import { PreviewDrawer } from './preview/PreviewDrawer'
import { RightPanel, useEditor } from 'contexts/EditorContext'
@ -10,13 +9,7 @@ import { RightPanel, useEditor } from 'contexts/EditorContext'
export const Board = () => {
const { rightPanel } = useEditor()
return (
<Flex
flex="1"
pos="relative"
bgColor="gray.50"
h={`calc(100vh - ${headerHeight}px)`}
marginTop={`${headerHeight}px`}
>
<Flex flex="1" pos="relative" bgColor="gray.50" h="full">
<DndContext>
<StepTypesList />
<Graph />

View File

@ -44,7 +44,6 @@ export const TextEditor = ({ initialValue, ids, onClose }: TextEditorProps) => {
}, [debouncedValue])
const save = (value: unknown[]) => {
console.log('SAVE', value)
if (value.length === 0) return
const html = serializeHtml(editor, {
nodes: value,

View File

@ -22,6 +22,7 @@ export const CreateBotButton = ({
onClick={handleClick}
paddingX={6}
whiteSpace={'normal'}
colorScheme="blue"
{...props}
>
<VStack spacing="6">

View File

@ -29,7 +29,7 @@ export const TypebotHeader = () => {
borderBottomWidth="1px"
justify="center"
align="center"
pos="fixed"
pos="relative"
h={`${headerHeight}px`}
zIndex={2}
bgColor="white"
@ -37,10 +37,7 @@ export const TypebotHeader = () => {
<HStack>
<Button
as={NextChakraLink}
href={{
pathname: `/typebots/${typebot?.id}/edit`,
query: { ...router.query, typebotId: [] },
}}
href={`/typebots/${typebot?.id}/edit`}
colorScheme={router.pathname.includes('/edit') ? 'blue' : 'gray'}
variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'}
>
@ -48,21 +45,15 @@ export const TypebotHeader = () => {
</Button>
<Button
as={NextChakraLink}
href={{
pathname: `/typebots/${typebot?.id}/design`,
query: { ...router.query, typebotId: [] },
}}
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
href={`/typebots/${typebot?.id}/theme`}
colorScheme={router.pathname.endsWith('theme') ? 'blue' : 'gray'}
variant={router.pathname.endsWith('theme') ? 'outline' : 'ghost'}
>
Theme
</Button>
<Button
as={NextChakraLink}
href={{
pathname: `/typebots/${typebot?.id}/design`,
query: { ...router.query, typebotId: [] },
}}
href={`/typebots/${typebot?.id}/design`}
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
>
@ -70,10 +61,7 @@ export const TypebotHeader = () => {
</Button>
<Button
as={NextChakraLink}
href={{
pathname: `/typebots/${typebot?.id}/share`,
query: { ...router.query, typebotId: [] },
}}
href={`/typebots/${typebot?.id}/share`}
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
>
@ -81,10 +69,7 @@ export const TypebotHeader = () => {
</Button>
<Button
as={NextChakraLink}
href={{
pathname: `/typebots/${typebot?.id}/results/responses`,
query: { ...router.query, typebotId: [] },
}}
href={`/typebots/${typebot?.id}/results/responses`}
colorScheme={router.pathname.includes('results') ? 'blue' : 'gray'}
variant={router.pathname.includes('results') ? 'outline' : 'ghost'}
>

View File

@ -0,0 +1,38 @@
import { Flex, Text } from '@chakra-ui/react'
import { Background, BackgroundType } from 'bot-engine'
import React from 'react'
import { ColorPicker } from '../ColorPicker'
type BackgroundContentProps = {
background: Background
onBackgroundContentChange: (content: string) => void
}
export const BackgroundContent = ({
background,
onBackgroundContentChange,
}: BackgroundContentProps) => {
const handleContentChange = (content: string) =>
onBackgroundContentChange(content)
switch (background.type) {
case BackgroundType.COLOR:
return (
<Flex justify="space-between" align="center">
<Text>Background color:</Text>
<ColorPicker
initialColor={background.content}
onColorChange={handleContentChange}
/>
</Flex>
)
case BackgroundType.IMAGE:
return (
<Flex>
<Text>Image</Text>
</Flex>
)
default:
return <></>
}
}

View File

@ -0,0 +1,45 @@
import { Stack, Text } from '@chakra-ui/react'
import { Background, BackgroundType } from 'bot-engine'
import { deepEqual } from 'fast-equals'
import React, { useEffect, useState } from 'react'
import { BackgroundContent } from './BackgroundContent'
import { BackgroundTypeRadioButtons } from './BackgroundTypeRadioButtons'
type Props = {
initialBackground?: Background
onBackgroundChange: (newBackground: Background) => void
}
export const BackgroundSelector = ({
initialBackground,
onBackgroundChange,
}: Props) => {
const [currentBackground, setCurrentBackground] = useState<Background>(
initialBackground ?? { type: BackgroundType.NONE, content: '' }
)
useEffect(() => {
if (deepEqual(currentBackground, initialBackground)) return
onBackgroundChange(currentBackground)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentBackground])
const handleBackgroundTypeChange = (type: BackgroundType) =>
setCurrentBackground({ ...currentBackground, type })
const handleBackgroundContentChange = (content: string) =>
setCurrentBackground({ ...currentBackground, content })
return (
<Stack spacing={4}>
<Text>Background</Text>
<BackgroundTypeRadioButtons
backgroundType={currentBackground.type}
onBackgroundTypeChange={handleBackgroundTypeChange}
/>
<BackgroundContent
background={currentBackground}
onBackgroundContentChange={handleBackgroundContentChange}
/>
</Stack>
)
}

View File

@ -0,0 +1,73 @@
import {
Box,
Flex,
HStack,
useRadio,
useRadioGroup,
UseRadioProps,
} from '@chakra-ui/react'
import { BackgroundType } from 'bot-engine'
import { ReactNode } from 'react'
type Props = {
backgroundType: BackgroundType
onBackgroundTypeChange: (type: BackgroundType) => void
}
export const BackgroundTypeRadioButtons = ({
backgroundType,
onBackgroundTypeChange,
}: Props) => {
const options = ['Color', 'Image', 'None']
const { getRootProps, getRadioProps } = useRadioGroup({
name: 'background-type',
defaultValue: backgroundType,
onChange: (nextVal: string) =>
onBackgroundTypeChange(nextVal as BackgroundType),
})
const group = getRootProps()
return (
<HStack {...group}>
{options.map((value) => {
const radio = getRadioProps({ value })
return (
<RadioCard key={value} {...radio}>
{value}
</RadioCard>
)
})}
</HStack>
)
}
export const RadioCard = (props: UseRadioProps & { children: ReactNode }) => {
const { getInputProps, getCheckboxProps } = useRadio(props)
const input = getInputProps()
const checkbox = getCheckboxProps()
return (
<Box as="label" flex="1">
<input {...input} />
<Flex
{...checkbox}
cursor="pointer"
borderWidth="1px"
borderRadius="md"
_checked={{
bg: 'orange.400',
color: 'white',
borderColor: 'orange.400',
}}
px={5}
py={2}
transition="background-color 150ms, color 150ms, border 150ms"
justifyContent="center"
>
{props.children}
</Flex>
</Box>
)
}

View File

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

View File

@ -0,0 +1,99 @@
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverArrow,
PopoverCloseButton,
PopoverHeader,
Center,
PopoverBody,
SimpleGrid,
Input,
Button,
} from '@chakra-ui/react'
import React, { useEffect, useState } from 'react'
const colorsSelection: `#${string}`[] = [
'#264653',
'#e9c46a',
'#2a9d8f',
'#7209b7',
'#023e8a',
'#ffe8d6',
'#d8f3dc',
'#4ea8de',
'#ffb4a2',
]
type Props = {
initialColor: string
onColorChange: (color: string) => void
}
export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
const [color, setColor] = useState(initialColor)
useEffect(() => {
onColorChange(color)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [color])
return (
<Popover variant="picker">
<PopoverTrigger>
<Button
aria-label={color}
background={color}
height="22px"
width="22px"
padding={0}
borderRadius={3}
borderWidth={1}
/>
</PopoverTrigger>
<PopoverContent width="170px">
<PopoverArrow bg={color} />
<PopoverCloseButton color="white" />
<PopoverHeader
height="100px"
backgroundColor={color}
borderTopLeftRadius={5}
borderTopRightRadius={5}
color={color === '#ffffff' ? 'gray.800' : 'white'}
>
<Center height="100%">{color}</Center>
</PopoverHeader>
<PopoverBody height="120px">
<SimpleGrid columns={5} spacing={2}>
{colorsSelection.map((c) => (
<Button
key={c}
aria-label={c}
background={c}
height="22px"
width="22px"
padding={0}
minWidth="unset"
borderRadius={3}
_hover={{ background: c }}
onClick={() => {
setColor(c)
}}
></Button>
))}
</SimpleGrid>
<Input
borderRadius={3}
marginTop={3}
placeholder="red.100"
size="sm"
value={color}
onChange={(e) => {
setColor(e.target.value)
}}
/>
</PopoverBody>
</PopoverContent>
</Popover>
)
}

View File

@ -0,0 +1,45 @@
import React, { useEffect, useState } from 'react'
import { Text, Flex } from '@chakra-ui/react'
import { SearchableDropdown } from './SearchableDropdown'
type FontSelectorProps = {
activeFont?: string
onSelectFont: (font: string) => void
}
export const FontSelector = ({
activeFont,
onSelectFont,
}: FontSelectorProps) => {
const [currentFont, setCurrentFont] = useState(activeFont)
const [googleFonts, setGoogleFonts] = useState<string[]>([])
useEffect(() => {
fetchPopularFonts().then(setGoogleFonts)
}, [])
const fetchPopularFonts = async () => {
const response = await fetch(
`https://www.googleapis.com/webfonts/v1/webfonts?key=AIzaSyD2YAiipBLNYg058Wm-sPE-e2dPDn_zX8w&sort=popularity`
)
return (await response.json()).items.map(
(item: { family: string }) => item.family
)
}
return (
<Flex justify="space-between" align="center">
<Text>Font</Text>
<SearchableDropdown
selectedItem={activeFont}
items={googleFonts}
onSelectItem={(nextFont) => {
if (nextFont !== currentFont) {
setCurrentFont(nextFont)
onSelectFont(nextFont)
}
}}
/>
</Flex>
)
}

View File

@ -0,0 +1,111 @@
import {
useDisclosure,
useOutsideClick,
Flex,
Popover,
PopoverTrigger,
Input,
PopoverContent,
Button,
Text,
} from '@chakra-ui/react'
import { useState, useRef, useEffect, ChangeEvent } from 'react'
export const SearchableDropdown = ({
selectedItem,
items,
onSelectItem,
}: {
selectedItem?: string
items: string[]
onSelectItem: (value: string) => void
}) => {
const { onOpen, onClose, isOpen } = useDisclosure()
const [inputValue, setInputValue] = useState(selectedItem)
const [filteredItems, setFilteredItems] = useState([
...items
.filter((item) =>
item.toLowerCase().includes((selectedItem ?? '').toLowerCase())
)
.slice(0, 50),
])
const dropdownRef = useRef(null)
const inputRef = useRef(null)
useEffect(() => {
if (filteredItems.length > 0) return
setFilteredItems([
...items
.filter((item) =>
item.toLowerCase().includes((selectedItem ?? '').toLowerCase())
)
.slice(0, 50),
])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items])
useOutsideClick({
ref: dropdownRef,
handler: onClose,
})
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value)
if (e.target.value === '') {
setFilteredItems([...items.slice(0, 50)])
return
}
setFilteredItems([
...items
.filter((item) =>
item.toLowerCase().includes((inputValue ?? '').toLowerCase())
)
.slice(0, 50),
])
}
return (
<Flex ref={dropdownRef}>
<Popover isOpen={isOpen} initialFocusRef={inputRef}>
<PopoverTrigger>
<Input
ref={inputRef}
value={inputValue}
onChange={onInputChange}
onClick={onOpen}
w="300px"
/>
</PopoverTrigger>
<PopoverContent maxH="35vh" overflowY="scroll" spacing="0" w="300px">
{filteredItems.length > 0 ? (
<>
{filteredItems.map((item, idx) => {
return (
<Button
minH="40px"
key={idx}
onClick={() => {
setInputValue(item)
onSelectItem(item)
onClose()
}}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
variant="ghost"
justifyContent="flex-start"
>
{item}
</Button>
)
})}
</>
) : (
<Text p={4}>Not found.</Text>
)}
</PopoverContent>
</Popover>
</Flex>
)
}

View File

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

View File

@ -0,0 +1,60 @@
import {
AccordionItem,
AccordionButton,
HStack,
Heading,
AccordionIcon,
AccordionPanel,
Stack,
} from '@chakra-ui/react'
import { PencilIcon } from 'assets/icons'
import { Background } from 'bot-engine'
import { useTypebot } from 'contexts/TypebotContext'
import React from 'react'
import { BackgroundSelector } from './BackgroundSelector'
import { FontSelector } from './FontSelector'
export const GeneralContent = () => {
const { typebot, updateTheme } = useTypebot()
const handleSelectFont = (font: string) => {
if (!typebot) return
updateTheme({
...typebot.theme,
general: { ...typebot.theme.general, font },
})
}
const handleBackgroundChange = (background: Background) => {
if (!typebot) return
updateTheme({
...typebot.theme,
general: { ...typebot.theme.general, background },
})
}
return (
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<PencilIcon />
<Heading fontSize="lg" color="gray.700">
General
</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
{typebot && (
<AccordionPanel as={Stack} pb={4} spacing={6}>
<FontSelector
activeFont={typebot.theme.general.font}
onSelectFont={handleSelectFont}
/>
<BackgroundSelector
initialBackground={typebot.theme.general.background}
onBackgroundChange={handleBackgroundChange}
/>
</AccordionPanel>
)}
</AccordionItem>
)
}

View File

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

View File

@ -0,0 +1,78 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Heading,
HStack,
Stack,
} from '@chakra-ui/react'
import { ChatIcon, CodeIcon, LayoutIcon } from 'assets/icons'
import React from 'react'
import { GeneralContent } from './GeneralContent'
export const SideMenu = () => {
return (
<Stack flex="1" maxW="400px" borderRightWidth={1} pt={10} spacing={10}>
<Heading fontSize="xl" textAlign="center">
Customize the theme
</Heading>
<Accordion allowMultiple allowToggle>
<GeneralContent />
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<LayoutIcon />
<Heading fontSize="lg" color="gray.700">
Layout
</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<ChatIcon />
<Heading fontSize="lg" color="gray.700">
Chat
</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<CodeIcon />
<Heading fontSize="lg" color="gray.700">
Custom CSS
</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}

View File

@ -0,0 +1,22 @@
import { Flex } from '@chakra-ui/react'
import { TypebotViewer } from 'bot-engine'
import { useTypebot } from 'contexts/TypebotContext'
import React, { useMemo } from 'react'
import { parseTypebotToPublicTypebot } from 'services/typebots'
import { SideMenu } from './SideMenu'
export const ThemeContent = () => {
const { typebot } = useTypebot()
const publicTypebot = useMemo(
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
[typebot]
)
return (
<Flex h="full" w="full">
<SideMenu />
<Flex flex="1">
{publicTypebot && <TypebotViewer typebot={publicTypebot} />}
</Flex>
</Flex>
)
}

View File

@ -1,5 +1,5 @@
import { useToast } from '@chakra-ui/react'
import { Block, Step, StepType, Target, Typebot } from 'bot-engine'
import { Block, Step, StepType, Target, Theme, Typebot } from 'bot-engine'
import { useRouter } from 'next/router'
import {
createContext,
@ -46,6 +46,7 @@ const typebotContext = createContext<{
target?: Target
}) => void
undo: () => void
updateTheme: (theme: Theme) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
@ -264,6 +265,11 @@ export const TypebotContext = ({
setLocalTypebot({ ...localTypebot, blocks })
}
const updateTheme = (theme: Theme) => {
if (!localTypebot) return
setLocalTypebot({ ...localTypebot, theme })
}
return (
<typebotContext.Provider
value={{
@ -279,6 +285,7 @@ export const TypebotContext = ({
save: saveTypebot,
removeBlock,
undo,
updateTheme,
}}
>
{children}

View File

@ -1,4 +1,4 @@
import { StartBlock, StepType } from 'bot-engine'
import { BackgroundType, StartBlock, StepType, Theme } from 'bot-engine'
import { Typebot, User } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
@ -38,8 +38,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
},
],
}
const theme: Theme = {
general: {
font: 'Open Sans',
background: { type: BackgroundType.NONE, content: '#ffffff' },
},
}
const typebot = await prisma.typebot.create({
data: { ...data, ownerId: user.id, startBlock },
data: { ...data, ownerId: user.id, startBlock, theme },
})
return res.send(typebot)
}

View File

@ -20,7 +20,7 @@ const TypebotEditPage = () => {
<EditorContext>
<KBarProvider actions={actions}>
<KBar />
<Flex overflow="hidden" h="100vh">
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<GraphProvider>
<Board />

View File

@ -0,0 +1,23 @@
import { Flex } from '@chakra-ui/layout'
import withAuth from 'components/HOC/withUser'
import { Seo } from 'components/Seo'
import { TypebotHeader } from 'components/shared/TypebotHeader'
import { ThemeContent } from 'components/theme/ThemeContent'
import { TypebotContext } from 'contexts/TypebotContext'
import { useRouter } from 'next/router'
import React from 'react'
const ThemePage = () => {
const { query } = useRouter()
return (
<TypebotContext typebotId={query.id?.toString()}>
<Seo title="Theme" />
<Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader />
<ThemeContent />
</Flex>
</TypebotContext>
)
}
export default withAuth(ThemePage)

View File

@ -5,6 +5,7 @@ import { useRouter } from 'next/router'
import { Seo } from 'components/Seo'
import { DashboardHeader } from 'components/dashboard/DashboardHeader'
import { createTypebot } from 'services/typebots'
import withAuth from 'components/HOC/withUser'
const TemplatesPage = () => {
const user = useUser()
@ -25,7 +26,7 @@ const TemplatesPage = () => {
folderId: router.query.folderId?.toString() ?? null,
})
if (error) toast({ description: error.message })
if (data) router.push(`/typebots/${data.id}`)
if (data) router.push(`/typebots/${data.id}/edit`)
setIsLoading(false)
}
@ -40,4 +41,4 @@ const TemplatesPage = () => {
)
}
export default TemplatesPage
export default withAuth(TemplatesPage)

View File

@ -155,4 +155,5 @@ export const parseTypebotToPublicTypebot = (
name: typebot.name,
startBlock: typebot.startBlock,
typebotId: typebot.id,
theme: typebot.theme,
})

View File

@ -11,22 +11,22 @@
"react-transition-group": "^4.4.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.1.1",
"@rollup/plugin-typescript": "^8.3.0",
"@types/react": "^17.0.37",
"@types/react-scroll": "^1.8.3",
"@types/react-transition-group": "^4.4.4",
"autoprefixer": "^10.4.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.7",
"typescript": "^4.5.4",
"rollup": "^2.61.1",
"rollup-plugin-dts": "^4.0.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.1.1",
"@rollup/plugin-typescript": "^8.3.0"
"tailwindcss": "^3.0.7",
"typescript": "^4.5.4"
},
"peerDependencies": {
"react": "^17.0.2"

View File

@ -16,11 +16,13 @@ export default [
file: packageJson.main,
format: 'cjs',
sourcemap: true,
inlineDynamicImports: true,
},
{
file: packageJson.module,
format: 'esm',
sourcemap: true,
inlineDynamicImports: true,
},
],
plugins: [

View File

@ -4,10 +4,9 @@
:root {
--typebot-container-bg-image: none;
--typebot-container-bg-color: #f7f8ff;
--typebot-container-font-family: 'Inter';
--typebot-container-bg-color: transparent;
--typebot-container-font-family: 'Open Sans';
--typebot-chat-view-max-width: 700px;
--typebot-chat-view-bg-color: #ffffff;
--typebot-chat-view-color: #303235;
--typebot-button-active-bg-color: #0042da;
@ -318,7 +317,6 @@ textarea {
.typebot-chat-view {
max-width: var(--typebot-chat-view-max-width);
background-color: var(--typebot-chat-view-bg-color);
}
.typebot-button.active {

View File

@ -3,6 +3,8 @@ import { PublicTypebot } from '..'
import { Block } from '..'
import { ChatBlock } from './ChatBlock/ChatBlock'
import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme'
export const ConversationContainer = ({
typebot,
@ -11,6 +13,7 @@ export const ConversationContainer = ({
typebot: PublicTypebot
onNewBlockVisisble: (blockId: string) => void
}) => {
const { document: frameDocument } = useFrame()
const [displayedBlocks, setDisplayedBlocks] = useState<Block[]>([])
const [isConversationEnded, setIsConversationEnded] = useState(false)
@ -28,6 +31,10 @@ export const ConversationContainer = ({
if (firstBlockId) displayNextBlock(firstBlockId)
}, [])
useEffect(() => {
setCssVariablesValue(typebot.theme, frameDocument.body.style)
}, [typebot.theme, frameDocument])
return (
<div
className="overflow-y-scroll w-full lg:w-3/4 min-h-full rounded lg:px-5 px-3 pt-10 relative scrollable-container typebot-chat-view"

View File

@ -1,5 +1,5 @@
import React from 'react'
import { PublicTypebot } from '../models'
import React, { useMemo } from 'react'
import { BackgroundType, PublicTypebot } from '../models'
import { TypebotContext } from '../contexts/TypebotContext'
import Frame from 'react-frame-component'
//@ts-ignore
@ -9,25 +9,47 @@ import { ResultContext } from '../contexts/ResultsContext'
export type TypebotViewerProps = {
typebot: PublicTypebot
onNewBlockVisisble: (blockId: string) => void
onNewBlockVisisble?: (blockId: string) => void
}
export const TypebotViewer = ({
typebot,
onNewBlockVisisble,
}: TypebotViewerProps) => {
const containerBgColor = useMemo(
() =>
typebot.theme.general.background.type === BackgroundType.COLOR
? typebot.theme.general.background.content
: 'transparent',
[typebot.theme.general.background]
)
const handleNewBlockVisible = (blockId: string) => {
if (onNewBlockVisisble) onNewBlockVisisble(blockId)
}
return (
<Frame
id="typebot-iframe"
head={<style>{style}</style>}
style={{ width: '100%' }}
>
<style
dangerouslySetInnerHTML={{
__html: `@import url('https://fonts.googleapis.com/css2?family=${typebot.theme.general.font}:wght@300;400;600&display=swap');`,
}}
/>
<TypebotContext typebot={typebot}>
<ResultContext typebotId={typebot.id}>
<div className="flex text-base overflow-hidden bg-cover h-screen w-screen typebot-container flex-col items-center">
<div
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
style={{
// We set this as inline style to avoid color flashing (due to SSR)
backgroundColor: containerBgColor,
}}
>
<div className="flex w-full h-full justify-center">
<ConversationContainer
typebot={typebot}
onNewBlockVisisble={onNewBlockVisisble}
onNewBlockVisisble={handleNewBlockVisible}
/>
</div>
</div>

View File

@ -1,10 +1,11 @@
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
import { Block, StartBlock } from '.'
import { Block, StartBlock, Theme } from '.'
export type PublicTypebot = Omit<
PublicTypebotFromPrisma,
'blocks' | 'startBlock'
'blocks' | 'startBlock' | 'theme'
> & {
blocks: Block[]
startBlock: StartBlock
theme: Theme
}

View File

@ -1,8 +1,12 @@
import { Typebot as TypebotFromPrisma } from 'db'
export type Typebot = Omit<TypebotFromPrisma, 'blocks' | 'startBlock'> & {
export type Typebot = Omit<
TypebotFromPrisma,
'blocks' | 'startBlock' | 'theme'
> & {
blocks: Block[]
startBlock: StartBlock
theme: Theme
}
export type StartBlock = {
@ -53,11 +57,20 @@ export type TextInputStep = StepBase & {
type: StepType.TEXT_INPUT
}
export type Button = {
id: string
content: string
target: {
type: 'block' | 'step'
id: string
export type Theme = {
general: {
font: string
background: Background
}
}
export enum BackgroundType {
COLOR = 'Color',
IMAGE = 'Image',
NONE = 'None',
}
export type Background = {
type: BackgroundType
content: string
}

View File

@ -0,0 +1,25 @@
import { BackgroundType, Theme } from '../models'
const cssVariableNames = {
container: {
bg: {
image: '--typebot-container-bg-image',
color: '--typebot-container-bg-color',
},
fontFamily: '--typebot-container-font-family',
},
}
export const setCssVariablesValue = (
theme: Theme,
documentStyle: CSSStyleDeclaration
) => {
const { background, font } = theme.general
documentStyle.setProperty(
background.type === BackgroundType.IMAGE
? cssVariableNames.container.bg.image
: cssVariableNames.container.bg.color,
background.content
)
documentStyle.setProperty(cssVariableNames.container.fontFamily, font)
}

View File

@ -4,16 +4,17 @@
"main": "./index.tsx",
"types": "./index.tsx",
"devDependencies": {
"prisma": "latest",
"prisma": "^3.7.0",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
"dependencies": {
"@prisma/client": "latest"
"@prisma/client": "^3.7.0"
},
"scripts": {
"dev": "yarn prisma db push && BROWSER=none yarn prisma studio",
"build": "prisma generate && prisma migrate deploy",
"migration:push": "dotenv -e ../../.env yarn prisma db push",
"migration:create": "dotenv -e ../../.env yarn prisma migrate dev",
"migration:reset": "dotenv -e ../../.env yarn prisma migrate reset"
}

View File

@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `theme` to the `PublicTypebot` table without a default value. This is not possible if the table is not empty.
- Added the required column `theme` to the `Typebot` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PublicTypebot" ADD COLUMN "theme" JSONB NOT NULL;
-- AlterTable
ALTER TABLE "Typebot" ADD COLUMN "theme" JSONB NOT NULL;

View File

@ -89,6 +89,7 @@ model Typebot {
folder DashboardFolder? @relation(fields: [folderId], references: [id])
blocks Json[]
startBlock Json
theme Json
}
model PublicTypebot {
@ -98,14 +99,15 @@ model PublicTypebot {
name String
blocks Json[]
startBlock Json
theme Json
}
model Result {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
typebotId String
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
answers Json[]
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
typebotId String
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
answers Json[]
isCompleted Boolean?
}

View File

@ -1180,22 +1180,22 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7"
integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==
"@prisma/client@latest":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.6.0.tgz#68a60cd4c73a369b11f72e173e86fd6789939293"
integrity sha512-ycSGY9EZGROtje0iCNsgC5Zqi/ttX2sO7BNMYaLsUMiTlf3F69ZPH+08pRo0hrDfkZzyimXYqeXJlaoYDH1w7A==
"@prisma/client@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.7.0.tgz#9cafc105f12635c95e9b7e7b18e8fbf52cf3f18a"
integrity sha512-fUJMvBOX5C7JPc0e3CJD6Gbelbu4dMJB4ScYpiht8HMUnRShw20ULOipTopjNtl6ekHQJ4muI7pXlQxWS9nMbw==
dependencies:
"@prisma/engines-version" "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727"
"@prisma/engines-version" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f"
"@prisma/engines-version@3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727":
version "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727.tgz#25aa447776849a774885866b998732b37ec4f4f5"
integrity sha512-vtoO2ys6mSfc8ONTWdcYztKN3GBU1tcKBj0aXObyjzSuGwHFcM/pEA0xF+n1W4/0TAJgfoPX2khNEit6g0jtNA==
"@prisma/engines-version@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f":
version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#055f36ac8b06c301332c14963cd0d6c795942c90"
integrity sha512-+qx2b+HK7BKF4VCa0LZ/t1QCXsu6SmvhUQyJkOD2aPpmOzket4fEnSKQZSB0i5tl7rwCDsvAiSeK8o7rf+yvwg==
"@prisma/engines@3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727":
version "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727.tgz#c68ede6aeffa9ef7743a32cfa6daf9172a4e15b3"
integrity sha512-dRClHS7DsTVchDKzeG72OaEyeDskCv91pnZ72Fftn0mp4BkUvX2LvWup65hCNzwwQm5IDd6A88APldKDnMiEMA==
"@prisma/engines@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f":
version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#12f28d5b78519fbd84c89a5bdff457ff5095e7a2"
integrity sha512-W549ub5NlgexNhR8EFstA/UwAWq3Zq0w9aNkraqsozVCt2CsX+lK4TK7IW5OZVSnxHwRjrgEAt3r9yPy8nZQRg==
"@reach/alert@0.13.2":
version "0.13.2"
@ -5971,12 +5971,12 @@ pretty-format@^3.8.0:
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=
prisma@latest:
version "3.6.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.6.0.tgz#99532abc02e045e58c6133a19771bdeb28cecdbe"
integrity sha512-6SqgHS/5Rq6HtHjsWsTxlj+ySamGyCLBUQfotc2lStOjPv52IQuDVpp58GieNqc9VnfuFyHUvTZw7aQB+G2fvQ==
prisma@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.7.0.tgz#9c73eeb2f16f767fdf523d0f4cc4c749734d62e2"
integrity sha512-pzgc95msPLcCHqOli7Hnabu/GRfSGSUWl5s2P6N13T/rgMB+NNeKbxCmzQiZT2yLOeLEPivV6YrW1oeQIwJxcg==
dependencies:
"@prisma/engines" "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727"
"@prisma/engines" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f"
process-nextick-args@~2.0.0:
version "2.0.1"