From 30ddb143b474fc03e1fd0a07266a5e31d883deb5 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 23 Dec 2021 09:37:42 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=B4=20Add=20theme=20page=20backbone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/builder/assets/icons.tsx | 24 ++++ apps/builder/components/board/Board.tsx | 9 +- .../StepNode/TextEditor/TextEditor.tsx | 1 - .../FolderContent/CreateBotButton.tsx | 1 + .../shared/TypebotHeader/TypebotHeader.tsx | 31 ++--- .../BackgroundSelector/BackgroundContent.tsx | 38 ++++++ .../BackgroundSelector/BackgroundSelector.tsx | 45 +++++++ .../BackgroundTypeRadioButtons.tsx | 73 ++++++++++++ .../BackgroundSelector/index.tsx | 1 + .../theme/GeneralContent/ColorPicker.tsx | 99 ++++++++++++++++ .../FontSelector/FontSelector.tsx | 45 +++++++ .../FontSelector/SearchableDropdown.tsx | 111 ++++++++++++++++++ .../GeneralContent/FontSelector/index.tsx | 1 + .../theme/GeneralContent/GeneralContent.tsx | 60 ++++++++++ .../components/theme/GeneralContent/index.ts | 1 + apps/builder/components/theme/SideMenu.tsx | 78 ++++++++++++ .../builder/components/theme/ThemeContent.tsx | 22 ++++ apps/builder/contexts/TypebotContext.tsx | 9 +- apps/builder/pages/api/typebots.ts | 10 +- apps/builder/pages/typebots/[id]/edit.tsx | 2 +- apps/builder/pages/typebots/[id]/theme.tsx | 23 ++++ apps/builder/pages/typebots/create.tsx | 5 +- apps/builder/services/typebots.ts | 1 + packages/bot-engine/package.json | 10 +- packages/bot-engine/rollup.config.js | 2 + packages/bot-engine/src/assets/style.css | 6 +- .../src/components/ConversationContainer.tsx | 7 ++ .../src/components/TypebotViewer.tsx | 32 ++++- .../bot-engine/src/models/publicTypebot.ts | 5 +- packages/bot-engine/src/models/typebot.ts | 27 +++-- packages/bot-engine/src/services/theme.ts | 25 ++++ packages/db/package.json | 5 +- .../20211223083710_add_theme/migration.sql | 12 ++ packages/db/prisma/schema.prisma | 14 ++- yarn.lock | 36 +++--- 35 files changed, 784 insertions(+), 87 deletions(-) create mode 100644 apps/builder/components/theme/GeneralContent/BackgroundSelector/BackgroundContent.tsx create mode 100644 apps/builder/components/theme/GeneralContent/BackgroundSelector/BackgroundSelector.tsx create mode 100644 apps/builder/components/theme/GeneralContent/BackgroundSelector/BackgroundTypeRadioButtons.tsx create mode 100644 apps/builder/components/theme/GeneralContent/BackgroundSelector/index.tsx create mode 100644 apps/builder/components/theme/GeneralContent/ColorPicker.tsx create mode 100644 apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx create mode 100644 apps/builder/components/theme/GeneralContent/FontSelector/SearchableDropdown.tsx create mode 100644 apps/builder/components/theme/GeneralContent/FontSelector/index.tsx create mode 100644 apps/builder/components/theme/GeneralContent/GeneralContent.tsx create mode 100644 apps/builder/components/theme/GeneralContent/index.ts create mode 100644 apps/builder/components/theme/SideMenu.tsx create mode 100644 apps/builder/components/theme/ThemeContent.tsx create mode 100644 apps/builder/pages/typebots/[id]/theme.tsx create mode 100644 packages/bot-engine/src/services/theme.ts create mode 100644 packages/db/prisma/migrations/20211223083710_add_theme/migration.sql diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx index 9cebf0eae..335b7da3d 100644 --- a/apps/builder/assets/icons.tsx +++ b/apps/builder/assets/icons.tsx @@ -161,3 +161,27 @@ export const TrashIcon = (props: IconProps) => ( ) + +export const LayoutIcon = (props: IconProps) => ( + + + + + +) + +export const CodeIcon = (props: IconProps) => ( + + + + +) + +export const PencilIcon = (props: IconProps) => ( + + + + + + +) diff --git a/apps/builder/components/board/Board.tsx b/apps/builder/components/board/Board.tsx index ae4f34d2c..ec63221d1 100644 --- a/apps/builder/components/board/Board.tsx +++ b/apps/builder/components/board/Board.tsx @@ -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 ( - + diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx index a49b3ff39..0a7684d48 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx @@ -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, diff --git a/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx b/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx index c12a0d046..ec1a5db38 100644 --- a/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx +++ b/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx @@ -22,6 +22,7 @@ export const CreateBotButton = ({ onClick={handleClick} paddingX={6} whiteSpace={'normal'} + colorScheme="blue" {...props} > diff --git a/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx b/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx index 1c3487b24..70756c9ae 100644 --- a/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx +++ b/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx @@ -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 = () => { + ))} + + { + setColor(e.target.value) + }} + /> + + + + ) +} diff --git a/apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx b/apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx new file mode 100644 index 000000000..bd9c54cd2 --- /dev/null +++ b/apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx @@ -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([]) + + 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 ( + + Font + { + if (nextFont !== currentFont) { + setCurrentFont(nextFont) + onSelectFont(nextFont) + } + }} + /> + + ) +} diff --git a/apps/builder/components/theme/GeneralContent/FontSelector/SearchableDropdown.tsx b/apps/builder/components/theme/GeneralContent/FontSelector/SearchableDropdown.tsx new file mode 100644 index 000000000..b81f6f3ca --- /dev/null +++ b/apps/builder/components/theme/GeneralContent/FontSelector/SearchableDropdown.tsx @@ -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) => { + 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 ( + + + + + + + {filteredItems.length > 0 ? ( + <> + {filteredItems.map((item, idx) => { + return ( + + ) + })} + + ) : ( + Not found. + )} + + + + ) +} diff --git a/apps/builder/components/theme/GeneralContent/FontSelector/index.tsx b/apps/builder/components/theme/GeneralContent/FontSelector/index.tsx new file mode 100644 index 000000000..0251a318f --- /dev/null +++ b/apps/builder/components/theme/GeneralContent/FontSelector/index.tsx @@ -0,0 +1 @@ +export { FontSelector } from './FontSelector' diff --git a/apps/builder/components/theme/GeneralContent/GeneralContent.tsx b/apps/builder/components/theme/GeneralContent/GeneralContent.tsx new file mode 100644 index 000000000..953955553 --- /dev/null +++ b/apps/builder/components/theme/GeneralContent/GeneralContent.tsx @@ -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 ( + + + + + + General + + + + + {typebot && ( + + + + + )} + + ) +} diff --git a/apps/builder/components/theme/GeneralContent/index.ts b/apps/builder/components/theme/GeneralContent/index.ts new file mode 100644 index 000000000..1e7c92e7d --- /dev/null +++ b/apps/builder/components/theme/GeneralContent/index.ts @@ -0,0 +1 @@ +export { GeneralContent } from './GeneralContent' diff --git a/apps/builder/components/theme/SideMenu.tsx b/apps/builder/components/theme/SideMenu.tsx new file mode 100644 index 000000000..7c5a9f0b5 --- /dev/null +++ b/apps/builder/components/theme/SideMenu.tsx @@ -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 ( + + + Customize the theme + + + + + + + + + + Layout + + + + + + 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. + + + + + + + + Chat + + + + + + 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. + + + + + + + + Custom CSS + + + + + + 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. + + + + + ) +} diff --git a/apps/builder/components/theme/ThemeContent.tsx b/apps/builder/components/theme/ThemeContent.tsx new file mode 100644 index 000000000..9db8596dd --- /dev/null +++ b/apps/builder/components/theme/ThemeContent.tsx @@ -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 ( + + + + {publicTypebot && } + + + ) +} diff --git a/apps/builder/contexts/TypebotContext.tsx b/apps/builder/contexts/TypebotContext.tsx index 05f04071f..ce5723f59 100644 --- a/apps/builder/contexts/TypebotContext.tsx +++ b/apps/builder/contexts/TypebotContext.tsx @@ -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 ( {children} diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts index f8985a884..0fd716807 100644 --- a/apps/builder/pages/api/typebots.ts +++ b/apps/builder/pages/api/typebots.ts @@ -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) } diff --git a/apps/builder/pages/typebots/[id]/edit.tsx b/apps/builder/pages/typebots/[id]/edit.tsx index dd9d940f2..1ceda0fab 100644 --- a/apps/builder/pages/typebots/[id]/edit.tsx +++ b/apps/builder/pages/typebots/[id]/edit.tsx @@ -20,7 +20,7 @@ const TypebotEditPage = () => { - + diff --git a/apps/builder/pages/typebots/[id]/theme.tsx b/apps/builder/pages/typebots/[id]/theme.tsx new file mode 100644 index 000000000..691dd8c61 --- /dev/null +++ b/apps/builder/pages/typebots/[id]/theme.tsx @@ -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 ( + + + + + + + + ) +} + +export default withAuth(ThemePage) diff --git a/apps/builder/pages/typebots/create.tsx b/apps/builder/pages/typebots/create.tsx index c666070c6..d57457df1 100644 --- a/apps/builder/pages/typebots/create.tsx +++ b/apps/builder/pages/typebots/create.tsx @@ -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) diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts index 8f262d7ca..89222cf10 100644 --- a/apps/builder/services/typebots.ts +++ b/apps/builder/services/typebots.ts @@ -155,4 +155,5 @@ export const parseTypebotToPublicTypebot = ( name: typebot.name, startBlock: typebot.startBlock, typebotId: typebot.id, + theme: typebot.theme, }) diff --git a/packages/bot-engine/package.json b/packages/bot-engine/package.json index 971157ba9..94f6a5c95 100644 --- a/packages/bot-engine/package.json +++ b/packages/bot-engine/package.json @@ -11,22 +11,22 @@ "react-transition-group": "^4.4.2" }, "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-scroll": "^1.8.3", "@types/react-transition-group": "^4.4.4", "autoprefixer": "^10.4.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.5", - "tailwindcss": "^3.0.7", - "typescript": "^4.5.4", "rollup": "^2.61.1", "rollup-plugin-dts": "^4.0.1", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", - "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-node-resolve": "^13.1.1", - "@rollup/plugin-typescript": "^8.3.0" + "tailwindcss": "^3.0.7", + "typescript": "^4.5.4" }, "peerDependencies": { "react": "^17.0.2" diff --git a/packages/bot-engine/rollup.config.js b/packages/bot-engine/rollup.config.js index 939030861..af871fe3d 100644 --- a/packages/bot-engine/rollup.config.js +++ b/packages/bot-engine/rollup.config.js @@ -16,11 +16,13 @@ export default [ file: packageJson.main, format: 'cjs', sourcemap: true, + inlineDynamicImports: true, }, { file: packageJson.module, format: 'esm', sourcemap: true, + inlineDynamicImports: true, }, ], plugins: [ diff --git a/packages/bot-engine/src/assets/style.css b/packages/bot-engine/src/assets/style.css index a4456662a..600aa5d13 100644 --- a/packages/bot-engine/src/assets/style.css +++ b/packages/bot-engine/src/assets/style.css @@ -4,10 +4,9 @@ :root { --typebot-container-bg-image: none; - --typebot-container-bg-color: #f7f8ff; - --typebot-container-font-family: 'Inter'; + --typebot-container-bg-color: transparent; + --typebot-container-font-family: 'Open Sans'; --typebot-chat-view-max-width: 700px; - --typebot-chat-view-bg-color: #ffffff; --typebot-chat-view-color: #303235; --typebot-button-active-bg-color: #0042da; @@ -318,7 +317,6 @@ textarea { .typebot-chat-view { max-width: var(--typebot-chat-view-max-width); - background-color: var(--typebot-chat-view-bg-color); } .typebot-button.active { diff --git a/packages/bot-engine/src/components/ConversationContainer.tsx b/packages/bot-engine/src/components/ConversationContainer.tsx index 6aef9a886..0a8620eca 100644 --- a/packages/bot-engine/src/components/ConversationContainer.tsx +++ b/packages/bot-engine/src/components/ConversationContainer.tsx @@ -3,6 +3,8 @@ import { PublicTypebot } from '..' import { Block } from '..' import { ChatBlock } from './ChatBlock/ChatBlock' +import { useFrame } from 'react-frame-component' +import { setCssVariablesValue } from '../services/theme' export const ConversationContainer = ({ typebot, @@ -11,6 +13,7 @@ export const ConversationContainer = ({ typebot: PublicTypebot onNewBlockVisisble: (blockId: string) => void }) => { + const { document: frameDocument } = useFrame() const [displayedBlocks, setDisplayedBlocks] = useState([]) const [isConversationEnded, setIsConversationEnded] = useState(false) @@ -28,6 +31,10 @@ export const ConversationContainer = ({ if (firstBlockId) displayNextBlock(firstBlockId) }, []) + useEffect(() => { + setCssVariablesValue(typebot.theme, frameDocument.body.style) + }, [typebot.theme, frameDocument]) + return (
void + onNewBlockVisisble?: (blockId: string) => void } export const TypebotViewer = ({ typebot, onNewBlockVisisble, }: 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 ( {style}} style={{ width: '100%' }} > +