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

@ -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'