🦴 Add theme page backbone
This commit is contained in:
@ -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'
|
Reference in New Issue
Block a user