♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@@ -0,0 +1,87 @@
import React from 'react'
import { AvatarProps } from 'models'
import {
Heading,
HStack,
Popover,
PopoverContent,
PopoverTrigger,
Stack,
Switch,
Image,
Flex,
Box,
Portal,
} from '@chakra-ui/react'
import { ImageUploadContent } from '@/components/ImageUploadContent'
import { DefaultAvatar } from '../DefaultAvatar'
type Props = {
uploadFilePath: string
title: string
avatarProps?: AvatarProps
isDefaultCheck?: boolean
onAvatarChange: (avatarProps: AvatarProps) => void
}
export const AvatarForm = ({
uploadFilePath,
title,
avatarProps,
isDefaultCheck = false,
onAvatarChange,
}: Props) => {
const isChecked = avatarProps ? avatarProps.isEnabled : isDefaultCheck
const handleOnCheck = () =>
onAvatarChange({ ...avatarProps, isEnabled: !isChecked })
const handleImageUrl = (url: string) =>
onAvatarChange({ isEnabled: isChecked, url })
const isDefaultAvatar = !avatarProps?.url || avatarProps.url.includes('{{')
return (
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Flex justifyContent="space-between">
<HStack>
<Heading as="label" fontSize="lg" htmlFor={title} mb="1">
{title}
</Heading>
<Switch isChecked={isChecked} id={title} onChange={handleOnCheck} />
</HStack>
{isChecked && (
<Popover isLazy>
<PopoverTrigger>
{isDefaultAvatar ? (
<Box>
<DefaultAvatar
cursor="pointer"
_hover={{ filter: 'brightness(.9)' }}
/>
</Box>
) : (
<Image
src={avatarProps.url}
alt="Website image"
cursor="pointer"
_hover={{ filter: 'brightness(.9)' }}
transition="filter 200ms"
rounded="full"
boxSize="40px"
objectFit="cover"
/>
)}
</PopoverTrigger>
<Portal>
<PopoverContent p="4">
<ImageUploadContent
filePath={uploadFilePath}
defaultUrl={avatarProps?.url}
onSubmit={handleImageUrl}
/>
</PopoverContent>
</Portal>
</Popover>
)}
</Flex>
</Stack>
)
}

View File

@@ -0,0 +1,35 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
buttons: ContainerColors
onButtonsChange: (buttons: ContainerColors) => void
}
export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onButtonsChange({ ...buttons, backgroundColor })
const handleTextChange = (color: string) =>
onButtonsChange({ ...buttons, color })
return (
<Stack data-testid="buttons-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={buttons.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={buttons.color}
onColorChange={handleTextChange}
/>
</Flex>
</Stack>
)
}

View File

@@ -0,0 +1,80 @@
import { Heading, Stack } from '@chakra-ui/react'
import { AvatarProps, ChatTheme, ContainerColors, InputColors } from 'models'
import React from 'react'
import { AvatarForm } from './AvatarForm'
import { ButtonsTheme } from './ButtonsTheme'
import { GuestBubbles } from './GuestBubbles'
import { HostBubbles } from './HostBubbles'
import { InputsTheme } from './InputsTheme'
type Props = {
typebotId: string
chatTheme: ChatTheme
onChatThemeChange: (chatTheme: ChatTheme) => void
}
export const ChatThemeSettings = ({
typebotId,
chatTheme,
onChatThemeChange,
}: Props) => {
const handleHostBubblesChange = (hostBubbles: ContainerColors) =>
onChatThemeChange({ ...chatTheme, hostBubbles })
const handleGuestBubblesChange = (guestBubbles: ContainerColors) =>
onChatThemeChange({ ...chatTheme, guestBubbles })
const handleButtonsChange = (buttons: ContainerColors) =>
onChatThemeChange({ ...chatTheme, buttons })
const handleInputsChange = (inputs: InputColors) =>
onChatThemeChange({ ...chatTheme, inputs })
const handleHostAvatarChange = (hostAvatar: AvatarProps) =>
onChatThemeChange({ ...chatTheme, hostAvatar })
const handleGuestAvatarChange = (guestAvatar: AvatarProps) =>
onChatThemeChange({ ...chatTheme, guestAvatar })
return (
<Stack spacing={6}>
<AvatarForm
uploadFilePath={`typebots/${typebotId}/hostAvatar`}
title="Bot avatar"
avatarProps={chatTheme.hostAvatar}
isDefaultCheck
onAvatarChange={handleHostAvatarChange}
/>
<AvatarForm
uploadFilePath={`typebots/${typebotId}/guestAvatar`}
title="User avatar"
avatarProps={chatTheme.guestAvatar}
onAvatarChange={handleGuestAvatarChange}
/>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Bot bubbles</Heading>
<HostBubbles
hostBubbles={chatTheme.hostBubbles}
onHostBubblesChange={handleHostBubblesChange}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">User bubbles</Heading>
<GuestBubbles
guestBubbles={chatTheme.guestBubbles}
onGuestBubblesChange={handleGuestBubblesChange}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Buttons</Heading>
<ButtonsTheme
buttons={chatTheme.buttons}
onButtonsChange={handleButtonsChange}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Inputs</Heading>
<InputsTheme
inputs={chatTheme.inputs}
onInputsChange={handleInputsChange}
/>
</Stack>
</Stack>
)
}

View File

@@ -0,0 +1,35 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
guestBubbles: ContainerColors
onGuestBubblesChange: (hostBubbles: ContainerColors) => void
}
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onGuestBubblesChange({ ...guestBubbles, backgroundColor })
const handleTextChange = (color: string) =>
onGuestBubblesChange({ ...guestBubbles, color })
return (
<Stack data-testid="guest-bubbles-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={guestBubbles.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={guestBubbles.color}
onColorChange={handleTextChange}
/>
</Flex>
</Stack>
)
}

View File

@@ -0,0 +1,35 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
hostBubbles: ContainerColors
onHostBubblesChange: (hostBubbles: ContainerColors) => void
}
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onHostBubblesChange({ ...hostBubbles, backgroundColor })
const handleTextChange = (color: string) =>
onHostBubblesChange({ ...hostBubbles, color })
return (
<Stack data-testid="host-bubbles-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={hostBubbles.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={hostBubbles.color}
onColorChange={handleTextChange}
/>
</Flex>
</Stack>
)
}

View File

@@ -0,0 +1,44 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { InputColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
inputs: InputColors
onInputsChange: (buttons: InputColors) => void
}
export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onInputsChange({ ...inputs, backgroundColor })
const handleTextChange = (color: string) =>
onInputsChange({ ...inputs, color })
const handlePlaceholderChange = (placeholderColor: string) =>
onInputsChange({ ...inputs, placeholderColor })
return (
<Stack data-testid="inputs-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={inputs.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={inputs.color}
onColorChange={handleTextChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Placeholder text:</Text>
<ColorPicker
initialColor={inputs.placeholderColor}
onColorChange={handlePlaceholderChange}
/>
</Flex>
</Stack>
)
}

View File

@@ -0,0 +1 @@
export { ChatThemeSettings } from './ChatThemeSettings'

View File

@@ -0,0 +1,17 @@
import { CodeEditor } from '@/components/CodeEditor'
import React from 'react'
type Props = {
customCss?: string
onCustomCssChange: (css: string) => void
}
export const CustomCssSettings = ({ customCss, onCustomCssChange }: Props) => {
return (
<CodeEditor
value={customCss ?? ''}
lang="css"
onChange={onCustomCssChange}
/>
)
}

View File

@@ -0,0 +1,38 @@
import { Icon, IconProps } from '@chakra-ui/react'
import React from 'react'
export const DefaultAvatar = (props: IconProps) => {
return (
<Icon
viewBox="0 0 75 75"
fill="none"
xmlns="http://www.w3.org/2000/svg"
boxSize="40px"
data-testid="default-avatar"
{...props}
>
<mask id="mask0" x="0" y="0" mask-type="alpha">
<circle cx="37.5" cy="37.5" r="37.5" fill="#0042DA" />
</mask>
<g mask="url(#mask0)">
<rect x="-30" y="-43" width="131" height="154" fill="#0042DA" />
<rect
x="2.50413"
y="120.333"
width="81.5597"
height="86.4577"
rx="2.5"
transform="rotate(-52.6423 2.50413 120.333)"
stroke="#FED23D"
strokeWidth="5"
/>
<circle cx="76.5" cy="-1.5" r="29" stroke="#FF8E20" strokeWidth="5" />
<path
d="M-49.8224 22L-15.5 -40.7879L18.8224 22H-49.8224Z"
stroke="#F7F8FF"
strokeWidth="5"
/>
</g>
</Icon>
)
}

View File

@@ -0,0 +1,40 @@
import { Flex, Text } from '@chakra-ui/react'
import { Background, BackgroundType } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../../components/ColorPicker'
type BackgroundContentProps = {
background?: Background
onBackgroundContentChange: (content: string) => void
}
const defaultBackgroundColor = '#ffffff'
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 ?? defaultBackgroundColor}
onColorChange={handleContentChange}
/>
</Flex>
)
case BackgroundType.IMAGE:
return (
<Flex>
<Text>Image</Text>
</Flex>
)
default:
return <></>
}
}

View File

@@ -0,0 +1,37 @@
import { Stack, Text } from '@chakra-ui/react'
import { Background, BackgroundType } from 'models'
import React from 'react'
import { BackgroundContent } from './BackgroundContent'
import { BackgroundTypeRadioButtons } from './BackgroundTypeRadioButtons'
type Props = {
background?: Background
onBackgroundChange: (newBackground: Background) => void
}
const defaultBackgroundType = BackgroundType.NONE
export const BackgroundSelector = ({
background,
onBackgroundChange,
}: Props) => {
const handleBackgroundTypeChange = (type: BackgroundType) =>
background && onBackgroundChange({ ...background, type })
const handleBackgroundContentChange = (content: string) =>
background && onBackgroundChange({ ...background, content })
return (
<Stack spacing={4}>
<Text>Background</Text>
<BackgroundTypeRadioButtons
backgroundType={background?.type ?? defaultBackgroundType}
onBackgroundTypeChange={handleBackgroundTypeChange}
/>
<BackgroundContent
background={background}
onBackgroundContentChange={handleBackgroundContentChange}
/>
</Stack>
)
}

View File

@@ -0,0 +1,73 @@
import {
Box,
Flex,
HStack,
useRadio,
useRadioGroup,
UseRadioProps,
} from '@chakra-ui/react'
import { BackgroundType } from 'models'
import { ReactNode } from 'react'
type Props = {
backgroundType: BackgroundType
onBackgroundTypeChange: (type: BackgroundType) => void
}
export const BackgroundTypeRadioButtons = ({
backgroundType,
onBackgroundTypeChange,
}: Props) => {
const options = ['Color', '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,50 @@
import React, { useEffect, useState } from 'react'
import { Text, HStack } from '@chakra-ui/react'
import { SearchableDropdown } from '@/components/SearchableDropdown'
import { env, isEmpty } from 'utils'
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 () => {
if (isEmpty(env('GOOGLE_API_KEY'))) return []
const response = await fetch(
`https://www.googleapis.com/webfonts/v1/webfonts?key=${env(
'GOOGLE_API_KEY'
)}&sort=popularity`
)
return (await response.json()).items.map(
(item: { family: string }) => item.family
)
}
const handleFontSelected = (nextFont: string) => {
if (nextFont == currentFont) return
setCurrentFont(nextFont)
onSelectFont(nextFont)
}
return (
<HStack justify="space-between" align="center">
<Text>Font</Text>
<SearchableDropdown
selectedItem={activeFont}
items={googleFonts}
onValueChange={handleFontSelected}
/>
</HStack>
)
}

View File

@@ -0,0 +1 @@
export { FontSelector } from './FontSelector'

View File

@@ -0,0 +1,34 @@
import { Stack } from '@chakra-ui/react'
import { Background, GeneralTheme } from 'models'
import React from 'react'
import { BackgroundSelector } from './BackgroundSelector'
import { FontSelector } from './FontSelector'
type Props = {
generalTheme: GeneralTheme
onGeneralThemeChange: (general: GeneralTheme) => void
}
export const GeneralSettings = ({
generalTheme,
onGeneralThemeChange,
}: Props) => {
const handleSelectFont = (font: string) =>
onGeneralThemeChange({ ...generalTheme, font })
const handleBackgroundChange = (background: Background) =>
onGeneralThemeChange({ ...generalTheme, background })
return (
<Stack spacing={6}>
<FontSelector
activeFont={generalTheme.font}
onSelectFont={handleSelectFont}
/>
<BackgroundSelector
background={generalTheme.background}
onBackgroundChange={handleBackgroundChange}
/>
</Stack>
)
}

View File

@@ -0,0 +1 @@
export { GeneralSettings } from './GeneralSettings'

View File

@@ -0,0 +1,30 @@
import { Seo } from '@/components/Seo'
import { TypebotHeader, useTypebot } from '@/features/editor'
import { Flex } from '@chakra-ui/react'
import { TypebotViewer } from 'bot-engine'
import { getViewerUrl } from 'utils'
import { ThemeSideMenu } from './ThemeSideMenu'
import { parseTypebotToPublicTypebot } from '@/features/publish'
export const ThemePage = () => {
const { typebot } = useTypebot()
const publicTypebot = typebot && parseTypebotToPublicTypebot(typebot)
return (
<Flex overflow="hidden" h="100vh" flexDir="column">
<Seo title="Theme" />
<TypebotHeader />
<Flex h="full" w="full">
<ThemeSideMenu />
<Flex flex="1">
{publicTypebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={publicTypebot}
/>
)}
</Flex>
</Flex>
</Flex>
)
}

View File

@@ -0,0 +1,101 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Heading,
HStack,
Stack,
} from '@chakra-ui/react'
import { ChatIcon, CodeIcon, PencilIcon } from '@/components/icons'
import { ChatTheme, GeneralTheme } from 'models'
import React from 'react'
import { ChatThemeSettings } from './ChatSettings'
import { CustomCssSettings } from './CustomCssSettings/CustomCssSettings'
import { GeneralSettings } from './GeneralSettings'
import { headerHeight, useTypebot } from '@/features/editor'
export const ThemeSideMenu = () => {
const { typebot, updateTypebot } = useTypebot()
const handleChatThemeChange = (chat: ChatTheme) =>
typebot && updateTypebot({ theme: { ...typebot.theme, chat } })
const handleGeneralThemeChange = (general: GeneralTheme) =>
typebot && updateTypebot({ theme: { ...typebot.theme, general } })
const handleCustomCssChange = (customCss: string) =>
typebot && updateTypebot({ theme: { ...typebot.theme, customCss } })
return (
<Stack
flex="1"
maxW="400px"
height={`calc(100vh - ${headerHeight}px)`}
borderRightWidth={1}
pt={10}
spacing={10}
overflowY="scroll"
pb="20"
>
<Heading fontSize="xl" textAlign="center">
Customize the theme
</Heading>
<Accordion allowMultiple defaultIndex={[0]}>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<PencilIcon />
<Heading fontSize="lg">General</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{typebot && (
<GeneralSettings
generalTheme={typebot.theme.general}
onGeneralThemeChange={handleGeneralThemeChange}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<ChatIcon />
<Heading fontSize="lg">Chat</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{typebot && (
<ChatThemeSettings
typebotId={typebot.id}
chatTheme={typebot.theme.chat}
onChatThemeChange={handleChatThemeChange}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<CodeIcon />
<Heading fontSize="lg">Custom CSS</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{typebot && (
<CustomCssSettings
customCss={typebot.theme.customCss}
onCustomCssChange={handleCustomCssChange}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}