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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ export const TypebotHeader = () => {
borderBottomWidth="1px" borderBottomWidth="1px"
justify="center" justify="center"
align="center" align="center"
pos="fixed" pos="relative"
h={`${headerHeight}px`} h={`${headerHeight}px`}
zIndex={2} zIndex={2}
bgColor="white" bgColor="white"
@ -37,10 +37,7 @@ export const TypebotHeader = () => {
<HStack> <HStack>
<Button <Button
as={NextChakraLink} as={NextChakraLink}
href={{ href={`/typebots/${typebot?.id}/edit`}
pathname: `/typebots/${typebot?.id}/edit`,
query: { ...router.query, typebotId: [] },
}}
colorScheme={router.pathname.includes('/edit') ? 'blue' : 'gray'} colorScheme={router.pathname.includes('/edit') ? 'blue' : 'gray'}
variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'} variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'}
> >
@ -48,21 +45,15 @@ export const TypebotHeader = () => {
</Button> </Button>
<Button <Button
as={NextChakraLink} as={NextChakraLink}
href={{ href={`/typebots/${typebot?.id}/theme`}
pathname: `/typebots/${typebot?.id}/design`, colorScheme={router.pathname.endsWith('theme') ? 'blue' : 'gray'}
query: { ...router.query, typebotId: [] }, variant={router.pathname.endsWith('theme') ? 'outline' : 'ghost'}
}}
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
> >
Theme Theme
</Button> </Button>
<Button <Button
as={NextChakraLink} as={NextChakraLink}
href={{ href={`/typebots/${typebot?.id}/design`}
pathname: `/typebots/${typebot?.id}/design`,
query: { ...router.query, typebotId: [] },
}}
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'} colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'} variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
> >
@ -70,10 +61,7 @@ export const TypebotHeader = () => {
</Button> </Button>
<Button <Button
as={NextChakraLink} as={NextChakraLink}
href={{ href={`/typebots/${typebot?.id}/share`}
pathname: `/typebots/${typebot?.id}/share`,
query: { ...router.query, typebotId: [] },
}}
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'} colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'} variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
> >
@ -81,10 +69,7 @@ export const TypebotHeader = () => {
</Button> </Button>
<Button <Button
as={NextChakraLink} as={NextChakraLink}
href={{ href={`/typebots/${typebot?.id}/results/responses`}
pathname: `/typebots/${typebot?.id}/results/responses`,
query: { ...router.query, typebotId: [] },
}}
colorScheme={router.pathname.includes('results') ? 'blue' : 'gray'} colorScheme={router.pathname.includes('results') ? 'blue' : 'gray'}
variant={router.pathname.includes('results') ? 'outline' : 'ghost'} 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 { 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 { useRouter } from 'next/router'
import { import {
createContext, createContext,
@ -46,6 +46,7 @@ const typebotContext = createContext<{
target?: Target target?: Target
}) => void }) => void
undo: () => void undo: () => void
updateTheme: (theme: Theme) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})
@ -264,6 +265,11 @@ export const TypebotContext = ({
setLocalTypebot({ ...localTypebot, blocks }) setLocalTypebot({ ...localTypebot, blocks })
} }
const updateTheme = (theme: Theme) => {
if (!localTypebot) return
setLocalTypebot({ ...localTypebot, theme })
}
return ( return (
<typebotContext.Provider <typebotContext.Provider
value={{ value={{
@ -279,6 +285,7 @@ export const TypebotContext = ({
save: saveTypebot, save: saveTypebot,
removeBlock, removeBlock,
undo, undo,
updateTheme,
}} }}
> >
{children} {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 { Typebot, User } from 'db'
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next' 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({ const typebot = await prisma.typebot.create({
data: { ...data, ownerId: user.id, startBlock }, data: { ...data, ownerId: user.id, startBlock, theme },
}) })
return res.send(typebot) return res.send(typebot)
} }

View File

@ -20,7 +20,7 @@ const TypebotEditPage = () => {
<EditorContext> <EditorContext>
<KBarProvider actions={actions}> <KBarProvider actions={actions}>
<KBar /> <KBar />
<Flex overflow="hidden" h="100vh"> <Flex overflow="hidden" h="100vh" flexDir="column">
<TypebotHeader /> <TypebotHeader />
<GraphProvider> <GraphProvider>
<Board /> <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 { Seo } from 'components/Seo'
import { DashboardHeader } from 'components/dashboard/DashboardHeader' import { DashboardHeader } from 'components/dashboard/DashboardHeader'
import { createTypebot } from 'services/typebots' import { createTypebot } from 'services/typebots'
import withAuth from 'components/HOC/withUser'
const TemplatesPage = () => { const TemplatesPage = () => {
const user = useUser() const user = useUser()
@ -25,7 +26,7 @@ const TemplatesPage = () => {
folderId: router.query.folderId?.toString() ?? null, folderId: router.query.folderId?.toString() ?? null,
}) })
if (error) toast({ description: error.message }) if (error) toast({ description: error.message })
if (data) router.push(`/typebots/${data.id}`) if (data) router.push(`/typebots/${data.id}/edit`)
setIsLoading(false) 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, name: typebot.name,
startBlock: typebot.startBlock, startBlock: typebot.startBlock,
typebotId: typebot.id, typebotId: typebot.id,
theme: typebot.theme,
}) })

View File

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

View File

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

View File

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

View File

@ -3,6 +3,8 @@ import { PublicTypebot } from '..'
import { Block } from '..' import { Block } from '..'
import { ChatBlock } from './ChatBlock/ChatBlock' import { ChatBlock } from './ChatBlock/ChatBlock'
import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme'
export const ConversationContainer = ({ export const ConversationContainer = ({
typebot, typebot,
@ -11,6 +13,7 @@ export const ConversationContainer = ({
typebot: PublicTypebot typebot: PublicTypebot
onNewBlockVisisble: (blockId: string) => void onNewBlockVisisble: (blockId: string) => void
}) => { }) => {
const { document: frameDocument } = useFrame()
const [displayedBlocks, setDisplayedBlocks] = useState<Block[]>([]) const [displayedBlocks, setDisplayedBlocks] = useState<Block[]>([])
const [isConversationEnded, setIsConversationEnded] = useState(false) const [isConversationEnded, setIsConversationEnded] = useState(false)
@ -28,6 +31,10 @@ export const ConversationContainer = ({
if (firstBlockId) displayNextBlock(firstBlockId) if (firstBlockId) displayNextBlock(firstBlockId)
}, []) }, [])
useEffect(() => {
setCssVariablesValue(typebot.theme, frameDocument.body.style)
}, [typebot.theme, frameDocument])
return ( return (
<div <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" 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 React, { useMemo } from 'react'
import { PublicTypebot } from '../models' import { BackgroundType, PublicTypebot } from '../models'
import { TypebotContext } from '../contexts/TypebotContext' import { TypebotContext } from '../contexts/TypebotContext'
import Frame from 'react-frame-component' import Frame from 'react-frame-component'
//@ts-ignore //@ts-ignore
@ -9,25 +9,47 @@ import { ResultContext } from '../contexts/ResultsContext'
export type TypebotViewerProps = { export type TypebotViewerProps = {
typebot: PublicTypebot typebot: PublicTypebot
onNewBlockVisisble: (blockId: string) => void onNewBlockVisisble?: (blockId: string) => void
} }
export const TypebotViewer = ({ export const TypebotViewer = ({
typebot, typebot,
onNewBlockVisisble, onNewBlockVisisble,
}: TypebotViewerProps) => { }: 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 ( return (
<Frame <Frame
id="typebot-iframe" id="typebot-iframe"
head={<style>{style}</style>} head={<style>{style}</style>}
style={{ width: '100%' }} 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}> <TypebotContext typebot={typebot}>
<ResultContext typebotId={typebot.id}> <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"> <div className="flex w-full h-full justify-center">
<ConversationContainer <ConversationContainer
typebot={typebot} typebot={typebot}
onNewBlockVisisble={onNewBlockVisisble} onNewBlockVisisble={handleNewBlockVisible}
/> />
</div> </div>
</div> </div>

View File

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

View File

@ -1,8 +1,12 @@
import { Typebot as TypebotFromPrisma } from 'db' import { Typebot as TypebotFromPrisma } from 'db'
export type Typebot = Omit<TypebotFromPrisma, 'blocks' | 'startBlock'> & { export type Typebot = Omit<
TypebotFromPrisma,
'blocks' | 'startBlock' | 'theme'
> & {
blocks: Block[] blocks: Block[]
startBlock: StartBlock startBlock: StartBlock
theme: Theme
} }
export type StartBlock = { export type StartBlock = {
@ -53,11 +57,20 @@ export type TextInputStep = StepBase & {
type: StepType.TEXT_INPUT type: StepType.TEXT_INPUT
} }
export type Button = { export type Theme = {
id: string general: {
content: string font: string
target: { background: Background
type: 'block' | 'step'
id: string
} }
} }
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", "main": "./index.tsx",
"types": "./index.tsx", "types": "./index.tsx",
"devDependencies": { "devDependencies": {
"prisma": "latest", "prisma": "^3.7.0",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "latest" "@prisma/client": "^3.7.0"
}, },
"scripts": { "scripts": {
"dev": "yarn prisma db push && BROWSER=none yarn prisma studio", "dev": "yarn prisma db push && BROWSER=none yarn prisma studio",
"build": "prisma generate && prisma migrate deploy", "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:create": "dotenv -e ../../.env yarn prisma migrate dev",
"migration:reset": "dotenv -e ../../.env yarn prisma migrate reset" "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]) folder DashboardFolder? @relation(fields: [folderId], references: [id])
blocks Json[] blocks Json[]
startBlock Json startBlock Json
theme Json
} }
model PublicTypebot { model PublicTypebot {
@ -98,14 +99,15 @@ model PublicTypebot {
name String name String
blocks Json[] blocks Json[]
startBlock Json startBlock Json
theme Json
} }
model Result { model Result {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) updatedAt DateTime @default(now())
typebotId String typebotId String
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
answers Json[] answers Json[]
isCompleted Boolean? isCompleted Boolean?
} }

View File

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