Skip to content

Commit

Permalink
✨ (theme) Custom font option (#1268)
Browse files Browse the repository at this point in the history
Closes #1249

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced components for customizing fonts, including Google and
custom font options.
- Enhanced theme customization by simplifying theme objects and adding
new font customization options.
- Implemented dynamic font injection for web pages based on
user-selected font configurations.

- **Bug Fixes**
- Fixed a condition in theme template card rendering to correctly check
avatar enablement.
- Corrected font property handling across various components to support
both string and object types.

- **Refactor**
	- Updated option properties in RadioButtons component to be readonly.
- Simplified logic for setting CSS variables for fonts, including checks
for font types and existence.

- **Documentation**
- Added constants and schemas for supporting new font types in theme
customization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
baptisteArno committed Feb 20, 2024
1 parent 927feae commit 7cf1a3e
Show file tree
Hide file tree
Showing 19 changed files with 341 additions and 158 deletions.
2 changes: 1 addition & 1 deletion apps/builder/src/components/inputs/RadioButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { ReactNode } from 'react'

type Props<T extends string> = {
options: (T | { value: T; label: ReactNode })[]
options: readonly (T | { value: T; label: ReactNode })[]
value?: T
defaultValue?: T
direction?: 'row' | 'column'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ const AvatarPreview = ({
avatar: NonNullable<Theme['chat']>['hostAvatar']
}) => {
const { t } = useTranslate()
if (avatar?.isEnabled) return null
if (!avatar?.isEnabled) return null
return avatar?.url ? (
<Image
src={avatar.url}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { TextInput } from '@/components/inputs'
import { Stack } from '@chakra-ui/react'
import { CustomFont } from '@typebot.io/schemas'

type Props = {
font: CustomFont
onFontChange: (font: CustomFont) => void
}

export const CustomFontForm = ({ font, onFontChange }: Props) => {
const updateFamily = (family: string) => onFontChange({ ...font, family })
const updateUrl = (url: string) => onFontChange({ ...font, url })
return (
<Stack>
<TextInput
direction="row"
label="Family:"
defaultValue={font.family}
onChange={updateFamily}
withVariableButton={false}
/>
<TextInput
direction="row"
label="URL:"
defaultValue={font.url}
onChange={updateUrl}
withVariableButton={false}
/>
</Stack>
)
}
15 changes: 15 additions & 0 deletions apps/builder/src/features/theme/components/general/FontForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Font } from '@typebot.io/schemas'
import { GoogleFontForm } from './GoogleFontForm'
import { CustomFontForm } from './CustomFontForm'

type Props = {
font: Font
onFontChange: (font: Font) => void
}

export const FontForm = ({ font, onFontChange }: Props) => {
if (typeof font === 'string' || font.type === 'Google')
return <GoogleFontForm font={font} onFontChange={onFontChange} />
if (font.type === 'Custom')
return <CustomFontForm font={font} onFontChange={onFontChange} />
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { Flex, FormLabel, Stack, Switch, useDisclosure } from '@chakra-ui/react'
import { Background, Theme } from '@typebot.io/schemas'
import {
Flex,
FormLabel,
Stack,
Switch,
useDisclosure,
Text,
} from '@chakra-ui/react'
import { Background, Font, Theme } from '@typebot.io/schemas'
import React from 'react'
import { BackgroundSelector } from './BackgroundSelector'
import { FontSelector } from './FontSelector'
import { LockTag } from '@/features/billing/components/LockTag'
import { Plan } from '@typebot.io/prisma'
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { useTranslate } from '@tolgee/react'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import {
defaultTheme,
fontTypes,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { trpc } from '@/lib/trpc'
import { env } from '@typebot.io/env'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { RadioButtons } from '@/components/inputs/RadioButtons'
import { FontForm } from './FontForm'

type Props = {
isBrandingEnabled: boolean
Expand All @@ -36,9 +47,19 @@ export const GeneralSettings = ({
const { mutate: trackClientEvents } =
trpc.telemetry.trackClientEvents.useMutation()

const handleSelectFont = (font: string) =>
const updateFont = (font: Font) =>
onGeneralThemeChange({ ...generalTheme, font })

const updateFontType = (type: (typeof fontTypes)[number]) => {
onGeneralThemeChange({
...generalTheme,
font:
typeof generalTheme?.font === 'string'
? { type }
: { ...generalTheme?.font, type },
})
}

const handleBackgroundChange = (background: Background) =>
onGeneralThemeChange({ ...generalTheme, background })

Expand All @@ -63,6 +84,11 @@ export const GeneralSettings = ({
onBrandingChange(!isBrandingEnabled)
}

const fontType =
(typeof generalTheme?.font === 'string'
? 'Google'
: generalTheme?.font?.type) ?? defaultTheme.general.font.type

return (
<Stack spacing={6}>
<ChangePlanModal
Expand All @@ -85,10 +111,18 @@ export const GeneralSettings = ({
onChange={updateBranding}
/>
</Flex>
<FontSelector
activeFont={generalTheme?.font ?? defaultTheme.general.font}
onSelectFont={handleSelectFont}
/>
<Stack>
<Text>{t('theme.sideMenu.global.font')}</Text>
<RadioButtons
options={fontTypes}
defaultValue={fontType}
onSelect={updateFontType}
/>
<FontForm
font={generalTheme?.font ?? defaultTheme.general.font}
onFontChange={updateFont}
/>
</Stack>
<BackgroundSelector
background={generalTheme?.background ?? defaultTheme.general.background}
onBackgroundChange={handleBackgroundChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Select } from '@/components/inputs/Select'
import { env } from '@typebot.io/env'
import { GoogleFont } from '@typebot.io/schemas'
import { useState, useEffect } from 'react'

type Props = {
font: GoogleFont | string
onFontChange: (font: GoogleFont) => void
}

export const GoogleFontForm = ({ font, onFontChange }: Props) => {
const [currentFont, setCurrentFont] = useState(
typeof font === 'string' ? font : font.family
)
const [googleFonts, setGoogleFonts] = useState<string[]>([])

useEffect(() => {
fetchPopularFonts().then(setGoogleFonts)
}, [])

const fetchPopularFonts = async () => {
if (!env.NEXT_PUBLIC_GOOGLE_API_KEY) return []
try {
const response = await fetch(
`https://www.googleapis.com/webfonts/v1/webfonts?key=${env.NEXT_PUBLIC_GOOGLE_API_KEY}&sort=popularity`
)
return (await response.json()).items.map(
(item: { family: string }) => item.family
)
} catch (error) {
console.error('Failed to fetch Google Fonts:', error)
return []
}
}

const handleFontSelected = (nextFont: string | undefined) => {
if (nextFont === currentFont || !nextFont) return
setCurrentFont(nextFont)
onFontChange({ type: 'Google', family: nextFont })
}

return (
<Select
selectedItem={currentFont}
items={googleFonts}
onSelect={handleFontSelected}
/>
)
}
84 changes: 22 additions & 62 deletions apps/builder/src/features/theme/galleryTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,7 @@ export const galleryTemplates: Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[] =
{
id: 'typebot-light',
name: 'Typebot Light',
theme: {
chat: {
inputs: {
color: '#303235',
backgroundColor: '#FFFFFF',
placeholderColor: '#9095A0',
},
buttons: { color: '#FFFFFF', backgroundColor: '#0042DA' },
hostAvatar: {
isEnabled: true,
},
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
guestBubbles: { color: '#FFFFFF', backgroundColor: '#FF8E21' },
},
general: {
font: 'Open Sans',
background: { type: BackgroundType.COLOR, content: '#ffffff' },
},
},
theme: {},
},
{
id: 'typebot-dark',
Expand All @@ -45,15 +27,9 @@ export const galleryTemplates: Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[] =
backgroundColor: '#1e293b',
placeholderColor: '#9095A0',
},
buttons: { color: '#ffffff', backgroundColor: '#1a5fff' },
hostAvatar: {
isEnabled: true,
},
hostBubbles: { color: '#ffffff', backgroundColor: '#1e293b' },
guestBubbles: { color: '#FFFFFF', backgroundColor: '#FF8E21' },
},
general: {
font: 'Open Sans',
background: { type: BackgroundType.COLOR, content: '#171923' },
},
},
Expand All @@ -63,19 +39,15 @@ export const galleryTemplates: Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[] =
name: 'Minimalist Black',
theme: {
chat: {
inputs: {
color: '#303235',
backgroundColor: '#FFFFFF',
placeholderColor: '#9095A0',
},
buttons: { color: '#FFFFFF', backgroundColor: '#303235' },
buttons: { backgroundColor: '#303235' },
hostAvatar: { isEnabled: false },
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
guestBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
},
general: {
font: 'Inter',
background: { type: BackgroundType.COLOR, content: '#ffffff' },
font: {
type: 'Google',
family: 'Inter',
},
},
},
},
Expand All @@ -84,19 +56,15 @@ export const galleryTemplates: Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[] =
name: 'Minimalist Teal',
theme: {
chat: {
inputs: {
color: '#303235',
backgroundColor: '#FFFFFF',
placeholderColor: '#9095A0',
},
buttons: { color: '#FFFFFF', backgroundColor: '#0d9488' },
buttons: { backgroundColor: '#0d9488' },
hostAvatar: { isEnabled: false },
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
guestBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
},
general: {
font: 'Inter',
background: { type: BackgroundType.COLOR, content: '#ffffff' },
font: {
type: 'Google',
family: 'Inter',
},
},
},
},
Expand All @@ -106,18 +74,14 @@ export const galleryTemplates: Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[] =
name: 'Bright Rain',
theme: {
chat: {
inputs: {
color: '#303235',
backgroundColor: '#FFFFFF',
placeholderColor: '#9095A0',
},
buttons: { color: '#fff', backgroundColor: '#D27A7D' },
hostAvatar: { isEnabled: true },
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
buttons: { backgroundColor: '#D27A7D' },
guestBubbles: { color: '#303235', backgroundColor: '#FDDDBF' },
},
general: {
font: 'Montserrat',
font: {
type: 'Google',
family: 'Montserrat',
},
background: {
type: BackgroundType.IMAGE,
content: getOrigin() + '/images/backgrounds/brightRain.jpeg',
Expand All @@ -130,18 +94,14 @@ export const galleryTemplates: Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[] =
name: 'Ray of Lights',
theme: {
chat: {
inputs: {
color: '#303235',
backgroundColor: '#FFFFFF',
placeholderColor: '#9095A0',
},
buttons: { color: '#fff', backgroundColor: '#1A2249' },
hostAvatar: { isEnabled: true },
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
guestBubbles: { color: '#fff', backgroundColor: '#1A2249' },
buttons: { backgroundColor: '#1A2249' },
guestBubbles: { backgroundColor: '#1A2249' },
},
general: {
font: 'Raleway',
font: {
type: 'Google',
family: 'Raleway',
},
background: {
type: BackgroundType.IMAGE,
content: getOrigin() + '/images/backgrounds/rayOfLights.jpeg',
Expand Down

0 comments on commit 7cf1a3e

Please sign in to comment.