🦴 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>
|
||||
</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 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 />
|
||||
|
@ -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,
|
||||
|
@ -22,6 +22,7 @@ export const CreateBotButton = ({
|
||||
onClick={handleClick}
|
||||
paddingX={6}
|
||||
whiteSpace={'normal'}
|
||||
colorScheme="blue"
|
||||
{...props}
|
||||
>
|
||||
<VStack spacing="6">
|
||||
|
@ -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'}
|
||||
>
|
||||
|
@ -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 { 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}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 />
|
||||
|
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 { 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)
|
||||
|
@ -155,4 +155,5 @@ export const parseTypebotToPublicTypebot = (
|
||||
name: typebot.name,
|
||||
startBlock: typebot.startBlock,
|
||||
typebotId: typebot.id,
|
||||
theme: typebot.theme,
|
||||
})
|
||||
|
Reference in New Issue
Block a user