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 = {
ignorePatterns: ['node_modules'],
env: {
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',
],
},
],
},
root: true,
extends: ['custom'],
}

View File

@ -106,20 +106,16 @@
"@types/qs": "6.9.7",
"@types/react": "18.0.25",
"@types/tinycolor2": "1.4.3",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"configs": "workspace:*",
"db": "workspace:*",
"dotenv": "16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"eslint-plugin-react": "7.31.10",
"models": "workspace:*",
"next-transpile-modules": "10.0.0",
"superjson": "^1.11.0",
"tsconfig": "workspace:*",
"typescript": "4.8.4",
"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 path from 'path'
import { playwrightBaseConfig } from 'configs/playwright'
import { playwrightBaseConfig } from 'utils/playwright/baseConfig'
const config: PlaywrightTestConfig = {
...playwrightBaseConfig,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,12 +47,11 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
)
}
const DraggableGroupNode = memo(
({
const NonMemoizedDraggableGroupNode = ({
group,
groupIndex,
onGroupDrag,
}: Props & { onGroupDrag: (newCoord: Coordinates) => void }) => {
}: Props & { onGroupDrag: (newCoord: Coordinates) => void }) => {
const {
connectingIds,
setConnectingIds,
@ -131,8 +130,7 @@ const DraggableGroupNode = memo(
const handleMouseLeave = () => {
if (isReadOnly) return
setMouseOverGroup(undefined)
if (connectingIds)
setConnectingIds({ ...connectingIds, target: undefined })
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
}
const startPreviewAtThisGroup = () => {
@ -239,5 +237,6 @@ const DraggableGroupNode = memo(
)}
</ContextMenu>
)
}
)
}
export const DraggableGroupNode = memo(NonMemoizedDraggableGroupNode)

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import {
} from '@chakra-ui/react'
import { ToolIcon, EyeIcon, EyeOffIcon, GripIcon } from '@/components/icons'
import { ResultHeaderCell } from 'models'
import React, { forwardRef, useState } from 'react'
import React, { useState } from 'react'
import { isNotDefined } from 'utils'
import {
DndContext,
@ -128,7 +128,7 @@ export const ColumnSettingsButton = ({
</SortableContext>
<Portal>
<DragOverlay dropAnimation={{ duration: 0 }}>
{draggingColumnId ? <SortableColumnOverlay /> : null}
{draggingColumnId ? <Flex /> : null}
</DragOverlay>
</Portal>
</DndContext>
@ -210,9 +210,3 @@ const SortableColumns = ({
</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,
Button,
chakra,
Checkbox,
Flex,
HStack,
Stack,
@ -26,6 +25,7 @@ import { Row } from './Row'
import { HeaderRow } from './HeaderRow'
import { CellValueType, TableData } from '../../types'
import { HeaderIcon } from '../../utils'
import { IndeterminateCheckbox } from './IndeterminateCheckbox'
type ResultsTableProps = {
resultHeader: ResultHeaderCell[]
@ -238,23 +238,3 @@ export const ResultsTable = ({
</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>
deleteCurrentWorkspace: () => Promise<void>
refreshWorkspace: (expectedUpdates: Partial<Workspace>) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})

View File

@ -88,7 +88,7 @@ const DeleteWorkspaceButton = ({
message={
<Text>
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>
}
confirmButtonLabel="Delete"

View File

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

View File

@ -1,4 +1,4 @@
import NextErrorComponent from 'next/error'
import NextErrorComponent, { ErrorProps } from 'next/error'
import * as Sentry from '@sentry/nextjs'
import { NextPageContext } from 'next'
@ -24,14 +24,14 @@ const MyError = ({
}
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
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
errorInitialProps.hasGetInitialPropsRun = true
// 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")
const response = await drive({
version: 'v3',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
auth: auth.client,
}).files.list({
q: "mimeType='application/vnd.google-apps.spreadsheet'",

View File

@ -128,5 +128,4 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
return methodNotAllowed(res)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-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) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const seconds = Math.floor((new Date() - new Date(date)) / 1000)
const seconds = Math.floor(
(new Date().getTime() - new Date(date).getTime()) / 1000
)
let interval = seconds / 31536000

View File

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

View File

@ -1,29 +1,4 @@
module.exports = {
ignorePatterns: ['node_modules'],
env: {
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],
},
root: true,
extends: ['custom'],
}

View File

@ -38,7 +38,7 @@ export const EasyEmbed = () => {
>
Embedding your typebot in your applications is a walk in the park.
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>
<Flex data-aos="fade">
<Button

View File

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

View File

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

View File

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

View File

@ -21,12 +21,12 @@ const AboutPage = () => {
textAlign="justify"
>
<Flex w="full">
<Heading as="h1">Typebot's story</Heading>
<Heading as="h1">Typebot&apos;s story</Heading>
</Flex>
<Text>
Typebot's team is composed of only me, Baptiste Arnaud, a Software
Engineer based in France.
Typebot&apos;s team is composed of only me, Baptiste Arnaud, a
Software Engineer based in France.
</Text>
<Flex w="full" justify="center">
<Box as="figure" maxW="200px">
@ -35,13 +35,13 @@ const AboutPage = () => {
</Flex>
<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.
</Text>
<Text>
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.
</Text>
<Text>
@ -55,15 +55,16 @@ const AboutPage = () => {
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
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>
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 />
<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
about the product. If you want to remove it and also have access to
other advanced features, you have to subscribe for $39 per month.
&quot;Made with Typebot&quot; small badge that potentially gets people
to know about the product. If you want to remove it and also have
access to other advanced features, you have to subscribe for $39 per
month.
</Text>
<Text>
If you have any questions, feel free to reach out to me at{' '}

View File

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

View File

@ -1,22 +1,10 @@
{
"extends": "tsconfig/nextjs.json",
"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": ".",
"composite": true
"paths": {
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View File

@ -1,43 +1,4 @@
module.exports = {
ignorePatterns: ['node_modules'],
env: {
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',
],
},
],
},
root: true,
extends: ['custom'],
}

View File

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

View File

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

View File

@ -1,5 +1,11 @@
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 React, { useEffect, useState } from 'react'
import { isDefined, isNotDefined } from 'utils'
@ -106,7 +112,7 @@ export const TypebotPage = ({
}
const handleNewAnswer = async (
answer: Answer & { uploadedFiles: boolean }
answer: AnswerInput & { uploadedFiles: boolean }
) => {
if (!resultId) return setError(new Error('Error: result was not created'))
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'
export const upsertAnswerQuery = async (
answer: Answer & { resultId: string } & { uploadedFiles?: boolean }
answer: AnswerInput & { resultId: string } & { uploadedFiles?: boolean }
) =>
sendRequest<Answer>({
url: `/api/typebots/t/results/r/answers`,

View File

@ -151,22 +151,30 @@ test('Should correctly parse metadata', async ({ page }) => {
).toBe(customMetadata.title)
expect(
await page.evaluate(
() => (document.querySelector('meta[name="description"]') as any).content
() =>
(document.querySelector('meta[name="description"]') as HTMLMetaElement)
.content
)
).toBe(customMetadata.description)
expect(
await page.evaluate(
() => (document.querySelector('meta[property="og:image"]') as any).content
() =>
(document.querySelector('meta[property="og:image"]') as HTMLMetaElement)
.content
)
).toBe(customMetadata.imageUrl)
expect(
await page.evaluate(() =>
(document.querySelector('link[rel="icon"]') as any).getAttribute('href')
(
document.querySelector('link[rel="icon"]') as HTMLLinkElement
).getAttribute('href')
)
).toBe(customMetadata.favIconUrl)
expect(
await page.evaluate(
() => (document.querySelector('meta[name="author"]') as any).content
() =>
(document.querySelector('meta[name="author"]') as HTMLMetaElement)
.content
)
).toBe('John Doe')
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 { NextPageContext } from 'next'
@ -24,14 +24,14 @@ const MyError = ({
}
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
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
errorInitialProps.hasGetInitialPropsRun = true
// Returning early because we don't want to log 404 errors to Sentry.

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ const defaultTransportOptions = {
const defaultFrom = {
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) => {
@ -54,9 +54,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
} = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as SendEmailOptions & {
resultValues: Omit<ResultValues, 'createdAt'> & {
createdAt: string
}
resultValues: ResultValues
fileUrls?: string
}
const { name: replyToName } = parseEmailRecipient(replyTo)
@ -166,9 +164,7 @@ const getEmailBody = async ({
resultValues,
}: {
typebotId: string
resultValues: Omit<ResultValues, 'createdAt'> & {
createdAt: string
}
resultValues: ResultValues
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined
> => {

View File

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

View File

@ -1,26 +1,10 @@
{
"extends": "tsconfig/nextjs.json",
"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": ".",
"paths": {
"@/*": ["src/*"]
}
},
"composite": true,
"downlevelIteration": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View File

@ -20,10 +20,13 @@
"test:viewer": "cd apps/viewer && pnpm test",
"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",
"generate-change-log": "pnpx gitmoji-changelog"
"generate-change-log": "pnpx gitmoji-changelog",
"lint": "turbo run lint",
"prepare": "husky install"
},
"devDependencies": {
"cz-emoji": "1.3.2-canary.2",
"husky": "^8.0.2",
"turbo": "1.6.3"
},
"config": {
@ -31,5 +34,5 @@
"path": "node_modules/cz-emoji"
}
},
"packageManager": "pnpm@7.16.1"
"packageManager": "pnpm@7.17.0"
}

View File

@ -1,48 +1,8 @@
module.exports = {
ignorePatterns: ['node_modules'],
env: {
browser: true,
es6: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'next/core-web-vitals',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
plugins: ['prettier', 'react', '@typescript-eslint'],
ignorePatterns: 'dist',
root: true,
extends: ['custom'],
rules: {
'react/no-unescaped-entities': [0],
'prettier/prettier': 'error',
'react/display-name': [0],
'@next/next/no-img-element': [0],
'no-restricted-imports': [
'error',
{
patterns: [
'*/src/*',
'src/*',
'*/src',
'@/features/*/*',
'@/index',
'!@/features/blocks/*',
'!@/features/*/api',
],
},
],
'@next/next/no-img-element': 'off',
'@next/next/no-html-link-for-pages': 'off',
},
}

View File

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

View File

@ -13,8 +13,10 @@ import { ResizeObserver } from 'resize-observer'
type Props = { hostAvatarSrc?: string; keepShowing: boolean }
export const AvatarSideContainer = forwardRef(
({ hostAvatarSrc, keepShowing }: Props, ref: ForwardedRef<unknown>) => {
export const AvatarSideContainer = forwardRef(function AvatarSideContainer(
{ hostAvatarSrc, keepShowing }: Props,
ref: ForwardedRef<unknown>
) {
const { document } = useFrame()
const [show, setShow] = useState(false)
const [avatarTopOffset, setAvatarTopOffset] = useState(0)
@ -67,5 +69,4 @@ export const AvatarSideContainer = forwardRef(
</CSSTransition>
</div>
)
}
)
})

View File

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

View File

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

View File

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

View File

@ -5,11 +5,10 @@ type ShortTextInputProps = {
onChange: (value: string) => void
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>
export const ShortTextInput = React.forwardRef(
(
export const ShortTextInput = React.forwardRef(function ShortTextInput(
{ onChange, ...props }: ShortTextInputProps,
ref: React.ForwardedRef<HTMLInputElement>
) => {
) {
return (
<input
ref={ref}
@ -21,5 +20,4 @@ export const ShortTextInput = React.forwardRef(
{...props}
/>
)
}
)
})

View File

@ -5,11 +5,11 @@ type TextareaProps = {
onChange: (value: string) => void
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'>
export const Textarea = React.forwardRef(
(
export const Textarea = React.forwardRef(function Textarea(
{ onChange, ...props }: TextareaProps,
ref: React.ForwardedRef<HTMLTextAreaElement>
) => (
) {
return (
<textarea
ref={ref}
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}
/>
)
)
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,10 @@
{
"extends": "tsconfig/react-library.json",
"include": ["src/**/*"],
"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": ".",
"paths": {
"@/*": ["src/*"]
},
"downlevelIteration": true
}
}
}

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'baptistearno-tsup'
import { defineConfig } from 'tsup'
export default defineConfig((options) => ({
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"
},
"devDependencies": {
"prisma": "4.6.1",
"typescript": "4.8.4",
"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",
"version": "1.0.0",
"description": "",
"main": "./index.ts",
"types": "./index.ts",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"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",
"serve": "http-server dist -a localhost -p 3223 -o"
"serve": "http-server dist -a localhost -p 3223 -o",
"lint": "eslint \"src/**/*.ts*\""
},
"keywords": [],
"author": "Baptiste Arnaud",
@ -22,7 +23,10 @@
"nodemailer": "6.8.0",
"react": "18.2.0",
"tsx": "3.12.1",
"utils": "workspace:*"
"utils": "workspace:*",
"eslint": "8.28.0",
"eslint-config-custom": "workspace:*",
"tsconfig": "workspace:*"
},
"peerDependencies": {
"@faire/mjml-react": "2.1.4",

View File

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

View File

@ -33,17 +33,18 @@ export const AlmostReachedStorageLimitEmail = ({
</MjmlSection>
<MjmlSection padding="0 24px" cssClass="smooth">
<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>
This means you've almost reached your storage limit. You currently
reached 80% of your {readableStorageLimit} storage limit.
This means you&apos;ve almost reached your storage limit. You
currently reached 80% of your {readableStorageLimit} storage
limit.
</Text>
<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>
To make sure it won't happen, you need to upgrade your plan or
delete existing results to free up space.
To make sure it won&apos;t happen, you need to upgrade your plan
or delete existing results to free up space.
</Text>
<MjmlSpacer height="24px" />
<Button link={url}>Upgrade workspace</Button>

View File

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

View File

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

View File

@ -34,14 +34,14 @@ export const ReachedStorageLimitEmail = ({
<MjmlSection padding="0 24px" cssClass="smooth">
<MjmlColumn>
<Text>
It just happened, you've reached your {readableStorageLimit}{' '}
It just happened, you&apos;ve reached your {readableStorageLimit}{' '}
storage limit 😮
</Text>
<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>
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. 🚀
</Text>

View File

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

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