Skip to content

Commit

Permalink
✨ (theme) Add theme templates
Browse files Browse the repository at this point in the history
Allows you to save your themes and select a theme from Typebot's gallery

Closes #275
  • Loading branch information
baptisteArno committed Mar 28, 2023
1 parent c1cf817 commit 38ed575
Show file tree
Hide file tree
Showing 49 changed files with 2,065 additions and 115 deletions.
46 changes: 26 additions & 20 deletions apps/builder/src/components/ColorPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
Stack,
ButtonProps,
} from '@chakra-ui/react'
import React, { ChangeEvent, useEffect, useState } from 'react'
import React, { ChangeEvent, useState } from 'react'
import tinyColor from 'tinycolor2'

const colorsSelection: `#${string}`[] = [
Expand All @@ -29,31 +29,37 @@ const colorsSelection: `#${string}`[] = [
]

type Props = {
initialColor?: string
value?: string
defaultValue?: string
onColorChange: (color: string) => void
}

export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
const [color, setColor] = useState(initialColor ?? '')
export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
const [color, setColor] = useState(defaultValue ?? '')
const displayedValue = value ?? color

useEffect(() => {
onColorChange(color)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [color])

const handleColorChange = (e: ChangeEvent<HTMLInputElement>) =>
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) => {
setColor(e.target.value)
onColorChange(e.target.value)
}

const handleClick = (color: string) => () => setColor(color)
const handleClick = (color: string) => () => {
setColor(color)
onColorChange(color)
}

return (
<Popover variant="picker" placement="right" isLazy>
<PopoverTrigger>
<Button
aria-label={'Pick a color'}
bgColor={color}
_hover={{ bgColor: `#${tinyColor(color).darken(10).toHex()}` }}
_active={{ bgColor: `#${tinyColor(color).darken(30).toHex()}` }}
bgColor={displayedValue}
_hover={{
bgColor: `#${tinyColor(displayedValue).darken(10).toHex()}`,
}}
_active={{
bgColor: `#${tinyColor(displayedValue).darken(30).toHex()}`,
}}
height="22px"
width="22px"
padding={0}
Expand All @@ -62,16 +68,16 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
/>
</PopoverTrigger>
<PopoverContent width="170px">
<PopoverArrow bg={color} />
<PopoverArrow bg={displayedValue} />
<PopoverCloseButton color="white" />
<PopoverHeader
height="100px"
backgroundColor={color}
backgroundColor={displayedValue}
borderTopLeftRadius={5}
borderTopRightRadius={5}
color={tinyColor(color).isLight() ? 'gray.800' : 'white'}
color={tinyColor(displayedValue).isLight() ? 'gray.800' : 'white'}
>
<Center height="100%">{color}</Center>
<Center height="100%">{displayedValue}</Center>
</PopoverHeader>
<PopoverBody as={Stack}>
<SimpleGrid columns={5} spacing={2}>
Expand All @@ -96,12 +102,12 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
placeholder="#2a9d8f"
aria-label="Color value"
size="sm"
value={color}
value={displayedValue}
onChange={handleColorChange}
/>
<NativeColorPicker
size="sm"
color={color}
color={displayedValue}
onColorChange={handleColorChange}
>
Advanced picker
Expand Down
21 changes: 12 additions & 9 deletions apps/builder/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,6 @@ export const CodeIcon = (props: IconProps) => (
</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>
)

export const EditIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
Expand Down Expand Up @@ -591,3 +582,15 @@ export const LargeRadiusIcon = (props: IconProps) => (
/>
</Icon>
)

export const DropletIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path>
</Icon>
)

export const TableIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"></path>
</Icon>
)
4 changes: 3 additions & 1 deletion apps/builder/src/components/inputs/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { MoreInfoTooltip } from '../MoreInfoTooltip'

type Props = {
items: string[]
value?: string
defaultValue?: string
debounceTimeout?: number
placeholder?: string
Expand All @@ -40,6 +41,7 @@ export const AutocompleteInput = ({
debounceTimeout,
placeholder,
withVariableButton = true,
value,
defaultValue,
label,
moreInfoTooltip,
Expand Down Expand Up @@ -178,7 +180,7 @@ export const AutocompleteInput = ({
<Input
autoComplete="off"
ref={inputRef}
value={inputValue}
value={value ?? inputValue}
onChange={(e) => changeValue(e.target.value)}
onFocus={onOpen}
onBlur={updateCarretPosition}
Expand Down
1 change: 0 additions & 1 deletion apps/builder/src/components/inputs/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export const CodeEditor = ({
}

const handleChange = (newValue: string) => {
if (isDefined(props.value)) return
setValue(newValue)
}

Expand Down
5 changes: 4 additions & 1 deletion apps/builder/src/components/inputs/RadioButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import { ReactNode } from 'react'

type Props<T extends string> = {
options: (T | { value: T; label: ReactNode })[]
defaultValue: T
value?: T
defaultValue?: T
onSelect: (newValue: T) => void
}
export const RadioButtons = <T extends string>({
options,
value,
defaultValue,
onSelect,
}: Props<T>) => {
const { getRootProps, getRadioProps } = useRadioGroup({
value,
defaultValue,
onChange: onSelect,
})
Expand Down
5 changes: 3 additions & 2 deletions apps/builder/src/components/inputs/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { MoreInfoTooltip } from '../MoreInfoTooltip'

export type TextInputProps = {
defaultValue?: string
onChange: (value: string) => void
onChange?: (value: string) => void
debounceTimeout?: number
label?: ReactNode
helperText?: ReactNode
Expand Down Expand Up @@ -66,7 +66,8 @@ export const TextInput = forwardRef(function TextInput(
localValue.length ?? 0
)
const onChange = useDebouncedCallback(
_onChange,
// eslint-disable-next-line @typescript-eslint/no-empty-function
_onChange ?? (() => {}),
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)

Expand Down
1 change: 1 addition & 0 deletions apps/builder/src/features/dashboard/api/parseNewTypebot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const parseNewTypebot = ({
groups: [startGroup],
edges: [],
variables: [],
selectedThemeTemplateId: null,
theme: {
...defaultTheme,
chat: {
Expand Down
27 changes: 14 additions & 13 deletions apps/builder/src/features/editor/providers/TypebotProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import {
LogicBlockType,
PublicTypebot,
ResultsTablePreferences,
Settings,
Theme,
Typebot,
Webhook,
} from '@typebot.io/schemas'
Expand Down Expand Up @@ -45,16 +42,20 @@ import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/conver

const autoSaveTimeout = 10000

type UpdateTypebotPayload = Partial<{
theme: Theme
settings: Settings
publicId: string
name: string
icon: string
customDomain: string | null
resultsTablePreferences: ResultsTablePreferences
isClosed: boolean
}>
type UpdateTypebotPayload = Partial<
Pick<
Typebot,
| 'theme'
| 'selectedThemeTemplateId'
| 'settings'
| 'publicId'
| 'name'
| 'icon'
| 'customDomain'
| 'resultsTablePreferences'
| 'isClosed'
>
>

export type SetTypebot = (
newPresent: Typebot | ((current: Typebot) => Typebot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
<HStack justify="space-between">
<Text>Background color</Text>
<ColorPicker
initialColor={buttonTheme?.backgroundColor}
defaultValue={buttonTheme?.backgroundColor}
onColorChange={updateBackgroundColor}
/>
</HStack>
<HStack justify="space-between">
<Text>Icon color</Text>
<ColorPicker
initialColor={buttonTheme?.iconColor}
defaultValue={buttonTheme?.iconColor}
onColorChange={updateIconColor}
/>
</HStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,28 @@ export const PreviewMessageThemeSettings = ({
<HStack justify="space-between">
<Text>Background color</Text>
<ColorPicker
initialColor={previewMessageTheme?.backgroundColor}
defaultValue={previewMessageTheme?.backgroundColor}
onColorChange={updateBackgroundColor}
/>
</HStack>
<HStack justify="space-between">
<Text>Text color</Text>
<ColorPicker
initialColor={previewMessageTheme?.textColor}
defaultValue={previewMessageTheme?.textColor}
onColorChange={updateTextColor}
/>
</HStack>
<HStack justify="space-between">
<Text>Close button background</Text>
<ColorPicker
initialColor={previewMessageTheme?.closeButtonBackgroundColor}
defaultValue={previewMessageTheme?.closeButtonBackgroundColor}
onColorChange={updateCloseButtonBackgroundColor}
/>
</HStack>
<HStack justify="space-between">
<Text>Close icon color</Text>
<ColorPicker
initialColor={previewMessageTheme?.closeButtonIconColor}
defaultValue={previewMessageTheme?.closeButtonIconColor}
onColorChange={updateCloseButtonIconColor}
/>
</HStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export const convertPublicTypebotToTypebot = (
isArchived: existingTypebot.isArchived,
isClosed: existingTypebot.isClosed,
resultsTablePreferences: existingTypebot.resultsTablePreferences,
selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId,
})
55 changes: 55 additions & 0 deletions apps/builder/src/features/theme/api/deleteThemeTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { ThemeTemplate, themeTemplateSchema } from '@typebot.io/schemas'
import { z } from 'zod'
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
import { WorkspaceRole } from '@typebot.io/prisma'

export const deleteThemeTemplate = authenticatedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/themeTemplates/{themeTemplateId}',
protect: true,
summary: 'Delete a theme template',
tags: ['Workspace', 'Theme'],
},
})
.input(
z.object({
workspaceId: z.string(),
themeTemplateId: z.string(),
})
)
.output(
z.object({
themeTemplate: themeTemplateSchema,
})
)
.mutation(
async ({ input: { themeTemplateId, workspaceId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findUnique({
where: { id: workspaceId },
select: {
members: true,
},
})
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
if (userRole === undefined || userRole === WorkspaceRole.GUEST)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})

const themeTemplate = (await prisma.themeTemplate.delete({
where: {
id: themeTemplateId,
},
})) as ThemeTemplate

return {
themeTemplate,
}
}
)
Loading

4 comments on commit 38ed575

@vercel
Copy link

@vercel vercel bot commented on 38ed575 Mar 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 38ed575 Mar 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs.typebot.io
docs-typebot-io.vercel.app
docs-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 38ed575 Mar 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

clo.closeer.work
cockroach.cr8.ai
faqs.nigerias.io
form.syncwin.com
haymanevents.com
kw.wpwakanda.com
myrentalhost.com
stan.vselise.com
start.taxtree.io
typebot.aloe.bot
voicehelp.cr8.ai
zap.fundviser.in
app.bouclidom.com
app.chatforms.net
bot.hostnation.de
bot.maitempah.com
bot.phuonghub.com
bot.reviewzer.com
bot.rihabilita.it
cares.urlabout.me
chat.gaswadern.de
fmm.wpwakanda.com
gentleman-shop.fr
k1.kandabrand.com
kp.pedroknoll.com
lb.ticketfute.com
ov1.wpwakanda.com
ov2.wpwakanda.com
ov3.wpwakanda.com
support.triplo.ai
viewer.typebot.io
welcome.triplo.ai
1988.bouclidom.com
andreimayer.com.br
bot.danyservice.it
bot.iconicbrows.it
bot.lucide.contact
bot.megafox.com.br
bot.neferlopez.com
bots.robomotion.io
cadu.uninta.edu.br
dicanatural.online
digitalhelp.com.au
goalsettingbot.com
pant.maxbot.com.br
pantherview.cr8.ai
bot.barrettamario.it
bot.cotemeuplano.com
bot.leadbooster.help
bot.mycompay.reviews
chat.hayurihijab.com
chatbee.agfunnel.com
click.sevenoways.com
connect.growthguy.in
forms.bonanza.design
hello.advergreen.com
kuiz.sistemniaga.com
menu.numero-primo.it
menukb.wpwakanda.com
offer.botscientis.us
sellmycarglasgow.com
chat.semanalimpanome.com.br
designguide.techyscouts.com
liveconvert2.kandalearn.com
presente.empresarias.com.mx
register.algorithmpress.com
sell.sellthemotorhome.co.uk
anamnese.odontopavani.com.br
austin.channelautomation.com
bot.marketingplusmindset.com
bot.seidibergamoseanchetu.it
desabafe.sergiolimajr.com.br
download.venturemarketing.in
piazzatorre.barrettamario.it
type.cookieacademyonline.com
upload.atlasoutfittersk9.com
bot.brigadeirosemdrama.com.br
forms.escoladeautomacao.com.br
onboarding.libertydreamcare.ie
type.talitasouzamarques.com.br
agendamento.sergiolimajr.com.br
anamnese.clinicamegasjdr.com.br
bookings.littlepartymonkeys.com
bot.comercializadoraomicron.com
elevateyourmind.groovepages.com
viewer-v2-typebot-io.vercel.app
yourfeedback.comebackreward.com
bot.cabin-rentals-of-georgia.net
gerador.verificadordehospedes.com
personal-trainer.barrettamario.it
preagendamento.sergiolimajr.com.br
studiotecnicoimmobiliaremerelli.it
download.thailandmicespecialist.com
register.thailandmicespecialist.com
bot.studiotecnicoimmobiliaremerelli.it
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
chrome-os-inquiry-system.itschromeos.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com

@vercel
Copy link

@vercel vercel bot commented on 38ed575 Mar 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-typebot-io.vercel.app
builder-v2-git-main-typebot-io.vercel.app
app.typebot.io

Please sign in to comment.