2
0

refactor(♻️ Add defaults everywhere (+ settings page)):

This commit is contained in:
Baptiste Arnaud
2022-01-25 18:19:37 +01:00
parent 21448bcc8a
commit c5aaa323d1
115 changed files with 1436 additions and 720 deletions

View File

@@ -0,0 +1,34 @@
module.exports = {
ignorePatterns: ['node_modules'],
env: {
browser: true,
es6: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'next/core-web-vitals',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
plugins: ['prettier', 'react', 'cypress', '@typescript-eslint'],
ignorePatterns: 'dist',
rules: {
'react/no-unescaped-entities': [0],
'prettier/prettier': 'error',
'react/display-name': [0],
'@next/next/no-img-element': [0],
},
}

View File

@@ -1,5 +1,6 @@
import { GoogleAnalyticsOptions } from 'models'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const gtag: any
const initGoogleAnalytics = (id: string): Promise<void> =>

View File

@@ -34,13 +34,20 @@
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"tailwindcss": "^3.0.11",
"typescript": "^4.5.4"
"typescript": "^4.5.4",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"eslint": "<8.0.0",
"eslint-config-next": "12.0.7",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-prettier": "^4.0.0"
},
"peerDependencies": {
"react": "^17.0.2"
},
"scripts": {
"build": "yarn rollup -c",
"dev": "yarn rollup -c --watch"
"dev": "yarn rollup -c --watch",
"lint": "eslint --fix -c ./.eslintrc.js \"./src/**/*.ts*\""
}
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useTypebot } from '../../contexts/TypebotContext'
import { HostAvatar } from '../avatars/HostAvatar'
import { useFrame } from 'react-frame-component'
@@ -22,6 +22,7 @@ export const AvatarSideContainer = () => {
return () => {
resizeObserver.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (

View File

@@ -35,11 +35,13 @@ export const ChatBlock = ({
const nextStep =
typebot.steps.byId[startStepId ?? stepIds[displayedSteps.length]]
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
autoScrollToBottom()
onNewStepDisplayed()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [displayedSteps])
const onNewStepDisplayed = async () => {

View File

@@ -42,6 +42,7 @@ const InputChatStep = ({
useEffect(() => {
addNewAvatarOffset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleSubmit = (value: string) => {

View File

@@ -24,11 +24,12 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
const url = useMemo(
() =>
parseVariables({ text: step.content?.url, variables: typebot.variables }),
[typebot.variables]
[step.content?.url, typebot.variables]
)
useEffect(() => {
showContentAfterMediaLoad()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const showContentAfterMediaLoad = () => {
@@ -83,6 +84,7 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
height: isTyping ? '2rem' : 'auto',
maxWidth: '100%',
}}
alt="Bubble image"
/>
</div>
</div>

View File

@@ -28,6 +28,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
const content = useMemo(
() =>
parseVariables({ text: step.content.html, variables: typebot.variables }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[typebot.variables]
)
@@ -40,6 +41,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
setTimeout(() => {
onTypingEnd()
}, typingTimeout)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onTypingEnd = () => {

View File

@@ -28,6 +28,7 @@ export const VideoBubble = ({ step, onTransitionEnd }: Props) => {
useEffect(() => {
showContentAfterMediaLoad()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const showContentAfterMediaLoad = () => {
@@ -86,6 +87,7 @@ const VideoContent = ({
}) => {
const url = useMemo(
() => parseVariables({ text: content?.url, variables: variables }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[variables]
)
if (!content?.type) return <></>

View File

@@ -13,6 +13,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
const { typebot } = useTypebot()
const items = useMemo(
() => filterTable(options?.itemIds ?? [], typebot.choiceItems),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const [selectedIds, setSelectedIds] = useState<string[]>([])
@@ -41,6 +42,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
<div className="flex flex-wrap">
{options?.itemIds.map((itemId) => (
<button
key={itemId}
role={options?.isMultipleChoice ? 'checkbox' : 'button'}
onClick={handleClick(itemId)}
className={

View File

@@ -26,7 +26,7 @@ type TextInputProps = {
}
export const TextInput = ({ step, onChange }: TextInputProps) => {
const inputRef = useRef<any>(null)
const inputRef = useRef<HTMLInputElement & HTMLTextAreaElement>(null)
useEffect(() => {
if (!inputRef.current) return
@@ -102,7 +102,8 @@ export const TextInput = ({ step, onChange }: TextInputProps) => {
case InputStepType.PHONE: {
return (
<PhoneInput
ref={inputRef}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref={inputRef as any}
onChange={handlePhoneNumberChange}
placeholder={
step.options?.labels?.placeholder ?? 'Your phone number...'

View File

@@ -5,7 +5,7 @@ import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme'
import { useAnswers } from '../contexts/AnswersContext'
import { deepEqual } from 'fast-equals'
import { Answer, Block, Edge, PublicTypebot } from 'models'
import { Answer, Block, PublicTypebot } from 'models'
type Props = {
typebot: PublicTypebot
@@ -45,6 +45,7 @@ export const ConversationContainer = ({
typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].edgeId
if (!firstEdgeId) return
displayNextBlock(firstEdgeId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
@@ -56,6 +57,7 @@ export const ConversationContainer = ({
if (!answer || deepEqual(localAnswer, answer)) return
setLocalAnswer(answer)
onNewAnswer(answer)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers])
return (

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { useMemo } from 'react'
import { TypebotContext } from '../contexts/TypebotContext'
import Frame from 'react-frame-component'
@@ -61,7 +62,7 @@ export const TypebotViewer = ({
}}
/>
<TypebotContext typebot={typebot}>
<AnswersContext typebotId={typebot.id}>
<AnswersContext>
<div
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
style={{
@@ -78,6 +79,17 @@ export const TypebotViewer = ({
onCompleted={handleCompleted}
/>
</div>
{typebot.settings.general.isBrandingEnabled && (
<a
href={'https://www.typebot.io/?utm_source=litebadge'}
target="_blank"
rel="noopener noreferrer"
className="fixed py-1 px-2 bg-white z-50 rounded shadow-md"
style={{ bottom: '20px' }}
>
Made with <span className="text-blue-500">Typebot</span>.
</a>
)}
</div>
</AnswersContext>
</TypebotContext>

View File

@@ -4,16 +4,11 @@ import React, { createContext, ReactNode, useContext, useState } from 'react'
const answersContext = createContext<{
answers: Answer[]
addAnswer: (answer: Answer) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export const AnswersContext = ({
children,
typebotId,
}: {
children: ReactNode
typebotId: string
}) => {
export const AnswersContext = ({ children }: { children: ReactNode }) => {
const [answers, setAnswers] = useState<Answer[]>([])
const addAnswer = (answer: Answer) =>

View File

@@ -5,6 +5,7 @@ const hostAvatarsContext = createContext<{
lastBubblesTopOffset: number[]
addNewAvatarOffset: () => void
updateLastAvatarOffset: (newOffset: number) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})

View File

@@ -4,6 +4,7 @@ import React, { createContext, ReactNode, useContext, useState } from 'react'
const typebotContext = createContext<{
typebot: PublicTypebot
updateVariableValue: (variableId: string, value: string) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})

View File

@@ -1,8 +1,8 @@
import { TypingEmulationSettings } from 'models'
import { TypingEmulation } from 'models'
export const computeTypingTimeout = (
bubbleContent: string,
typingSettings: TypingEmulationSettings
typingSettings: TypingEmulation
) => {
const wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
const typedWordsPerMinute = typingSettings.speed

View File

@@ -1,4 +1,4 @@
import { ChoiceInputStep, ChoiceItem, Table, Target } from 'models'
import { ChoiceInputStep, ChoiceItem, Table } from 'models'
export const getSingleChoiceTargetId = (
currentStep: ChoiceInputStep,

View File

@@ -10,7 +10,6 @@ import {
Cell,
GoogleSheetsGetOptions,
GoogleAnalyticsStep,
Webhook,
WebhookStep,
} from 'models'
import { stringify } from 'qs'

View File

@@ -28,7 +28,7 @@ export const isMathFormula = (str?: string) =>
['*', '/', '+', '-'].some((val) => str && str.includes(val))
export const evaluateExpression = (str: string) => {
let result = replaceCommasWithDots(str)
const result = replaceCommasWithDots(str)
try {
const evaluatedNumber = safeEval(result) as number
if (countDecimals(evaluatedNumber) > 2) {

View File

@@ -138,7 +138,7 @@ model Result {
typebotId String
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
answers Answer[]
isCompleted Boolean?
isCompleted Boolean
}
model Answer {

View File

@@ -18,6 +18,6 @@ export type PublicTypebot = Omit<
choiceItems: Table<ChoiceItem>
variables: Table<Variable>
edges: Table<Edge>
theme?: Theme
settings?: Settings
theme: Theme
settings: Settings
}

View File

@@ -1,9 +1,31 @@
export type Settings = {
typingEmulation?: TypingEmulationSettings
general: GeneralSettings
typingEmulation: TypingEmulation
metadata: Metadata
}
export type TypingEmulationSettings = {
enabled?: boolean
speed?: number
maxDelay?: number
export type GeneralSettings = {
isBrandingEnabled: boolean
}
export type TypingEmulation = {
enabled: boolean
speed: number
maxDelay: number
}
export type Metadata = {
title?: string
description: string
imageUrl?: string
favIconUrl?: string
}
export const defaultSettings: Settings = {
general: { isBrandingEnabled: true },
typingEmulation: { enabled: true, speed: 300, maxDelay: 1.5 },
metadata: {
description:
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.',
},
}

View File

@@ -20,12 +20,12 @@ export type TextBubbleStep = StepBase & {
export type ImageBubbleStep = StepBase & {
type: BubbleStepType.IMAGE
content?: ImageBubbleContent
content: ImageBubbleContent
}
export type VideoBubbleStep = StepBase & {
type: BubbleStepType.VIDEO
content?: VideoBubbleContent
content: VideoBubbleContent
}
export type TextBubbleContent = {
@@ -49,3 +49,13 @@ export type VideoBubbleContent = {
url?: string
id?: string
}
export const defaultTextBubbleContent: TextBubbleContent = {
html: '',
richText: [],
plainText: '',
}
export const defaultImageBubbleContent: ImageBubbleContent = {}
export const defaultVideoBubbleContent: VideoBubbleContent = {}

View File

@@ -25,37 +25,41 @@ export type InputStepOptions =
| EmailInputOptions
| DateInputOptions
| UrlInputOptions
| PhoneNumberInputOptions
| ChoiceInputOptions
export type TextInputStep = StepBase & {
type: InputStepType.TEXT
options?: TextInputOptions
options: TextInputOptions
}
export type NumberInputStep = StepBase & {
type: InputStepType.NUMBER
options?: NumberInputOptions
options: NumberInputOptions
}
export type EmailInputStep = StepBase & {
type: InputStepType.EMAIL
options?: EmailInputOptions
options: EmailInputOptions
}
export type UrlInputStep = StepBase & {
type: InputStepType.URL
options?: UrlInputOptions
options: UrlInputOptions
}
export type DateInputStep = StepBase & {
type: InputStepType.DATE
options?: DateInputOptions
options: DateInputOptions
}
export type PhoneNumberInputStep = StepBase & {
type: InputStepType.PHONE
options?: OptionBase & InputTextOptionsBase
options: OptionBase & InputTextOptionsBase
}
export type PhoneNumberInputOptions = OptionBase & InputTextOptionsBase
export type ChoiceInputStep = StepBase & {
type: InputStepType.CHOICE
options: ChoiceInputOptions
@@ -70,19 +74,19 @@ export type ChoiceItem = {
type OptionBase = { variableId?: string }
type InputTextOptionsBase = {
labels?: { placeholder?: string; button?: string }
labels: { placeholder: string; button: string }
}
export type ChoiceInputOptions = OptionBase & {
itemIds: string[]
isMultipleChoice?: boolean
buttonLabel?: string
isMultipleChoice: boolean
buttonLabel: string
}
export type DateInputOptions = OptionBase & {
labels?: { button?: string; from?: string; to?: string }
hasTime?: boolean
isRange?: boolean
labels: { button: string; from: string; to: string }
hasTime: boolean
isRange: boolean
}
export type EmailInputOptions = OptionBase & InputTextOptionsBase
@@ -91,7 +95,7 @@ export type UrlInputOptions = OptionBase & InputTextOptionsBase
export type TextInputOptions = OptionBase &
InputTextOptionsBase & {
isLong?: boolean
isLong: boolean
}
export type NumberInputOptions = OptionBase &
@@ -100,3 +104,41 @@ export type NumberInputOptions = OptionBase &
max?: number
step?: number
}
const defaultButtonLabel = 'Send'
export const defaultTextInputOptions: TextInputOptions = {
isLong: false,
labels: { button: defaultButtonLabel, placeholder: 'Type your answer...' },
}
export const defaultNumberInputOptions: NumberInputOptions = {
labels: { button: defaultButtonLabel, placeholder: 'Type a number...' },
}
export const defaultEmailInputOptions: EmailInputOptions = {
labels: { button: defaultButtonLabel, placeholder: 'Type your email...' },
}
export const defaultUrlInputOptions: UrlInputOptions = {
labels: { button: defaultButtonLabel, placeholder: 'Type a URL...' },
}
export const defaultDateInputOptions: DateInputOptions = {
hasTime: false,
isRange: false,
labels: { button: defaultButtonLabel, from: 'From:', to: 'To:' },
}
export const defaultPhoneInputOptions: PhoneNumberInputOptions = {
labels: {
button: defaultButtonLabel,
placeholder: 'Type your phone number...',
},
}
export const defaultChoiceInputOptions: ChoiceInputOptions = {
buttonLabel: defaultButtonLabel,
isMultipleChoice: false,
itemIds: [],
}

View File

@@ -19,17 +19,17 @@ export enum IntegrationStepType {
export type GoogleSheetsStep = StepBase & {
type: IntegrationStepType.GOOGLE_SHEETS
options?: GoogleSheetsOptions
options: GoogleSheetsOptions
}
export type GoogleAnalyticsStep = StepBase & {
type: IntegrationStepType.GOOGLE_ANALYTICS
options?: GoogleAnalyticsOptions
options: GoogleAnalyticsOptions
}
export type WebhookStep = StepBase & {
type: IntegrationStepType.WEBHOOK
options?: WebhookOptions
options: WebhookOptions
}
export type GoogleAnalyticsOptions = {
@@ -113,3 +113,9 @@ export type WebhookResponse = {
statusCode: number
data?: unknown
}
export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {}
export const defaultGoogleAnalyticsOptions: GoogleAnalyticsOptions = {}
export const defaultWebhookOptions: WebhookOptions = {}

View File

@@ -16,7 +16,7 @@ export type LogicStepOptions =
export type SetVariableStep = StepBase & {
type: LogicStepType.SET_VARIABLE
options?: SetVariableOptions
options: SetVariableOptions
}
export type ConditionStep = StepBase & {
@@ -28,7 +28,7 @@ export type ConditionStep = StepBase & {
export type RedirectStep = StepBase & {
type: LogicStepType.REDIRECT
options?: RedirectOptions
options: RedirectOptions
}
export enum LogicalOperator {
@@ -47,13 +47,13 @@ export enum ComparisonOperators {
export type ConditionOptions = {
comparisons: Table<Comparison>
logicalOperator?: LogicalOperator
logicalOperator: LogicalOperator
}
export type Comparison = {
id: string
variableId?: string
comparisonOperator: ComparisonOperators
comparisonOperator?: ComparisonOperators
value?: string
}
@@ -64,5 +64,14 @@ export type SetVariableOptions = {
export type RedirectOptions = {
url?: string
isNewTab?: boolean
isNewTab: boolean
}
export const defaultSetVariablesOptions: SetVariableOptions = {}
export const defaultConditionOptions: ConditionOptions = {
comparisons: { byId: {}, allIds: [] },
logicalOperator: LogicalOperator.AND,
}
export const defaultRedirectOptions: RedirectOptions = { isNewTab: false }

View File

@@ -31,6 +31,13 @@ export type DraggableStepType =
| LogicStepType
| IntegrationStepType
export type StepWithOptions = InputStep | LogicStep | IntegrationStep
export type StepWithOptionsType =
| InputStepType
| LogicStepType
| IntegrationStepType
export type StepOptions =
| InputStepOptions
| LogicStepOptions

View File

@@ -1,28 +1,28 @@
export type Theme = {
general?: GeneralTheme
chat?: ChatTheme
general: GeneralTheme
chat: ChatTheme
customCss?: string
}
export type GeneralTheme = {
font?: string
background?: Background
font: string
background: Background
}
export type ChatTheme = {
hostBubbles?: ContainerColors
guestBubbles?: ContainerColors
buttons?: ContainerColors
inputs?: InputColors
hostBubbles: ContainerColors
guestBubbles: ContainerColors
buttons: ContainerColors
inputs: InputColors
}
export type ContainerColors = {
backgroundColor?: string
color?: string
backgroundColor: string
color: string
}
export type InputColors = ContainerColors & {
placeholderColor?: string
placeholderColor: string
}
export enum BackgroundType {
@@ -35,3 +35,17 @@ export type Background = {
type: BackgroundType
content?: string
}
export const defaultTheme: Theme = {
chat: {
hostBubbles: { backgroundColor: '#F7F8FF', color: '#303235' },
guestBubbles: { backgroundColor: '#FF8E21', color: '#FFFFFF' },
buttons: { backgroundColor: '#0042DA', color: '#FFFFFF' },
inputs: {
backgroundColor: '#FFFFFF',
color: '#303235',
placeholderColor: '#9095A0',
},
},
general: { font: 'Open Sans', background: { type: BackgroundType.NONE } },
}

View File

@@ -23,8 +23,8 @@ export type Typebot = Omit<
variables: Table<Variable>
edges: Table<Edge>
webhooks: Table<Webhook>
theme?: Theme
settings?: Settings
theme: Theme
settings: Settings
}
export type Block = {

View File

@@ -14,6 +14,8 @@ import {
TextInputStep,
TextBubbleStep,
WebhookStep,
StepType,
StepWithOptionsType,
} from 'models'
export const sendRequest = async <ResponseData>({
@@ -78,3 +80,14 @@ export const isIntegrationStep = (step: Step): step is IntegrationStep =>
export const isWebhookStep = (step: Step): step is WebhookStep =>
step.type === IntegrationStepType.WEBHOOK
export const isBubbleStepType = (type: StepType): type is BubbleStepType =>
(Object.values(BubbleStepType) as string[]).includes(type)
export const stepTypeHasOption = (
type: StepType
): type is StepWithOptionsType =>
(Object.values(InputStepType) as string[])
.concat(Object.values(LogicStepType))
.concat(Object.values(IntegrationStepType))
.includes(type)