🦴 Add theme page backbone
This commit is contained in:
@ -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>
|
||||||
|
)
|
||||||
|
@ -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 />
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
|
@ -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'}
|
||||||
>
|
>
|
||||||
|
@ -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 <></>
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { BackgroundSelector } from './BackgroundSelector'
|
99
apps/builder/components/theme/GeneralContent/ColorPicker.tsx
Normal file
99
apps/builder/components/theme/GeneralContent/ColorPicker.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { FontSelector } from './FontSelector'
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
1
apps/builder/components/theme/GeneralContent/index.ts
Normal file
1
apps/builder/components/theme/GeneralContent/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { GeneralContent } from './GeneralContent'
|
78
apps/builder/components/theme/SideMenu.tsx
Normal file
78
apps/builder/components/theme/SideMenu.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
22
apps/builder/components/theme/ThemeContent.tsx
Normal file
22
apps/builder/components/theme/ThemeContent.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
|
23
apps/builder/pages/typebots/[id]/theme.tsx
Normal file
23
apps/builder/pages/typebots/[id]/theme.tsx
Normal 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)
|
@ -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)
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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"
|
||||||
|
@ -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: [
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
25
packages/bot-engine/src/services/theme.ts
Normal file
25
packages/bot-engine/src/services/theme.ts
Normal 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)
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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;
|
@ -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,6 +99,7 @@ model PublicTypebot {
|
|||||||
name String
|
name String
|
||||||
blocks Json[]
|
blocks Json[]
|
||||||
startBlock Json
|
startBlock Json
|
||||||
|
theme Json
|
||||||
}
|
}
|
||||||
|
|
||||||
model Result {
|
model Result {
|
||||||
|
36
yarn.lock
36
yarn.lock
@ -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"
|
||||||
|
Reference in New Issue
Block a user