2
0

♻️ Add shared eslint config

This commit is contained in:
Baptiste Arnaud
2022-11-21 11:12:43 +01:00
parent e09adf5c64
commit 451ffbcacf
123 changed files with 1151 additions and 1523 deletions

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint

View File

@ -1,43 +1,4 @@
module.exports = { module.exports = {
ignorePatterns: ['node_modules'], root: true,
env: { extends: ['custom'],
browser: true,
es6: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'next/core-web-vitals',
],
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: ['react', '@typescript-eslint'],
rules: {
'react/no-unescaped-entities': [0],
'react/display-name': [0],
'no-restricted-imports': [
'error',
{
patterns: [
'*/src/*',
'src/*',
'*/src',
'@/features/*/*',
'!@/features/blocks/*',
'!@/features/*/api',
],
},
],
},
} }

View File

@ -106,20 +106,16 @@
"@types/qs": "6.9.7", "@types/qs": "6.9.7",
"@types/react": "18.0.25", "@types/react": "18.0.25",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"configs": "workspace:*",
"db": "workspace:*", "db": "workspace:*",
"dotenv": "16.0.3", "dotenv": "16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"eslint-plugin-react": "7.31.10",
"models": "workspace:*", "models": "workspace:*",
"next-transpile-modules": "10.0.0", "next-transpile-modules": "10.0.0",
"superjson": "^1.11.0", "superjson": "^1.11.0",
"tsconfig": "workspace:*", "tsconfig": "workspace:*",
"typescript": "4.8.4", "typescript": "4.8.4",
"utils": "workspace:*", "utils": "workspace:*",
"zod": "3.19.1" "zod": "3.19.1",
"eslint": "8.28.0",
"eslint-config-custom": "workspace:*"
} }
} }

View File

@ -1,6 +1,6 @@
import { PlaywrightTestConfig } from '@playwright/test' import { PlaywrightTestConfig } from '@playwright/test'
import path from 'path' import path from 'path'
import { playwrightBaseConfig } from 'configs/playwright' import { playwrightBaseConfig } from 'utils/playwright/baseConfig'
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
...playwrightBaseConfig, ...playwrightBaseConfig,

View File

@ -36,7 +36,6 @@ export const SearchableDropdown = ({
const { onOpen, onClose, isOpen } = useDisclosure() const { onOpen, onClose, isOpen } = useDisclosure()
const [inputValue, setInputValue] = useState(selectedItem ?? '') const [inputValue, setInputValue] = useState(selectedItem ?? '')
const debounced = useDebouncedCallback( const debounced = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function
onValueChange ? onValueChange : () => {}, onValueChange ? onValueChange : () => {},
env('E2E_TEST') === 'true' ? 0 : debounceTimeout env('E2E_TEST') === 'true' ? 0 : debounceTimeout
) )

View File

@ -209,7 +209,7 @@ export const VariableSearchInput = ({
leftIcon={<PlusIcon />} leftIcon={<PlusIcon />}
bgColor={keyboardFocusIndex === 0 ? 'gray.200' : 'transparent'} bgColor={keyboardFocusIndex === 0 ? 'gray.200' : 'transparent'}
> >
Create "{inputValue}" Create &quot;{inputValue}&quot;
</Button> </Button>
)} )}
{filteredItems.length > 0 && ( {filteredItems.length > 0 && (

View File

@ -24,7 +24,6 @@ const userContext = createContext<{
currentWorkspaceId?: string currentWorkspaceId?: string
updateUser: (newUser: Partial<User>) => void updateUser: (newUser: Partial<User>) => void
saveUser: (newUser?: Partial<User>) => Promise<void> saveUser: (newUser?: Partial<User>) => Promise<void>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -24,7 +24,7 @@ export const SignInPage = ({ type }: Props) => {
</Heading> </Heading>
{type === 'signin' ? ( {type === 'signin' ? (
<Text> <Text>
Don't have an account?{' '} Don&apos;t have an account?{' '}
<TextLink href="/register">Sign up for free</TextLink> <TextLink href="/register">Sign up for free</TextLink>
</Text> </Text>
) : ( ) : (

View File

@ -52,8 +52,8 @@ export const UsageContent = ({ workspace }: Props) => {
p="3" p="3"
label={ label={
<Text> <Text>
Your typebots are popular! You will soon reach your plan's Your typebots are popular! You will soon reach your
chats limit. 🚀 plan&apos;s chats limit. 🚀
<br /> <br />
<br /> <br />
Make sure to <strong>update your plan</strong> to increase Make sure to <strong>update your plan</strong> to increase
@ -111,8 +111,8 @@ export const UsageContent = ({ workspace }: Props) => {
p="3" p="3"
label={ label={
<Text> <Text>
Your typebots are popular! You will soon reach your plan's Your typebots are popular! You will soon reach your
storage limit. 🚀 plan&apos;s storage limit. 🚀
<br /> <br />
<br /> <br />
Make sure to <strong>update your plan</strong> in order to Make sure to <strong>update your plan</strong> in order to

View File

@ -90,7 +90,7 @@ export const ChangePlanForm = () => {
<Text color="gray.500"> <Text color="gray.500">
Need custom limits? Specific features?{' '} Need custom limits? Specific features?{' '}
<TextLink href={'https://typebot.io/enterprise-lead-form'} isExternal> <TextLink href={'https://typebot.io/enterprise-lead-form'} isExternal>
Let's chat! Let&apos;s chat!
</TextLink> </TextLink>
</Text> </Text>
</Stack> </Stack>

View File

@ -162,14 +162,20 @@ const ActionOptions = ({
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions) onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
const UpdatingCellItem = useMemo( const UpdatingCellItem = useMemo(
() => (props: TableListItemProps<Cell>) => () =>
<CellWithValueStack {...props} columns={sheet?.columns ?? []} />, function Component(props: TableListItemProps<Cell>) {
return <CellWithValueStack {...props} columns={sheet?.columns ?? []} />
},
[sheet?.columns] [sheet?.columns]
) )
const ExtractingCellItem = useMemo( const ExtractingCellItem = useMemo(
() => (props: TableListItemProps<ExtractingCell>) => () =>
<CellWithVariableIdStack {...props} columns={sheet?.columns ?? []} />, function Component(props: TableListItemProps<ExtractingCell>) {
return (
<CellWithVariableIdStack {...props} columns={sheet?.columns ?? []} />
)
},
[sheet?.columns] [sheet?.columns]
) )

View File

@ -143,8 +143,10 @@ export const WebhookSettings = ({
} }
const ResponseMappingInputs = useMemo( const ResponseMappingInputs = useMemo(
() => (props: TableListItemProps<ResponseVariableMapping>) => () =>
<DataVariableInputs {...props} dataItems={responseKeys} />, function Component(props: TableListItemProps<ResponseVariableMapping>) {
return <DataVariableInputs {...props} dataItems={responseKeys} />
},
[responseKeys] [responseKeys]
) )

View File

@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const getDeepKeys = (obj: any): string[] => { export const getDeepKeys = (obj: any): string[] => {
let keys: string[] = [] let keys: string[] = []
for (const key in obj) { for (const key in obj) {

View File

@ -36,7 +36,7 @@ export const AnnoucementModal = ({ isOpen, onClose }: Props) => {
<Modal isOpen={isOpen} onClose={handleCloseClick} size="2xl"> <Modal isOpen={isOpen} onClose={handleCloseClick} size="2xl">
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader>What's new in Typebot 2.0?</ModalHeader> <ModalHeader>What&apos;s new in Typebot 2.0?</ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody as={Stack} spacing="6" pb="10"> <ModalBody as={Stack} spacing="6" pb="10">
<Text>Typebo 2.0 has been launched February the 15th 🎉.</Text> <Text>Typebo 2.0 has been launched February the 15th 🎉.</Text>

View File

@ -8,7 +8,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { TypebotViewer } from 'bot-engine' import { TypebotViewer } from 'bot-engine'
import { useUser } from '@/features/account' import { useUser } from '@/features/account'
import { Answer, Typebot } from 'models' import { AnswerInput, Typebot } from 'models'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { getViewerUrl, sendRequest } from 'utils' import { getViewerUrl, sendRequest } from 'utils'
import confetti from 'canvas-confetti' import confetti from 'canvas-confetti'
@ -79,7 +79,7 @@ export const OnboardingModal = ({ totalTypebots }: Props) => {
setTypebot(data as Typebot) setTypebot(data as Typebot)
} }
const handleNewAnswer = async (answer: Answer) => { const handleNewAnswer = async (answer: AnswerInput) => {
const isName = answer.variableId === 'cl126f4hf000i2e6d8zvzc3t1' const isName = answer.variableId === 'cl126f4hf000i2e6d8zvzc3t1'
const isCompany = answer.variableId === 'cl126jqww000w2e6dq9yv4ifq' const isCompany = answer.variableId === 'cl126jqww000w2e6dq9yv4ifq'
const isCategories = answer.variableId === 'cl126mo3t001b2e6dvyi16bkd' const isCategories = answer.variableId === 'cl126mo3t001b2e6dvyi16bkd'

View File

@ -11,7 +11,7 @@ enum ActionType {
Flush = 'FLUSH', Flush = 'FLUSH',
} }
export interface Actions<T> { export interface Actions<T extends { updatedAt: string } | undefined> {
set: ( set: (
newPresent: T | ((current: T) => T), newPresent: T | ((current: T) => T),
options?: { updateDate: boolean } options?: { updateDate: boolean }
@ -24,13 +24,13 @@ export interface Actions<T> {
presentRef: React.MutableRefObject<T> presentRef: React.MutableRefObject<T>
} }
interface Action<T> { interface Action<T extends { updatedAt: string } | undefined> {
type: ActionType type: ActionType
newPresent?: T newPresent?: T
updateDate?: boolean updateDate?: boolean
} }
export interface State<T> { export interface State<T extends { updatedAt: string } | undefined> {
past: T[] past: T[]
present: T present: T
future: T[] future: T[]
@ -42,7 +42,10 @@ const initialState = {
future: [], future: [],
} }
const reducer = <T>(state: State<T>, action: Action<T>) => { const reducer = <T extends { updatedAt: string } | undefined>(
state: State<T>,
action: Action<T>
) => {
const { past, present, future } = state const { past, present, future } = state
switch (action.type) { switch (action.type) {
@ -98,8 +101,6 @@ const reducer = <T>(state: State<T>, action: Action<T>) => {
past: [...past, present].filter(isDefined), past: [...past, present].filter(isDefined),
present: { present: {
...newPresent, ...newPresent,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
updatedAt: updateDate ? new Date() : newPresent.updatedAt, updatedAt: updateDate ? new Date() : newPresent.updatedAt,
}, },
future: [], future: [],
@ -111,11 +112,13 @@ const reducer = <T>(state: State<T>, action: Action<T>) => {
} }
} }
const useUndo = <T>(initialPresent: T): [State<T>, Actions<T>] => { const useUndo = <T extends { updatedAt: string } | undefined>(
initialPresent: T
): [State<T>, Actions<T>] => {
const [state, dispatch] = useReducer(reducer, { const [state, dispatch] = useReducer(reducer, {
...initialState, ...initialState,
present: initialPresent, present: initialPresent,
}) as [State<T>, React.Dispatch<Action<T>>] })
const presentRef = useRef<T>(initialPresent) const presentRef = useRef<T>(initialPresent)
const canUndo = state.past.length !== 0 const canUndo = state.past.length !== 0
@ -136,7 +139,7 @@ const useUndo = <T>(initialPresent: T): [State<T>, Actions<T>] => {
const set = useCallback( const set = useCallback(
(newPresent: T | ((current: T) => T), options = { updateDate: true }) => { (newPresent: T | ((current: T) => T), options = { updateDate: true }) => {
const updatedTypebot = const updatedTypebot =
'id' in newPresent newPresent && 'id' in newPresent
? newPresent ? newPresent
: (newPresent as (current: T) => T)(presentRef.current) : (newPresent as (current: T) => T)(presentRef.current)
presentRef.current = updatedTypebot presentRef.current = updatedTypebot
@ -153,7 +156,10 @@ const useUndo = <T>(initialPresent: T): [State<T>, Actions<T>] => {
dispatch({ type: ActionType.Flush }) dispatch({ type: ActionType.Flush })
}, []) }, [])
return [state, { set, undo, redo, flush, canUndo, canRedo, presentRef }] return [
state as State<T>,
{ set, undo, redo, flush, canUndo, canRedo, presentRef },
]
} }
export default useUndo export default useUndo

View File

@ -16,7 +16,6 @@ const editorContext = createContext<{
setRightPanel: Dispatch<SetStateAction<RightPanel | undefined>> setRightPanel: Dispatch<SetStateAction<RightPanel | undefined>>
startPreviewAtGroup: string | undefined startPreviewAtGroup: string | undefined
setStartPreviewAtGroup: Dispatch<SetStateAction<string | undefined>> setStartPreviewAtGroup: Dispatch<SetStateAction<string | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -88,7 +88,6 @@ const typebotContext = createContext<
ItemsActions & ItemsActions &
VariablesActions & VariablesActions &
EdgesActions EdgesActions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
>({}) >({})

View File

@ -14,7 +14,6 @@ const typebotDndContext = createContext<{
setDraggedTypebot: Dispatch<SetStateAction<TypebotInDashboard | undefined>> setDraggedTypebot: Dispatch<SetStateAction<TypebotInDashboard | undefined>>
mouseOverFolderId?: string | null mouseOverFolderId?: string | null
setMouseOverFolderId: Dispatch<SetStateAction<string | undefined | null>> setMouseOverFolderId: Dispatch<SetStateAction<string | undefined | null>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
}>({}) }>({})

View File

@ -47,197 +47,196 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
) )
} }
const DraggableGroupNode = memo( const NonMemoizedDraggableGroupNode = ({
({ group,
group, groupIndex,
groupIndex, onGroupDrag,
onGroupDrag, }: Props & { onGroupDrag: (newCoord: Coordinates) => void }) => {
}: Props & { onGroupDrag: (newCoord: Coordinates) => void }) => { const {
const { connectingIds,
connectingIds, setConnectingIds,
setConnectingIds, previewingEdge,
previewingEdge, isReadOnly,
isReadOnly, focusedGroupId,
focusedGroupId, setFocusedGroupId,
setFocusedGroupId, graphPosition,
graphPosition, } = useGraph()
} = useGraph() const { typebot, updateGroup } = useTypebot()
const { typebot, updateGroup } = useTypebot() const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd() const { setRightPanel, setStartPreviewAtGroup } = useEditor()
const { setRightPanel, setStartPreviewAtGroup } = useEditor()
const [isMouseDown, setIsMouseDown] = useState(false) const [isMouseDown, setIsMouseDown] = useState(false)
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const [currentCoordinates, setCurrentCoordinates] = useState( const [currentCoordinates, setCurrentCoordinates] = useState(
group.graphCoordinates group.graphCoordinates
)
const [groupTitle, setGroupTitle] = useState(group.title)
// When the group is moved from external action (e.g. undo/redo), update the current coordinates
useEffect(() => {
setCurrentCoordinates({
x: group.graphCoordinates.x,
y: group.graphCoordinates.y,
})
}, [group.graphCoordinates.x, group.graphCoordinates.y])
// Same for group title
useEffect(() => {
setGroupTitle(group.title)
}, [group.title])
const isPreviewing =
previewingEdge?.from.groupId === group.id ||
(previewingEdge?.to.groupId === group.id &&
isNotDefined(previewingEdge.to.blockId))
const isStartGroup =
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
const groupRef = useRef<HTMLDivElement | null>(null)
const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100)
useEffect(() => {
if (!currentCoordinates || isReadOnly) return
if (
currentCoordinates?.x === group.graphCoordinates.x &&
currentCoordinates.y === group.graphCoordinates.y
) )
const [groupTitle, setGroupTitle] = useState(group.title) return
updateGroup(groupIndex, { graphCoordinates: currentCoordinates })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedGroupPosition])
// When the group is moved from external action (e.g. undo/redo), update the current coordinates useEffect(() => {
useEffect(() => { setIsConnecting(
setCurrentCoordinates({ connectingIds?.target?.groupId === group.id &&
x: group.graphCoordinates.x, isNotDefined(connectingIds.target?.blockId)
y: group.graphCoordinates.y,
})
}, [group.graphCoordinates.x, group.graphCoordinates.y])
// Same for group title
useEffect(() => {
setGroupTitle(group.title)
}, [group.title])
const isPreviewing =
previewingEdge?.from.groupId === group.id ||
(previewingEdge?.to.groupId === group.id &&
isNotDefined(previewingEdge.to.blockId))
const isStartGroup =
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
const groupRef = useRef<HTMLDivElement | null>(null)
const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100)
useEffect(() => {
if (!currentCoordinates || isReadOnly) return
if (
currentCoordinates?.x === group.graphCoordinates.x &&
currentCoordinates.y === group.graphCoordinates.y
)
return
updateGroup(groupIndex, { graphCoordinates: currentCoordinates })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedGroupPosition])
useEffect(() => {
setIsConnecting(
connectingIds?.target?.groupId === group.id &&
isNotDefined(connectingIds.target?.blockId)
)
}, [connectingIds, group.id])
const handleTitleSubmit = (title: string) =>
title.length > 0 ? updateGroup(groupIndex, { title }) : undefined
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
}
const handleMouseEnter = () => {
if (isReadOnly) return
if (mouseOverGroup?.id !== group.id && !isStartGroup)
setMouseOverGroup({ id: group.id, ref: groupRef })
if (connectingIds)
setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
}
const handleMouseLeave = () => {
if (isReadOnly) return
setMouseOverGroup(undefined)
if (connectingIds)
setConnectingIds({ ...connectingIds, target: undefined })
}
const startPreviewAtThisGroup = () => {
setStartPreviewAtGroup(group.id)
setRightPanel(RightPanel.PREVIEW)
}
useDrag(
({ first, last, offset: [offsetX, offsetY], event, target }) => {
event.stopPropagation()
if ((target as HTMLElement).classList.contains('prevent-group-drag'))
return
if (first) {
setFocusedGroupId(group.id)
setIsMouseDown(true)
}
if (last) {
setIsMouseDown(false)
}
const newCoord = {
x: offsetX / graphPosition.scale,
y: offsetY / graphPosition.scale,
}
setCurrentCoordinates(newCoord)
onGroupDrag(newCoord)
},
{
target: groupRef,
pointer: { keys: false },
from: () => [
currentCoordinates.x * graphPosition.scale,
currentCoordinates.y * graphPosition.scale,
],
}
) )
}, [connectingIds, group.id])
return ( const handleTitleSubmit = (title: string) =>
<ContextMenu<HTMLDivElement> title.length > 0 ? updateGroup(groupIndex, { title }) : undefined
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
isDisabled={isReadOnly || isStartGroup} const handleMouseDown = (e: React.MouseEvent) => {
> e.stopPropagation()
{(ref, isOpened) => (
<Stack
ref={setMultipleRefs([ref, groupRef])}
data-testid="group"
p="4"
rounded="xl"
bgColor="#ffffff"
borderWidth="2px"
borderColor={
isConnecting || isOpened || isPreviewing ? 'blue.400' : '#ffffff'
}
w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${currentCoordinates?.x ?? 0}px, ${
currentCoordinates?.y ?? 0
}px)`,
}}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={focusedGroupId === group.id ? 10 : 1}
>
<Editable
value={groupTitle}
onChange={setGroupTitle}
onSubmit={handleTitleSubmit}
fontWeight="semibold"
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
pr="8"
>
<EditablePreview
_hover={{ bgColor: 'gray.200' }}
px="1"
userSelect={'none'}
/>
<EditableInput minW="0" px="1" className="prevent-group-drag" />
</Editable>
{typebot && (
<BlockNodesList
groupId={group.id}
blocks={group.blocks}
groupIndex={groupIndex}
groupRef={ref}
isStartGroup={isStartGroup}
/>
)}
<IconButton
icon={<PlayIcon />}
aria-label={'Preview bot from this group'}
pos="absolute"
right={2}
top={0}
size="sm"
variant="outline"
onClick={startPreviewAtThisGroup}
/>
</Stack>
)}
</ContextMenu>
)
} }
)
const handleMouseEnter = () => {
if (isReadOnly) return
if (mouseOverGroup?.id !== group.id && !isStartGroup)
setMouseOverGroup({ id: group.id, ref: groupRef })
if (connectingIds)
setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
}
const handleMouseLeave = () => {
if (isReadOnly) return
setMouseOverGroup(undefined)
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
}
const startPreviewAtThisGroup = () => {
setStartPreviewAtGroup(group.id)
setRightPanel(RightPanel.PREVIEW)
}
useDrag(
({ first, last, offset: [offsetX, offsetY], event, target }) => {
event.stopPropagation()
if ((target as HTMLElement).classList.contains('prevent-group-drag'))
return
if (first) {
setFocusedGroupId(group.id)
setIsMouseDown(true)
}
if (last) {
setIsMouseDown(false)
}
const newCoord = {
x: offsetX / graphPosition.scale,
y: offsetY / graphPosition.scale,
}
setCurrentCoordinates(newCoord)
onGroupDrag(newCoord)
},
{
target: groupRef,
pointer: { keys: false },
from: () => [
currentCoordinates.x * graphPosition.scale,
currentCoordinates.y * graphPosition.scale,
],
}
)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
isDisabled={isReadOnly || isStartGroup}
>
{(ref, isOpened) => (
<Stack
ref={setMultipleRefs([ref, groupRef])}
data-testid="group"
p="4"
rounded="xl"
bgColor="#ffffff"
borderWidth="2px"
borderColor={
isConnecting || isOpened || isPreviewing ? 'blue.400' : '#ffffff'
}
w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${currentCoordinates?.x ?? 0}px, ${
currentCoordinates?.y ?? 0
}px)`,
}}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={focusedGroupId === group.id ? 10 : 1}
>
<Editable
value={groupTitle}
onChange={setGroupTitle}
onSubmit={handleTitleSubmit}
fontWeight="semibold"
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
pr="8"
>
<EditablePreview
_hover={{ bgColor: 'gray.200' }}
px="1"
userSelect={'none'}
/>
<EditableInput minW="0" px="1" className="prevent-group-drag" />
</Editable>
{typebot && (
<BlockNodesList
groupId={group.id}
blocks={group.blocks}
groupIndex={groupIndex}
groupRef={ref}
isStartGroup={isStartGroup}
/>
)}
<IconButton
icon={<PlayIcon />}
aria-label={'Preview bot from this group'}
pos="absolute"
right={2}
top={0}
size="sm"
variant="outline"
onClick={startPreviewAtThisGroup}
/>
</Stack>
)}
</ContextMenu>
)
}
export const DraggableGroupNode = memo(NonMemoizedDraggableGroupNode)

View File

@ -27,7 +27,6 @@ const graphDndContext = createContext<{
setMouseOverGroup: Dispatch<SetStateAction<NodeInfo | undefined>> setMouseOverGroup: Dispatch<SetStateAction<NodeInfo | undefined>>
mouseOverBlock?: NodeInfo mouseOverBlock?: NodeInfo
setMouseOverBlock: Dispatch<SetStateAction<NodeInfo | undefined>> setMouseOverBlock: Dispatch<SetStateAction<NodeInfo | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -74,7 +74,6 @@ const graphContext = createContext<{
isReadOnly: boolean isReadOnly: boolean
focusedGroupId?: string focusedGroupId?: string
setFocusedGroupId: Dispatch<SetStateAction<string | undefined>> setFocusedGroupId: Dispatch<SetStateAction<string | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({ }>({
graphPosition: graphPositionDefaultValue, graphPosition: graphPositionDefaultValue,

View File

@ -12,7 +12,6 @@ import { GroupsCoordinates, Coordinates } from './GraphProvider'
const groupsCoordinatesContext = createContext<{ const groupsCoordinatesContext = createContext<{
groupsCoordinates: GroupsCoordinates groupsCoordinates: GroupsCoordinates
updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -17,7 +17,6 @@ const resultsContext = createContext<{
onDeleteResults: (totalResultsDeleted: number) => void onDeleteResults: (totalResultsDeleted: number) => void
fetchNextPage: () => void fetchNextPage: () => void
refetchResults: () => void refetchResults: () => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -13,7 +13,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ToolIcon, EyeIcon, EyeOffIcon, GripIcon } from '@/components/icons' import { ToolIcon, EyeIcon, EyeOffIcon, GripIcon } from '@/components/icons'
import { ResultHeaderCell } from 'models' import { ResultHeaderCell } from 'models'
import React, { forwardRef, useState } from 'react' import React, { useState } from 'react'
import { isNotDefined } from 'utils' import { isNotDefined } from 'utils'
import { import {
DndContext, DndContext,
@ -128,7 +128,7 @@ export const ColumnSettingsButton = ({
</SortableContext> </SortableContext>
<Portal> <Portal>
<DragOverlay dropAnimation={{ duration: 0 }}> <DragOverlay dropAnimation={{ duration: 0 }}>
{draggingColumnId ? <SortableColumnOverlay /> : null} {draggingColumnId ? <Flex /> : null}
</DragOverlay> </DragOverlay>
</Portal> </Portal>
</DndContext> </DndContext>
@ -210,9 +210,3 @@ const SortableColumns = ({
</Flex> </Flex>
) )
} }
const SortableColumnOverlay = forwardRef(
(_, ref: React.LegacyRef<HTMLDivElement>) => {
return <HStack ref={ref}></HStack>
}
)

View File

@ -0,0 +1,23 @@
import { Checkbox, Flex } from '@chakra-ui/react'
import React from 'react'
const TableCheckBox = (
{ indeterminate, checked, ...rest }: any,
ref: React.LegacyRef<HTMLInputElement>
) => {
const defaultRef = React.useRef()
const resolvedRef: any = ref || defaultRef
return (
<Flex justify="center" data-testid="checkbox">
<Checkbox
ref={resolvedRef}
{...rest}
isIndeterminate={indeterminate}
isChecked={checked}
/>
</Flex>
)
}
export const IndeterminateCheckbox = React.forwardRef(TableCheckBox)

View File

@ -2,7 +2,6 @@ import {
Box, Box,
Button, Button,
chakra, chakra,
Checkbox,
Flex, Flex,
HStack, HStack,
Stack, Stack,
@ -26,6 +25,7 @@ import { Row } from './Row'
import { HeaderRow } from './HeaderRow' import { HeaderRow } from './HeaderRow'
import { CellValueType, TableData } from '../../types' import { CellValueType, TableData } from '../../types'
import { HeaderIcon } from '../../utils' import { HeaderIcon } from '../../utils'
import { IndeterminateCheckbox } from './IndeterminateCheckbox'
type ResultsTableProps = { type ResultsTableProps = {
resultHeader: ResultHeaderCell[] resultHeader: ResultHeaderCell[]
@ -238,23 +238,3 @@ export const ResultsTable = ({
</Stack> </Stack>
) )
} }
const IndeterminateCheckbox = React.forwardRef(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
({ indeterminate, checked, ...rest }: any, ref) => {
const defaultRef = React.useRef()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolvedRef: any = ref || defaultRef
return (
<Flex justify="center" data-testid="checkbox">
<Checkbox
ref={resolvedRef}
{...rest}
isIndeterminate={indeterminate}
isChecked={checked}
/>
</Flex>
)
}
)

View File

@ -30,7 +30,6 @@ const workspaceContext = createContext<{
) => Promise<void> ) => Promise<void>
deleteCurrentWorkspace: () => Promise<void> deleteCurrentWorkspace: () => Promise<void>
refreshWorkspace: (expectedUpdates: Partial<Workspace>) => void refreshWorkspace: (expectedUpdates: Partial<Workspace>) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -88,7 +88,7 @@ const DeleteWorkspaceButton = ({
message={ message={
<Text> <Text>
Are you sure you want to delete {workspaceName} workspace? All its Are you sure you want to delete {workspaceName} workspace? All its
folders, typebots and results will be deleted forever.' folders, typebots and results will be deleted forever.
</Text> </Text>
} }
confirmButtonLabel="Delete" confirmButtonLabel="Delete"

View File

@ -7,8 +7,7 @@ export const useAutoSave = <T>(
item, item,
debounceTimeout, debounceTimeout,
}: { }: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any handler: () => Promise<void>
handler: () => Promise<any>
item?: T item?: T
debounceTimeout: number debounceTimeout: number
}, },

View File

@ -106,5 +106,4 @@ const components = {
}, },
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const customTheme: any = extendTheme({ colors, fonts, components }) export const customTheme: any = extendTheme({ colors, fonts, components })

View File

@ -1,4 +1,4 @@
import NextErrorComponent from 'next/error' import NextErrorComponent, { ErrorProps } from 'next/error'
import * as Sentry from '@sentry/nextjs' import * as Sentry from '@sentry/nextjs'
import { NextPageContext } from 'next' import { NextPageContext } from 'next'
@ -24,14 +24,14 @@ const MyError = ({
} }
MyError.getInitialProps = async (context: NextPageContext) => { MyError.getInitialProps = async (context: NextPageContext) => {
const errorInitialProps = await NextErrorComponent.getInitialProps(context) const errorInitialProps = (await NextErrorComponent.getInitialProps(
context
)) as ErrorProps & { hasGetInitialPropsRun: boolean }
const { res, err, asPath } = context const { res, err, asPath } = context
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when // Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run // getInitialProps has run
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
errorInitialProps.hasGetInitialPropsRun = true errorInitialProps.hasGetInitialPropsRun = true
// Returning early because we don't want to log 404 errors to Sentry. // Returning early because we don't want to log 404 errors to Sentry.

View File

@ -18,8 +18,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(404).send("Couldn't find credentials in database") return res.status(404).send("Couldn't find credentials in database")
const response = await drive({ const response = await drive({
version: 'v3', version: 'v3',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
auth: auth.client, auth: auth.client,
}).files.list({ }).files.list({
q: "mimeType='application/vnd.google-apps.spreadsheet'", q: "mimeType='application/vnd.google-apps.spreadsheet'",

View File

@ -128,5 +128,4 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
return methodNotAllowed(res) return methodNotAllowed(res)
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default withSentry(cors(webhookHandler as any)) export default withSentry(cors(webhookHandler as any))

View File

@ -76,9 +76,9 @@ export const readFile = (file: File): Promise<string> => {
} }
export const timeSince = (date: string) => { export const timeSince = (date: string) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment const seconds = Math.floor(
//@ts-ignore (new Date().getTime() - new Date(date).getTime()) / 1000
const seconds = Math.floor((new Date() - new Date(date)) / 1000) )
let interval = seconds / 31536000 let interval = seconds / 31536000

View File

@ -6,6 +6,5 @@
"@/*": ["src/*"] "@/*": ["src/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
"exclude": ["node_modules"]
} }

View File

@ -1,29 +1,4 @@
module.exports = { module.exports = {
ignorePatterns: ['node_modules'], root: true,
env: { extends: ['custom'],
browser: true,
es6: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'next/core-web-vitals',
],
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: ['react', '@typescript-eslint'],
rules: {
'react/no-unescaped-entities': [0],
},
} }

View File

@ -38,7 +38,7 @@ export const EasyEmbed = () => {
> >
Embedding your typebot in your applications is a walk in the park. Embedding your typebot in your applications is a walk in the park.
Typebot gives you several step-by-step platform-specific Typebot gives you several step-by-step platform-specific
instructions. Your typebot will always feel "native". instructions. Your typebot will always feel &quot;native&quot;.
</Text> </Text>
<Flex data-aos="fade"> <Flex data-aos="fade">
<Button <Button

View File

@ -67,8 +67,8 @@ export const RealTimeResults = () => {
data-aos="fade" data-aos="fade"
> >
One of the main advantage of a chat application is that you collect One of the main advantage of a chat application is that you collect
the user's responses on each question.{' '} the user&apos;s responses on each question.{' '}
<strong>You won't loose any valuable data.</strong> <strong>You won&apos;t loose any valuable data.</strong>
</Text> </Text>
<Flex> <Flex>
<Button <Button

View File

@ -10,7 +10,7 @@ export const Testimonials = () => {
<Flex as="section" justify="center"> <Flex as="section" justify="center">
<VStack spacing={12} pt={'52'} px="4"> <VStack spacing={12} pt={'52'} px="4">
<Heading textAlign={'center'} data-aos="fade"> <Heading textAlign={'center'} data-aos="fade">
They've tried, they never looked back. 💙 They&apos;ve tried, they never looked back. 💙
</Heading> </Heading>
<Stack <Stack
direction={{ base: 'column', xl: 'row' }} direction={{ base: 'column', xl: 'row' }}

View File

@ -92,7 +92,6 @@ const components = {
}, },
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const theme: any = extendTheme({ export const theme: any = extendTheme({
fonts, fonts,
components, components,

View File

@ -5,6 +5,7 @@
"dev": "ENVSH_ENV=.env.local bash ../../env.sh next dev -p 3002", "dev": "ENVSH_ENV=.env.local bash ../../env.sh next dev -p 3002",
"start": "next start", "start": "next start",
"build": "next build", "build": "next build",
"lint": "next lint",
"analyze": "cross-env ANALYZE=true next build" "analyze": "cross-env ANALYZE=true next build"
}, },
"dependencies": { "dependencies": {
@ -30,16 +31,14 @@
"@types/aos": "3.0.4", "@types/aos": "3.0.4",
"@types/node": "18.11.9", "@types/node": "18.11.9",
"@types/react": "18.0.25", "@types/react": "18.0.25",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.27.0", "eslint": "8.28.0",
"eslint-config-next": "13.0.3", "eslint-config-custom": "workspace:*",
"eslint-plugin-react": "7.31.10",
"next-transpile-modules": "10.0.0", "next-transpile-modules": "10.0.0",
"postcss": "8.4.19", "postcss": "8.4.19",
"prettier": "2.7.1", "prettier": "2.7.1",
"typescript": "4.8.4" "typescript": "4.8.4",
"tsconfig": "workspace:*"
} }
} }

View File

@ -21,12 +21,12 @@ const AboutPage = () => {
textAlign="justify" textAlign="justify"
> >
<Flex w="full"> <Flex w="full">
<Heading as="h1">Typebot's story</Heading> <Heading as="h1">Typebot&apos;s story</Heading>
</Flex> </Flex>
<Text> <Text>
Typebot's team is composed of only me, Baptiste Arnaud, a Software Typebot&apos;s team is composed of only me, Baptiste Arnaud, a
Engineer based in France. Software Engineer based in France.
</Text> </Text>
<Flex w="full" justify="center"> <Flex w="full" justify="center">
<Box as="figure" maxW="200px"> <Box as="figure" maxW="200px">
@ -35,13 +35,13 @@ const AboutPage = () => {
</Flex> </Flex>
<Text> <Text>
I'm passionate about great product UX and, during the first COVID I&apos;m passionate about great product UX and, during the first COVID
lockdown, I decided to create my own Typeform alternative. lockdown, I decided to create my own Typeform alternative.
</Text> </Text>
<Text> <Text>
Typebot was launched in July 2020. It is completely independent, Typebot was launched in July 2020. It is completely independent,
self-funded, and bootstrapped. At the current stage, I'm not self-funded, and bootstrapped. At the current stage, I&apos;m not
interested in raising funds or taking investments. interested in raising funds or taking investments.
</Text> </Text>
<Text> <Text>
@ -55,15 +55,16 @@ const AboutPage = () => {
With Typebot, I want to create the best bot-building experience. My With Typebot, I want to create the best bot-building experience. My
goal is to empower you as a user and help you build great user goal is to empower you as a user and help you build great user
experiences. Also, privacy comes first. While using Typebot, you experiences. Also, privacy comes first. While using Typebot, you
aren't tracked by some third-party analytics tool. aren&apos;t tracked by some third-party analytics tool.
</Text> </Text>
<Text> <Text>
I'm working hard on making a living from Typebot with a simple I&apos;m working hard on making a living from Typebot with a simple
business model: <br /> business model: <br />
<br /> You can use the tool for free but your forms will contain a <br /> You can use the tool for free but your forms will contain a
"Made with Typebot" small badge that potentially gets people to know &quot;Made with Typebot&quot; small badge that potentially gets people
about the product. If you want to remove it and also have access to to know about the product. If you want to remove it and also have
other advanced features, you have to subscribe for $39 per month. access to other advanced features, you have to subscribe for $39 per
month.
</Text> </Text>
<Text> <Text>
If you have any questions, feel free to reach out to me at{' '} If you have any questions, feel free to reach out to me at{' '}

View File

@ -56,7 +56,7 @@ const Pricing = () => {
<VStack> <VStack>
<Heading fontSize="6xl">Plans fit for you</Heading> <Heading fontSize="6xl">Plans fit for you</Heading>
<Text maxW="900px" fontSize="xl" textAlign="center"> <Text maxW="900px" fontSize="xl" textAlign="center">
Whether you're a{' '} Whether you&apos;re a{' '}
<Text as="span" color="orange.200" fontWeight="bold"> <Text as="span" color="orange.200" fontWeight="bold">
solo business owner solo business owner
</Text>{' '} </Text>{' '}
@ -100,7 +100,7 @@ const Pricing = () => {
<Text fontSize="lg"> <Text fontSize="lg">
Need custom limits? Specific features?{' '} Need custom limits? Specific features?{' '}
<TextLink href={'https://typebot.io/enterprise-lead-form'}> <TextLink href={'https://typebot.io/enterprise-lead-form'}>
Let's chat! Let&apos;s chat!
</TextLink> </TextLink>
</Text> </Text>
</Stack> </Stack>
@ -155,9 +155,9 @@ const Faq = () => {
<AccordionPanel pb={4}> <AccordionPanel pb={4}>
You will receive an email notification once you reached 80% of this You will receive an email notification once you reached 80% of this
limit. Then, once you reach 100%, your users will still be able to limit. Then, once you reach 100%, your users will still be able to
chat with your bot but their uploads won't be stored anymore. You will chat with your bot but their uploads won&apos;t be stored anymore. You
need to upgrade the limit or free up some space to continue collecting will need to upgrade the limit or free up some space to continue
your users' files. collecting your users&apos; files.
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem> <AccordionItem>
@ -189,7 +189,7 @@ const Faq = () => {
<TextLink href="mailto:baptiste@typebot.io"> <TextLink href="mailto:baptiste@typebot.io">
shoot me an email shoot me an email
</TextLink>{' '} </TextLink>{' '}
and we'll figure things out 😀 and we&apos;ll figure things out 😀
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>

View File

@ -1,22 +1,10 @@
{ {
"extends": "tsconfig/nextjs.json",
"compilerOptions": { "compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".", "baseUrl": ".",
"composite": true "paths": {
"@/*": ["src/*"]
}
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
"exclude": ["node_modules"]
} }

View File

@ -1,43 +1,4 @@
module.exports = { module.exports = {
ignorePatterns: ['node_modules'], root: true,
env: { extends: ['custom'],
browser: true,
es6: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'next/core-web-vitals',
],
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: ['react', '@typescript-eslint'],
rules: {
'react/no-unescaped-entities': [0],
'react/display-name': [0],
'no-restricted-imports': [
'error',
{
patterns: [
'*/src/*',
'src/*',
'*/src',
'@/features/*/*',
'!@/features/blocks/*',
'!@/features/*/api',
],
},
],
},
} }

View File

@ -39,21 +39,17 @@
"@types/qs": "6.9.7", "@types/qs": "6.9.7",
"@types/react": "18.0.25", "@types/react": "18.0.25",
"@types/sanitize-html": "2.6.2", "@types/sanitize-html": "2.6.2",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"dotenv": "16.0.3", "dotenv": "16.0.3",
"emails": "workspace:*", "emails": "workspace:*",
"eslint": "8.27.0", "eslint": "8.28.0",
"eslint-config-next": "13.0.3", "eslint-config-custom": "workspace:*",
"eslint-plugin-react": "7.31.10",
"eslint-plugin-react-hooks": "4.6.0",
"google-auth-library": "8.7.0", "google-auth-library": "8.7.0",
"models": "workspace:*", "models": "workspace:*",
"next-transpile-modules": "10.0.0", "next-transpile-modules": "10.0.0",
"node-fetch": "^3.3.0", "node-fetch": "^3.3.0",
"papaparse": "5.3.2", "papaparse": "5.3.2",
"tsconfig": "workspace:*",
"typescript": "4.8.4", "typescript": "4.8.4",
"utils": "workspace:*", "utils": "workspace:*"
"configs": "workspace:*"
} }
} }

View File

@ -1,6 +1,6 @@
import { PlaywrightTestConfig } from '@playwright/test' import { PlaywrightTestConfig } from '@playwright/test'
import path from 'path' import path from 'path'
import { playwrightBaseConfig } from 'configs/playwright' import { playwrightBaseConfig } from 'utils/playwright/baseConfig'
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
...playwrightBaseConfig, ...playwrightBaseConfig,

View File

@ -1,5 +1,11 @@
import { TypebotViewer } from 'bot-engine' import { TypebotViewer } from 'bot-engine'
import { Answer, PublicTypebot, Typebot, VariableWithValue } from 'models' import {
Answer,
AnswerInput,
PublicTypebot,
Typebot,
VariableWithValue,
} from 'models'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { isDefined, isNotDefined } from 'utils' import { isDefined, isNotDefined } from 'utils'
@ -106,7 +112,7 @@ export const TypebotPage = ({
} }
const handleNewAnswer = async ( const handleNewAnswer = async (
answer: Answer & { uploadedFiles: boolean } answer: AnswerInput & { uploadedFiles: boolean }
) => { ) => {
if (!resultId) return setError(new Error('Error: result was not created')) if (!resultId) return setError(new Error('Error: result was not created'))
if (publishedTypebot.settings.general.isResultSavingEnabled !== false) { if (publishedTypebot.settings.general.isResultSavingEnabled !== false) {

View File

@ -1,8 +1,8 @@
import { Answer } from 'models' import { Answer, AnswerInput } from 'models'
import { sendRequest } from 'utils' import { sendRequest } from 'utils'
export const upsertAnswerQuery = async ( export const upsertAnswerQuery = async (
answer: Answer & { resultId: string } & { uploadedFiles?: boolean } answer: AnswerInput & { resultId: string } & { uploadedFiles?: boolean }
) => ) =>
sendRequest<Answer>({ sendRequest<Answer>({
url: `/api/typebots/t/results/r/answers`, url: `/api/typebots/t/results/r/answers`,

View File

@ -151,22 +151,30 @@ test('Should correctly parse metadata', async ({ page }) => {
).toBe(customMetadata.title) ).toBe(customMetadata.title)
expect( expect(
await page.evaluate( await page.evaluate(
() => (document.querySelector('meta[name="description"]') as any).content () =>
(document.querySelector('meta[name="description"]') as HTMLMetaElement)
.content
) )
).toBe(customMetadata.description) ).toBe(customMetadata.description)
expect( expect(
await page.evaluate( await page.evaluate(
() => (document.querySelector('meta[property="og:image"]') as any).content () =>
(document.querySelector('meta[property="og:image"]') as HTMLMetaElement)
.content
) )
).toBe(customMetadata.imageUrl) ).toBe(customMetadata.imageUrl)
expect( expect(
await page.evaluate(() => await page.evaluate(() =>
(document.querySelector('link[rel="icon"]') as any).getAttribute('href') (
document.querySelector('link[rel="icon"]') as HTMLLinkElement
).getAttribute('href')
) )
).toBe(customMetadata.favIconUrl) ).toBe(customMetadata.favIconUrl)
expect( expect(
await page.evaluate( await page.evaluate(
() => (document.querySelector('meta[name="author"]') as any).content () =>
(document.querySelector('meta[name="author"]') as HTMLMetaElement)
.content
) )
).toBe('John Doe') ).toBe('John Doe')
await expect( await expect(

View File

@ -1,4 +1,4 @@
import NextErrorComponent from 'next/error' import NextErrorComponent, { ErrorProps } from 'next/error'
import * as Sentry from '@sentry/nextjs' import * as Sentry from '@sentry/nextjs'
import { NextPageContext } from 'next' import { NextPageContext } from 'next'
@ -24,14 +24,14 @@ const MyError = ({
} }
MyError.getInitialProps = async (context: NextPageContext) => { MyError.getInitialProps = async (context: NextPageContext) => {
const errorInitialProps = await NextErrorComponent.getInitialProps(context) const errorInitialProps = (await NextErrorComponent.getInitialProps(
context
)) as ErrorProps & { hasGetInitialPropsRun: boolean }
const { res, err, asPath } = context const { res, err, asPath } = context
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when // Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run // getInitialProps has run
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
errorInitialProps.hasGetInitialPropsRun = true errorInitialProps.hasGetInitialPropsRun = true
// Returning early because we don't want to log 404 errors to Sentry. // Returning early because we don't want to log 404 errors to Sentry.

View File

@ -84,18 +84,19 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}`, }`,
}) })
} catch (err) { } catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (typeof err === 'object' && err && 'raw' in err) {
const error = err as any const error = (err as { raw: Stripe.StripeRawError }).raw
return 'raw' in error res.status(error.statusCode ?? 500).send({
? res.status(error.raw.statusCode).send({ error: {
error: { name: `${error.type} ${error.param}`,
name: `${error.raw.type} ${error.raw.param}`, message: error.message,
message: error.raw.message, },
}, })
}) } else {
: res.status(500).send({ res.status(500).send({
error, err,
}) })
}
} }
} }
return methodNotAllowed(res) return methodNotAllowed(res)

View File

@ -25,6 +25,7 @@ import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
import { parseSampleResult } from '@/features/webhook/api' import { parseSampleResult } from '@/features/webhook/api'
const cors = initMiddleware(Cors()) const cors = initMiddleware(Cors())
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res) await cors(req, res)
if (req.method === 'POST') { if (req.method === 'POST') {
@ -34,11 +35,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { resultValues, variables } = ( const { resultValues, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as { ) as {
resultValues: resultValues: ResultValues | undefined
| (Omit<ResultValues, 'createdAt'> & {
createdAt: string
})
| undefined
variables: Variable[] variables: Variable[]
} }
const typebot = (await prisma.typebot.findUnique({ const typebot = (await prisma.typebot.findUnique({
@ -87,9 +84,7 @@ export const executeWebhook =
webhook: Webhook, webhook: Webhook,
variables: Variable[], variables: Variable[],
groupId: string, groupId: string,
resultValues?: Omit<ResultValues, 'createdAt'> & { resultValues?: ResultValues,
createdAt: string
},
resultId?: string resultId?: string
): Promise<WebhookResponse> => { ): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method) if (!webhook.url || !webhook.method)
@ -199,9 +194,7 @@ const getBodyContent =
groupId, groupId,
}: { }: {
body?: string | null body?: string | null
resultValues?: Omit<ResultValues, 'createdAt'> & { resultValues?: ResultValues
createdAt: string
}
groupId: string groupId: string
}): Promise<string | undefined> => { }): Promise<string | undefined> => {
if (!body) return if (!body) return
@ -228,7 +221,6 @@ const convertKeyValueTableToObject = (
}, {}) }, {})
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const safeJsonParse = (json: string): { data: any; isJson: boolean } => { const safeJsonParse = (json: string): { data: any; isJson: boolean } => {
try { try {
return { data: JSON.parse(json), isJson: true } return { data: JSON.parse(json), isJson: true }

View File

@ -16,6 +16,7 @@ import Cors from 'cors'
import { executeWebhook } from '../../executeWebhook' import { executeWebhook } from '../../executeWebhook'
const cors = initMiddleware(Cors()) const cors = initMiddleware(Cors())
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res) await cors(req, res)
if (req.method === 'POST') { if (req.method === 'POST') {
@ -26,11 +27,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { resultValues, variables } = ( const { resultValues, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as { ) as {
resultValues: resultValues: ResultValues
| (Omit<ResultValues, 'createdAt'> & {
createdAt: string
})
| undefined
variables: Variable[] variables: Variable[]
} }
const typebot = (await prisma.typebot.findUnique({ const typebot = (await prisma.typebot.findUnique({

View File

@ -1,16 +1,11 @@
import { withSentry } from '@sentry/nextjs' import { withSentry } from '@sentry/nextjs'
import { WorkspaceRole } from 'db'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { InputBlockType, PublicTypebot } from 'models' import { InputBlockType, PublicTypebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api' import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api'
import { byId, getStorageLimit, isDefined, env } from 'utils' import { byId } from 'utils'
import {
sendAlmostReachedStorageLimitEmail,
sendReachedStorageLimitEmail,
} from 'emails'
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8 // const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
const handler = async ( const handler = async (
req: NextApiRequest, req: NextApiRequest,
@ -57,112 +52,111 @@ const handler = async (
return methodNotAllowed(res) return methodNotAllowed(res)
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // const checkStorageLimit = async (typebotId: string) => {
const checkStorageLimit = async (typebotId: string) => { // const typebot = await prisma.typebot.findFirst({
const typebot = await prisma.typebot.findFirst({ // where: { id: typebotId },
where: { id: typebotId }, // include: {
include: { // workspace: {
workspace: { // select: {
select: { // id: true,
id: true, // additionalStorageIndex: true,
additionalStorageIndex: true, // plan: true,
plan: true, // storageLimitFirstEmailSentAt: true,
storageLimitFirstEmailSentAt: true, // storageLimitSecondEmailSentAt: true,
storageLimitSecondEmailSentAt: true, // customStorageLimit: true,
customStorageLimit: true, // },
}, // },
}, // },
}, // })
}) // if (!typebot?.workspace) throw new Error('Workspace not found')
if (!typebot?.workspace) throw new Error('Workspace not found') // const { workspace } = typebot
const { workspace } = typebot // const {
const { // _sum: { storageUsed: totalStorageUsed },
_sum: { storageUsed: totalStorageUsed }, // } = await prisma.answer.aggregate({
} = await prisma.answer.aggregate({ // where: {
where: { // storageUsed: { gt: 0 },
storageUsed: { gt: 0 }, // result: {
result: { // typebot: {
typebot: { // workspace: {
workspace: { // id: typebot?.workspaceId,
id: typebot?.workspaceId, // },
}, // },
}, // },
}, // },
}, // _sum: { storageUsed: true },
_sum: { storageUsed: true }, // })
}) // if (!totalStorageUsed) return false
if (!totalStorageUsed) return false // const hasSentFirstEmail = workspace.storageLimitFirstEmailSentAt !== null
const hasSentFirstEmail = workspace.storageLimitFirstEmailSentAt !== null // const hasSentSecondEmail = workspace.storageLimitSecondEmailSentAt !== null
const hasSentSecondEmail = workspace.storageLimitSecondEmailSentAt !== null // const storageLimit = getStorageLimit(typebot.workspace)
const storageLimit = getStorageLimit(typebot.workspace) // const storageLimitBytes = storageLimit * 1024 * 1024 * 1024
const storageLimitBytes = storageLimit * 1024 * 1024 * 1024 // if (
if ( // totalStorageUsed >= storageLimitBytes * LIMIT_EMAIL_TRIGGER_PERCENT &&
totalStorageUsed >= storageLimitBytes * LIMIT_EMAIL_TRIGGER_PERCENT && // !hasSentFirstEmail &&
!hasSentFirstEmail && // env('E2E_TEST') !== 'true'
env('E2E_TEST') !== 'true' // )
) // await sendAlmostReachStorageLimitNotification({
await sendAlmostReachStorageLimitNotification({ // workspaceId: workspace.id,
workspaceId: workspace.id, // storageLimit,
storageLimit, // })
}) // if (
if ( // totalStorageUsed >= storageLimitBytes &&
totalStorageUsed >= storageLimitBytes && // !hasSentSecondEmail &&
!hasSentSecondEmail && // env('E2E_TEST') !== 'true'
env('E2E_TEST') !== 'true' // )
) // await sendReachStorageLimitNotification({
await sendReachStorageLimitNotification({ // workspaceId: workspace.id,
workspaceId: workspace.id, // storageLimit,
storageLimit, // })
}) // return totalStorageUsed >= storageLimitBytes
return totalStorageUsed >= storageLimitBytes // }
}
const sendAlmostReachStorageLimitNotification = async ({ // const sendAlmostReachStorageLimitNotification = async ({
workspaceId, // workspaceId,
storageLimit, // storageLimit,
}: { // }: {
workspaceId: string // workspaceId: string
storageLimit: number // storageLimit: number
}) => { // }) => {
const members = await prisma.memberInWorkspace.findMany({ // const members = await prisma.memberInWorkspace.findMany({
where: { role: WorkspaceRole.ADMIN, workspaceId }, // where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } }, // include: { user: { select: { email: true } } },
}) // })
await sendAlmostReachedStorageLimitEmail({ // await sendAlmostReachedStorageLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined), // to: members.map((member) => member.user.email).filter(isDefined),
storageLimit, // storageLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`, // url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
}) // })
await prisma.workspace.update({ // await prisma.workspace.update({
where: { id: workspaceId }, // where: { id: workspaceId },
data: { storageLimitFirstEmailSentAt: new Date() }, // data: { storageLimitFirstEmailSentAt: new Date() },
}) // })
} // }
const sendReachStorageLimitNotification = async ({ // const sendReachStorageLimitNotification = async ({
workspaceId, // workspaceId,
storageLimit, // storageLimit,
}: { // }: {
workspaceId: string // workspaceId: string
storageLimit: number // storageLimit: number
}) => { // }) => {
const members = await prisma.memberInWorkspace.findMany({ // const members = await prisma.memberInWorkspace.findMany({
where: { role: WorkspaceRole.ADMIN, workspaceId }, // where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } }, // include: { user: { select: { email: true } } },
}) // })
await sendReachedStorageLimitEmail({ // await sendReachedStorageLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined), // to: members.map((member) => member.user.email).filter(isDefined),
storageLimit, // storageLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`, // url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
}) // })
await prisma.workspace.update({ // await prisma.workspace.update({
where: { id: workspaceId }, // where: { id: workspaceId },
data: { storageLimitSecondEmailSentAt: new Date() }, // data: { storageLimitSecondEmailSentAt: new Date() },
}) // })
} // }
export default withSentry(handler) export default withSentry(handler)

View File

@ -31,7 +31,7 @@ const defaultTransportOptions = {
const defaultFrom = { const defaultFrom = {
name: process.env.SMTP_FROM?.split(' <')[0].replace(/"/g, ''), name: process.env.SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
email: process.env.SMTP_FROM?.match(/\<(.*)\>/)?.pop(), email: process.env.SMTP_FROM?.match(/<(.*)>/)?.pop(),
} }
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@ -54,9 +54,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
} = ( } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as SendEmailOptions & { ) as SendEmailOptions & {
resultValues: Omit<ResultValues, 'createdAt'> & { resultValues: ResultValues
createdAt: string
}
fileUrls?: string fileUrls?: string
} }
const { name: replyToName } = parseEmailRecipient(replyTo) const { name: replyToName } = parseEmailRecipient(replyTo)
@ -166,9 +164,7 @@ const getEmailBody = async ({
resultValues, resultValues,
}: { }: {
typebotId: string typebotId: string
resultValues: Omit<ResultValues, 'createdAt'> & { resultValues: ResultValues
createdAt: string
}
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise< } & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined { html?: string; text?: string } | undefined
> => { > => {

View File

@ -1,16 +1,10 @@
import { authenticateUser } from '@/features/auth/api' import { authenticateUser } from '@/features/auth/api'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { Workspace, WorkspaceRole } from 'db'
import {
sendAlmostReachedChatsLimitEmail,
sendReachedChatsLimitEmail,
} from 'emails'
import { ResultWithAnswers } from 'models' import { ResultWithAnswers } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { env, getChatsLimit, isDefined } from 'utils'
import { methodNotAllowed } from 'utils/api' import { methodNotAllowed } from 'utils/api'
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8 // const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') { if (req.method === 'GET') {
@ -63,106 +57,105 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
methodNotAllowed(res) methodNotAllowed(res)
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // const checkChatsUsage = async (
const checkChatsUsage = async ( // workspace: Pick<
workspace: Pick< // Workspace,
Workspace, // | 'id'
| 'id' // | 'plan'
| 'plan' // | 'additionalChatsIndex'
| 'additionalChatsIndex' // | 'chatsLimitFirstEmailSentAt'
| 'chatsLimitFirstEmailSentAt' // | 'chatsLimitSecondEmailSentAt'
| 'chatsLimitSecondEmailSentAt' // | 'customChatsLimit'
| 'customChatsLimit' // >
> // ) => {
) => { // const chatsLimit = getChatsLimit(workspace)
const chatsLimit = getChatsLimit(workspace) // if (chatsLimit === -1) return
if (chatsLimit === -1) return // const now = new Date()
const now = new Date() // const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1) // const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0) // const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1) // const chatsCount = await prisma.result.count({
const chatsCount = await prisma.result.count({ // where: {
where: { // typebot: { workspaceId: workspace.id },
typebot: { workspaceId: workspace.id }, // hasStarted: true,
hasStarted: true, // createdAt: { gte: firstDayOfMonth, lte: lastDayOfMonth },
createdAt: { gte: firstDayOfMonth, lte: lastDayOfMonth }, // },
}, // })
}) // const hasSentFirstEmail =
const hasSentFirstEmail = // workspace.chatsLimitFirstEmailSentAt !== null &&
workspace.chatsLimitFirstEmailSentAt !== null && // workspace.chatsLimitFirstEmailSentAt < firstDayOfNextMonth &&
workspace.chatsLimitFirstEmailSentAt < firstDayOfNextMonth && // workspace.chatsLimitFirstEmailSentAt > firstDayOfMonth
workspace.chatsLimitFirstEmailSentAt > firstDayOfMonth // const hasSentSecondEmail =
const hasSentSecondEmail = // workspace.chatsLimitSecondEmailSentAt !== null &&
workspace.chatsLimitSecondEmailSentAt !== null && // workspace.chatsLimitSecondEmailSentAt < firstDayOfNextMonth &&
workspace.chatsLimitSecondEmailSentAt < firstDayOfNextMonth && // workspace.chatsLimitSecondEmailSentAt > firstDayOfMonth
workspace.chatsLimitSecondEmailSentAt > firstDayOfMonth // if (
if ( // chatsCount >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
chatsCount >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT && // !hasSentFirstEmail &&
!hasSentFirstEmail && // env('E2E_TEST') !== 'true'
env('E2E_TEST') !== 'true' // )
) // await sendAlmostReachChatsLimitNotification({
await sendAlmostReachChatsLimitNotification({ // workspaceId: workspace.id,
workspaceId: workspace.id, // chatsLimit,
chatsLimit, // })
}) // if (
if ( // chatsCount >= chatsLimit &&
chatsCount >= chatsLimit && // !hasSentSecondEmail &&
!hasSentSecondEmail && // env('E2E_TEST') !== 'true'
env('E2E_TEST') !== 'true' // )
) // await sendReachedAlertNotification({
await sendReachedAlertNotification({ // workspaceId: workspace.id,
workspaceId: workspace.id, // chatsLimit,
chatsLimit, // })
}) // return chatsCount >= chatsLimit
return chatsCount >= chatsLimit // }
}
const sendAlmostReachChatsLimitNotification = async ({ // const sendAlmostReachChatsLimitNotification = async ({
workspaceId, // workspaceId,
chatsLimit, // chatsLimit,
}: { // }: {
workspaceId: string // workspaceId: string
chatsLimit: number // chatsLimit: number
}) => { // }) => {
const members = await prisma.memberInWorkspace.findMany({ // const members = await prisma.memberInWorkspace.findMany({
where: { role: WorkspaceRole.ADMIN, workspaceId }, // where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } }, // include: { user: { select: { email: true } } },
}) // })
await sendAlmostReachedChatsLimitEmail({ // await sendAlmostReachedChatsLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined), // to: members.map((member) => member.user.email).filter(isDefined),
chatsLimit, // chatsLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`, // url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
}) // })
await prisma.workspace.update({ // await prisma.workspace.update({
where: { id: workspaceId }, // where: { id: workspaceId },
data: { chatsLimitFirstEmailSentAt: new Date() }, // data: { chatsLimitFirstEmailSentAt: new Date() },
}) // })
} // }
const sendReachedAlertNotification = async ({ // const sendReachedAlertNotification = async ({
workspaceId, // workspaceId,
chatsLimit, // chatsLimit,
}: { // }: {
workspaceId: string // workspaceId: string
chatsLimit: number // chatsLimit: number
}) => { // }) => {
const members = await prisma.memberInWorkspace.findMany({ // const members = await prisma.memberInWorkspace.findMany({
where: { role: WorkspaceRole.ADMIN, workspaceId }, // where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } }, // include: { user: { select: { email: true } } },
}) // })
await sendReachedChatsLimitEmail({ // await sendReachedChatsLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined), // to: members.map((member) => member.user.email).filter(isDefined),
chatsLimit, // chatsLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`, // url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
}) // })
await prisma.workspace.update({ // await prisma.workspace.update({
where: { id: workspaceId }, // where: { id: workspaceId },
data: { chatsLimitSecondEmailSentAt: new Date() }, // data: { chatsLimitSecondEmailSentAt: new Date() },
}) // })
} // }
export default handler export default handler

View File

@ -1,26 +1,10 @@
{ {
"extends": "tsconfig/nextjs.json",
"compilerOptions": { "compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, }
"composite": true,
"downlevelIteration": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
"exclude": ["node_modules"]
} }

View File

@ -20,10 +20,13 @@
"test:viewer": "cd apps/viewer && pnpm test", "test:viewer": "cd apps/viewer && pnpm test",
"db:migrate": "cd packages/db && pnpm migration:deploy", "db:migrate": "cd packages/db && pnpm migration:deploy",
"build:ci": "turbo run build --filter=builder... --filter=viewer... && ENVSH_ENV=./apps/builder/.env.docker ENVSH_OUTPUT=./apps/builder/public/__env.js bash env.sh && ENVSH_ENV=./apps/viewer/.env.docker ENVSH_OUTPUT=./apps/viewer/public/__env.js bash env.sh", "build:ci": "turbo run build --filter=builder... --filter=viewer... && ENVSH_ENV=./apps/builder/.env.docker ENVSH_OUTPUT=./apps/builder/public/__env.js bash env.sh && ENVSH_ENV=./apps/viewer/.env.docker ENVSH_OUTPUT=./apps/viewer/public/__env.js bash env.sh",
"generate-change-log": "pnpx gitmoji-changelog" "generate-change-log": "pnpx gitmoji-changelog",
"lint": "turbo run lint",
"prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"cz-emoji": "1.3.2-canary.2", "cz-emoji": "1.3.2-canary.2",
"husky": "^8.0.2",
"turbo": "1.6.3" "turbo": "1.6.3"
}, },
"config": { "config": {
@ -31,5 +34,5 @@
"path": "node_modules/cz-emoji" "path": "node_modules/cz-emoji"
} }
}, },
"packageManager": "pnpm@7.16.1" "packageManager": "pnpm@7.17.0"
} }

View File

@ -1,48 +1,8 @@
module.exports = { module.exports = {
ignorePatterns: ['node_modules'], root: true,
env: { extends: ['custom'],
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', '@typescript-eslint'],
ignorePatterns: 'dist',
rules: { rules: {
'react/no-unescaped-entities': [0], '@next/next/no-img-element': 'off',
'prettier/prettier': 'error', '@next/next/no-html-link-for-pages': 'off',
'react/display-name': [0],
'@next/next/no-img-element': [0],
'no-restricted-imports': [
'error',
{
patterns: [
'*/src/*',
'src/*',
'*/src',
'@/features/*/*',
'@/index',
'!@/features/blocks/*',
'!@/features/*/api',
],
},
],
}, },
} }

View File

@ -6,9 +6,9 @@
"module": "dist/index.mjs", "module": "dist/index.mjs",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"build": "tsup", "build": "pnpm tsc --noEmit && tsup",
"dev": "tsup --watch", "dev": "tsup --watch",
"lint": "eslint --fix -c ./.eslintrc.js \"./src/**/*.ts*\"" "lint": "eslint \"src/**/*.ts*\""
}, },
"dependencies": { "dependencies": {
"@stripe/react-stripe-js": "1.15.0", "@stripe/react-stripe-js": "1.15.0",
@ -28,16 +28,11 @@
"@types/react-phone-number-input": "3.0.14", "@types/react-phone-number-input": "3.0.14",
"@types/react-scroll": "1.8.5", "@types/react-scroll": "1.8.5",
"@types/react-transition-group": "4.4.5", "@types/react-transition-group": "4.4.5",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"baptistearno-tsup": "^0.1.0", "tsup": "6.5.0",
"db": "workspace:*", "db": "workspace:*",
"eslint": "8.27.0", "eslint": "8.28.0",
"eslint-config-next": "13.0.3", "eslint-config-custom": "workspace:*",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.10",
"models": "workspace:*", "models": "workspace:*",
"postcss": "8.4.19", "postcss": "8.4.19",
"prettier": "2.7.1", "prettier": "2.7.1",
@ -46,7 +41,8 @@
"tailwindcss": "3.2.4", "tailwindcss": "3.2.4",
"typescript": "4.8.4", "typescript": "4.8.4",
"utils": "workspace:*", "utils": "workspace:*",
"typebot-js": "workspace:*" "typebot-js": "workspace:*",
"tsconfig": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
"db": "workspace:*", "db": "workspace:*",

View File

@ -13,59 +13,60 @@ import { ResizeObserver } from 'resize-observer'
type Props = { hostAvatarSrc?: string; keepShowing: boolean } type Props = { hostAvatarSrc?: string; keepShowing: boolean }
export const AvatarSideContainer = forwardRef( export const AvatarSideContainer = forwardRef(function AvatarSideContainer(
({ hostAvatarSrc, keepShowing }: Props, ref: ForwardedRef<unknown>) => { { hostAvatarSrc, keepShowing }: Props,
const { document } = useFrame() ref: ForwardedRef<unknown>
const [show, setShow] = useState(false) ) {
const [avatarTopOffset, setAvatarTopOffset] = useState(0) const { document } = useFrame()
const [show, setShow] = useState(false)
const [avatarTopOffset, setAvatarTopOffset] = useState(0)
const refreshTopOffset = () => { const refreshTopOffset = () => {
if (!scrollingSideGroupRef.current || !avatarContainer.current) return if (!scrollingSideGroupRef.current || !avatarContainer.current) return
const { height } = scrollingSideGroupRef.current.getBoundingClientRect() const { height } = scrollingSideGroupRef.current.getBoundingClientRect()
const { height: avatarHeight } = const { height: avatarHeight } =
avatarContainer.current.getBoundingClientRect() avatarContainer.current.getBoundingClientRect()
setAvatarTopOffset(height - avatarHeight) setAvatarTopOffset(height - avatarHeight)
}
const scrollingSideGroupRef = useRef<HTMLDivElement>(null)
const avatarContainer = useRef<HTMLDivElement>(null)
useImperativeHandle(ref, () => ({
refreshTopOffset,
}))
useEffect(() => {
if (!document) return
setShow(true)
const resizeObserver = new ResizeObserver(refreshTopOffset)
resizeObserver.observe(document.body)
return () => {
resizeObserver.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div
className="flex w-6 xs:w-10 mr-2 mb-2 flex-shrink-0 items-center relative typebot-avatar-container "
ref={scrollingSideGroupRef}
>
<CSSTransition
classNames="bubble"
timeout={500}
in={show && keepShowing}
unmountOnExit
>
<div
className="absolute w-6 xs:w-10 h-6 xs:h-10 mb-4 xs:mb-2 flex items-center top-0"
ref={avatarContainer}
style={{
top: `${avatarTopOffset}px`,
transition: 'top 350ms ease-out, opacity 500ms',
}}
>
<Avatar avatarSrc={hostAvatarSrc} />
</div>
</CSSTransition>
</div>
)
} }
) const scrollingSideGroupRef = useRef<HTMLDivElement>(null)
const avatarContainer = useRef<HTMLDivElement>(null)
useImperativeHandle(ref, () => ({
refreshTopOffset,
}))
useEffect(() => {
if (!document) return
setShow(true)
const resizeObserver = new ResizeObserver(refreshTopOffset)
resizeObserver.observe(document.body)
return () => {
resizeObserver.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div
className="flex w-6 xs:w-10 mr-2 mb-2 flex-shrink-0 items-center relative typebot-avatar-container "
ref={scrollingSideGroupRef}
>
<CSSTransition
classNames="bubble"
timeout={500}
in={show && keepShowing}
unmountOnExit
>
<div
className="absolute w-6 xs:w-10 h-6 xs:h-10 mb-4 xs:mb-2 flex items-center top-0"
ref={avatarContainer}
style={{
top: `${avatarTopOffset}px`,
transition: 'top 350ms ease-out, opacity 500ms',
}}
>
<Avatar avatarSrc={hostAvatarSrc} />
</div>
</CSSTransition>
</div>
)
})

View File

@ -53,7 +53,7 @@ export const InputChatBlock = ({
blockId: block.id, blockId: block.id,
groupId: block.groupId, groupId: block.groupId,
content: value, content: value,
variableId: variableId ?? null, variableId,
uploadedFiles: block.type === InputBlockType.FILE, uploadedFiles: block.type === InputBlockType.FILE,
}) })
if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry) if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry)

View File

@ -246,7 +246,6 @@ const ChatChunks = ({
}: Props) => { }: Props) => {
const [isSkipped, setIsSkipped] = useState(false) const [isSkipped, setIsSkipped] = useState(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const avatarSideContainerRef = useRef<any>() const avatarSideContainerRef = useRef<any>()
useEffect(() => { useEffect(() => {

View File

@ -8,6 +8,7 @@ import { ConversationContainer } from './ConversationContainer'
import { AnswersProvider } from '../providers/AnswersProvider' import { AnswersProvider } from '../providers/AnswersProvider'
import { import {
Answer, Answer,
AnswerInput,
BackgroundType, BackgroundType,
Edge, Edge,
PublicTypebot, PublicTypebot,
@ -27,7 +28,9 @@ export type TypebotViewerProps = {
startGroupId?: string startGroupId?: string
isLoading?: boolean isLoading?: boolean
onNewGroupVisible?: (edge: Edge) => void onNewGroupVisible?: (edge: Edge) => void
onNewAnswer?: (answer: Answer & { uploadedFiles: boolean }) => Promise<void> onNewAnswer?: (
answer: AnswerInput & { uploadedFiles: boolean }
) => Promise<void>
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
onCompleted?: () => void onCompleted?: () => void
onVariablesUpdated?: (variables: VariableWithValue[]) => void onVariablesUpdated?: (variables: VariableWithValue[]) => void
@ -58,7 +61,7 @@ export const TypebotViewer = ({
const handleNewGroupVisible = (edge: Edge) => const handleNewGroupVisible = (edge: Edge) =>
onNewGroupVisible && onNewGroupVisible(edge) onNewGroupVisible && onNewGroupVisible(edge)
const handleNewAnswer = (answer: Answer & { uploadedFiles: boolean }) => const handleNewAnswer = (answer: AnswerInput & { uploadedFiles: boolean }) =>
onNewAnswer && onNewAnswer(answer) onNewAnswer && onNewAnswer(answer)
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>

View File

@ -5,21 +5,19 @@ type ShortTextInputProps = {
onChange: (value: string) => void onChange: (value: string) => void
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> } & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>
export const ShortTextInput = React.forwardRef( export const ShortTextInput = React.forwardRef(function ShortTextInput(
( { onChange, ...props }: ShortTextInputProps,
{ onChange, ...props }: ShortTextInputProps, ref: React.ForwardedRef<HTMLInputElement>
ref: React.ForwardedRef<HTMLInputElement> ) {
) => { return (
return ( <input
<input ref={ref}
ref={ref} className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input" type="text"
type="text" style={{ fontSize: '16px' }}
style={{ fontSize: '16px' }} autoFocus={!isMobile}
autoFocus={!isMobile} onChange={(e) => onChange(e.target.value)}
onChange={(e) => onChange(e.target.value)} {...props}
{...props} />
/> )
) })
}
)

View File

@ -5,11 +5,11 @@ type TextareaProps = {
onChange: (value: string) => void onChange: (value: string) => void
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> } & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'>
export const Textarea = React.forwardRef( export const Textarea = React.forwardRef(function Textarea(
( { onChange, ...props }: TextareaProps,
{ onChange, ...props }: TextareaProps, ref: React.ForwardedRef<HTMLTextAreaElement>
ref: React.ForwardedRef<HTMLTextAreaElement> ) {
) => ( return (
<textarea <textarea
ref={ref} ref={ref}
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input" className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
@ -22,4 +22,4 @@ export const Textarea = React.forwardRef(
{...props} {...props}
/> />
) )
) })

View File

@ -9,6 +9,7 @@ import { parseVariables } from '@/features/variables'
import { useChat } from '@/providers/ChatProvider' import { useChat } from '@/providers/ChatProvider'
import { useTypebot } from '@/providers/TypebotProvider' import { useTypebot } from '@/providers/TypebotProvider'
import { createPaymentIntentQuery } from '../../queries/createPaymentIntentQuery' import { createPaymentIntentQuery } from '../../queries/createPaymentIntentQuery'
import { Stripe } from '@stripe/stripe-js'
type Props = { type Props = {
options: PaymentInputOptions options: PaymentInputOptions
@ -23,8 +24,7 @@ export const StripePaymentForm = ({ options, onSuccess }: Props) => {
onNewLog, onNewLog,
} = useTypebot() } = useTypebot()
const { window: frameWindow, document: frameDocument } = useFrame() const { window: frameWindow, document: frameDocument } = useFrame()
// eslint-disable-next-line @typescript-eslint/no-explicit-any const [stripe, setStripe] = useState<Stripe | null>(null)
const [stripe, setStripe] = useState<any>(null)
const [clientSecret, setClientSecret] = useState('') const [clientSecret, setClientSecret] = useState('')
const [amountLabel, setAmountLabel] = useState('') const [amountLabel, setAmountLabel] = useState('')

View File

@ -19,7 +19,6 @@ export const PhoneInput = ({
hasGuestAvatar, hasGuestAvatar,
}: PhoneInputProps) => { }: PhoneInputProps) => {
const [inputValue, setInputValue] = useState(defaultValue ?? '') const [inputValue, setInputValue] = useState(defaultValue ?? '')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const inputRef = useRef<any>(null) const inputRef = useRef<any>(null)
const handleChange = (inputValue: Value | undefined) => const handleChange = (inputValue: Value | undefined) =>

View File

@ -1,7 +1,14 @@
import { GoogleAnalyticsOptions } from 'models' import { GoogleAnalyticsOptions } from 'models'
// eslint-disable-next-line @typescript-eslint/no-explicit-any declare const gtag: (
declare const gtag: any type: string,
action: string | undefined,
options: {
event_category: string | undefined
event_label: string | undefined
value: number | undefined
}
) => void
const initGoogleAnalytics = (id: string): Promise<void> => const initGoogleAnalytics = (id: string): Promise<void> =>
new Promise((resolve) => { new Promise((resolve) => {

View File

@ -1,20 +1,20 @@
import { safeStringify } from '@/features/variables' import { safeStringify } from '@/features/variables'
import { import {
Answer, Answer,
AnswerInput,
ResultValues, ResultValues,
VariableWithUnknowValue, VariableWithUnknowValue,
VariableWithValue, VariableWithValue,
} from 'models' } from 'models'
import React, { createContext, ReactNode, useContext, useState } from 'react' import { createContext, ReactNode, useContext, useState } from 'react'
const answersContext = createContext<{ const answersContext = createContext<{
resultId?: string resultId?: string
resultValues: ResultValues resultValues: ResultValues
addAnswer: ( addAnswer: (
answer: Answer & { uploadedFiles: boolean } answer: AnswerInput & { uploadedFiles: boolean }
) => Promise<void> | undefined ) => Promise<void> | undefined
updateVariables: (variables: VariableWithUnknowValue[]) => void updateVariables: (variables: VariableWithUnknowValue[]) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})
@ -26,7 +26,7 @@ export const AnswersProvider = ({
}: { }: {
resultId?: string resultId?: string
onNewAnswer: ( onNewAnswer: (
answer: Answer & { uploadedFiles: boolean } answer: AnswerInput & { uploadedFiles: boolean }
) => Promise<void> | undefined ) => Promise<void> | undefined
onVariablesUpdated?: (variables: VariableWithValue[]) => void onVariablesUpdated?: (variables: VariableWithValue[]) => void
children: ReactNode children: ReactNode
@ -34,10 +34,10 @@ export const AnswersProvider = ({
const [resultValues, setResultValues] = useState<ResultValues>({ const [resultValues, setResultValues] = useState<ResultValues>({
answers: [], answers: [],
variables: [], variables: [],
createdAt: new Date().toISOString(), createdAt: new Date(),
}) })
const addAnswer = (answer: Answer & { uploadedFiles: boolean }) => { const addAnswer = (answer: AnswerInput & { uploadedFiles: boolean }) => {
setResultValues((resultValues) => ({ setResultValues((resultValues) => ({
...resultValues, ...resultValues,
answers: [...resultValues.answers, answer], answers: [...resultValues.answers, answer],

View File

@ -2,7 +2,6 @@ import React, { createContext, ReactNode, useContext } from 'react'
const chatContext = createContext<{ const chatContext = createContext<{
scroll: () => void scroll: () => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -39,7 +39,6 @@ const typebotContext = createContext<{
edgeId: string edgeId: string
}) => void }) => void
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -1,24 +1,10 @@
{ {
"extends": "tsconfig/react-library.json",
"include": ["src/**/*"],
"compilerOptions": { "compilerOptions": {
"lib": ["ES2017", "DOM"],
"target": "es5",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDeclarationOnly": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, }
"downlevelIteration": true
} }
} }

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'baptistearno-tsup' import { defineConfig } from 'tsup'
export default defineConfig((options) => ({ export default defineConfig((options) => ({
entry: ['src/index.ts'], entry: ['src/index.ts'],

View File

@ -1,12 +0,0 @@
{
"name": "configs",
"version": "1.0.0",
"license": "AGPL-3.0-or-later",
"private": true,
"devDependencies": {
"@playwright/test": "1.27.1",
"@types/node": "18.11.9",
"dotenv": "16.0.3",
"utils": "workspace:*"
}
}

View File

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

View File

@ -1,6 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true
}
}

View File

@ -19,9 +19,9 @@
"@prisma/client": "4.6.1" "@prisma/client": "4.6.1"
}, },
"devDependencies": { "devDependencies": {
"prisma": "4.6.1",
"typescript": "4.8.4",
"dotenv-cli": "6.0.0", "dotenv-cli": "6.0.0",
"tsconfig": "workspace:*" "prisma": "4.6.1",
"tsconfig": "workspace:*",
"typescript": "4.8.4"
} }
} }

View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['custom'],
}

View File

@ -2,12 +2,13 @@
"name": "emails", "name": "emails",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "./index.ts", "main": "./src/index.ts",
"types": "./index.ts", "types": "./src/index.ts",
"scripts": { "scripts": {
"preview": "concurrently \"pnpm run watch\" \"sleep 5 && pnpm run serve\" -n \"watch,serve\" -c \"bgBlue.bold,bgMagenta.bold\"", "preview": "concurrently \"pnpm run watch\" \"sleep 5 && pnpm run serve\" -n \"watch,serve\" -c \"bgBlue.bold,bgMagenta.bold\"",
"watch": "tsx watch ./preview.tsx --clear-screen=false", "watch": "tsx watch ./preview.tsx --clear-screen=false",
"serve": "http-server dist -a localhost -p 3223 -o" "serve": "http-server dist -a localhost -p 3223 -o",
"lint": "eslint \"src/**/*.ts*\""
}, },
"keywords": [], "keywords": [],
"author": "Baptiste Arnaud", "author": "Baptiste Arnaud",
@ -22,7 +23,10 @@
"nodemailer": "6.8.0", "nodemailer": "6.8.0",
"react": "18.2.0", "react": "18.2.0",
"tsx": "3.12.1", "tsx": "3.12.1",
"utils": "workspace:*" "utils": "workspace:*",
"eslint": "8.28.0",
"eslint-config-custom": "workspace:*",
"tsconfig": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
"@faire/mjml-react": "2.1.4", "@faire/mjml-react": "2.1.4",

View File

@ -42,15 +42,15 @@ export const AlmostReachedChatsLimitEmail = ({
</MjmlSection> </MjmlSection>
<MjmlSection padding="0 24px" cssClass="smooth"> <MjmlSection padding="0 24px" cssClass="smooth">
<MjmlColumn> <MjmlColumn>
<Text>Your bots are chatting a lot. That's amazing. 💙</Text> <Text>Your bots are chatting a lot. That&apos;s amazing. 💙</Text>
<Text> <Text>
This means you've almost reached your monthly chats limit. You This means you&apos;ve almost reached your monthly chats limit.
currently reached 80% of {readableChatsLimit} chats. You currently reached 80% of {readableChatsLimit} chats.
</Text> </Text>
<Text>This limit will be reset on {readableResetDate}.</Text> <Text>This limit will be reset on {readableResetDate}.</Text>
<Text fontWeight={800}> <Text fontWeight={800}>
Your bots won't start the chat if you reach the limit before this Your bots won&apos;t start the chat if you reach the limit before
date this date
</Text> </Text>
<Text> <Text>
If you need more monthly responses, you will need to upgrade your If you need more monthly responses, you will need to upgrade your

View File

@ -33,17 +33,18 @@ export const AlmostReachedStorageLimitEmail = ({
</MjmlSection> </MjmlSection>
<MjmlSection padding="0 24px" cssClass="smooth"> <MjmlSection padding="0 24px" cssClass="smooth">
<MjmlColumn> <MjmlColumn>
<Text>Your bots are working a lot. That's amazing. 🤖</Text> <Text>Your bots are working a lot. That&apos;s amazing. 🤖</Text>
<Text> <Text>
This means you've almost reached your storage limit. You currently This means you&apos;ve almost reached your storage limit. You
reached 80% of your {readableStorageLimit} storage limit. currently reached 80% of your {readableStorageLimit} storage
limit.
</Text> </Text>
<Text fontWeight={800}> <Text fontWeight={800}>
Your bots won't collect new files once you reach the limit Your bots won&apos;t collect new files once you reach the limit
</Text> </Text>
<Text> <Text>
To make sure it won't happen, you need to upgrade your plan or To make sure it won&apos;t happen, you need to upgrade your plan
delete existing results to free up space. or delete existing results to free up space.
</Text> </Text>
<MjmlSpacer height="24px" /> <MjmlSpacer height="24px" />
<Button link={url}>Upgrade workspace</Button> <Button link={url}>Upgrade workspace</Button>

View File

@ -42,7 +42,7 @@ export const GuestInvitationEmail = ({
</Text> </Text>
<Text> <Text>
From now on you will see this typebot in your dashboard under his From now on you will see this typebot in your dashboard under his
workspace "{workspaceName}" 👍 workspace &quot;{workspaceName}&quot; 👍
</Text> </Text>
<Text> <Text>
Make sure to log in as <i>{guestEmail}</i>. Make sure to log in as <i>{guestEmail}</i>.

View File

@ -43,15 +43,15 @@ export const ReachedChatsLimitEmail = ({
<MjmlSection padding="0 24px" cssClass="smooth"> <MjmlSection padding="0 24px" cssClass="smooth">
<MjmlColumn> <MjmlColumn>
<Text> <Text>
It just happened, you've reached your monthly {readableChatsLimit}{' '} It just happened, you&apos;ve reached your monthly{' '}
chats limit 😮 {readableChatsLimit} chats limit 😮
</Text> </Text>
<Text fontWeight={800}> <Text fontWeight={800}>
It means your bots are closed until {readableResetDate} It means your bots are closed until {readableResetDate}
</Text> </Text>
<Text> <Text>
If you'd like to continue chatting with your users this month, If you&apos;d like to continue chatting with your users this
then you need to upgrade your plan. 🚀 month, then you need to upgrade your plan. 🚀
</Text> </Text>
<MjmlSpacer height="24px" /> <MjmlSpacer height="24px" />

View File

@ -34,14 +34,14 @@ export const ReachedStorageLimitEmail = ({
<MjmlSection padding="0 24px" cssClass="smooth"> <MjmlSection padding="0 24px" cssClass="smooth">
<MjmlColumn> <MjmlColumn>
<Text> <Text>
It just happened, you've reached your {readableStorageLimit}{' '} It just happened, you&apos;ve reached your {readableStorageLimit}{' '}
storage limit 😮 storage limit 😮
</Text> </Text>
<Text fontWeight={800}> <Text fontWeight={800}>
It means your bots won't collect new files from your users It means your bots won&apos;t collect new files from your users
</Text> </Text>
<Text> <Text>
If you'd like to continue collecting files, then you need to If you&apos;d like to continue collecting files, then you need to
upgrade your plan or remove existing results to free up space. 🚀 upgrade your plan or remove existing results to free up space. 🚀
</Text> </Text>

View File

@ -1,6 +1,5 @@
{ {
"compilerOptions": { "extends": "tsconfig/react-library.json",
"jsx": "react", "include": ["src/**/*"],
"esModuleInterop": true "exclude": ["dist", "node_modules"]
}
} }

Some files were not shown because too many files have changed in this diff Show More