-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source. For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code. There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
- .
+# Functional Source License, Version 1.1, Apache 2.0 Future License
+
+## Abbreviation
+
+FSL-1.1-Apache-2.0
+
+## Notice
+
+Copyright 2023-2024 Typebot
+
+## Terms and Conditions
+
+### Licensor ("We")
+
+The party offering the Software under these Terms and Conditions.
+
+### The Software
+
+The "Software" is each version of the software that we make available under
+these Terms and Conditions, as indicated by our inclusion of these Terms and
+Conditions with the Software.
+
+### License Grant
+
+Subject to your compliance with this License Grant and the Patents,
+Redistribution and Trademark clauses below, we hereby grant you the right to
+use, copy, modify, create derivative works, publicly perform, publicly display
+and redistribute the Software for any Permitted Purpose identified below.
+
+### Permitted Purpose
+
+A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
+means making the Software available to others in a commercial product or
+service that:
+
+1. substitutes for the Software;
+
+2. substitutes for any other product or service we offer using the Software
+ that exists as of the date we make the Software available; or
+
+3. offers the same or substantially similar functionality as the Software.
+
+Permitted Purposes specifically include using the Software:
+
+1. for your internal use and access;
+
+2. for non-commercial education;
+
+3. for non-commercial research; and
+
+4. in connection with professional services that you provide to a licensee
+ using the Software in accordance with these Terms and Conditions.
+
+### Patents
+
+To the extent your use for a Permitted Purpose would necessarily infringe our
+patents, the license grant above includes a license under our patents. If you
+make a claim against any party that the Software infringes or contributes to
+the infringement of any patent, then your patent license to the Software ends
+immediately.
+
+### Redistribution
+
+The Terms and Conditions apply to all copies, modifications and derivatives of
+the Software.
+
+If you redistribute any copies, modifications or derivatives of the Software,
+you must include a copy of or a link to these Terms and Conditions and not
+remove any copyright notices provided in or with the Software.
+
+### Disclaimer
+
+THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
+PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
+
+IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
+SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
+EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
+
+### Trademarks
+
+Except for displaying the License Details and identifying us as the origin of
+the Software, you have no right under these Terms and Conditions to use our
+trademarks, trade names, service marks or product names.
+
+## Grant of Future License
+
+We hereby irrevocably grant you an additional license to use the Software under
+the Apache License, Version 2.0 that is effective on the second anniversary of
+the date we make the Software available. On or after that date, you may use the
+Software under the Apache License, Version 2.0, in which case the following
+will apply:
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License.
+
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
diff --git a/README.md b/README.md
index 087f3c14c2..5ff3a07b4d 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-Typebot is an open-source chatbot builder. It allows you to create advanced chatbots visually, embed them anywhere on your web/mobile apps, and collect results in real-time
+Typebot is an Fair Source chatbot builder. It allows you to create advanced chatbots visually, embed them anywhere on your web/mobile apps, and collect results in real-time
@@ -79,7 +79,7 @@ Built for **developers**:
The easiest way to get started with Typebot is with [the official managed service in the Cloud](https://app.typebot.io). You'll have high availability, backups, security, and maintenance all managed for you by me, [Baptiste, Typebot's founder](https://twitter.com/baptisteArno).
The cloud version can save a substantial amount of developer time and resources. For most sites this ends up being the best value option and the revenue goes to funding the maintenance and further development of Typebot.
-So you’ll be supporting open source software and getting a great service! 💙
+So you’ll be supporting fair source software and getting a great service! 💙
## Support & Community
@@ -110,4 +110,4 @@ Made with [contrib.rocks](https://contrib.rocks).
## License
-Most of Typebot's code is open-source under the GNU Affero General Public License Version 3 (AGPLv3). You will find more information about the license and how to comply with it [here](https://docs.typebot.io/self-hosting#license-requirements).
+Typebot's code is protected under a Functional Source License. You will find more information about the license and how to comply with it [here](https://docs.typebot.io/self-hosting#license-requirements).
diff --git a/apps/builder/.eslintignore b/apps/builder/.eslintignore
deleted file mode 100644
index ebd9b0fa7c..0000000000
--- a/apps/builder/.eslintignore
+++ /dev/null
@@ -1 +0,0 @@
-src/test/reporters
\ No newline at end of file
diff --git a/apps/builder/.eslintrc.js b/apps/builder/.eslintrc.js
deleted file mode 100644
index b56159ea9c..0000000000
--- a/apps/builder/.eslintrc.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- root: true,
- extends: ['custom'],
-}
diff --git a/apps/builder/next-env.d.ts b/apps/builder/next-env.d.ts
index 4f11a03dc6..a4a7b3f5cf 100644
--- a/apps/builder/next-env.d.ts
+++ b/apps/builder/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
diff --git a/apps/builder/next.config.mjs b/apps/builder/next.config.mjs
index 896f5b5704..f5b5e85585 100644
--- a/apps/builder/next.config.mjs
+++ b/apps/builder/next.config.mjs
@@ -1,99 +1,97 @@
-import { withSentryConfig } from '@sentry/nextjs'
-import { join, dirname } from 'path'
-import '@typebot.io/env/dist/env.mjs'
-import { configureRuntimeEnv } from 'next-runtime-env/build/configure.js'
-import { fileURLToPath } from 'url'
+import { dirname, join } from "path";
+import { withSentryConfig } from "@sentry/nextjs";
+import "@typebot.io/env/compiled";
+import { fileURLToPath } from "url";
+import { configureRuntimeEnv } from "next-runtime-env/build/configure.js";
-const __filename = fileURLToPath(import.meta.url)
+const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename)
+const __dirname = dirname(__filename);
const injectViewerUrlIfVercelPreview = (val) => {
if (
- (val && typeof val === 'string' && val.length > 0) ||
- process.env.VERCEL_ENV !== 'preview' ||
+ (val && typeof val === "string" && val.length > 0) ||
+ process.env.VERCEL_ENV !== "preview" ||
!process.env.VERCEL_BUILDER_PROJECT_NAME ||
!process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME
)
- return
+ return;
process.env.NEXT_PUBLIC_VIEWER_URL =
`https://${process.env.VERCEL_BRANCH_URL}`.replace(
process.env.VERCEL_BUILDER_PROJECT_NAME,
- process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME
- )
- if (process.env.NEXT_PUBLIC_CHAT_API_URL?.includes('{{pr_id}}'))
+ process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME,
+ );
+ if (process.env.NEXT_PUBLIC_CHAT_API_URL?.includes("{{pr_id}}"))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
- '{{pr_id}}',
- process.env.VERCEL_GIT_PULL_REQUEST_ID
- )
-}
+ "{{pr_id}}",
+ process.env.VERCEL_GIT_PULL_REQUEST_ID,
+ );
+};
-injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL)
+injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL);
-configureRuntimeEnv()
+configureRuntimeEnv();
/** @type {import('next').NextConfig} */
const nextConfig = {
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ transpilePackages: ["@typebot.io/billing", "@typebot.io/blocks-bubbles"],
reactStrictMode: true,
- output: 'standalone',
- transpilePackages: [
- '@typebot.io/lib',
- '@typebot.io/schemas',
- '@typebot.io/emails',
- '@typebot.io/env',
- ],
+ output: "standalone",
i18n: {
- defaultLocale: 'en',
- locales: ['en', 'fr', 'pt', 'pt-BR', 'de', 'ro', 'es', 'it', 'el'],
+ defaultLocale: "en",
+ locales: ["en", "fr", "pt", "pt-BR", "de", "ro", "es", "it", "el"],
},
experimental: {
- outputFileTracingRoot: join(__dirname, '../../'),
- serverComponentsExternalPackages: ['isolated-vm'],
+ outputFileTracingRoot: join(__dirname, "../../"),
+ serverComponentsExternalPackages: ["isolated-vm"],
},
webpack: (config, { isServer }) => {
- if (isServer) return config
+ if (isServer) return config;
- config.resolve.alias['minio'] = false
- config.resolve.alias['qrcode'] = false
- config.resolve.alias['isolated-vm'] = false
- return config
+ config.resolve.alias["minio"] = false;
+ config.resolve.alias["qrcode"] = false;
+ config.resolve.alias["isolated-vm"] = false;
+ return config;
},
headers: async () => {
return [
{
- source: '/(.*)?',
+ source: "/(.*)?",
headers: [
{
- key: 'X-Frame-Options',
- value: 'SAMEORIGIN',
+ key: "X-Frame-Options",
+ value: "SAMEORIGIN",
},
],
},
- ]
+ ];
},
async rewrites() {
return process.env.NEXT_PUBLIC_POSTHOG_KEY
? [
{
- source: '/ingest/:path*',
+ source: "/ingest/:path*",
destination:
(process.env.NEXT_PUBLIC_POSTHOG_HOST ??
- 'https://app.posthog.com') + '/:path*',
+ "https://app.posthog.com") + "/:path*",
},
{
- source: '/healthz',
- destination: '/api/health',
+ source: "/healthz",
+ destination: "/api/health",
},
]
: [
{
- source: '/healthz',
- destination: '/api/health',
+ source: "/healthz",
+ destination: "/api/health",
},
- ]
+ ];
},
-}
+};
export default process.env.NEXT_PUBLIC_SENTRY_DSN
? withSentryConfig(
@@ -104,7 +102,7 @@ export default process.env.NEXT_PUBLIC_SENTRY_DSN
// Suppresses source map uploading logs during build
silent: true,
- release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-builder',
+ release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + "-builder",
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
},
@@ -116,13 +114,13 @@ export default process.env.NEXT_PUBLIC_SENTRY_DSN
widenClientFileUpload: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
- tunnelRoute: '/monitoring',
+ tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
- }
+ },
)
- : nextConfig
+ : nextConfig;
diff --git a/apps/builder/package.json b/apps/builder/package.json
index 83d37f0ad1..b804ed5151 100644
--- a/apps/builder/package.json
+++ b/apps/builder/package.json
@@ -1,16 +1,12 @@
{
"name": "builder",
- "version": "0.1.0",
- "license": "AGPL-3.0-or-later",
"scripts": {
"dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3000",
"build": "dotenv -e ./.env -e ../../.env -- next build",
"start": "dotenv -e ./.env -e ../../.env -- next start",
- "lint": "dotenv -e ./.env -e ../../.env -- next lint",
- "test": "dotenv -e ./.env -e ../../.env -- pnpm playwright test",
- "test:show-report": "pnpm playwright show-report src/test/reporters",
- "test:ui": "dotenv -e ./.env -e ../../.env -- pnpm playwright test --ui",
- "format:check": "prettier --check ./src --ignore-path ../../.prettierignore"
+ "test": "dotenv -e ./.env -e ../../.env -- playwright test",
+ "test:show-report": "playwright show-report src/test/reporters",
+ "test:ui": "dotenv -e ./.env -e ../../.env -- playwright test --ui"
},
"dependencies": {
"@braintree/sanitize-url": "7.0.1",
@@ -22,13 +18,10 @@
"@dnd-kit/utilities": "3.2.1",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
- "@faire/mjml-react": "3.3.0",
"@giphy/js-fetch-api": "5.0.0",
- "@giphy/js-types": "4.4.0",
- "@giphy/js-util": "5.0.0",
"@giphy/react-components": "7.1.0",
"@googleapis/drive": "8.0.0",
- "@lilyrose2798/trpc-openapi": "^1.3.9",
+ "@typebot.io/trpc-openapi": "workspace:*",
"@paralleldrive/cuid2": "2.2.1",
"@sentry/nextjs": "7.77.0",
"@tanstack/react-query": "4.29.19",
@@ -39,13 +32,17 @@
"@trpc/next": "10.40.0",
"@trpc/react-query": "10.40.0",
"@trpc/server": "10.40.0",
+ "@typebot.io/blocks-core": "workspace:*",
+ "@typebot.io/blocks-inputs": "workspace:*",
+ "@typebot.io/blocks-logic": "workspace:*",
"@typebot.io/bot-engine": "workspace:*",
+ "@typebot.io/credentials": "workspace:*",
"@typebot.io/emails": "workspace:*",
"@typebot.io/env": "workspace:*",
- "@typebot.io/js": "workspace:*",
"@typebot.io/nextjs": "workspace:*",
"@typebot.io/theme": "workspace:*",
- "@udecode/cn": "29.0.1",
+ "@typebot.io/typebot": "workspace:*",
+ "@typebot.io/whatsapp": "workspace:*",
"@udecode/plate-basic-marks": "30.5.3",
"@udecode/plate-common": "30.4.5",
"@udecode/plate-core": "30.4.5",
@@ -57,30 +54,27 @@
"@uiw/react-codemirror": "4.21.24",
"@upstash/ratelimit": "0.4.3",
"@use-gesture/react": "10.2.27",
+ "bot-engine": "workspace:*",
"browser-image-compression": "2.0.2",
"canvas-confetti": "1.6.0",
"codemirror": "6.0.1",
"deep-object-diff": "1.1.9",
"dequal": "2.0.3",
"emojilib": "3.0.10",
- "focus-visible": "5.2.0",
"framer-motion": "11.1.7",
"google-auth-library": "8.9.0",
"google-spreadsheet": "4.1.1",
"immer": "10.0.2",
- "ioredis": "^5.4.1",
- "isolated-vm": "5.0.1",
+ "ioredis": "5.4.1",
"jsonwebtoken": "9.0.1",
"ky": "1.2.4",
- "libphonenumber-js": "1.10.37",
- "micro": "10.0.1",
"micro-cors": "0.1.1",
- "next": "14.1.0",
+ "next": "14.2.13",
"next-auth": "4.22.1",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.8",
"nprogress": "0.2.0",
- "openai": "4.47.1",
+ "openai": "4.52.7",
"papaparse": "5.4.1",
"pexels": "^1.4.0",
"prettier": "2.8.8",
@@ -106,7 +100,6 @@
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@typebot.io/lib": "workspace:*",
- "@typebot.io/migrations": "workspace:*",
"@typebot.io/playwright": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"@typebot.io/radar": "workspace:*",
@@ -128,11 +121,7 @@
"@types/tinycolor2": "1.4.3",
"dotenv": "16.4.5",
"dotenv-cli": "7.4.1",
- "eslint": "8.44.0",
- "eslint-config-custom": "workspace:*",
"next-runtime-env": "1.6.2",
- "superjson": "1.12.4",
- "typescript": "5.4.5",
- "zod": "3.22.4"
+ "superjson": "1.12.4"
}
}
diff --git a/apps/builder/playwright.config.ts b/apps/builder/playwright.config.ts
index 16b8555560..d116734e01 100644
--- a/apps/builder/playwright.config.ts
+++ b/apps/builder/playwright.config.ts
@@ -1,8 +1,8 @@
-import { defineConfig, devices } from '@playwright/test'
-import { resolve } from 'path'
+import { resolve } from "path";
+import { defineConfig, devices } from "@playwright/test";
// eslint-disable-next-line @typescript-eslint/no-var-requires
-require('dotenv').config({ path: resolve(__dirname, '../../.env') })
+require("dotenv").config({ path: resolve(__dirname, "../../.env") });
export default defineConfig({
timeout: process.env.CI ? 50 * 1000 : 40 * 1000,
@@ -13,44 +13,44 @@ export default defineConfig({
workers: process.env.CI ? 1 : 4,
retries: process.env.CI ? 2 : 1,
reporter: [
- [process.env.CI ? 'github' : 'list'],
- ['html', { outputFolder: 'src/test/reporters' }],
+ [process.env.CI ? "github" : "list"],
+ ["html", { outputFolder: "src/test/reporters" }],
],
maxFailures: 10,
webServer: process.env.CI
? {
- command: 'pnpm run start',
+ command: "bun start",
timeout: 60_000,
reuseExistingServer: true,
port: 3000,
}
: undefined,
- outputDir: './src/test/results',
+ outputDir: "./src/test/results",
use: {
- trace: 'on-first-retry',
- locale: 'en-US',
+ trace: "on-first-retry",
+ locale: "en-US",
baseURL: process.env.NEXTAUTH_URL,
- storageState: './src/test/storageState.json',
- permissions: ['microphone'],
+ storageState: "./src/test/storageState.json",
+ permissions: ["microphone"],
launchOptions: {
args: [
- '--use-fake-ui-for-media-stream',
- '--use-fake-device-for-media-stream',
+ "--use-fake-ui-for-media-stream",
+ "--use-fake-device-for-media-stream",
],
},
},
projects: [
{
- name: 'setup db',
+ name: "setup db",
testMatch: /global\.setup\.ts/,
},
{
- name: 'chromium',
+ name: "chromium",
use: {
- ...devices['Desktop Chrome'],
+ ...devices["Desktop Chrome"],
viewport: { width: 1400, height: 1000 },
},
- dependencies: ['setup db'],
+ dependencies: ["setup db"],
},
],
-})
+});
diff --git a/apps/builder/public/templates/quick-carb-calculator.json b/apps/builder/public/templates/quick-carb-calculator.json
index 6ee2cb4331..7e35cffe19 100644
--- a/apps/builder/public/templates/quick-carb-calculator.json
+++ b/apps/builder/public/templates/quick-carb-calculator.json
@@ -66,7 +66,10 @@
{ "id": "omopw9oy3srowddb0o3f4xs5", "content": "Swim 🏊♂️" },
{ "id": "anpwbn8hvolyod2vsh06et68", "content": "Ride 🚴♂️" },
{ "id": "uto1ghh5c6icwygszercxybz", "content": "Run 🏃" },
- { "id": "sau0amab8nmeqfqyhhzle03v", "content": "Triathlon 🏊♂️🚴♂️🏃" },
+ {
+ "id": "sau0amab8nmeqfqyhhzle03v",
+ "content": "Triathlon 🏊♂️🚴♂️🏃"
+ },
{ "id": "tqvjhk0qmvn43h5hyc9ibdd3", "content": "Swimrun 🏊♂️🏃" },
{
"id": "mu0m5v7m55vfikbs29lpcm3h",
diff --git a/apps/builder/sentry.client.config.ts b/apps/builder/sentry.client.config.ts
index 96c8e4c9df..52d3ad2582 100644
--- a/apps/builder/sentry.client.config.ts
+++ b/apps/builder/sentry.client.config.ts
@@ -1,12 +1,12 @@
-import * as Sentry from '@sentry/nextjs'
+import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
ignoreErrors: [
- 'ResizeObserver loop limit exceeded',
- 'ResizeObserver loop completed with undelivered notifications.',
- 'ResizeObserver is not defined',
+ "ResizeObserver loop limit exceeded",
+ "ResizeObserver loop completed with undelivered notifications.",
+ "ResizeObserver is not defined",
"Can't find variable: ResizeObserver",
],
- release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-builder',
-})
+ release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + "-builder",
+});
diff --git a/apps/builder/sentry.server.config.ts b/apps/builder/sentry.server.config.ts
index a0092a1ae6..019d877eda 100644
--- a/apps/builder/sentry.server.config.ts
+++ b/apps/builder/sentry.server.config.ts
@@ -1,12 +1,12 @@
-import * as Sentry from '@sentry/nextjs'
-import prisma from '@typebot.io/lib/prisma'
+import * as Sentry from "@sentry/nextjs";
+import prisma from "@typebot.io/prisma";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
- release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-builder',
+ release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + "-builder",
integrations: [
new Sentry.Integrations.Prisma({
client: prisma,
}),
],
-})
+});
diff --git a/apps/builder/src/assets/styles/routerProgressBar.css b/apps/builder/src/assets/styles/routerProgressBar.css
index 681503a539..91d911eb35 100644
--- a/apps/builder/src/assets/styles/routerProgressBar.css
+++ b/apps/builder/src/assets/styles/routerProgressBar.css
@@ -22,8 +22,8 @@
right: 0px;
width: 100px;
height: 100%;
- box-shadow: 0 0 10px var(--chakra-colors-blue-500),
- 0 0 5px var(--chakra-colors-blue-500);
+ box-shadow: 0 0 10px var(--chakra-colors-blue-500), 0 0 5px
+ var(--chakra-colors-blue-500);
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
diff --git a/apps/builder/src/components/AlertInfo.tsx b/apps/builder/src/components/AlertInfo.tsx
index 44fcd112c9..a4dfba102c 100644
--- a/apps/builder/src/components/AlertInfo.tsx
+++ b/apps/builder/src/components/AlertInfo.tsx
@@ -1,8 +1,8 @@
-import { AlertProps, Alert, AlertIcon } from '@chakra-ui/react'
+import { Alert, AlertIcon, type AlertProps } from "@chakra-ui/react";
export const AlertInfo = (props: AlertProps) => (
{props.children}
-)
+);
diff --git a/apps/builder/src/components/ColorPicker.tsx b/apps/builder/src/components/ColorPicker.tsx
index 9fe01a6b0e..de03834af2 100644
--- a/apps/builder/src/components/ColorPicker.tsx
+++ b/apps/builder/src/components/ColorPicker.tsx
@@ -1,43 +1,43 @@
import {
+ Box,
+ Button,
+ type ButtonProps,
+ Center,
+ Input,
Popover,
- PopoverTrigger,
- PopoverContent,
PopoverArrow,
+ PopoverBody,
PopoverCloseButton,
+ PopoverContent,
PopoverHeader,
- Center,
- PopoverBody,
+ PopoverTrigger,
SimpleGrid,
- Input,
- Button,
Stack,
- ButtonProps,
- Box,
-} from '@chakra-ui/react'
-import { useTranslate } from '@tolgee/react'
-import React, { useState } from 'react'
-import tinyColor from 'tinycolor2'
-import { useDebouncedCallback } from 'use-debounce'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import React, { useState } from "react";
+import tinyColor from "tinycolor2";
+import { useDebouncedCallback } from "use-debounce";
const colorsSelection: `#${string}`[] = [
- '#666460',
- '#FFFFFF',
- '#A87964',
- '#D09C46',
- '#DE8031',
- '#598E71',
- '#4A8BB2',
- '#9B74B7',
- '#C75F96',
- '#0042DA',
-]
+ "#666460",
+ "#FFFFFF",
+ "#A87964",
+ "#D09C46",
+ "#DE8031",
+ "#598E71",
+ "#4A8BB2",
+ "#9B74B7",
+ "#C75F96",
+ "#0042DA",
+];
type Props = {
- value?: string
- defaultValue?: string
- isDisabled?: boolean
- onColorChange: (color: string) => void
-}
+ value?: string;
+ defaultValue?: string;
+ isDisabled?: boolean;
+ onColorChange: (color: string) => void;
+};
export const ColorPicker = ({
value,
@@ -45,25 +45,25 @@ export const ColorPicker = ({
isDisabled,
onColorChange,
}: Props) => {
- const { t } = useTranslate()
- const [color, setColor] = useState(defaultValue ?? '')
- const displayedValue = value ?? color
+ const { t } = useTranslate();
+ const [color, setColor] = useState(defaultValue ?? "");
+ const displayedValue = value ?? color;
const handleColorChange = (color: string) => {
- setColor(color)
- onColorChange(color)
- }
+ setColor(color);
+ onColorChange(color);
+ };
const handleClick = (color: string) => () => {
- setColor(color)
- onColorChange(color)
- }
+ setColor(color);
+ onColorChange(color);
+ };
return (
{displayedValue}
@@ -98,7 +98,7 @@ export const ColorPicker = ({
padding={0}
minWidth="unset"
borderRadius={3}
- borderWidth={color === '#FFFFFF' ? 1 : undefined}
+ borderWidth={color === "#FFFFFF" ? 1 : undefined}
_hover={{ background: color }}
onClick={handleClick(color)}
/>
@@ -108,7 +108,7 @@ export const ColorPicker = ({
borderRadius={3}
marginTop={3}
placeholder="#2a9d8f"
- aria-label={t('colorPicker.colorValue.ariaLabel')}
+ aria-label={t("colorPicker.colorValue.ariaLabel")}
size="sm"
value={displayedValue}
onChange={(e) => handleColorChange(e.target.value)}
@@ -118,25 +118,25 @@ export const ColorPicker = ({
color={displayedValue}
onColorChange={handleColorChange}
>
- {t('colorPicker.advancedColors')}
+ {t("colorPicker.advancedColors")}
- )
-}
+ );
+};
const NativeColorPicker = ({
color,
onColorChange,
...props
}: {
- color: string
- onColorChange: (color: string) => void
+ color: string;
+ onColorChange: (color: string) => void;
} & ButtonProps) => {
const debouncedOnColorChange = useDebouncedCallback((color: string) => {
- onColorChange(color)
- }, 200)
+ onColorChange(color);
+ }, 200);
return (
<>
@@ -151,5 +151,5 @@ const NativeColorPicker = ({
onChange={(e) => debouncedOnColorChange(e.target.value)}
/>
>
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/ConfirmModal.tsx b/apps/builder/src/components/ConfirmModal.tsx
index 0ac9b5469f..32c55e7559 100644
--- a/apps/builder/src/components/ConfirmModal.tsx
+++ b/apps/builder/src/components/ConfirmModal.tsx
@@ -1,4 +1,3 @@
-import { useRef, useState } from 'react'
import {
AlertDialog,
AlertDialogBody,
@@ -7,18 +6,19 @@ import {
AlertDialogHeader,
AlertDialogOverlay,
Button,
-} from '@chakra-ui/react'
-import { useTranslate } from '@tolgee/react'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { useRef, useState } from "react";
type ConfirmDeleteModalProps = {
- isOpen: boolean
- onConfirm: () => Promise | unknown
- onClose: () => void
- message: JSX.Element
- title?: string
- confirmButtonLabel: string
- confirmButtonColor?: 'blue' | 'red'
-}
+ isOpen: boolean;
+ onConfirm: () => Promise | unknown;
+ onClose: () => void;
+ message: JSX.Element;
+ title?: string;
+ confirmButtonLabel: string;
+ confirmButtonColor?: "blue" | "red";
+};
export const ConfirmModal = ({
title,
@@ -27,23 +27,23 @@ export const ConfirmModal = ({
onClose,
confirmButtonLabel,
onConfirm,
- confirmButtonColor = 'red',
+ confirmButtonColor = "red",
}: ConfirmDeleteModalProps) => {
- const { t } = useTranslate()
- const [confirmLoading, setConfirmLoading] = useState(false)
- const cancelRef = useRef(null)
+ const { t } = useTranslate();
+ const [confirmLoading, setConfirmLoading] = useState(false);
+ const cancelRef = useRef(null);
const onConfirmClick = async () => {
- setConfirmLoading(true)
+ setConfirmLoading(true);
try {
- await onConfirm()
+ await onConfirm();
} catch (e) {
- setConfirmLoading(false)
- return setConfirmLoading(false)
+ setConfirmLoading(false);
+ return setConfirmLoading(false);
}
- setConfirmLoading(false)
- onClose()
- }
+ setConfirmLoading(false);
+ onClose();
+ };
return (
- {title ?? t('confirmModal.defaultTitle')}
+ {title ?? t("confirmModal.defaultTitle")}
{message}
- {t('cancel')}
+ {t("cancel")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/ContextMenu.tsx b/apps/builder/src/components/ContextMenu.tsx
index c40f1767c1..80689d6473 100644
--- a/apps/builder/src/components/ContextMenu.tsx
+++ b/apps/builder/src/components/ContextMenu.tsx
@@ -1,82 +1,77 @@
-import * as React from 'react'
import {
- MutableRefObject,
- useCallback,
- useEffect,
- useRef,
- useState,
-} from 'react'
-import {
- useEventListener,
- Portal,
Menu,
MenuButton,
- PortalProps,
- MenuButtonProps,
- MenuProps,
-} from '@chakra-ui/react'
+ type MenuButtonProps,
+ type MenuProps,
+ Portal,
+ type PortalProps,
+ useEventListener,
+} from "@chakra-ui/react";
+import type { MutableRefObject } from "react";
+import * as React from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
export interface ContextMenuProps {
- onOpen?: () => void
- renderMenu: ({ onClose }: { onClose: () => void }) => JSX.Element | null
+ onOpen?: () => void;
+ renderMenu: ({ onClose }: { onClose: () => void }) => JSX.Element | null;
children: (
ref: MutableRefObject,
- isOpened: boolean
- ) => JSX.Element | null
- menuProps?: MenuProps
- portalProps?: PortalProps
- menuButtonProps?: MenuButtonProps
- isDisabled?: boolean
+ isOpened: boolean,
+ ) => JSX.Element | null;
+ menuProps?: MenuProps;
+ portalProps?: PortalProps;
+ menuButtonProps?: MenuButtonProps;
+ isDisabled?: boolean;
}
export function ContextMenu(
- props: ContextMenuProps
+ props: ContextMenuProps,
) {
- const [isOpened, setIsOpened] = useState(false)
- const [isRendered, setIsRendered] = useState(false)
- const [isDeferredOpen, setIsDeferredOpen] = useState(false)
- const [position, setPosition] = useState<[number, number]>([0, 0])
- const targetRef = useRef(null)
+ const [isOpened, setIsOpened] = useState(false);
+ const [isRendered, setIsRendered] = useState(false);
+ const [isDeferredOpen, setIsDeferredOpen] = useState(false);
+ const [position, setPosition] = useState<[number, number]>([0, 0]);
+ const targetRef = useRef(null);
useEffect(() => {
if (isOpened) {
setTimeout(() => {
- setIsRendered(true)
+ setIsRendered(true);
setTimeout(() => {
- setIsDeferredOpen(true)
- })
- })
+ setIsDeferredOpen(true);
+ });
+ });
} else {
- setIsDeferredOpen(false)
+ setIsDeferredOpen(false);
const timeout = setTimeout(() => {
- setIsRendered(isOpened)
- }, 1000)
- return () => clearTimeout(timeout)
+ setIsRendered(isOpened);
+ }, 1000);
+ return () => clearTimeout(timeout);
}
- }, [isOpened])
+ }, [isOpened]);
useEventListener(
- 'contextmenu',
+ "contextmenu",
(e) => {
- if (props.isDisabled) return
+ if (props.isDisabled) return;
if (e.currentTarget === targetRef.current) {
- e.preventDefault()
- e.stopPropagation()
- props.onOpen?.()
- setIsOpened(true)
- setPosition([e.pageX, e.pageY])
+ e.preventDefault();
+ e.stopPropagation();
+ props.onOpen?.();
+ setIsOpened(true);
+ setPosition([e.pageX, e.pageY]);
} else {
- setIsOpened(false)
+ setIsOpened(false);
}
},
- targetRef.current
- )
+ targetRef.current,
+ );
const onCloseHandler = useCallback(() => {
- props.menuProps?.onClose?.()
- setIsOpened(false)
+ props.menuProps?.onClose?.();
+ setIsOpened(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.menuProps?.onClose, setIsOpened])
+ }, [props.menuProps?.onClose, setIsOpened]);
return (
<>
@@ -94,10 +89,10 @@ export function ContextMenu(
w={1}
h={1}
style={{
- position: 'absolute',
+ position: "absolute",
left: position[0],
top: position[1],
- cursor: 'default',
+ cursor: "default",
}}
{...props.menuButtonProps}
/>
@@ -106,5 +101,5 @@ export function ContextMenu(
)}
>
- )
+ );
}
diff --git a/apps/builder/src/components/CopyButton.tsx b/apps/builder/src/components/CopyButton.tsx
index 9dcbb154e8..5a7808a214 100644
--- a/apps/builder/src/components/CopyButton.tsx
+++ b/apps/builder/src/components/CopyButton.tsx
@@ -1,37 +1,37 @@
-import React, { useEffect } from 'react'
-import { ButtonProps, Button, useClipboard } from '@chakra-ui/react'
-import { useTranslate } from '@tolgee/react'
+import { Button, type ButtonProps, useClipboard } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import React, { useEffect } from "react";
interface CopyButtonProps extends ButtonProps {
- textToCopy: string
- onCopied?: () => void
+ textToCopy: string;
+ onCopied?: () => void;
text?: {
- copy: string
- copied: string
- }
+ copy: string;
+ copied: string;
+ };
}
export const CopyButton = (props: CopyButtonProps) => {
- const { textToCopy, onCopied, ...buttonProps } = props
- const { hasCopied, onCopy, setValue } = useClipboard(textToCopy)
- const { t } = useTranslate()
+ const { textToCopy, onCopied, ...buttonProps } = props;
+ const { hasCopied, onCopy, setValue } = useClipboard(textToCopy);
+ const { t } = useTranslate();
useEffect(() => {
- setValue(textToCopy)
- }, [setValue, textToCopy])
+ setValue(textToCopy);
+ }, [setValue, textToCopy]);
return (
{
- onCopy()
- if (onCopied) onCopied()
+ onCopy();
+ if (onCopied) onCopied();
}}
{...buttonProps}
>
{!hasCopied
- ? props.text?.copy ?? t('copy')
- : props.text?.copied ?? t('copied')}
+ ? (props.text?.copy ?? t("copy"))
+ : (props.text?.copied ?? t("copied"))}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/DropdownList.tsx b/apps/builder/src/components/DropdownList.tsx
index bab7be1905..72a96a1deb 100644
--- a/apps/builder/src/components/DropdownList.tsx
+++ b/apps/builder/src/components/DropdownList.tsx
@@ -1,7 +1,7 @@
+import { ChevronLeftIcon } from "@/components/icons";
import {
Button,
- ButtonProps,
- chakra,
+ type ButtonProps,
FormControl,
FormHelperText,
FormLabel,
@@ -12,34 +12,35 @@ import {
MenuList,
Portal,
Stack,
-} from '@chakra-ui/react'
-import { ChevronLeftIcon } from '@/components/icons'
-import React, { ReactNode } from 'react'
-import { MoreInfoTooltip } from './MoreInfoTooltip'
+ chakra,
+} from "@chakra-ui/react";
+import type { ReactNode } from "react";
+import React from "react";
+import { MoreInfoTooltip } from "./MoreInfoTooltip";
type Item =
| string
| number
| {
- label: string
- value: string
- }
+ label: string;
+ value: string;
+ };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Props = {
- currentItem: string | number | undefined
+ currentItem: string | number | undefined;
onItemSelect: (
value: T extends string ? T : T extends number ? T : string,
- item?: T
- ) => void
- items: readonly T[]
- placeholder?: string
- label?: string
- isRequired?: boolean
- direction?: 'row' | 'column'
- helperText?: ReactNode
- moreInfoTooltip?: string
-}
+ item?: T,
+ ) => void;
+ items: readonly T[];
+ placeholder?: string;
+ label?: string;
+ isRequired?: boolean;
+ direction?: "row" | "column";
+ helperText?: ReactNode;
+ moreInfoTooltip?: string;
+};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const DropdownList = ({
@@ -49,31 +50,33 @@ export const DropdownList = ({
placeholder,
label,
isRequired,
- direction = 'column',
+ direction = "column",
helperText,
moreInfoTooltip,
...props
}: Props & ButtonProps) => {
const handleMenuItemClick = (item: T) => () => {
- if (typeof item === 'string' || typeof item === 'number')
- onItemSelect(item as T extends string ? T : T extends number ? T : string)
+ if (typeof item === "string" || typeof item === "number")
+ onItemSelect(
+ item as T extends string ? T : T extends number ? T : string,
+ );
else
onItemSelect(
item.value as T extends string ? T : T extends number ? T : string,
- item
- )
- }
+ item,
+ );
+ };
return (
{label && (
- {label}{' '}
+ {label}{" "}
{moreInfoTooltip && (
{moreInfoTooltip}
)}
@@ -82,7 +85,7 @@ export const DropdownList = ({
}
+ rightIcon={ }
colorScheme="gray"
justifyContent="space-between"
textAlign="left"
@@ -93,17 +96,17 @@ export const DropdownList = ({
{currentItem
? getItemLabel(
items?.find((item) =>
- typeof item === 'string' || typeof item === 'number'
+ typeof item === "string" || typeof item === "number"
? currentItem === item
- : currentItem === item.value
- )
+ : currentItem === item.value,
+ ),
)
- : placeholder ?? 'Select an item'}
+ : (placeholder ?? "Select an item")}
-
+
{items.map((item) => (
({
textOverflow="ellipsis"
onClick={handleMenuItemClick(item)}
>
- {typeof item === 'object' ? item.label : item}
+ {typeof item === "object" ? item.label : item}
))}
@@ -122,11 +125,11 @@ export const DropdownList = ({
{helperText && {helperText} }
- )
-}
+ );
+};
const getItemLabel = (item?: Item) => {
- if (!item) return ''
- if (typeof item === 'object') return item.label
- return item
-}
+ if (!item) return "";
+ if (typeof item === "object") return item.label;
+ return item;
+};
diff --git a/apps/builder/src/components/EditableEmojiOrImageIcon.tsx b/apps/builder/src/components/EditableEmojiOrImageIcon.tsx
index fad061edaf..6b145d631c 100644
--- a/apps/builder/src/components/EditableEmojiOrImageIcon.tsx
+++ b/apps/builder/src/components/EditableEmojiOrImageIcon.tsx
@@ -1,27 +1,28 @@
+import { useParentModal } from "@/features/graph/providers/ParentModalProvider";
+import type { FilePathUploadProps } from "@/features/upload/api/generateUploadUrl";
import {
+ Flex,
Popover,
+ PopoverContent,
+ PopoverTrigger,
+ Portal,
Tooltip,
chakra,
- PopoverTrigger,
- PopoverContent,
- Flex,
useColorModeValue,
- Portal,
-} from '@chakra-ui/react'
-import React, { RefObject } from 'react'
-import { EmojiOrImageIcon } from './EmojiOrImageIcon'
-import { ImageUploadContent } from './ImageUploadContent'
-import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
-import { useTranslate } from '@tolgee/react'
-import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import type { RefObject } from "react";
+import React from "react";
+import { EmojiOrImageIcon } from "./EmojiOrImageIcon";
+import { ImageUploadContent } from "./ImageUploadContent";
type Props = {
- uploadFileProps: FilePathUploadProps
- icon?: string | null
- parentModalRef?: RefObject | undefined
- onChangeIcon: (icon: string) => void
- boxSize?: string
-}
+ uploadFileProps: FilePathUploadProps;
+ icon?: string | null;
+ parentModalRef?: RefObject | undefined;
+ onChangeIcon: (icon: string) => void;
+ boxSize?: string;
+};
export const EditableEmojiOrImageIcon = ({
uploadFileProps,
@@ -29,15 +30,15 @@ export const EditableEmojiOrImageIcon = ({
onChangeIcon,
boxSize,
}: Props) => {
- const { t } = useTranslate()
- const { ref: parentModalRef } = useParentModal()
- const bg = useColorModeValue('gray.100', 'gray.700')
+ const { t } = useTranslate();
+ const { ref: parentModalRef } = useParentModal();
+ const bg = useColorModeValue("gray.100", "gray.700");
return (
{({ onClose }: { onClose: () => void }) => (
<>
-
+
)}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/EmojiOrImageIcon.tsx b/apps/builder/src/components/EmojiOrImageIcon.tsx
index f68da2f986..a5c61478dd 100644
--- a/apps/builder/src/components/EmojiOrImageIcon.tsx
+++ b/apps/builder/src/components/EmojiOrImageIcon.tsx
@@ -1,29 +1,29 @@
-import { ToolIcon } from '@/components/icons'
-import React from 'react'
-import { chakra, IconProps, Image } from '@chakra-ui/react'
-import { isSvgSrc } from '@typebot.io/lib/utils'
+import { ToolIcon } from "@/components/icons";
+import { type IconProps, Image, chakra } from "@chakra-ui/react";
+import { isSvgSrc } from "@typebot.io/lib/utils";
+import React from "react";
type Props = {
- icon?: string | null
- emojiFontSize?: string
- boxSize?: string
- defaultIcon?: (props: IconProps) => JSX.Element
-}
+ icon?: string | null;
+ emojiFontSize?: string;
+ boxSize?: string;
+ defaultIcon?: (props: IconProps) => JSX.Element;
+};
export const EmojiOrImageIcon = ({
icon,
- boxSize = '25px',
+ boxSize = "25px",
emojiFontSize,
defaultIcon = ToolIcon,
}: Props) => {
return (
<>
{icon ? (
- icon.startsWith('http') || isSvgSrc(icon) ? (
+ icon.startsWith("http") || isSvgSrc(icon) ? (
@@ -36,5 +36,5 @@ export const EmojiOrImageIcon = ({
defaultIcon({ boxSize })
)}
>
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/GoogleLogo.tsx b/apps/builder/src/components/GoogleLogo.tsx
index 91f2ca0177..40b4d84a7e 100644
--- a/apps/builder/src/components/GoogleLogo.tsx
+++ b/apps/builder/src/components/GoogleLogo.tsx
@@ -1,4 +1,4 @@
-import { IconProps, Icon } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const GoogleLogo = (props: IconProps) => (
@@ -21,4 +21,4 @@ export const GoogleLogo = (props: IconProps) => (
/>
-)
+);
diff --git a/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx b/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx
index f94eb74ca5..8b00563047 100644
--- a/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx
+++ b/apps/builder/src/components/ImageUploadContent/GiphyPicker.tsx
@@ -1,25 +1,25 @@
-import { Flex, Stack, Text } from '@chakra-ui/react'
-import { GiphyFetch } from '@giphy/js-fetch-api'
-import { Grid } from '@giphy/react-components'
-import { GiphyLogo } from '../logos/GiphyLogo'
-import React, { useState } from 'react'
-import { TextInput } from '../inputs'
-import { env } from '@typebot.io/env'
+import { Flex, Stack, Text } from "@chakra-ui/react";
+import { GiphyFetch } from "@giphy/js-fetch-api";
+import { Grid } from "@giphy/react-components";
+import { env } from "@typebot.io/env";
+import React, { useState } from "react";
+import { TextInput } from "../inputs";
+import { GiphyLogo } from "../logos/GiphyLogo";
type GiphySearchFormProps = {
- onSubmit: (url: string) => void
-}
+ onSubmit: (url: string) => void;
+};
-const giphyFetch = new GiphyFetch(env.NEXT_PUBLIC_GIPHY_API_KEY ?? '')
+const giphyFetch = new GiphyFetch(env.NEXT_PUBLIC_GIPHY_API_KEY ?? "");
export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => {
- const [inputValue, setInputValue] = useState('')
+ const [inputValue, setInputValue] = useState("");
const fetchGifs = (offset: number) =>
- giphyFetch.search(inputValue, { offset, limit: 10 })
+ giphyFetch.search(inputValue, { offset, limit: 10 });
const fetchGifsTrending = (offset: number) =>
- giphyFetch.trending({ offset, limit: 10 })
+ giphyFetch.trending({ offset, limit: 10 });
return !env.NEXT_PUBLIC_GIPHY_API_KEY ? (
NEXT_PUBLIC_GIPHY_API_KEY is missing in environment
@@ -39,15 +39,15 @@ export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => {
{
- e.preventDefault()
- onSubmit(gif.images.downsized.url)
+ e.preventDefault();
+ onSubmit(gif.images.downsized.url);
}}
- fetchGifs={inputValue === '' ? fetchGifsTrending : fetchGifs}
+ fetchGifs={inputValue === "" ? fetchGifsTrending : fetchGifs}
width={475}
columns={3}
className="my-4"
/>
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/ImageUploadContent/IconPicker.tsx b/apps/builder/src/components/ImageUploadContent/IconPicker.tsx
index a1ed01541c..867dfa698e 100644
--- a/apps/builder/src/components/ImageUploadContent/IconPicker.tsx
+++ b/apps/builder/src/components/ImageUploadContent/IconPicker.tsx
@@ -1,113 +1,115 @@
import {
Button,
- Stack,
- Image,
HStack,
- useColorModeValue,
+ Image,
SimpleGrid,
+ Stack,
Text,
-} from '@chakra-ui/react'
-import { useEffect, useMemo, useRef, useState } from 'react'
-import { iconNames } from './iconNames'
-import { TextInput } from '../inputs'
-import { ColorPicker } from '../ColorPicker'
-import { useTranslate } from '@tolgee/react'
+ useColorModeValue,
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { useEffect, useMemo, useRef, useState } from "react";
+import { ColorPicker } from "../ColorPicker";
+import { TextInput } from "../inputs";
+import { iconNames } from "./iconNames";
-const batchSize = 200
+const batchSize = 200;
type Props = {
- onIconSelected: (url: string) => void
-}
+ onIconSelected: (url: string) => void;
+};
-const localStorageRecentIconNamesKey = 'recentIconNames'
-const localStorageDefaultIconColorKey = 'defaultIconColor'
+const localStorageRecentIconNamesKey = "recentIconNames";
+const localStorageDefaultIconColorKey = "defaultIconColor";
export const IconPicker = ({ onIconSelected }: Props) => {
- const initialIconColor = useColorModeValue('#222222', '#ffffff')
- const scrollContainer = useRef(null)
- const bottomElement = useRef(null)
+ const initialIconColor = useColorModeValue("#222222", "#ffffff");
+ const scrollContainer = useRef(null);
+ const bottomElement = useRef(null);
const [displayedIconNames, setDisplayedIconNames] = useState(
- iconNames.slice(0, batchSize)
- )
- const searchQuery = useRef('')
- const [selectedColor, setSelectedColor] = useState(initialIconColor)
+ iconNames.slice(0, batchSize),
+ );
+ const searchQuery = useRef("");
+ const [selectedColor, setSelectedColor] = useState(initialIconColor);
const isWhite = useMemo(
() =>
- initialIconColor === '#222222' &&
- (selectedColor.toLowerCase() === '#ffffff' ||
- selectedColor.toLowerCase() === '#fff' ||
- selectedColor === 'white'),
- [initialIconColor, selectedColor]
- )
- const [recentIconNames, setRecentIconNames] = useState([])
- const { t } = useTranslate()
+ initialIconColor === "#222222" &&
+ (selectedColor.toLowerCase() === "#ffffff" ||
+ selectedColor.toLowerCase() === "#fff" ||
+ selectedColor === "white"),
+ [initialIconColor, selectedColor],
+ );
+ const [recentIconNames, setRecentIconNames] = useState([]);
+ const { t } = useTranslate();
useEffect(() => {
- const recentIconNames = localStorage.getItem(localStorageRecentIconNamesKey)
+ const recentIconNames = localStorage.getItem(
+ localStorageRecentIconNamesKey,
+ );
const defaultIconColor = localStorage.getItem(
- localStorageDefaultIconColorKey
- )
- if (recentIconNames) setRecentIconNames(JSON.parse(recentIconNames))
- if (defaultIconColor) setSelectedColor(defaultIconColor)
- }, [])
+ localStorageDefaultIconColorKey,
+ );
+ if (recentIconNames) setRecentIconNames(JSON.parse(recentIconNames));
+ if (defaultIconColor) setSelectedColor(defaultIconColor);
+ }, []);
useEffect(() => {
- if (!bottomElement.current) return
+ if (!bottomElement.current) return;
const observer = new IntersectionObserver(handleObserver, {
root: scrollContainer.current,
- rootMargin: '200px',
- })
- if (bottomElement.current) observer.observe(bottomElement.current)
+ rootMargin: "200px",
+ });
+ if (bottomElement.current) observer.observe(bottomElement.current);
return () => {
- observer.disconnect()
- }
- }, [])
+ observer.disconnect();
+ };
+ }, []);
const handleObserver = (entities: IntersectionObserverEntry[]) => {
- const target = entities[0]
+ const target = entities[0];
if (target.isIntersecting && searchQuery.current.length <= 2)
setDisplayedIconNames((displayedIconNames) => [
...displayedIconNames,
...iconNames.slice(
displayedIconNames.length,
- displayedIconNames.length + batchSize
+ displayedIconNames.length + batchSize,
),
- ])
- }
+ ]);
+ };
const searchIcon = async (query: string) => {
- searchQuery.current = query
+ searchQuery.current = query;
if (query.length <= 2)
- return setDisplayedIconNames(iconNames.slice(0, batchSize))
+ return setDisplayedIconNames(iconNames.slice(0, batchSize));
const filteredIconNames = iconNames.filter((iconName) =>
- iconName.toLowerCase().includes(query.toLowerCase())
- )
- setDisplayedIconNames(filteredIconNames)
- }
+ iconName.toLowerCase().includes(query.toLowerCase()),
+ );
+ setDisplayedIconNames(filteredIconNames);
+ };
const updateColor = (color: string) => {
- if (!color.startsWith('#')) return
- localStorage.setItem(localStorageDefaultIconColorKey, color)
- setSelectedColor(color)
- }
+ if (!color.startsWith("#")) return;
+ localStorage.setItem(localStorageDefaultIconColorKey, color);
+ setSelectedColor(color);
+ };
const selectIcon = async (iconName: string) => {
localStorage.setItem(
localStorageRecentIconNamesKey,
- JSON.stringify([...new Set([iconName, ...recentIconNames].slice(0, 30))])
- )
- const svg = await (await fetch(`/icons/${iconName}.svg`)).text()
+ JSON.stringify([...new Set([iconName, ...recentIconNames].slice(0, 30))]),
+ );
+ const svg = await (await fetch(`/icons/${iconName}.svg`)).text();
const dataUri = `data:image/svg+xml;utf8,${svg
- .replace('
{
{recentIconNames.map((iconName) => (
{
{displayedIconNames.map((iconName) => (
{
- )
-}
+ );
+};
const Icon = ({ name, color }: { name: string; color: string }) => {
- const [svg, setSvg] = useState('')
+ const [svg, setSvg] = useState("");
const dataUri = useMemo(
() =>
`data:image/svg+xml;utf8,${svg.replace(
- ' {
fetch(`/icons/${name}.svg`)
.then((response) => response.text())
- .then((text) => setSvg(text))
- }, [name])
+ .then((text) => setSvg(text));
+ }, [name]);
- if (!svg) return null
+ if (!svg) return null;
- return
-}
+ return ;
+};
diff --git a/apps/builder/src/components/ImageUploadContent/ImageUploadContent.tsx b/apps/builder/src/components/ImageUploadContent/ImageUploadContent.tsx
index e6cd71a6aa..66769c000c 100644
--- a/apps/builder/src/components/ImageUploadContent/ImageUploadContent.tsx
+++ b/apps/builder/src/components/ImageUploadContent/ImageUploadContent.tsx
@@ -1,122 +1,123 @@
-import { useState } from 'react'
-import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
-import { UploadButton } from './UploadButton'
-import { GiphyPicker } from './GiphyPicker'
-import { TextInput } from '../inputs/TextInput'
-import { EmojiSearchableList } from './emoji/EmojiSearchableList'
-import { UnsplashPicker } from './UnsplashPicker'
-import { IconPicker } from './IconPicker'
-import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
-import { useTranslate } from '@tolgee/react'
+import type { FilePathUploadProps } from "@/features/upload/api/generateUploadUrl";
+import { Button, Flex, HStack, Stack } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { useState } from "react";
+import { TextInput } from "../inputs/TextInput";
+import { GiphyPicker } from "./GiphyPicker";
+import { IconPicker } from "./IconPicker";
+import { UnsplashPicker } from "./UnsplashPicker";
+import { UploadButton } from "./UploadButton";
+import { EmojiSearchableList } from "./emoji/EmojiSearchableList";
-type Tabs = 'link' | 'upload' | 'giphy' | 'emoji' | 'unsplash' | 'icon'
+type Tabs = "link" | "upload" | "giphy" | "emoji" | "unsplash" | "icon";
type Props = {
- uploadFileProps: FilePathUploadProps | undefined
- defaultUrl?: string
- imageSize?: 'small' | 'regular' | 'thumb'
- initialTab?: Tabs
- linkWithVariableButton?: boolean
- onSubmit: (url: string) => void
- onClose?: () => void
+ uploadFileProps: FilePathUploadProps | undefined;
+ defaultUrl?: string;
+ imageSize?: "small" | "regular" | "thumb";
+ initialTab?: Tabs;
+ linkWithVariableButton?: boolean;
+ onSubmit: (url: string) => void;
+ onClose?: () => void;
} & (
| {
- includedTabs?: Tabs[]
+ includedTabs?: Tabs[];
}
| {
- excludedTabs?: Tabs[]
+ excludedTabs?: Tabs[];
}
-)
+);
const defaultDisplayedTabs: Tabs[] = [
- 'link',
- 'upload',
- 'giphy',
- 'emoji',
- 'unsplash',
- 'icon',
-]
+ "link",
+ "upload",
+ "giphy",
+ "emoji",
+ "unsplash",
+ "icon",
+];
export const ImageUploadContent = ({
uploadFileProps,
defaultUrl,
onSubmit,
- imageSize = 'regular',
+ imageSize = "regular",
onClose,
initialTab,
linkWithVariableButton,
...props
}: Props) => {
const includedTabs =
- 'includedTabs' in props
- ? props.includedTabs ?? defaultDisplayedTabs
- : defaultDisplayedTabs
- const excludedTabs = 'excludedTabs' in props ? props.excludedTabs ?? [] : []
+ "includedTabs" in props
+ ? (props.includedTabs ?? defaultDisplayedTabs)
+ : defaultDisplayedTabs;
+ const excludedTabs =
+ "excludedTabs" in props ? (props.excludedTabs ?? []) : [];
const displayedTabs = defaultDisplayedTabs.filter(
- (tab) => !excludedTabs.includes(tab) && includedTabs.includes(tab)
- )
+ (tab) => !excludedTabs.includes(tab) && includedTabs.includes(tab),
+ );
const [currentTab, setCurrentTab] = useState(
- initialTab ?? displayedTabs[0]
- )
+ initialTab ?? displayedTabs[0],
+ );
const handleSubmit = (url: string) => {
- onSubmit(url)
- onClose && onClose()
- }
+ onSubmit(url);
+ onClose && onClose();
+ };
return (
- {displayedTabs.includes('link') && (
+ {displayedTabs.includes("link") && (
setCurrentTab('link')}
+ variant={currentTab === "link" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("link")}
size="sm"
>
Link
)}
- {displayedTabs.includes('upload') && (
+ {displayedTabs.includes("upload") && (
setCurrentTab('upload')}
+ variant={currentTab === "upload" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("upload")}
size="sm"
>
Upload
)}
- {displayedTabs.includes('emoji') && (
+ {displayedTabs.includes("emoji") && (
setCurrentTab('emoji')}
+ variant={currentTab === "emoji" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("emoji")}
size="sm"
>
Emoji
)}
- {displayedTabs.includes('giphy') && (
+ {displayedTabs.includes("giphy") && (
setCurrentTab('giphy')}
+ variant={currentTab === "giphy" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("giphy")}
size="sm"
>
Giphy
)}
- {displayedTabs.includes('unsplash') && (
+ {displayedTabs.includes("unsplash") && (
setCurrentTab('unsplash')}
+ variant={currentTab === "unsplash" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("unsplash")}
size="sm"
>
Unsplash
)}
- {displayedTabs.includes('icon') && (
+ {displayedTabs.includes("icon") && (
setCurrentTab('icon')}
+ variant={currentTab === "icon" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("icon")}
size="sm"
>
Icon
@@ -133,8 +134,8 @@ export const ImageUploadContent = ({
linkWithVariableButton={linkWithVariableButton}
/>
- )
-}
+ );
+};
const BodyContent = ({
uploadFileProps,
@@ -144,49 +145,49 @@ const BodyContent = ({
linkWithVariableButton,
onSubmit,
}: {
- uploadFileProps?: FilePathUploadProps
- tab: Tabs
- defaultUrl?: string
- imageSize: 'small' | 'regular' | 'thumb'
- linkWithVariableButton?: boolean
- onSubmit: (url: string) => void
+ uploadFileProps?: FilePathUploadProps;
+ tab: Tabs;
+ defaultUrl?: string;
+ imageSize: "small" | "regular" | "thumb";
+ linkWithVariableButton?: boolean;
+ onSubmit: (url: string) => void;
}) => {
switch (tab) {
- case 'upload': {
- if (!uploadFileProps) return null
+ case "upload": {
+ if (!uploadFileProps) return null;
return (
- )
+ );
}
- case 'link':
+ case "link":
return (
- )
- case 'giphy':
- return
- case 'emoji':
- return
- case 'unsplash':
- return
- case 'icon':
- return
+ );
+ case "giphy":
+ return ;
+ case "emoji":
+ return ;
+ case "unsplash":
+ return ;
+ case "icon":
+ return ;
}
-}
+};
-type ContentProps = { onNewUrl: (url: string) => void }
+type ContentProps = { onNewUrl: (url: string) => void };
const UploadFileContent = ({
uploadFileProps,
onNewUrl,
}: ContentProps & { uploadFileProps: FilePathUploadProps }) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
return (
@@ -196,31 +197,31 @@ const UploadFileContent = ({
onFileUploaded={onNewUrl}
colorScheme="blue"
>
- {t('editor.header.uploadTab.uploadButton.label')}
+ {t("editor.header.uploadTab.uploadButton.label")}
- )
-}
+ );
+};
const EmbedLinkContent = ({
defaultUrl,
onNewUrl,
withVariableButton,
}: ContentProps & { defaultUrl?: string; withVariableButton?: boolean }) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
return (
- )
-}
+ );
+};
const GiphyContent = ({ onNewUrl }: ContentProps) => (
-)
+);
diff --git a/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx b/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx
index d2c043ad32..1167d837d5 100644
--- a/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx
+++ b/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @next/next/no-img-element */
import {
Alert,
AlertIcon,
@@ -13,122 +12,123 @@ import {
Stack,
Text,
useColorModeValue,
-} from '@chakra-ui/react'
-import { isDefined } from '@typebot.io/lib'
-import { useCallback, useEffect, useRef, useState } from 'react'
-import { createApi } from 'unsplash-js'
-import { Basic as UnsplashImageType } from 'unsplash-js/dist/methods/photos/types'
-import { TextInput } from '../inputs'
-import { UnsplashLogo } from '../logos/UnsplashLogo'
-import { TextLink } from '../TextLink'
-import { env } from '@typebot.io/env'
+} from "@chakra-ui/react";
+import { env } from "@typebot.io/env";
+import { isDefined } from "@typebot.io/lib/utils";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { createApi } from "unsplash-js";
+import type { Basic as UnsplashPhoto } from "unsplash-js/dist/methods/photos/types";
+import { TextLink } from "../TextLink";
+import { TextInput } from "../inputs";
+import { UnsplashLogo } from "../logos/UnsplashLogo";
+/* eslint-disable @next/next/no-img-element */
const api = createApi({
- accessKey: env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY ?? '',
-})
+ accessKey: env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY ?? "",
+});
type Props = {
- imageSize: 'regular' | 'small' | 'thumb'
- onImageSelect: (imageUrl: string) => void
-}
+ imageSize: "regular" | "small" | "thumb";
+ onImageSelect: (imageUrl: string) => void;
+};
export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
- const unsplashLogoFillColor = useColorModeValue('black', 'white')
- const [isFetching, setIsFetching] = useState(false)
- const [images, setImages] = useState([])
- const [error, setError] = useState(null)
- const [searchQuery, setSearchQuery] = useState('')
- const scrollContainer = useRef(null)
- const bottomAnchor = useRef(null)
+ const unsplashLogoFillColor = useColorModeValue("black", "white");
+ const [isFetching, setIsFetching] = useState(false);
+ const [images, setImages] = useState([]);
+ const [error, setError] = useState(null);
+ const [searchQuery, setSearchQuery] = useState("");
+ const scrollContainer = useRef(null);
+ const bottomAnchor = useRef(null);
- const [nextPage, setNextPage] = useState(0)
+ const [nextPage, setNextPage] = useState(0);
const fetchNewImages = useCallback(async (query: string, page: number) => {
- if (query === '') return searchRandomImages()
- if (query.length <= 2) return
- setError(null)
- setIsFetching(true)
+ if (query === "") return searchRandomImages();
+ if (query.length <= 2) return;
+ setError(null);
+ setIsFetching(true);
try {
const result = await api.search.getPhotos({
query,
perPage: 30,
- orientation: 'landscape',
+ orientation: "landscape",
page,
- })
- if (result.errors) setError(result.errors[0])
+ });
+ if (result.errors) setError(result.errors[0]);
if (isDefined(result.response)) {
- if (page === 0) setImages(result.response.results)
+ if (page === 0) setImages(result.response.results);
else
setImages((images) => [
...images,
...(result.response?.results ?? []),
- ])
- setNextPage((page) => page + 1)
+ ]);
+ setNextPage((page) => page + 1);
}
} catch (err) {
- if (err && typeof err === 'object' && 'message' in err)
- setError(err.message as string)
- setError('Something went wrong')
+ if (err && typeof err === "object" && "message" in err)
+ setError(err.message as string);
+ setError("Something went wrong");
}
- setIsFetching(false)
- }, [])
+ setIsFetching(false);
+ }, []);
useEffect(() => {
- if (!bottomAnchor.current) return
+ if (!bottomAnchor.current) return;
const observer = new IntersectionObserver(
(entities: IntersectionObserverEntry[]) => {
- const target = entities[0]
- if (target.isIntersecting) fetchNewImages(searchQuery, nextPage + 1)
+ const target = entities[0];
+ if (target.isIntersecting) fetchNewImages(searchQuery, nextPage + 1);
},
{
root: scrollContainer.current,
- }
- )
+ },
+ );
if (bottomAnchor.current && nextPage > 0)
- observer.observe(bottomAnchor.current)
+ observer.observe(bottomAnchor.current);
return () => {
- observer.disconnect()
- }
- }, [fetchNewImages, nextPage, searchQuery])
+ observer.disconnect();
+ };
+ }, [fetchNewImages, nextPage, searchQuery]);
const searchRandomImages = async () => {
- setError(null)
- setIsFetching(true)
+ setError(null);
+ setIsFetching(true);
try {
const result = await api.photos.getRandom({
count: 30,
- orientation: 'landscape',
- })
+ orientation: "landscape",
+ });
- if (result.errors) setError(result.errors[0])
+ if (result.errors) setError(result.errors[0]);
if (isDefined(result.response))
setImages(
- Array.isArray(result.response) ? result.response : [result.response]
- )
+ Array.isArray(result.response) ? result.response : [result.response],
+ );
} catch (err) {
- if (err && typeof err === 'object' && 'message' in err)
- setError(err.message as string)
- setError('Something went wrong')
+ if (err && typeof err === "object" && "message" in err)
+ setError(err.message as string);
+ setError("Something went wrong");
}
- setIsFetching(false)
- }
+ setIsFetching(false);
+ };
- const selectImage = (image: UnsplashImageType) => {
- const url = image.urls[imageSize]
+ const selectImage = (image: UnsplashPhoto) => {
+ const url = image.urls[imageSize];
api.photos.trackDownload({
downloadLocation: image.links.download_location,
- })
- if (isDefined(url)) onImageSelect(url)
- }
+ });
+ if (isDefined(url)) onImageSelect(url);
+ };
useEffect(() => {
- searchRandomImages()
- }, [])
+ searchRandomImages();
+ }, []);
if (!env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY)
return (
NEXT_PUBLIC_UNSPLASH_ACCESS_KEY is missing in environment
- )
+ );
return (
@@ -137,8 +137,8 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
autoFocus
placeholder="Search..."
onChange={(query) => {
- setSearchQuery(query)
- fetchNewImages(query, 0)
+ setSearchQuery(query);
+ fetchNewImages(query, 0);
}}
withVariableButton={false}
debounceTimeout={500}
@@ -183,18 +183,18 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
)}
- )
-}
+ );
+};
type UnsplashImageProps = {
- image: UnsplashImageType
- onClick: () => void
-}
+ image: UnsplashPhoto;
+ onClick: () => void;
+};
const UnsplashImage = ({ image, onClick }: UnsplashImageProps) => {
- const [isImageHovered, setIsImageHovered] = useState(false)
+ const [isImageHovered, setIsImageHovered] = useState(false);
- const { user, urls, alt_description } = image
+ const { user, urls, alt_description } = image;
return (
{
{
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx
index 4d150cb126..fe2a54920d 100644
--- a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx
+++ b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx
@@ -1,15 +1,16 @@
-import { useToast } from '@/hooks/useToast'
-import { Button, ButtonProps, chakra } from '@chakra-ui/react'
-import { ChangeEvent, useState } from 'react'
-import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
-import { trpc } from '@/lib/trpc'
-import { compressFile } from '@/helpers/compressFile'
+import type { FilePathUploadProps } from "@/features/upload/api/generateUploadUrl";
+import { compressFile } from "@/helpers/compressFile";
+import { useToast } from "@/hooks/useToast";
+import { trpc } from "@/lib/trpc";
+import { Button, type ButtonProps, chakra } from "@chakra-ui/react";
+import type { ChangeEvent } from "react";
+import { useState } from "react";
type UploadButtonProps = {
- fileType: 'image' | 'audio'
- filePathProps: FilePathUploadProps
- onFileUploaded: (url: string) => void
-} & ButtonProps
+ fileType: "image" | "audio";
+ filePathProps: FilePathUploadProps;
+ onFileUploaded: (url: string) => void;
+} & ButtonProps;
export const UploadButton = ({
fileType,
@@ -17,47 +18,50 @@ export const UploadButton = ({
onFileUploaded,
...props
}: UploadButtonProps) => {
- const [isUploading, setIsUploading] = useState(false)
- const { showToast } = useToast()
- const [file, setFile] = useState()
+ const [isUploading, setIsUploading] = useState(false);
+ const { showToast } = useToast();
+ const [file, setFile] = useState();
const { mutate } = trpc.generateUploadUrl.useMutation({
onSettled: () => {
- setIsUploading(false)
+ setIsUploading(false);
},
onSuccess: async (data) => {
- if (!file) return
- const formData = new FormData()
+ if (!file) return;
+ const formData = new FormData();
Object.entries(data.formData).forEach(([key, value]) => {
- formData.append(key, value)
- })
- formData.append('file', file)
+ formData.append(key, value);
+ });
+ formData.append("file", file);
const upload = await fetch(data.presignedUrl, {
- method: 'POST',
+ method: "POST",
body: formData,
- })
+ });
if (!upload.ok) {
- showToast({ description: 'Error while trying to upload the file.' })
- return
+ showToast({ description: "Error while trying to upload the file." });
+ return;
}
- onFileUploaded(data.fileUrl + '?v=' + Date.now())
+ onFileUploaded(data.fileUrl + "?v=" + Date.now());
},
- })
+ });
const handleInputChange = async (e: ChangeEvent) => {
- if (!e.target?.files) return
- setIsUploading(true)
- const file = e.target.files[0] as File | undefined
+ if (!e.target?.files) return;
+ setIsUploading(true);
+ const file = e.target.files[0] as File | undefined;
if (!file)
- return showToast({ description: 'Could not read file.', status: 'error' })
- setFile(await compressFile(file))
+ return showToast({
+ description: "Could not read file.",
+ status: "error",
+ });
+ setFile(await compressFile(file));
mutate({
filePathProps,
fileType: file.type,
- })
- }
+ });
+ };
return (
<>
@@ -67,7 +71,7 @@ export const UploadButton = ({
id="file-input"
display="none"
onChange={handleInputChange}
- accept={fileType === 'image' ? 'image/*' : 'audio/*'}
+ accept={fileType === "image" ? "image/*" : "audio/*"}
/>
>
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/ImageUploadContent/emoji/EmojiSearchableList.tsx b/apps/builder/src/components/ImageUploadContent/emoji/EmojiSearchableList.tsx
index 075632ada4..9145177655 100644
--- a/apps/builder/src/components/ImageUploadContent/emoji/EmojiSearchableList.tsx
+++ b/apps/builder/src/components/ImageUploadContent/emoji/EmojiSearchableList.tsx
@@ -1,120 +1,121 @@
-import emojis from './emojiList.json'
-import emojiTagsData from 'emojilib'
import {
- Stack,
- SimpleGrid,
- GridItem,
Button,
- Input as ClassicInput,
+ GridItem,
+ Input,
+ SimpleGrid,
+ Stack,
Text,
-} from '@chakra-ui/react'
-import { useState, ChangeEvent, useEffect, useRef } from 'react'
-import { useTranslate } from '@tolgee/react'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import emojiTagsData from "emojilib";
+import type { ChangeEvent } from "react";
+import { useEffect, useRef, useState } from "react";
+import emojis from "./emojiList.json";
-const emojiTags = emojiTagsData as Record
+const emojiTags = emojiTagsData as Record;
-const people = emojis['Smileys & Emotion'].concat(emojis['People & Body'])
-const nature = emojis['Animals & Nature']
-const food = emojis['Food & Drink']
-const activities = emojis['Activities']
-const travel = emojis['Travel & Places']
-const objects = emojis['Objects']
-const symbols = emojis['Symbols']
-const flags = emojis['Flags']
+const people = emojis["Smileys & Emotion"].concat(emojis["People & Body"]);
+const nature = emojis["Animals & Nature"];
+const food = emojis["Food & Drink"];
+const activities = emojis["Activities"];
+const travel = emojis["Travel & Places"];
+const objects = emojis["Objects"];
+const symbols = emojis["Symbols"];
+const flags = emojis["Flags"];
-const localStorageRecentEmojisKey = 'recentEmojis'
+const localStorageRecentEmojisKey = "recentEmojis";
export const EmojiSearchableList = ({
onEmojiSelected,
}: {
- onEmojiSelected: (emoji: string) => void
+ onEmojiSelected: (emoji: string) => void;
}) => {
- const scrollContainer = useRef(null)
- const bottomElement = useRef(null)
- const [isSearching, setIsSearching] = useState(false)
- const [filteredPeople, setFilteredPeople] = useState(people)
- const [filteredAnimals, setFilteredAnimals] = useState(nature)
- const [filteredFood, setFilteredFood] = useState(food)
- const [filteredTravel, setFilteredTravel] = useState(travel)
- const [filteredActivities, setFilteredActivities] = useState(activities)
- const [filteredObjects, setFilteredObjects] = useState(objects)
- const [filteredSymbols, setFilteredSymbols] = useState(symbols)
- const [filteredFlags, setFilteredFlags] = useState(flags)
- const [totalDisplayedCategories, setTotalDisplayedCategories] = useState(1)
- const [recentEmojis, setRecentEmojis] = useState([])
- const { t } = useTranslate()
+ const scrollContainer = useRef(null);
+ const bottomElement = useRef(null);
+ const [isSearching, setIsSearching] = useState(false);
+ const [filteredPeople, setFilteredPeople] = useState(people);
+ const [filteredAnimals, setFilteredAnimals] = useState(nature);
+ const [filteredFood, setFilteredFood] = useState(food);
+ const [filteredTravel, setFilteredTravel] = useState(travel);
+ const [filteredActivities, setFilteredActivities] = useState(activities);
+ const [filteredObjects, setFilteredObjects] = useState(objects);
+ const [filteredSymbols, setFilteredSymbols] = useState(symbols);
+ const [filteredFlags, setFilteredFlags] = useState(flags);
+ const [totalDisplayedCategories, setTotalDisplayedCategories] = useState(1);
+ const [recentEmojis, setRecentEmojis] = useState([]);
+ const { t } = useTranslate();
useEffect(() => {
- const recentIconNames = localStorage.getItem(localStorageRecentEmojisKey)
- if (!recentIconNames) return
- setRecentEmojis(JSON.parse(recentIconNames))
- }, [])
+ const recentIconNames = localStorage.getItem(localStorageRecentEmojisKey);
+ if (!recentIconNames) return;
+ setRecentEmojis(JSON.parse(recentIconNames));
+ }, []);
useEffect(() => {
- if (!bottomElement.current) return
+ if (!bottomElement.current) return;
const observer = new IntersectionObserver(handleObserver, {
root: scrollContainer.current,
- })
- if (bottomElement.current) observer.observe(bottomElement.current)
+ });
+ if (bottomElement.current) observer.observe(bottomElement.current);
return () => {
- observer.disconnect()
- }
- }, [])
+ observer.disconnect();
+ };
+ }, []);
const handleObserver = (entities: IntersectionObserverEntry[]) => {
- const target = entities[0]
- if (target.isIntersecting) setTotalDisplayedCategories((c) => c + 1)
- }
+ const target = entities[0];
+ if (target.isIntersecting) setTotalDisplayedCategories((c) => c + 1);
+ };
const handleSearchChange = async (e: ChangeEvent) => {
- const searchValue = e.target.value
- if (searchValue.length <= 2 && isSearching) return resetEmojiList()
- setIsSearching(true)
- setTotalDisplayedCategories(8)
+ const searchValue = e.target.value;
+ if (searchValue.length <= 2 && isSearching) return resetEmojiList();
+ setIsSearching(true);
+ setTotalDisplayedCategories(8);
const byTag = (emoji: string) =>
- emojiTags[emoji].find((tag) => tag.includes(searchValue))
- setFilteredPeople(people.filter(byTag))
- setFilteredAnimals(nature.filter(byTag))
- setFilteredFood(food.filter(byTag))
- setFilteredTravel(travel.filter(byTag))
- setFilteredActivities(activities.filter(byTag))
- setFilteredObjects(objects.filter(byTag))
- setFilteredSymbols(symbols.filter(byTag))
- setFilteredFlags(flags.filter(byTag))
- }
+ emojiTags[emoji].find((tag) => tag.includes(searchValue));
+ setFilteredPeople(people.filter(byTag));
+ setFilteredAnimals(nature.filter(byTag));
+ setFilteredFood(food.filter(byTag));
+ setFilteredTravel(travel.filter(byTag));
+ setFilteredActivities(activities.filter(byTag));
+ setFilteredObjects(objects.filter(byTag));
+ setFilteredSymbols(symbols.filter(byTag));
+ setFilteredFlags(flags.filter(byTag));
+ };
const resetEmojiList = () => {
- setTotalDisplayedCategories(1)
- setIsSearching(false)
- setFilteredPeople(people)
- setFilteredAnimals(nature)
- setFilteredFood(food)
- setFilteredTravel(travel)
- setFilteredActivities(activities)
- setFilteredObjects(objects)
- setFilteredSymbols(symbols)
- setFilteredFlags(flags)
- }
+ setTotalDisplayedCategories(1);
+ setIsSearching(false);
+ setFilteredPeople(people);
+ setFilteredAnimals(nature);
+ setFilteredFood(food);
+ setFilteredTravel(travel);
+ setFilteredActivities(activities);
+ setFilteredObjects(objects);
+ setFilteredSymbols(symbols);
+ setFilteredFlags(flags);
+ };
const selectEmoji = (emoji: string) => {
localStorage.setItem(
localStorageRecentEmojisKey,
- JSON.stringify([...new Set([emoji, ...recentEmojis].slice(0, 30))])
- )
- onEmojiSelected(emoji)
- }
+ JSON.stringify([...new Set([emoji, ...recentEmojis].slice(0, 30))]),
+ );
+ onEmojiSelected(emoji);
+ };
return (
-
{recentEmojis.length > 0 && (
- {t('emojiList.categories.recent.label')}
+ {t("emojiList.categories.recent.label")}
@@ -122,7 +123,7 @@ export const EmojiSearchableList = ({
{filteredPeople.length > 0 && (
- {t('emojiList.categories.people.label')}
+ {t("emojiList.categories.people.label")}
@@ -130,7 +131,7 @@ export const EmojiSearchableList = ({
{filteredAnimals.length > 0 && totalDisplayedCategories >= 2 && (
- {t('emojiList.categories.animalsAndNature.label')}
+ {t("emojiList.categories.animalsAndNature.label")}
@@ -138,7 +139,7 @@ export const EmojiSearchableList = ({
{filteredFood.length > 0 && totalDisplayedCategories >= 3 && (
- {t('emojiList.categories.foodAndDrink.label')}
+ {t("emojiList.categories.foodAndDrink.label")}
@@ -146,7 +147,7 @@ export const EmojiSearchableList = ({
{filteredTravel.length > 0 && totalDisplayedCategories >= 4 && (
- {t('emojiList.categories.travelAndPlaces.label')}
+ {t("emojiList.categories.travelAndPlaces.label")}
@@ -154,7 +155,7 @@ export const EmojiSearchableList = ({
{filteredActivities.length > 0 && totalDisplayedCategories >= 5 && (
- {t('emojiList.categories.activities.label')}
+ {t("emojiList.categories.activities.label")}
@@ -162,7 +163,7 @@ export const EmojiSearchableList = ({
{filteredObjects.length > 0 && totalDisplayedCategories >= 6 && (
- {t('emojiList.categories.objects.label')}
+ {t("emojiList.categories.objects.label")}
@@ -170,7 +171,7 @@ export const EmojiSearchableList = ({
{filteredSymbols.length > 0 && totalDisplayedCategories >= 7 && (
- {t('emojiList.categories.symbols.label')}
+ {t("emojiList.categories.symbols.label")}
@@ -178,7 +179,7 @@ export const EmojiSearchableList = ({
{filteredFlags.length > 0 && totalDisplayedCategories >= 8 && (
- {t('emojiList.categories.flags.label')}
+ {t("emojiList.categories.flags.label")}
@@ -186,17 +187,17 @@ export const EmojiSearchableList = ({
- )
-}
+ );
+};
const EmojiGrid = ({
emojis,
onEmojiClick,
}: {
- emojis: string[]
- onEmojiClick: (emoji: string) => void
+ emojis: string[];
+ onEmojiClick: (emoji: string) => void;
}) => {
- const handleClick = (emoji: string) => () => onEmojiClick(emoji)
+ const handleClick = (emoji: string) => () => onEmojiClick(emoji);
return (
))}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/ImageUploadContent/index.tsx b/apps/builder/src/components/ImageUploadContent/index.tsx
index 7bbc1a5746..2d8cacb3fc 100644
--- a/apps/builder/src/components/ImageUploadContent/index.tsx
+++ b/apps/builder/src/components/ImageUploadContent/index.tsx
@@ -1 +1 @@
-export { ImageUploadContent } from './ImageUploadContent'
+export { ImageUploadContent } from "./ImageUploadContent";
diff --git a/apps/builder/src/components/MoreInfoTooltip.tsx b/apps/builder/src/components/MoreInfoTooltip.tsx
index 28e79c1a84..9dfb20ccc0 100644
--- a/apps/builder/src/components/MoreInfoTooltip.tsx
+++ b/apps/builder/src/components/MoreInfoTooltip.tsx
@@ -1,9 +1,9 @@
-import { Tooltip, chakra } from '@chakra-ui/react'
-import { HelpCircleIcon } from './icons'
+import { Tooltip, chakra } from "@chakra-ui/react";
+import { HelpCircleIcon } from "./icons";
type Props = {
- children: React.ReactNode
-}
+ children: React.ReactNode;
+};
export const MoreInfoTooltip = ({ children }: Props) => {
return (
@@ -12,5 +12,5 @@ export const MoreInfoTooltip = ({ children }: Props) => {
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/MotionStack.tsx b/apps/builder/src/components/MotionStack.tsx
index cfc73e7984..c5c2cbe31e 100644
--- a/apps/builder/src/components/MotionStack.tsx
+++ b/apps/builder/src/components/MotionStack.tsx
@@ -1,12 +1,12 @@
-import { forwardRef, Stack, StackProps } from '@chakra-ui/react'
-import { motion, MotionProps, isValidMotionProp } from 'framer-motion'
+import { Stack, type StackProps, forwardRef } from "@chakra-ui/react";
+import { type MotionProps, isValidMotionProp, motion } from "framer-motion";
export const MotionStack = motion(
- forwardRef((props, ref) => {
+ forwardRef((props, ref) => {
const chakraProps = Object.fromEntries(
- Object.entries(props).filter(([key]) => !isValidMotionProp(key))
- )
+ Object.entries(props).filter(([key]) => !isValidMotionProp(key)),
+ );
- return
- })
-)
+ return ;
+ }),
+);
diff --git a/apps/builder/src/components/NewVersionPopup.tsx b/apps/builder/src/components/NewVersionPopup.tsx
index 0a4580dc6e..3beeba9cb4 100644
--- a/apps/builder/src/components/NewVersionPopup.tsx
+++ b/apps/builder/src/components/NewVersionPopup.tsx
@@ -1,5 +1,5 @@
-import { useTypebot } from '@/features/editor/providers/TypebotProvider'
-import { trpc } from '@/lib/trpc'
+import { useTypebot } from "@/features/editor/providers/TypebotProvider";
+import { trpc } from "@/lib/trpc";
import {
Button,
DarkMode,
@@ -8,31 +8,31 @@ import {
SlideFade,
Stack,
Text,
-} from '@chakra-ui/react'
-import { useEffect, useState } from 'react'
-import { PackageIcon } from './icons'
+} from "@chakra-ui/react";
+import { useEffect, useState } from "react";
+import { PackageIcon } from "./icons";
export const NewVersionPopup = () => {
- const { typebot, save } = useTypebot()
- const [isReloading, setIsReloading] = useState(false)
- const { data } = trpc.getAppVersionProcedure.useQuery()
- const [currentVersion, setCurrentVersion] = useState()
- const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false)
+ const { typebot, save } = useTypebot();
+ const [isReloading, setIsReloading] = useState(false);
+ const { data } = trpc.getAppVersionProcedure.useQuery();
+ const [currentVersion, setCurrentVersion] = useState();
+ const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false);
useEffect(() => {
- if (!data?.commitSha) return
- if (currentVersion === data.commitSha) return
- setCurrentVersion(data.commitSha)
- if (currentVersion === undefined) return
- setIsNewVersionAvailable(true)
- }, [data, currentVersion])
+ if (!data?.commitSha) return;
+ if (currentVersion === data.commitSha) return;
+ setCurrentVersion(data.commitSha);
+ if (currentVersion === undefined) return;
+ setIsNewVersionAvailable(true);
+ }, [data, currentVersion]);
const saveAndReload = async () => {
- if (isReloading) return
- setIsReloading(true)
- if (save) await save()
- window.location.reload()
- }
+ if (isReloading) return;
+ setIsReloading(true);
+ if (save) await save();
+ window.location.reload();
+ };
return (
@@ -40,9 +40,9 @@ export const NewVersionPopup = () => {
in={isNewVersionAvailable}
offsetY="20px"
style={{
- position: 'fixed',
- bottom: '18px',
- left: '18px',
+ position: "fixed",
+ bottom: "18px",
+ left: "18px",
zIndex: 42,
}}
unmountOnExit
@@ -61,7 +61,7 @@ export const NewVersionPopup = () => {
- {' '}
+ {" "}
New version available!
@@ -72,7 +72,7 @@ export const NewVersionPopup = () => {
- {typebot?.id ? 'Save and reload' : 'Reload'}
+ {typebot?.id ? "Save and reload" : "Reload"}
@@ -80,5 +80,5 @@ export const NewVersionPopup = () => {
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/PrimitiveList.tsx b/apps/builder/src/components/PrimitiveList.tsx
index e41da46ef5..b4e9debfa6 100644
--- a/apps/builder/src/components/PrimitiveList.tsx
+++ b/apps/builder/src/components/PrimitiveList.tsx
@@ -1,86 +1,89 @@
-import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
-import { TrashIcon, PlusIcon } from '@/components/icons'
-import React, { useEffect, useState } from 'react'
-import { createId } from '@paralleldrive/cuid2'
+import { PlusIcon, TrashIcon } from "@/components/icons";
+import { Box, Button, Fade, Flex, IconButton, Stack } from "@chakra-ui/react";
+import { createId } from "@paralleldrive/cuid2";
+import React, { useEffect, useState } from "react";
-type ItemWithId = { id: string; value?: T }
+type ItemWithId = {
+ id: string;
+ value?: T;
+};
export type TableListItemProps = {
- item: T
- onItemChange: (item: T) => void
-}
+ item: T;
+ onItemChange: (item: T) => void;
+};
type Props = {
- initialItems?: T[]
- addLabel?: string
- newItemDefaultProps?: Partial
- hasDefaultItem?: boolean
- ComponentBetweenItems?: (props: unknown) => JSX.Element
- onItemsChange: (items: T[]) => void
- children: (props: TableListItemProps) => JSX.Element
-}
+ initialItems?: T[];
+ addLabel?: string;
+ newItemDefaultProps?: Partial;
+ hasDefaultItem?: boolean;
+ ComponentBetweenItems?: (props: unknown) => JSX.Element;
+ onItemsChange: (items: T[]) => void;
+ children: (props: TableListItemProps) => JSX.Element;
+};
const addIdToItems = (
- items: T[]
-): ItemWithId[] => items.map((item) => ({ id: createId(), value: item }))
+ items: T[],
+): ItemWithId[] => items.map((item) => ({ id: createId(), value: item }));
const removeIdFromItems = (
- items: ItemWithId[]
-): T[] => items.map((item) => item.value as T)
+ items: ItemWithId[],
+): T[] => items.map((item) => item.value as T);
export const PrimitiveList = ({
initialItems,
- addLabel = 'Add',
+ addLabel = "Add",
hasDefaultItem,
children,
ComponentBetweenItems,
onItemsChange,
}: Props) => {
- const [items, setItems] = useState[]>()
- const [showDeleteIndex, setShowDeleteIndex] = useState(null)
+ const [items, setItems] = useState[]>();
+ const [showDeleteIndex, setShowDeleteIndex] = useState(null);
useEffect(() => {
- if (items) return
+ if (items) return;
if (initialItems) {
- setItems(addIdToItems(initialItems))
+ setItems(addIdToItems(initialItems));
} else if (hasDefaultItem) {
- setItems(addIdToItems([]))
+ setItems(addIdToItems([]));
} else {
- setItems([])
+ setItems([]);
}
- }, [hasDefaultItem, initialItems, items])
+ }, [hasDefaultItem, initialItems, items]);
const createItem = () => {
- if (!items) return
- const newItems = [...items, { id: createId() }]
- setItems(newItems)
- onItemsChange(removeIdFromItems(newItems))
- }
+ if (!items) return;
+ const newItems = [...items, { id: createId() }];
+ setItems(newItems);
+ onItemsChange(removeIdFromItems(newItems));
+ };
const updateItem = (itemIndex: number, newValue: T) => {
- if (!items) return
+ if (!items) return;
const newItems = items.map((item, idx) =>
- idx === itemIndex ? { ...item, value: newValue } : item
- )
- setItems(newItems)
- onItemsChange(removeIdFromItems(newItems))
- }
+ idx === itemIndex ? { ...item, value: newValue } : item,
+ );
+ setItems(newItems);
+ onItemsChange(removeIdFromItems(newItems));
+ };
const deleteItem = (itemIndex: number) => () => {
- if (!items) return
- const newItems = [...items]
- newItems.splice(itemIndex, 1)
- setItems([...newItems])
- onItemsChange(removeIdFromItems([...newItems]))
- }
+ if (!items) return;
+ const newItems = [...items];
+ newItems.splice(itemIndex, 1);
+ setItems([...newItems]);
+ onItemsChange(removeIdFromItems([...newItems]));
+ };
const handleMouseEnter = (itemIndex: number) => () =>
- setShowDeleteIndex(itemIndex)
+ setShowDeleteIndex(itemIndex);
const handleCellChange = (itemIndex: number) => (item: T) =>
- updateItem(itemIndex, item)
+ updateItem(itemIndex, item);
- const handleMouseLeave = () => setShowDeleteIndex(null)
+ const handleMouseLeave = () => setShowDeleteIndex(null);
return (
@@ -104,9 +107,9 @@ export const PrimitiveList = ({
({
{addLabel}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/Seo.tsx b/apps/builder/src/components/Seo.tsx
index 34dd9f8edb..99a3f3861e 100644
--- a/apps/builder/src/components/Seo.tsx
+++ b/apps/builder/src/components/Seo.tsx
@@ -1,25 +1,25 @@
-import { env } from '@typebot.io/env'
-import Head from 'next/head'
+import { env } from "@typebot.io/env";
+import Head from "next/head";
const getOrigin = () => {
- if (typeof window !== 'undefined') {
- return window.location.origin
+ if (typeof window !== "undefined") {
+ return window.location.origin;
}
- return env.NEXTAUTH_URL
-}
+ return env.NEXTAUTH_URL;
+};
export const Seo = ({
title,
- description = 'Create and publish conversational forms that collect 4 times more answers and feel native to your product',
+ description = "Create and publish conversational forms that collect 4 times more answers and feel native to your product",
imagePreviewUrl = `${getOrigin()}/images/og.png`,
}: {
- title: string
- description?: string
- currentUrl?: string
- imagePreviewUrl?: string
+ title: string;
+ description?: string;
+ currentUrl?: string;
+ imagePreviewUrl?: string;
}) => {
- const formattedTitle = `${title} | Typebot`
+ const formattedTitle = `${title} | Typebot`;
return (
@@ -38,5 +38,5 @@ export const Seo = ({
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/SetVariableLabel.tsx b/apps/builder/src/components/SetVariableLabel.tsx
index 4efd273958..36f4b30f74 100644
--- a/apps/builder/src/components/SetVariableLabel.tsx
+++ b/apps/builder/src/components/SetVariableLabel.tsx
@@ -1,29 +1,29 @@
-import { useColorModeValue, HStack, Tag, Text } from '@chakra-ui/react'
-import { useTranslate } from '@tolgee/react'
-import { Variable } from '@typebot.io/schemas'
+import { HStack, Tag, Text, useColorModeValue } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import type { Variable } from "@typebot.io/variables/schemas";
export const SetVariableLabel = ({
variableId,
variables,
}: {
- variableId: string
- variables?: Variable[]
+ variableId: string;
+ variables?: Variable[];
}) => {
- const { t } = useTranslate()
- const textColor = useColorModeValue('gray.600', 'gray.400')
+ const { t } = useTranslate();
+ const textColor = useColorModeValue("gray.600", "gray.400");
const variableName = variables?.find(
- (variable) => variable.id === variableId
- )?.name
+ (variable) => variable.id === variableId,
+ )?.name;
- if (!variableName) return null
+ if (!variableName) return null;
return (
- {t('variables.set')}
+ {t("variables.set")}
{variableName}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/SupportBubble.tsx b/apps/builder/src/components/SupportBubble.tsx
index b9b6b84fe3..9539e725f3 100644
--- a/apps/builder/src/components/SupportBubble.tsx
+++ b/apps/builder/src/components/SupportBubble.tsx
@@ -1,43 +1,44 @@
-import { useTypebot } from '@/features/editor/providers/TypebotProvider'
-import { useUser } from '@/features/account/hooks/useUser'
-import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
-import React, { useEffect, useState } from 'react'
-import { Bubble, BubbleProps } from '@typebot.io/nextjs'
-import { planToReadable } from '@/features/billing/helpers/planToReadable'
-import { Plan } from '@typebot.io/prisma'
+import { useUser } from "@/features/account/hooks/useUser";
+import { planToReadable } from "@/features/billing/helpers/planToReadable";
+import { useTypebot } from "@/features/editor/providers/TypebotProvider";
+import { useWorkspace } from "@/features/workspace/WorkspaceProvider";
+import type { BubbleProps } from "@typebot.io/js";
+import { Bubble } from "@typebot.io/nextjs";
+import { Plan } from "@typebot.io/prisma/enum";
+import { useEffect, useState } from "react";
-export const SupportBubble = (props: Omit) => {
- const { typebot } = useTypebot()
- const { user } = useUser()
- const { workspace } = useWorkspace()
+export const SupportBubble = (props: Omit) => {
+ const { typebot } = useTypebot();
+ const { user } = useUser();
+ const { workspace } = useWorkspace();
- const [lastViewedTypebotId, setLastViewedTypebotId] = useState(typebot?.id)
+ const [lastViewedTypebotId, setLastViewedTypebotId] = useState(typebot?.id);
useEffect(() => {
- if (!typebot?.id) return
- if (lastViewedTypebotId === typebot?.id) return
- setLastViewedTypebotId(typebot?.id)
- }, [lastViewedTypebotId, typebot?.id])
+ if (!typebot?.id) return;
+ if (lastViewedTypebotId === typebot?.id) return;
+ setLastViewedTypebotId(typebot?.id);
+ }, [lastViewedTypebotId, typebot?.id]);
- if (!workspace?.plan || workspace.plan === Plan.FREE) return null
+ if (!workspace?.plan || workspace.plan === Plan.FREE) return null;
return (
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/SwitchWithRelatedSettings.tsx b/apps/builder/src/components/SwitchWithRelatedSettings.tsx
index 9e26394a1e..728723fe71 100644
--- a/apps/builder/src/components/SwitchWithRelatedSettings.tsx
+++ b/apps/builder/src/components/SwitchWithRelatedSettings.tsx
@@ -1,17 +1,18 @@
-import React from 'react'
-import { SwitchWithLabel, SwitchWithLabelProps } from './inputs/SwitchWithLabel'
-import { Stack } from '@chakra-ui/react'
+import { Stack } from "@chakra-ui/react";
+import React from "react";
+import type { SwitchWithLabelProps } from "./inputs/SwitchWithLabel";
+import { SwitchWithLabel } from "./inputs/SwitchWithLabel";
-type Props = SwitchWithLabelProps
+type Props = SwitchWithLabelProps;
export const SwitchWithRelatedSettings = ({ children, ...props }: Props) => (
{props.initialValue && children}
-)
+);
diff --git a/apps/builder/src/components/TableList.tsx b/apps/builder/src/components/TableList.tsx
index a12716346b..b9a5a74b91 100644
--- a/apps/builder/src/components/TableList.tsx
+++ b/apps/builder/src/components/TableList.tsx
@@ -1,3 +1,4 @@
+import { PlusIcon, TrashIcon } from "@/components/icons";
import {
Box,
Button,
@@ -6,35 +7,34 @@ import {
IconButton,
SlideFade,
Stack,
-} from '@chakra-ui/react'
-import { TrashIcon, PlusIcon } from '@/components/icons'
-import { createId } from '@paralleldrive/cuid2'
-import React, { useEffect, useState } from 'react'
+} from "@chakra-ui/react";
+import { createId } from "@paralleldrive/cuid2";
+import React, { useEffect, useState } from "react";
const defaultItem = {
id: createId(),
-}
+};
export type TableListItemProps = {
- item: T
- onItemChange: (item: T) => void
-}
+ item: T;
+ onItemChange: (item: T) => void;
+};
type Props = {
- initialItems?: T[]
- isOrdered?: boolean
- addLabel?: string
- newItemDefaultProps?: Partial
- hasDefaultItem?: boolean
- ComponentBetweenItems?: (props: unknown) => JSX.Element
- onItemsChange: (items: T[]) => void
- children: (props: TableListItemProps) => JSX.Element
-}
+ initialItems?: T[];
+ isOrdered?: boolean;
+ addLabel?: string;
+ newItemDefaultProps?: Partial;
+ hasDefaultItem?: boolean;
+ ComponentBetweenItems?: (props: unknown) => JSX.Element;
+ onItemsChange: (items: T[]) => void;
+ children: (props: TableListItemProps) => JSX.Element;
+};
export const TableList = ({
initialItems,
isOrdered,
- addLabel = 'Add',
+ addLabel = "Add",
newItemDefaultProps,
hasDefaultItem,
children,
@@ -43,58 +43,58 @@ export const TableList = ({
}: Props) => {
const [items, setItems] = useState(
addIdsIfMissing(initialItems) ??
- (hasDefaultItem ? ([defaultItem] as T[]) : [])
- )
- const [showDeleteIndex, setShowDeleteIndex] = useState(null)
+ (hasDefaultItem ? ([defaultItem] as T[]) : []),
+ );
+ const [showDeleteIndex, setShowDeleteIndex] = useState(null);
useEffect(() => {
if (items.length && initialItems && initialItems?.length === 0)
- setItems(initialItems)
- }, [initialItems, items.length])
+ setItems(initialItems);
+ }, [initialItems, items.length]);
const createItem = () => {
- const id = createId()
- const newItem = { id, ...newItemDefaultProps } as T
- setItems([...items, newItem])
- onItemsChange([...items, newItem])
- }
+ const id = createId();
+ const newItem = { id, ...newItemDefaultProps } as T;
+ setItems([...items, newItem]);
+ onItemsChange([...items, newItem]);
+ };
const insertItem = (itemIndex: number) => () => {
- const id = createId()
- const newItem = { id } as T
- const newItems = [...items]
- newItems.splice(itemIndex + 1, 0, newItem)
- setItems(newItems)
- onItemsChange(newItems)
- }
+ const id = createId();
+ const newItem = { id } as T;
+ const newItems = [...items];
+ newItems.splice(itemIndex + 1, 0, newItem);
+ setItems(newItems);
+ onItemsChange(newItems);
+ };
const updateItem = (itemIndex: number, updates: Partial) => {
const newItems = items.map((item, idx) =>
- idx === itemIndex ? { ...item, ...updates } : item
- )
- setItems(newItems)
- onItemsChange(newItems)
- }
+ idx === itemIndex ? { ...item, ...updates } : item,
+ );
+ setItems(newItems);
+ onItemsChange(newItems);
+ };
const deleteItem = (itemIndex: number) => () => {
- const newItems = [...items]
- newItems.splice(itemIndex, 1)
- setItems([...newItems])
- onItemsChange([...newItems])
- }
+ const newItems = [...items];
+ newItems.splice(itemIndex, 1);
+ setItems([...newItems]);
+ onItemsChange([...newItems]);
+ };
const handleMouseEnter = (itemIndex: number) => () =>
- setShowDeleteIndex(itemIndex)
+ setShowDeleteIndex(itemIndex);
const handleCellChange = (itemIndex: number) => (item: T) =>
- updateItem(itemIndex, item)
+ updateItem(itemIndex, item);
- const handleMouseLeave = () => setShowDeleteIndex(null)
+ const handleMouseLeave = () => setShowDeleteIndex(null);
return (
{items.map((item, itemIndex) => (
-
+
{itemIndex !== 0 && ComponentBetweenItems && (
)}
@@ -110,9 +110,9 @@ export const TableList = ({
@@ -131,8 +131,8 @@ export const TableList = ({
offsetY="-5px"
in={showDeleteIndex === itemIndex}
style={{
- position: 'absolute',
- top: '-15px',
+ position: "absolute",
+ top: "-15px",
}}
unmountOnExit
>
@@ -150,8 +150,8 @@ export const TableList = ({
offsetY="5px"
in={showDeleteIndex === itemIndex}
style={{
- position: 'absolute',
- bottom: '5px',
+ position: "absolute",
+ bottom: "5px",
}}
unmountOnExit
>
@@ -180,11 +180,11 @@ export const TableList = ({
)}
- )
-}
+ );
+};
const addIdsIfMissing = (items?: T[]): T[] | undefined =>
items?.map((item) => ({
id: createId(),
...item,
- }))
+ }));
diff --git a/apps/builder/src/components/TagsInput.tsx b/apps/builder/src/components/TagsInput.tsx
index c35ac010ae..d803456af8 100644
--- a/apps/builder/src/components/TagsInput.tsx
+++ b/apps/builder/src/components/TagsInput.tsx
@@ -1,100 +1,100 @@
+import { colors } from "@/lib/theme";
import {
HStack,
IconButton,
- Wrap,
+ Input,
Text,
+ Wrap,
WrapItem,
- Input,
-} from '@chakra-ui/react'
-import { useRef, useState } from 'react'
-import { CloseIcon } from './icons'
-import { colors } from '@/lib/theme'
-import { AnimatePresence, motion } from 'framer-motion'
-import { convertStrToList } from '@typebot.io/lib/convertStrToList'
-import { isEmpty } from '@typebot.io/lib/utils'
+} from "@chakra-ui/react";
+import { convertStrToList } from "@typebot.io/lib/convertStrToList";
+import { isEmpty } from "@typebot.io/lib/utils";
+import { AnimatePresence, motion } from "framer-motion";
+import { useRef, useState } from "react";
+import { CloseIcon } from "./icons";
type Props = {
- items?: string[]
- placeholder?: string
- onChange: (value: string[]) => void
-}
+ items?: string[];
+ placeholder?: string;
+ onChange: (value: string[]) => void;
+};
export const TagsInput = ({ items, placeholder, onChange }: Props) => {
- const inputRef = useRef(null)
- const [inputValue, setInputValue] = useState('')
- const [isFocused, setIsFocused] = useState(false)
- const [focusedTagIndex, setFocusedTagIndex] = useState()
+ const inputRef = useRef(null);
+ const [inputValue, setInputValue] = useState("");
+ const [isFocused, setIsFocused] = useState(false);
+ const [focusedTagIndex, setFocusedTagIndex] = useState();
const handleInputChange = (e: React.ChangeEvent) => {
- e.preventDefault()
- setFocusedTagIndex(undefined)
- setInputValue(e.target.value)
+ e.preventDefault();
+ setFocusedTagIndex(undefined);
+ setInputValue(e.target.value);
if (e.target.value.length - inputValue.length > 0) {
- const values = convertStrToList(e.target.value)
+ const values = convertStrToList(e.target.value);
if (values.length > 1) {
- onChange([...(items ?? []), ...convertStrToList(e.target.value)])
- setInputValue('')
+ onChange([...(items ?? []), ...convertStrToList(e.target.value)]);
+ setInputValue("");
}
}
- }
+ };
const handleKeyDown = (e: React.KeyboardEvent) => {
- if (!items) return
+ if (!items) return;
- if (e.key === 'Backspace') {
+ if (e.key === "Backspace") {
if (focusedTagIndex !== undefined) {
if (focusedTagIndex === items.length - 1) {
- setFocusedTagIndex((idx) => idx! - 1)
+ setFocusedTagIndex((idx) => idx! - 1);
}
- removeItem(focusedTagIndex)
- return
+ removeItem(focusedTagIndex);
+ return;
}
- if (inputValue === '' && focusedTagIndex === undefined) {
- setFocusedTagIndex(items?.length - 1)
- return
+ if (inputValue === "" && focusedTagIndex === undefined) {
+ setFocusedTagIndex(items?.length - 1);
+ return;
}
}
- if (e.key === 'ArrowLeft') {
+ if (e.key === "ArrowLeft") {
if (focusedTagIndex !== undefined) {
- if (focusedTagIndex === 0) return
- setFocusedTagIndex(focusedTagIndex - 1)
- return
+ if (focusedTagIndex === 0) return;
+ setFocusedTagIndex(focusedTagIndex - 1);
+ return;
}
if (inputRef.current?.selectionStart === 0 && items) {
- setFocusedTagIndex(items.length - 1)
- return
+ setFocusedTagIndex(items.length - 1);
+ return;
}
}
- if (e.key === 'ArrowRight' && focusedTagIndex !== undefined) {
+ if (e.key === "ArrowRight" && focusedTagIndex !== undefined) {
if (focusedTagIndex === items.length - 1) {
- setFocusedTagIndex(undefined)
- return
+ setFocusedTagIndex(undefined);
+ return;
}
- setFocusedTagIndex(focusedTagIndex + 1)
+ setFocusedTagIndex(focusedTagIndex + 1);
}
- }
+ };
const removeItem = (index: number) => {
- if (!items) return
- const newItems = [...items]
- newItems.splice(index, 1)
- onChange(newItems)
- }
+ if (!items) return;
+ const newItems = [...items];
+ newItems.splice(index, 1);
+ onChange(newItems);
+ };
const addItem = (e: React.FormEvent) => {
- e.preventDefault()
- if (isEmpty(inputValue)) return
- setInputValue('')
- onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()])
- }
+ e.preventDefault();
+ if (isEmpty(inputValue)) return;
+ setInputValue("");
+ onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()]);
+ };
return (
{
{items?.map((item, index) => (
@@ -138,17 +138,17 @@ export const TagsInput = ({ items, placeholder, onChange }: Props) => {
- )
-}
+ );
+};
const Tag = ({
isFocused,
content,
onDeleteClick,
}: {
- isFocused?: boolean
- content: string
- onDeleteClick: () => void
+ isFocused?: boolean;
+ content: string;
+ onDeleteClick: () => void;
}) => (
{content}
@@ -170,4 +170,4 @@ const Tag = ({
onClick={onDeleteClick}
/>
-)
+);
diff --git a/apps/builder/src/components/TextLink.tsx b/apps/builder/src/components/TextLink.tsx
index 5bd28d2254..0db9da1902 100644
--- a/apps/builder/src/components/TextLink.tsx
+++ b/apps/builder/src/components/TextLink.tsx
@@ -1,9 +1,9 @@
-import Link, { LinkProps } from 'next/link'
-import React from 'react'
-import { chakra, HStack, TextProps } from '@chakra-ui/react'
-import { ExternalLinkIcon } from '@/components/icons'
+import { ExternalLinkIcon } from "@/components/icons";
+import { HStack, type TextProps, chakra } from "@chakra-ui/react";
+import Link, { type LinkProps } from "next/link";
+import React from "react";
-type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean }
+type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean };
export const TextLink = ({
children,
@@ -22,7 +22,7 @@ export const TextLink = ({
replace={replace}
scroll={scroll}
prefetch={prefetch}
- target={isExternal ? '_blank' : undefined}
+ target={isExternal ? "_blank" : undefined}
>
{isExternal ? (
@@ -37,4 +37,4 @@ export const TextLink = ({
)}
-)
+);
diff --git a/apps/builder/src/components/TimeSince.tsx b/apps/builder/src/components/TimeSince.tsx
index 4dc614dea4..928edfb013 100644
--- a/apps/builder/src/components/TimeSince.tsx
+++ b/apps/builder/src/components/TimeSince.tsx
@@ -1,46 +1,46 @@
-import { T } from '@tolgee/react'
+import { T } from "@tolgee/react";
type Props = {
- date: string
-}
+ date: string;
+};
export const TimeSince = ({ date }: Props) => {
const seconds = Math.floor(
- (new Date().getTime() - new Date(date).getTime()) / 1000
- )
+ (new Date().getTime() - new Date(date).getTime()) / 1000,
+ );
- let interval = seconds / 31536000
+ let interval = seconds / 31536000;
if (interval > 1) {
return (
- )
+ );
}
- interval = seconds / 2592000
+ interval = seconds / 2592000;
if (interval > 1) {
return (
- )
+ );
}
- interval = seconds / 86400
+ interval = seconds / 86400;
if (interval > 1) {
return (
- )
+ );
}
- interval = seconds / 3600
+ interval = seconds / 3600;
if (interval > 1) {
return (
- )
+ );
}
- interval = seconds / 60
+ interval = seconds / 60;
if (interval > 1) {
return (
- )
+ );
}
return (
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/Toast.tsx b/apps/builder/src/components/Toast.tsx
index 47b1b6dae6..2198a23602 100644
--- a/apps/builder/src/components/Toast.tsx
+++ b/apps/builder/src/components/Toast.tsx
@@ -10,27 +10,27 @@ import {
Stack,
Text,
useColorModeValue,
-} from '@chakra-ui/react'
-import { AlertIcon, CloseIcon, InfoIcon, SmileIcon } from './icons'
-import { CodeEditor } from './inputs/CodeEditor'
-import { LanguageName } from '@uiw/codemirror-extensions-langs'
+} from "@chakra-ui/react";
+import type { LanguageName } from "@uiw/codemirror-extensions-langs";
+import { AlertIcon, CloseIcon, InfoIcon, SmileIcon } from "./icons";
+import { CodeEditor } from "./inputs/CodeEditor";
export type ToastProps = {
- title?: string
- description?: string
+ title?: string;
+ description?: string;
details?: {
- content: string
- lang: LanguageName
- }
- status?: 'info' | 'error' | 'success'
- icon?: React.ReactNode
- primaryButton?: React.ReactNode
- secondaryButton?: React.ReactNode
- onClose: () => void
-}
+ content: string;
+ lang: LanguageName;
+ };
+ status?: "info" | "error" | "success";
+ icon?: React.ReactNode;
+ primaryButton?: React.ReactNode;
+ secondaryButton?: React.ReactNode;
+ onClose: () => void;
+};
export const Toast = ({
- status = 'error',
+ status = "error",
title,
description,
details,
@@ -39,8 +39,8 @@ export const Toast = ({
secondaryButton,
onClose,
}: ToastProps) => {
- const bgColor = useColorModeValue('white', 'gray.800')
- const detailsLabelColor = useColorModeValue('gray.600', 'gray.400')
+ const bgColor = useColorModeValue("white", "gray.800");
+ const detailsLabelColor = useColorModeValue("gray.600", "gray.400");
return (
- {' '}
+ {" "}
{title && {title} }
@@ -106,19 +106,19 @@ export const Toast = ({
right={1}
/>
- )
-}
+ );
+};
const Icon = ({
customIcon,
status,
}: {
- customIcon?: React.ReactNode
- status: ToastProps['status']
+ customIcon?: React.ReactNode;
+ status: ToastProps["status"];
}) => {
- const accentColor = useColorModeValue('50', '0')
- const color = parseColor(status)
- const icon = parseIcon(status, customIcon)
+ const accentColor = useColorModeValue("50", "0");
+ const color = parseColor(status);
+ const icon = parseIcon(status, customIcon);
return (
- )
-}
+ );
+};
-const parseColor = (status: ToastProps['status']) => {
- if (!status) return 'red'
+const parseColor = (status: ToastProps["status"]) => {
+ if (!status) return "red";
switch (status) {
- case 'error':
- return 'red'
- case 'success':
- return 'green'
- case 'info':
- return 'blue'
+ case "error":
+ return "red";
+ case "success":
+ return "green";
+ case "info":
+ return "blue";
}
-}
+};
const parseIcon = (
- status: ToastProps['status'],
- customIcon?: React.ReactNode
+ status: ToastProps["status"],
+ customIcon?: React.ReactNode,
) => {
- if (customIcon) return customIcon
+ if (customIcon) return customIcon;
switch (status) {
- case 'error':
- return
- case 'success':
- return
- case 'info':
- return
+ case "error":
+ return ;
+ case "success":
+ return ;
+ case "info":
+ return ;
}
-}
+};
diff --git a/apps/builder/src/components/Toaster.tsx b/apps/builder/src/components/Toaster.tsx
index 4a8d04cc7a..f418efd19a 100644
--- a/apps/builder/src/components/Toaster.tsx
+++ b/apps/builder/src/components/Toaster.tsx
@@ -1,9 +1,9 @@
-import { colors } from '@/lib/theme'
-import { useColorMode, useColorModeValue } from '@chakra-ui/react'
-import { Toaster as SonnerToaster } from 'sonner'
+import { colors } from "@/lib/theme";
+import { useColorMode, useColorModeValue } from "@chakra-ui/react";
+import { Toaster as SonnerToaster } from "sonner";
export const Toaster = () => {
- const { colorMode } = useColorMode()
+ const { colorMode } = useColorMode();
const theme = useColorModeValue(
{
bg: undefined,
@@ -13,9 +13,9 @@ export const Toaster = () => {
{
bg: colors.gray[900],
actionBg: colors.blue[400],
- actionColor: 'white',
- }
- )
+ actionColor: "white",
+ },
+ );
return (
{
},
}}
/>
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/TypebotLogo.tsx b/apps/builder/src/components/TypebotLogo.tsx
index 96a22d0812..b0fef87b7a 100644
--- a/apps/builder/src/components/TypebotLogo.tsx
+++ b/apps/builder/src/components/TypebotLogo.tsx
@@ -1,4 +1,4 @@
-import { IconProps, Icon } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const TypebotLogo = ({
isDark,
@@ -9,7 +9,7 @@ export const TypebotLogo = ({
width="800"
height="800"
rx="80"
- fill={isDark ? 'white' : '#0042DA'}
+ fill={isDark ? "white" : "#0042DA"}
/>
-)
+);
diff --git a/apps/builder/src/components/UnlockPlanAlertInfo.tsx b/apps/builder/src/components/UnlockPlanAlertInfo.tsx
index 29d726c6e0..7db6c5c65e 100644
--- a/apps/builder/src/components/UnlockPlanAlertInfo.tsx
+++ b/apps/builder/src/components/UnlockPlanAlertInfo.tsx
@@ -1,23 +1,21 @@
+import type { ChangePlanModalProps } from "@/features/billing/components/ChangePlanModal";
+import { ChangePlanModal } from "@/features/billing/components/ChangePlanModal";
import {
Alert,
AlertIcon,
- AlertProps,
+ type AlertProps,
Button,
HStack,
Text,
useDisclosure,
-} from '@chakra-ui/react'
-import React from 'react'
-import {
- ChangePlanModal,
- ChangePlanModalProps,
-} from '@/features/billing/components/ChangePlanModal'
-import { useTranslate } from '@tolgee/react'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import React from "react";
type Props = {
- buttonLabel?: string
+ buttonLabel?: string;
} & AlertProps &
- Pick
+ Pick;
export const UnlockPlanAlertInfo = ({
buttonLabel,
@@ -25,8 +23,8 @@ export const UnlockPlanAlertInfo = ({
excludedPlans,
...props
}: Props) => {
- const { t } = useTranslate()
- const { isOpen, onOpen, onClose } = useDisclosure()
+ const { t } = useTranslate();
+ const { isOpen, onOpen, onClose } = useDisclosure();
return (
{props.children}
- {buttonLabel ?? t('billing.upgradeAlert.buttonDefaultLabel')}
+ {buttonLabel ?? t("billing.upgradeAlert.buttonDefaultLabel")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx b/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx
index a29c85bba4..b95c8b9d49 100644
--- a/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx
+++ b/apps/builder/src/components/VideoUploadContent/PexelsPicker.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-hooks/exhaustive-deps */
import {
Alert,
AlertIcon,
@@ -12,87 +11,93 @@ import {
Spinner,
Stack,
Text,
-} from '@chakra-ui/react'
-import { isDefined } from '@typebot.io/lib'
-import { useCallback, useEffect, useRef, useState } from 'react'
-import { createClient, Video, ErrorResponse, Videos } from 'pexels'
-import { TextInput } from '../inputs'
-import { TextLink } from '../TextLink'
-import { env } from '@typebot.io/env'
-import { PexelsLogo } from '../logos/PexelsLogo'
+} from "@chakra-ui/react";
+import { env } from "@typebot.io/env";
+import { isDefined } from "@typebot.io/lib/utils";
+import {
+ type ErrorResponse,
+ type Video,
+ type Videos,
+ createClient,
+} from "pexels";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { TextLink } from "../TextLink";
+import { TextInput } from "../inputs";
+import { PexelsLogo } from "../logos/PexelsLogo";
-const client = createClient(env.NEXT_PUBLIC_PEXELS_API_KEY ?? 'dummy')
+/* eslint-disable react-hooks/exhaustive-deps */
+const client = createClient(env.NEXT_PUBLIC_PEXELS_API_KEY ?? "dummy");
type Props = {
- videoSize: 'large' | 'medium' | 'small'
- onVideoSelect: (videoUrl: string) => void
-}
+ videoSize: "large" | "medium" | "small";
+ onVideoSelect: (videoUrl: string) => void;
+};
export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => {
- const [isFetching, setIsFetching] = useState(false)
- const [videos, setVideos] = useState([])
- const [error, setError] = useState(null)
- const [searchQuery, setSearchQuery] = useState('')
- const scrollContainer = useRef(null)
- const bottomAnchor = useRef(null)
+ const [isFetching, setIsFetching] = useState(false);
+ const [videos, setVideos] = useState([]);
+ const [error, setError] = useState(null);
+ const [searchQuery, setSearchQuery] = useState("");
+ const scrollContainer = useRef(null);
+ const bottomAnchor = useRef(null);
- const [nextPage, setNextPage] = useState(0)
+ const [nextPage, setNextPage] = useState(0);
const fetchNewVideos = useCallback(async (query: string, page: number) => {
- if (query === '') getInitialVideos()
+ if (query === "") getInitialVideos();
if (query.length <= 2) {
- setNextPage(0)
- return
+ setNextPage(0);
+ return;
}
- setError(null)
- setIsFetching(true)
+ setError(null);
+ setIsFetching(true);
try {
const result = await client.videos.search({
query,
per_page: 24,
size: videoSize,
page,
- })
+ });
if ((result as ErrorResponse).error)
- setError((result as ErrorResponse).error)
+ setError((result as ErrorResponse).error);
if (isDefined((result as Videos).videos)) {
- if (page === 0) setVideos((result as Videos).videos)
+ if (page === 0) setVideos((result as Videos).videos);
else
setVideos((videos) => [
...videos,
...((result as Videos)?.videos ?? []),
- ])
- setNextPage((page) => page + 1)
+ ]);
+ setNextPage((page) => page + 1);
}
} catch (err) {
- if (err && typeof err === 'object' && 'message' in err)
- setError(err.message as string)
- setError('Something went wrong')
+ if (err && typeof err === "object" && "message" in err)
+ setError(err.message as string);
+ setError("Something went wrong");
}
- setIsFetching(false)
- }, [])
+ setIsFetching(false);
+ }, []);
useEffect(() => {
- if (!bottomAnchor.current) return
+ if (!bottomAnchor.current) return;
const observer = new IntersectionObserver(
(entities: IntersectionObserverEntry[]) => {
- const target = entities[0]
- if (target.isIntersecting) fetchNewVideos(searchQuery, nextPage + 1)
+ const target = entities[0];
+ if (target.isIntersecting) fetchNewVideos(searchQuery, nextPage + 1);
},
{
root: scrollContainer.current,
- }
- )
+ },
+ );
if (bottomAnchor.current && nextPage > 0)
- observer.observe(bottomAnchor.current)
+ observer.observe(bottomAnchor.current);
return () => {
- observer.disconnect()
- }
- }, [fetchNewVideos, nextPage, searchQuery])
+ observer.disconnect();
+ };
+ }, [fetchNewVideos, nextPage, searchQuery]);
const getInitialVideos = async () => {
- setError(null)
- setIsFetching(true)
+ setError(null);
+ setIsFetching(true);
client.videos
.popular({
per_page: 24,
@@ -100,31 +105,31 @@ export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => {
})
.then((res) => {
if ((res as ErrorResponse).error) {
- setError((res as ErrorResponse).error)
+ setError((res as ErrorResponse).error);
}
- setVideos((res as Videos).videos)
- setIsFetching(false)
+ setVideos((res as Videos).videos);
+ setIsFetching(false);
})
.catch((err) => {
- if (err && typeof err === 'object' && 'message' in err)
- setError(err.message as string)
- setError('Something went wrong')
- setIsFetching(false)
- })
- }
+ if (err && typeof err === "object" && "message" in err)
+ setError(err.message as string);
+ setError("Something went wrong");
+ setIsFetching(false);
+ });
+ };
const selectVideo = (video: Video) => {
- const videoUrl = video.video_files[0].link
- if (isDefined(videoUrl)) onVideoSelect(videoUrl)
- }
+ const videoUrl = video.video_files[0].link;
+ if (isDefined(videoUrl)) onVideoSelect(videoUrl);
+ };
useEffect(() => {
- if (!env.NEXT_PUBLIC_PEXELS_API_KEY) return
- getInitialVideos()
- }, [])
+ if (!env.NEXT_PUBLIC_PEXELS_API_KEY) return;
+ getInitialVideos();
+ }, []);
if (!env.NEXT_PUBLIC_PEXELS_API_KEY)
- return NEXT_PUBLIC_PEXELS_API_KEY is missing in environment
+ return NEXT_PUBLIC_PEXELS_API_KEY is missing in environment ;
return (
@@ -133,8 +138,8 @@ export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => {
autoFocus
placeholder="Search..."
onChange={(query) => {
- setSearchQuery(query)
- fetchNewVideos(query, 0)
+ setSearchQuery(query);
+ fetchNewVideos(query, 0);
}}
withVariableButton={false}
debounceTimeout={500}
@@ -174,41 +179,41 @@ export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => {
)}
- )
-}
+ );
+};
type PexelsVideoProps = {
- video: Video
- onClick: () => void
-}
+ video: Video;
+ onClick: () => void;
+};
const PexelsVideo = ({ video, onClick }: PexelsVideoProps) => {
- const { user, url, video_pictures } = video
- const [isImageHovered, setIsImageHovered] = useState(false)
+ const { user, url, video_pictures } = video;
+ const [isImageHovered, setIsImageHovered] = useState(false);
const [thumbnailImage, setThumbnailImage] = useState(
- video_pictures[0].picture
- )
- const [imageIndex, setImageIndex] = useState(1)
+ video_pictures[0].picture,
+ );
+ const [imageIndex, setImageIndex] = useState(1);
useEffect(() => {
- let interval: NodeJS.Timer
+ let interval: NodeJS.Timer;
if (isImageHovered && video_pictures.length > 0) {
interval = setInterval(() => {
- setImageIndex((prevIndex) => (prevIndex + 1) % video_pictures.length)
- setThumbnailImage(video_pictures[imageIndex].picture)
- }, 200)
+ setImageIndex((prevIndex) => (prevIndex + 1) % video_pictures.length);
+ setThumbnailImage(video_pictures[imageIndex].picture);
+ }, 200);
} else {
- setThumbnailImage(video_pictures[0].picture)
- setImageIndex(1)
+ setThumbnailImage(video_pictures[0].picture);
+ setImageIndex(1);
}
return () => {
if (interval) {
- clearInterval(interval)
+ clearInterval(interval);
}
- }
- }, [isImageHovered, imageIndex, video_pictures])
+ };
+ }, [isImageHovered, imageIndex, video_pictures]);
return (
{
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx b/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx
index 2dda4fe9b3..d316aa26a3 100644
--- a/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx
+++ b/apps/builder/src/components/VideoUploadContent/VideoLinkEmbedContent.tsx
@@ -1,38 +1,38 @@
-import { Stack, Text } from '@chakra-ui/react'
-import { useTranslate } from '@tolgee/react'
-import { VideoBubbleBlock } from '@typebot.io/schemas'
-import { TextInput } from '@/components/inputs'
-import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
-import { SwitchWithLabel } from '../inputs/SwitchWithLabel'
+import { TextInput } from "@/components/inputs";
+import { Stack, Text } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { defaultVideoBubbleContent } from "@typebot.io/blocks-bubbles/video/constants";
+import type { VideoBubbleBlock } from "@typebot.io/blocks-bubbles/video/schema";
+import { SwitchWithLabel } from "../inputs/SwitchWithLabel";
export const VideoLinkEmbedContent = ({
content,
updateUrl,
onSubmit,
}: {
- content?: VideoBubbleBlock['content']
- updateUrl: (url: string) => void
- onSubmit: (content: VideoBubbleBlock['content']) => void
+ content?: VideoBubbleBlock["content"];
+ updateUrl: (url: string) => void;
+ onSubmit: (content: VideoBubbleBlock["content"]) => void;
}) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const updateAspectRatio = (aspectRatio?: string) => {
return onSubmit({
...content,
aspectRatio,
- })
- }
+ });
+ };
const updateMaxWidth = (maxWidth?: string) => {
return onSubmit({
...content,
maxWidth,
- })
- }
+ });
+ };
const updateAutoPlay = (isAutoplayEnabled: boolean) => {
- return onSubmit({ ...content, isAutoplayEnabled })
- }
+ return onSubmit({ ...content, isAutoplayEnabled });
+ };
const updateControlsDisplay = (areControlsDisplayed: boolean) => {
if (areControlsDisplayed === false) {
@@ -41,28 +41,28 @@ export const VideoLinkEmbedContent = ({
...content,
isAutoplayEnabled: true,
areControlsDisplayed,
- })
+ });
}
- return onSubmit({ ...content, areControlsDisplayed })
- }
+ return onSubmit({ ...content, areControlsDisplayed });
+ };
return (
<>
- {t('video.urlInput.helperText')}
+ {t("video.urlInput.helperText")}
{content?.url && (
)}
- {content?.url && content?.type === 'url' && (
+ {content?.url && content?.type === "url" && (
)}
>
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/icons.tsx b/apps/builder/src/components/icons.tsx
index bad82bc2e3..65137a372e 100644
--- a/apps/builder/src/components/icons.tsx
+++ b/apps/builder/src/components/icons.tsx
@@ -1,12 +1,12 @@
-import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react'
+import { Icon, type IconProps, useColorModeValue } from "@chakra-ui/react";
export const featherIconsBaseProps: IconProps = {
- fill: 'none',
- stroke: 'currentColor',
- strokeWidth: '2px',
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
-}
+ fill: "none",
+ stroke: "currentColor",
+ strokeWidth: "2px",
+ strokeLinecap: "round",
+ strokeLinejoin: "round",
+};
// 99% of these icons are from Feather icons (https://feathericons.com/)
@@ -15,7 +15,7 @@ export const SettingsIcon = (props: IconProps) => (
-)
+);
export const LogOutIcon = (props: IconProps) => (
@@ -23,32 +23,32 @@ export const LogOutIcon = (props: IconProps) => (
-)
+);
export const ChevronLeftIcon = (props: IconProps) => (
-)
+);
export const ChevronRightIcon = (props: IconProps) => (
-)
+);
export const ChevronDownIcon = (props: IconProps) => (
-)
+);
export const PlusIcon = (props: IconProps) => (
-)
+);
export const FolderIcon = (props: IconProps) => (
(
>
-)
+);
export const MoreVerticalIcon = (props: IconProps) => (
@@ -67,7 +67,7 @@ export const MoreVerticalIcon = (props: IconProps) => (
-)
+);
export const MoreHorizontalIcon = (props: IconProps) => (
@@ -75,7 +75,7 @@ export const MoreHorizontalIcon = (props: IconProps) => (
-)
+);
export const GlobeIcon = (props: IconProps) => (
@@ -83,13 +83,13 @@ export const GlobeIcon = (props: IconProps) => (
-)
+);
export const ToolIcon = (props: IconProps) => (
-)
+);
export const FolderPlusIcon = (props: IconProps) => (
@@ -97,7 +97,7 @@ export const FolderPlusIcon = (props: IconProps) => (
-)
+);
export const TextIcon = (props: IconProps) => (
@@ -105,7 +105,7 @@ export const TextIcon = (props: IconProps) => (
-)
+);
export const ImageIcon = (props: IconProps) => (
@@ -113,7 +113,7 @@ export const ImageIcon = (props: IconProps) => (
-)
+);
export const CalendarIcon = (props: IconProps) => (
@@ -122,21 +122,21 @@ export const CalendarIcon = (props: IconProps) => (
-)
+);
export const FlagIcon = (props: IconProps) => (
-)
+);
export const BoldIcon = (props: IconProps) => (
-)
+);
export const ItalicIcon = (props: IconProps) => (
@@ -144,21 +144,21 @@ export const ItalicIcon = (props: IconProps) => (
-)
+);
export const UnderlineIcon = (props: IconProps) => (
-)
+);
export const LinkIcon = (props: IconProps) => (
-)
+);
export const SaveIcon = (props: IconProps) => (
@@ -166,26 +166,26 @@ export const SaveIcon = (props: IconProps) => (
-)
+);
export const CheckIcon = (props: IconProps) => (
-)
+);
export const ChatIcon = (props: IconProps) => (
-)
+);
export const TrashIcon = (props: IconProps) => (
-)
+);
export const LayoutIcon = (props: IconProps) => (
@@ -193,20 +193,20 @@ export const LayoutIcon = (props: IconProps) => (
-)
+);
export const CodeIcon = (props: IconProps) => (
-)
+);
export const EditIcon = (props: IconProps) => (
-)
+);
export const UploadIcon = (props: IconProps) => (
@@ -215,7 +215,7 @@ export const UploadIcon = (props: IconProps) => (
-)
+);
export const DownloadIcon = (props: IconProps) => (
@@ -223,7 +223,7 @@ export const DownloadIcon = (props: IconProps) => (
-)
+);
export const NumberIcon = (props: IconProps) => (
@@ -232,40 +232,40 @@ export const NumberIcon = (props: IconProps) => (
-)
+);
export const EmailIcon = (props: IconProps) => (
-)
+);
export const PhoneIcon = (props: IconProps) => (
-)
+);
export const CheckSquareIcon = (props: IconProps) => (
-)
+);
export const FilterIcon = (props: IconProps) => (
-)
+);
export const UserIcon = (props: IconProps) => (
-)
+);
export const ExpandIcon = (props: IconProps) => (
@@ -274,7 +274,7 @@ export const ExpandIcon = (props: IconProps) => (
-)
+);
export const ExternalLinkIcon = (props: IconProps) => (
@@ -282,7 +282,7 @@ export const ExternalLinkIcon = (props: IconProps) => (
-)
+);
export const FilmIcon = (props: IconProps) => (
@@ -295,13 +295,13 @@ export const FilmIcon = (props: IconProps) => (
-)
+);
export const ThunderIcon = (props: IconProps) => (
-)
+);
export const GripIcon = (props: IconProps) => (
@@ -312,56 +312,56 @@ export const GripIcon = (props: IconProps) => (
-)
+);
export const LockedIcon = (props: IconProps) => (
-)
+);
export const UnlockedIcon = (props: IconProps) => (
-)
+);
export const UndoIcon = (props: IconProps) => (
-)
+);
export const RedoIcon = (props: IconProps) => (
-)
+);
export const FileIcon = (props: IconProps) => (
-)
+);
export const EyeIcon = (props: IconProps) => (
-)
+);
export const SendEmailIcon = (props: IconProps) => (
-)
+);
export const GithubIcon = (props: IconProps) => (
@@ -369,10 +369,10 @@ export const GithubIcon = (props: IconProps) => (
fillRule="evenodd"
clipRule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
- fill={useColorModeValue('#24292f', 'white')}
+ fill={useColorModeValue("#24292f", "white")}
/>
-)
+);
export const UsersIcon = (props: IconProps) => (
@@ -381,7 +381,7 @@ export const UsersIcon = (props: IconProps) => (
-)
+);
export const AlignLeftTextIcon = (props: IconProps) => (
@@ -390,7 +390,7 @@ export const AlignLeftTextIcon = (props: IconProps) => (
-)
+);
export const BoxIcon = (props: IconProps) => (
@@ -398,7 +398,7 @@ export const BoxIcon = (props: IconProps) => (
-)
+);
export const HelpCircleIcon = (props: IconProps) => (
@@ -406,14 +406,14 @@ export const HelpCircleIcon = (props: IconProps) => (
-)
+);
export const CopyIcon = (props: IconProps) => (
-)
+);
export const TemplateIcon = (props: IconProps) => (
@@ -422,13 +422,13 @@ export const TemplateIcon = (props: IconProps) => (
-)
+);
export const MinusIcon = (props: IconProps) => (
-)
+);
export const LaptopIcon = (props: IconProps) => (
@@ -444,7 +444,7 @@ export const LaptopIcon = (props: IconProps) => (
strokeLinejoin="round"
/>
-)
+);
export const MouseIcon = (props: IconProps) => (
@@ -454,7 +454,7 @@ export const MouseIcon = (props: IconProps) => (
strokeLinecap="round"
/>
-)
+);
export const HardDriveIcon = (props: IconProps) => (
@@ -463,26 +463,26 @@ export const HardDriveIcon = (props: IconProps) => (
-)
+);
export const CreditCardIcon = (props: IconProps) => (
-)
+);
export const PlayIcon = (props: IconProps) => (
-)
+);
export const StarIcon = (props: IconProps) => (
-)
+);
export const BuoyIcon = (props: IconProps) => (
@@ -493,14 +493,14 @@ export const BuoyIcon = (props: IconProps) => (
-)
+);
export const EyeOffIcon = (props: IconProps) => (
-)
+);
export const AlertIcon = (props: IconProps) => (
@@ -508,14 +508,14 @@ export const AlertIcon = (props: IconProps) => (
-)
+);
export const CloudOffIcon = (props: IconProps) => (
-)
+);
export const ListIcon = (props: IconProps) => (
@@ -526,7 +526,7 @@ export const ListIcon = (props: IconProps) => (
-)
+);
export const PackageIcon = (props: IconProps) => (
@@ -535,14 +535,14 @@ export const PackageIcon = (props: IconProps) => (
-)
+);
export const CloseIcon = (props: IconProps) => (
-)
+);
export const NoRadiusIcon = (props: IconProps) => (
@@ -555,7 +555,7 @@ export const NoRadiusIcon = (props: IconProps) => (
mask="url(#path-1-inside-1_1009_3)"
/>
-)
+);
export const MediumRadiusIcon = (props: IconProps) => (
@@ -568,7 +568,7 @@ export const MediumRadiusIcon = (props: IconProps) => (
mask="url(#path-1-inside-1_1009_4)"
/>
-)
+);
export const LargeRadiusIcon = (props: IconProps) => (
@@ -581,19 +581,19 @@ export const LargeRadiusIcon = (props: IconProps) => (
mask="url(#path-1-inside-1_1009_5)"
/>
-)
+);
export const DropletIcon = (props: IconProps) => (
-)
+);
export const TableIcon = (props: IconProps) => (
-)
+);
export const ShuffleIcon = (props: IconProps) => (
@@ -603,7 +603,7 @@ export const ShuffleIcon = (props: IconProps) => (
-)
+);
export const InfoIcon = (props: IconProps) => (
@@ -611,7 +611,7 @@ export const InfoIcon = (props: IconProps) => (
-)
+);
export const SmileIcon = (props: IconProps) => (
@@ -620,21 +620,21 @@ export const SmileIcon = (props: IconProps) => (
-)
+);
export const BookIcon = (props: IconProps) => (
-)
+);
export const ChevronLastIcon = (props: IconProps) => (
-)
+);
export const XCircleIcon = (props: IconProps) => (
@@ -642,7 +642,7 @@ export const XCircleIcon = (props: IconProps) => (
-)
+);
export const LightBulbIcon = (props: IconProps) => (
@@ -650,7 +650,7 @@ export const LightBulbIcon = (props: IconProps) => (
-)
+);
export const UnlinkIcon = (props: IconProps) => (
@@ -661,7 +661,7 @@ export const UnlinkIcon = (props: IconProps) => (
-)
+);
export const RepeatIcon = (props: IconProps) => (
@@ -670,14 +670,14 @@ export const RepeatIcon = (props: IconProps) => (
-)
+);
export const BracesIcon = (props: IconProps) => (
-)
+);
export const VideoPopoverIcon = (props: IconProps) => (
@@ -694,7 +694,7 @@ export const VideoPopoverIcon = (props: IconProps) => (
strokeLinejoin="round"
/>
-)
+);
export const WalletIcon = (props: IconProps) => (
@@ -702,4 +702,4 @@ export const WalletIcon = (props: IconProps) => (
-)
+);
diff --git a/apps/builder/src/components/inputs/AutocompleteInput.tsx b/apps/builder/src/components/inputs/AutocompleteInput.tsx
index 7af38e9175..e2b35723ec 100644
--- a/apps/builder/src/components/inputs/AutocompleteInput.tsx
+++ b/apps/builder/src/components/inputs/AutocompleteInput.tsx
@@ -1,37 +1,37 @@
+import { useParentModal } from "@/features/graph/providers/ParentModalProvider";
+import { VariablesButton } from "@/features/variables/components/VariablesButton";
+import { injectVariableInText } from "@/features/variables/helpers/injectVariableInTextInput";
+import { focusInput } from "@/helpers/focusInput";
+import { useOutsideClick } from "@/hooks/useOutsideClick";
import {
- useDisclosure,
- Popover,
- PopoverContent,
Button,
- useColorModeValue,
+ FormControl,
+ HStack,
+ Input,
+ Popover,
PopoverAnchor,
+ PopoverContent,
Portal,
- Input,
- HStack,
- FormControl,
-} from '@chakra-ui/react'
-import { useState, useRef, useEffect } from 'react'
-import { useDebouncedCallback } from 'use-debounce'
-import { isDefined } from '@typebot.io/lib'
-import { useOutsideClick } from '@/hooks/useOutsideClick'
-import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
-import { VariablesButton } from '@/features/variables/components/VariablesButton'
-import { Variable } from '@typebot.io/schemas'
-import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput'
-import { focusInput } from '@/helpers/focusInput'
-import { env } from '@typebot.io/env'
+ useColorModeValue,
+ useDisclosure,
+} from "@chakra-ui/react";
+import { env } from "@typebot.io/env";
+import { isDefined } from "@typebot.io/lib/utils";
+import type { Variable } from "@typebot.io/variables/schemas";
+import { useEffect, useRef, useState } from "react";
+import { useDebouncedCallback } from "use-debounce";
type Props = {
- items: string[] | undefined
- value?: string
- defaultValue?: string
- debounceTimeout?: number
- placeholder?: string
- withVariableButton?: boolean
- moreInfoTooltip?: string
- isRequired?: boolean
- onChange: (value: string) => void
-}
+ items: string[] | undefined;
+ value?: string;
+ defaultValue?: string;
+ debounceTimeout?: number;
+ placeholder?: string;
+ withVariableButton?: boolean;
+ moreInfoTooltip?: string;
+ isRequired?: boolean;
+ onChange: (value: string) => void;
+};
export const AutocompleteInput = ({
items,
@@ -43,116 +43,116 @@ export const AutocompleteInput = ({
defaultValue,
isRequired,
}: Props) => {
- const bg = useColorModeValue('gray.200', 'gray.700')
- const { onOpen, onClose, isOpen } = useDisclosure()
- const [isTouched, setIsTouched] = useState(false)
- const [inputValue, setInputValue] = useState(defaultValue ?? '')
+ const bg = useColorModeValue("gray.200", "gray.700");
+ const { onOpen, onClose, isOpen } = useDisclosure();
+ const [isTouched, setIsTouched] = useState(false);
+ const [inputValue, setInputValue] = useState(defaultValue ?? "");
const [carretPosition, setCarretPosition] = useState(
- inputValue.length ?? 0
- )
+ inputValue.length ?? 0,
+ );
const onChange = useDebouncedCallback(
_onChange,
- env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
- )
+ env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout,
+ );
useEffect(() => {
- if (isTouched || inputValue !== '' || !defaultValue || defaultValue === '')
- return
- setInputValue(defaultValue ?? '')
- }, [defaultValue, inputValue, isTouched])
+ if (isTouched || inputValue !== "" || !defaultValue || defaultValue === "")
+ return;
+ setInputValue(defaultValue ?? "");
+ }, [defaultValue, inputValue, isTouched]);
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
- >()
- const dropdownRef = useRef(null)
- const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
- const inputRef = useRef(null)
- const { ref: parentModalRef } = useParentModal()
+ >();
+ const dropdownRef = useRef(null);
+ const itemsRef = useRef<(HTMLButtonElement | null)[]>([]);
+ const inputRef = useRef(null);
+ const { ref: parentModalRef } = useParentModal();
const filteredItems = (
- inputValue === ''
- ? items ?? []
+ inputValue === ""
+ ? (items ?? [])
: [
...(items ?? []).filter(
(item) =>
- item.toLowerCase().startsWith((inputValue ?? '').toLowerCase()) &&
- item.toLowerCase() !== inputValue.toLowerCase()
+ item.toLowerCase().startsWith((inputValue ?? "").toLowerCase()) &&
+ item.toLowerCase() !== inputValue.toLowerCase(),
),
]
- ).slice(0, 50)
+ ).slice(0, 50);
useOutsideClick({
ref: dropdownRef,
handler: onClose,
isEnabled: isOpen,
- })
+ });
useEffect(
() => () => {
- onChange.flush()
+ onChange.flush();
},
- [onChange]
- )
+ [onChange],
+ );
const changeValue = (value: string) => {
- if (!isTouched) setIsTouched(true)
- if (!isOpen) onOpen()
- setInputValue(value)
- onChange(value)
- }
+ if (!isTouched) setIsTouched(true);
+ if (!isOpen) onOpen();
+ setInputValue(value);
+ onChange(value);
+ };
const handleItemClick = (value: string) => () => {
- setInputValue(value)
- onChange(value)
- setKeyboardFocusIndex(undefined)
- inputRef.current?.focus()
- }
+ setInputValue(value);
+ onChange(value);
+ setKeyboardFocusIndex(undefined);
+ inputRef.current?.focus();
+ };
const updateFocusedDropdownItem = (
- e: React.KeyboardEvent
+ e: React.KeyboardEvent,
) => {
- if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
- handleItemClick(filteredItems[keyboardFocusIndex])()
- return setKeyboardFocusIndex(undefined)
+ if (e.key === "Enter" && isDefined(keyboardFocusIndex)) {
+ handleItemClick(filteredItems[keyboardFocusIndex])();
+ return setKeyboardFocusIndex(undefined);
}
- if (e.key === 'ArrowDown') {
- if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
+ if (e.key === "ArrowDown") {
+ if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0);
if (keyboardFocusIndex === filteredItems.length - 1)
- return setKeyboardFocusIndex(0)
+ return setKeyboardFocusIndex(0);
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- })
- return setKeyboardFocusIndex(keyboardFocusIndex + 1)
+ behavior: "smooth",
+ block: "nearest",
+ });
+ return setKeyboardFocusIndex(keyboardFocusIndex + 1);
}
- if (e.key === 'ArrowUp') {
+ if (e.key === "ArrowUp") {
if (keyboardFocusIndex === 0 || keyboardFocusIndex === undefined)
- return setKeyboardFocusIndex(filteredItems.length - 1)
+ return setKeyboardFocusIndex(filteredItems.length - 1);
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- })
- setKeyboardFocusIndex(keyboardFocusIndex - 1)
+ behavior: "smooth",
+ block: "nearest",
+ });
+ setKeyboardFocusIndex(keyboardFocusIndex - 1);
}
- }
+ };
const handleVariableSelected = (variable?: Variable) => {
- if (!variable) return
+ if (!variable) return;
const { text, carretPosition: newCarretPosition } = injectVariableInText({
variable,
text: inputValue,
at: carretPosition,
- })
- changeValue(text)
- focusInput({ at: newCarretPosition, input: inputRef.current })
- }
+ });
+ changeValue(text);
+ focusInput({ at: newCarretPosition, input: inputRef.current });
+ };
const updateCarretPosition = (e: React.FocusEvent) => {
- const carretPosition = e.target.selectionStart
- if (!carretPosition) return
- setCarretPosition(carretPosition)
- }
+ const carretPosition = e.target.selectionStart;
+ if (!carretPosition) return;
+ setCarretPosition(carretPosition);
+ };
return (
@@ -173,7 +173,7 @@ export const AutocompleteInput = ({
onFocus={onOpen}
onBlur={updateCarretPosition}
onKeyDown={updateFocusedDropdownItem}
- placeholder={!items ? 'Loading...' : placeholder}
+ placeholder={!items ? "Loading..." : placeholder}
isDisabled={!items}
/>
@@ -189,29 +189,27 @@ export const AutocompleteInput = ({
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
- <>
- {filteredItems.map((item, idx) => {
- return (
- (itemsRef.current[idx] = el)}
- minH="40px"
- key={idx}
- onClick={handleItemClick(item)}
- fontSize="16px"
- fontWeight="normal"
- rounded="none"
- colorScheme="gray"
- role="menuitem"
- variant="ghost"
- bg={keyboardFocusIndex === idx ? bg : 'transparent'}
- justifyContent="flex-start"
- transition="none"
- >
- {item}
-
- )
- })}
- >
+ {filteredItems.map((item, idx) => {
+ return (
+ (itemsRef.current[idx] = el)}
+ minH="40px"
+ key={idx}
+ onClick={handleItemClick(item)}
+ fontSize="16px"
+ fontWeight="normal"
+ rounded="none"
+ colorScheme="gray"
+ role="menuitem"
+ variant="ghost"
+ bg={keyboardFocusIndex === idx ? bg : "transparent"}
+ justifyContent="flex-start"
+ transition="none"
+ >
+ {item}
+
+ );
+ })}
)}
@@ -221,5 +219,5 @@ export const AutocompleteInput = ({
)}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/inputs/CodeEditor.tsx b/apps/builder/src/components/inputs/CodeEditor.tsx
index 4b989a5f6e..f5169cfe84 100644
--- a/apps/builder/src/components/inputs/CodeEditor.tsx
+++ b/apps/builder/src/components/inputs/CodeEditor.tsx
@@ -1,5 +1,6 @@
+import { VariablesButton } from "@/features/variables/components/VariablesButton";
import {
- BoxProps,
+ type BoxProps,
Fade,
FormControl,
FormHelperText,
@@ -8,36 +9,39 @@ import {
Stack,
useColorModeValue,
useDisclosure,
-} from '@chakra-ui/react'
-import React, { ReactNode, useEffect, useRef, useState } from 'react'
-import { useDebouncedCallback } from 'use-debounce'
-import { VariablesButton } from '@/features/variables/components/VariablesButton'
-import { Variable } from '@typebot.io/schemas'
-import { env } from '@typebot.io/env'
-import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror'
-import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'
-import { githubLight } from '@uiw/codemirror-theme-github'
-import { LanguageName, loadLanguage } from '@uiw/codemirror-extensions-langs'
-import { isDefined } from '@udecode/plate-common'
-import { CopyButton } from '../CopyButton'
-import { MoreInfoTooltip } from '../MoreInfoTooltip'
+} from "@chakra-ui/react";
+import { env } from "@typebot.io/env";
+import type { Variable } from "@typebot.io/variables/schemas";
+import { isDefined } from "@udecode/plate-common";
+import {
+ type LanguageName,
+ loadLanguage,
+} from "@uiw/codemirror-extensions-langs";
+import { githubLight } from "@uiw/codemirror-theme-github";
+import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
+import CodeMirror, { type ReactCodeMirrorRef } from "@uiw/react-codemirror";
+import type { ReactNode } from "react";
+import React, { useEffect, useRef, useState } from "react";
+import { useDebouncedCallback } from "use-debounce";
+import { CopyButton } from "../CopyButton";
+import { MoreInfoTooltip } from "../MoreInfoTooltip";
type Props = {
- label?: string
- value?: string
- defaultValue?: string
- lang: LanguageName
- isReadOnly?: boolean
- debounceTimeout?: number
- withVariableButton?: boolean
- height?: string
- maxHeight?: string
- minWidth?: string
- moreInfoTooltip?: string
- helperText?: ReactNode
- isRequired?: boolean
- onChange?: (value: string) => void
-}
+ label?: string;
+ value?: string;
+ defaultValue?: string;
+ lang: LanguageName;
+ isReadOnly?: boolean;
+ debounceTimeout?: number;
+ withVariableButton?: boolean;
+ height?: string;
+ maxHeight?: string;
+ minWidth?: string;
+ moreInfoTooltip?: string;
+ helperText?: ReactNode;
+ isRequired?: boolean;
+ onChange?: (value: string) => void;
+};
export const CodeEditor = ({
label,
defaultValue,
@@ -46,57 +50,57 @@ export const CodeEditor = ({
helperText,
isRequired,
onChange,
- height = '250px',
- maxHeight = '70vh',
+ height = "250px",
+ maxHeight = "70vh",
minWidth,
withVariableButton = true,
isReadOnly = false,
debounceTimeout = 1000,
...props
-}: Props & Omit) => {
- const theme = useColorModeValue(githubLight, tokyoNight)
- const codeEditor = useRef(null)
- const [carretPosition, setCarretPosition] = useState(0)
- const isVariableButtonDisplayed = withVariableButton && !isReadOnly
- const [value, _setValue] = useState(defaultValue ?? '')
- const { onOpen, onClose, isOpen } = useDisclosure()
+}: Props & Omit) => {
+ const theme = useColorModeValue(githubLight, tokyoNight);
+ const codeEditor = useRef(null);
+ const [carretPosition, setCarretPosition] = useState(0);
+ const isVariableButtonDisplayed = withVariableButton && !isReadOnly;
+ const [value, _setValue] = useState(defaultValue ?? "");
+ const { onOpen, onClose, isOpen } = useDisclosure();
const setValue = useDebouncedCallback(
(value) => {
- _setValue(value)
- onChange && onChange(value)
+ _setValue(value);
+ onChange && onChange(value);
},
- env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
- )
+ env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout,
+ );
- const handleVariableSelected = (variable?: Pick) => {
- codeEditor.current?.view?.focus()
- const insert = `{{${variable?.name}}}`
+ const handleVariableSelected = (variable?: Pick) => {
+ codeEditor.current?.view?.focus();
+ const insert = `{{${variable?.name}}}`;
codeEditor.current?.view?.dispatch({
changes: {
from: carretPosition,
insert,
},
selection: { anchor: carretPosition + insert.length },
- })
- }
+ });
+ };
const handleChange = (newValue: string) => {
- setValue(newValue)
- }
+ setValue(newValue);
+ };
const rememberCarretPosition = () => {
setCarretPosition(
- codeEditor.current?.view?.state?.selection.asSingle().main.head ?? 0
- )
- }
+ codeEditor.current?.view?.state?.selection.asSingle().main.head ?? 0,
+ );
+ };
useEffect(
() => () => {
- setValue.flush()
+ setValue.flush();
},
- [setValue]
- )
+ [setValue],
+ );
return (
{label && (
- {label}{' '}
+ {label}{" "}
{moreInfoTooltip && (
{moreInfoTooltip}
)}
@@ -117,9 +121,9 @@ export const CodeEditor = ({
@@ -157,7 +161,7 @@ export const CodeEditor = ({
extensions={[loadLanguage(lang)].filter(isDefined)}
editable={!isReadOnly}
style={{
- width: isVariableButtonDisplayed ? 'calc(100% - 32px)' : '100%',
+ width: isVariableButtonDisplayed ? "calc(100% - 32px)" : "100%",
}}
spellCheck={false}
basicSetup={{
@@ -186,5 +190,5 @@ export const CodeEditor = ({
{helperText && {helperText} }
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/inputs/NumberInput.tsx b/apps/builder/src/components/inputs/NumberInput.tsx
index 8b7d7027df..fb10a7bbf3 100644
--- a/apps/builder/src/components/inputs/NumberInput.tsx
+++ b/apps/builder/src/components/inputs/NumberInput.tsx
@@ -1,40 +1,44 @@
-import { VariablesButton } from '@/features/variables/components/VariablesButton'
+import { VariablesButton } from "@/features/variables/components/VariablesButton";
import {
- NumberInputProps,
NumberInput as ChakraNumberInput,
- NumberInputField,
- NumberInputStepper,
- NumberIncrementStepper,
- NumberDecrementStepper,
- HStack,
FormControl,
+ FormHelperText,
FormLabel,
+ HStack,
+ NumberDecrementStepper,
+ NumberIncrementStepper,
+ NumberInputField,
+ type NumberInputProps,
+ NumberInputStepper,
Stack,
Text,
- FormHelperText,
-} from '@chakra-ui/react'
-import { Variable, VariableString } from '@typebot.io/schemas'
-import { ReactNode, useEffect, useState } from 'react'
-import { useDebouncedCallback } from 'use-debounce'
-import { env } from '@typebot.io/env'
-import { MoreInfoTooltip } from '../MoreInfoTooltip'
+} from "@chakra-ui/react";
+import { env } from "@typebot.io/env";
+import type { Variable, VariableString } from "@typebot.io/variables/schemas";
+import type { ReactNode } from "react";
+import { useEffect, useState } from "react";
+import { useDebouncedCallback } from "use-debounce";
+import { MoreInfoTooltip } from "../MoreInfoTooltip";
type Value = HasVariable extends true | undefined
? number | VariableString
- : number
+ : number;
type Props = {
- defaultValue: Value | undefined
- debounceTimeout?: number
- withVariableButton?: HasVariable
- label?: string
- moreInfoTooltip?: string
- isRequired?: boolean
- direction?: 'row' | 'column'
- suffix?: string
- helperText?: ReactNode
- onValueChange: (value?: Value) => void
-} & Omit
+ defaultValue: Value | undefined;
+ debounceTimeout?: number;
+ withVariableButton?: HasVariable;
+ label?: string;
+ moreInfoTooltip?: string;
+ isRequired?: boolean;
+ direction?: "row" | "column";
+ suffix?: string;
+ helperText?: ReactNode;
+ onValueChange: (value?: Value) => void;
+} & Omit<
+ NumberInputProps,
+ "defaultValue" | "value" | "onChange" | "isRequired"
+>;
export const NumberInput = ({
defaultValue,
@@ -44,57 +48,57 @@ export const NumberInput = ({
label,
moreInfoTooltip,
isRequired,
- direction = 'column',
+ direction = "column",
suffix,
helperText,
...props
}: Props) => {
- const [isTouched, setIsTouched] = useState(false)
- const [value, setValue] = useState(defaultValue?.toString() ?? '')
+ const [isTouched, setIsTouched] = useState(false);
+ const [value, setValue] = useState(defaultValue?.toString() ?? "");
const onValueChangeDebounced = useDebouncedCallback(
onValueChange,
- env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
- )
+ env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout,
+ );
useEffect(() => {
- if (isTouched || value !== '' || !defaultValue) return
- setValue(defaultValue?.toString() ?? '')
- }, [defaultValue, isTouched, value])
+ if (isTouched || value !== "" || !defaultValue) return;
+ setValue(defaultValue?.toString() ?? "");
+ }, [defaultValue, isTouched, value]);
useEffect(
() => () => {
- onValueChangeDebounced.flush()
+ onValueChangeDebounced.flush();
},
- [onValueChangeDebounced]
- )
+ [onValueChangeDebounced],
+ );
const handleValueChange = (newValue: string) => {
- if (!isTouched) setIsTouched(true)
- if (value.startsWith('{{') && value.endsWith('}}') && newValue !== '')
- return
- setValue(newValue)
- if (newValue.endsWith('.') || newValue.endsWith(',')) return
- if (newValue === '') return onValueChangeDebounced(undefined)
+ if (!isTouched) setIsTouched(true);
+ if (value.startsWith("{{") && value.endsWith("}}") && newValue !== "")
+ return;
+ setValue(newValue);
+ if (newValue.endsWith(".") || newValue.endsWith(",")) return;
+ if (newValue === "") return onValueChangeDebounced(undefined);
if (
- newValue.startsWith('{{') &&
- newValue.endsWith('}}') &&
+ newValue.startsWith("{{") &&
+ newValue.endsWith("}}") &&
newValue.length > 4 &&
(withVariableButton ?? true)
) {
- onValueChangeDebounced(newValue as Value)
- return
+ onValueChangeDebounced(newValue as Value);
+ return;
}
- const numberedValue = parseFloat(newValue)
- if (isNaN(numberedValue)) return
- onValueChangeDebounced(numberedValue)
- }
+ const numberedValue = Number.parseFloat(newValue);
+ if (isNaN(numberedValue)) return;
+ onValueChangeDebounced(numberedValue);
+ };
const handleVariableSelected = (variable?: Variable) => {
- if (!variable) return
- const newValue = `{{${variable.name}}}`
- handleValueChange(newValue)
- }
+ if (!variable) return;
+ const newValue = `{{${variable.name}}}`;
+ handleValueChange(newValue);
+ };
const Input = (
({
- )
+ );
return (
{label && (
- {label}{' '}
+ {label}{" "}
{moreInfoTooltip && (
{moreInfoTooltip}
)}
)}
-
- {withVariableButton ?? true ? (
+
+ {(withVariableButton ?? true) ? (
{Input}
@@ -140,5 +144,5 @@ export const NumberInput = ({
{helperText ? {helperText} : null}
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/inputs/RadioButtons.tsx b/apps/builder/src/components/inputs/RadioButtons.tsx
index 30afd23781..5753ad691f 100644
--- a/apps/builder/src/components/inputs/RadioButtons.tsx
+++ b/apps/builder/src/components/inputs/RadioButtons.tsx
@@ -2,58 +2,58 @@ import {
Box,
Flex,
Stack,
+ type UseRadioProps,
useColorModeValue,
useRadio,
useRadioGroup,
- UseRadioProps,
-} from '@chakra-ui/react'
-import { ReactNode } from 'react'
+} from "@chakra-ui/react";
+import type { ReactNode } from "react";
type Props = {
- options: readonly (T | { value: T; label: ReactNode })[]
- value?: T
- defaultValue?: T
- direction?: 'row' | 'column'
- size?: 'md' | 'sm'
- onSelect: (newValue: T) => void
-}
+ options: readonly (T | { value: T; label: ReactNode })[];
+ value?: T;
+ defaultValue?: T;
+ direction?: "row" | "column";
+ size?: "md" | "sm";
+ onSelect: (newValue: T) => void;
+};
export const RadioButtons = ({
options,
value,
defaultValue,
- direction = 'row',
- size = 'md',
+ direction = "row",
+ size = "md",
onSelect,
}: Props) => {
const { getRootProps, getRadioProps } = useRadioGroup({
value,
defaultValue,
onChange: onSelect,
- })
+ });
- const group = getRootProps()
+ const group = getRootProps();
return (
{options.map((item) => {
- const radio = getRadioProps({ value: parseValue(item) })
+ const radio = getRadioProps({ value: parseValue(item) });
return (
{parseLabel(item)}
- )
+ );
})}
- )
-}
+ );
+};
export const RadioCard = (
- props: UseRadioProps & { children: ReactNode; size?: 'md' | 'sm' }
+ props: UseRadioProps & { children: ReactNode; size?: "md" | "sm" },
) => {
- const { getInputProps, getCheckboxProps } = useRadio(props)
+ const { getInputProps, getCheckboxProps } = useRadio(props);
- const input = getInputProps()
- const checkbox = getCheckboxProps()
+ const input = getInputProps();
+ const checkbox = getCheckboxProps();
return (
@@ -64,28 +64,28 @@ export const RadioCard = (
borderWidth="2px"
borderRadius="md"
_checked={{
- borderColor: 'blue.400',
+ borderColor: "blue.400",
}}
_hover={{
- bgColor: useColorModeValue('gray.100', 'gray.700'),
+ bgColor: useColorModeValue("gray.100", "gray.700"),
}}
_active={{
- bgColor: useColorModeValue('gray.200', 'gray.600'),
+ bgColor: useColorModeValue("gray.200", "gray.600"),
}}
- px={props.size === 'sm' ? 3 : 5}
- py={props.size === 'sm' ? 1 : 2}
+ px={props.size === "sm" ? 3 : 5}
+ py={props.size === "sm" ? 1 : 2}
transition="background-color 150ms, color 150ms, border 150ms"
justifyContent="center"
- fontSize={props.size === 'sm' ? 'sm' : undefined}
+ fontSize={props.size === "sm" ? "sm" : undefined}
>
{props.children}
- )
-}
+ );
+};
const parseValue = (item: string | { value: string; label: ReactNode }) =>
- typeof item === 'string' ? item : item.value
+ typeof item === "string" ? item : item.value;
const parseLabel = (item: string | { value: string; label: ReactNode }) =>
- typeof item === 'string' ? item : item.label
+ typeof item === "string" ? item : item.label;
diff --git a/apps/builder/src/components/inputs/Select.tsx b/apps/builder/src/components/inputs/Select.tsx
index 604829ecf1..0a5fa4fb38 100644
--- a/apps/builder/src/components/inputs/Select.tsx
+++ b/apps/builder/src/components/inputs/Select.tsx
@@ -1,43 +1,44 @@
+import { useParentModal } from "@/features/graph/providers/ParentModalProvider";
+import { useOutsideClick } from "@/hooks/useOutsideClick";
import {
- useDisclosure,
+ Box,
+ Button,
Flex,
- Popover,
+ HStack,
+ IconButton,
Input,
- PopoverContent,
- Button,
- useColorModeValue,
- PopoverAnchor,
- Portal,
InputGroup,
InputRightElement,
+ Popover,
+ PopoverAnchor,
+ PopoverContent,
+ Portal,
Text,
- Box,
- IconButton,
- HStack,
-} from '@chakra-ui/react'
-import { useState, useRef, ChangeEvent, useEffect } from 'react'
-import { isDefined } from '@typebot.io/lib'
-import { useOutsideClick } from '@/hooks/useOutsideClick'
-import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
-import { ChevronDownIcon, CloseIcon } from '../icons'
+ useColorModeValue,
+ useDisclosure,
+} from "@chakra-ui/react";
+import { isDefined } from "@typebot.io/lib/utils";
+import type { ChangeEvent } from "react";
+import { useEffect, useRef, useState } from "react";
+import { ChevronDownIcon, CloseIcon } from "../icons";
-const dropdownCloseAnimationDuration = 300
+const dropdownCloseAnimationDuration = 300;
type RichItem = {
- icon?: JSX.Element
- label: string
- value: string
- extras?: Record
-}
+ icon?: JSX.Element;
+ label: string;
+ value: string;
+ extras?: Record;
+};
-type Item = string | RichItem
+type Item = string | RichItem;
type Props = {
- selectedItem?: string
- items: readonly T[] | undefined
- placeholder?: string
- onSelect?: (value: string | undefined, item?: T) => void
-}
+ selectedItem?: string;
+ items: readonly T[] | undefined;
+ placeholder?: string;
+ onSelect?: (value: string | undefined, item?: T) => void;
+};
export const Select = ({
selectedItem,
@@ -45,115 +46,115 @@ export const Select = ({
items,
onSelect,
}: Props) => {
- const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
- const selectedItemBgColor = useColorModeValue('blue.50', 'blue.400')
- const [isTouched, setIsTouched] = useState(false)
- const { onOpen, onClose, isOpen } = useDisclosure()
+ const focusedItemBgColor = useColorModeValue("gray.200", "gray.700");
+ const selectedItemBgColor = useColorModeValue("blue.50", "blue.400");
+ const [isTouched, setIsTouched] = useState(false);
+ const { onOpen, onClose, isOpen } = useDisclosure();
const [inputValue, setInputValue] = useState(
getItemLabel(
items?.find((item) =>
- typeof item === 'string'
+ typeof item === "string"
? selectedItem === item
- : selectedItem === item.value
- ) ?? selectedItem
- )
- )
+ : selectedItem === item.value,
+ ) ?? selectedItem,
+ ),
+ );
useEffect(() => {
- if (!items || typeof items[0] === 'string' || !selectedItem || isTouched)
- return
+ if (!items || typeof items[0] === "string" || !selectedItem || isTouched)
+ return;
setInputValue(
getItemLabel(
(items as readonly RichItem[]).find(
- (item) => selectedItem === item.value
- ) ?? selectedItem
- )
- )
- }, [isTouched, items, selectedItem])
+ (item) => selectedItem === item.value,
+ ) ?? selectedItem,
+ ),
+ );
+ }, [isTouched, items, selectedItem]);
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
- >()
- const dropdownRef = useRef(null)
- const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
- const inputRef = useRef(null)
- const { ref: parentModalRef } = useParentModal()
+ >();
+ const dropdownRef = useRef(null);
+ const itemsRef = useRef<(HTMLButtonElement | null)[]>([]);
+ const inputRef = useRef(null);
+ const { ref: parentModalRef } = useParentModal();
const filteredItems = isTouched
? [
...(items ?? []).filter((item) =>
getItemLabel(item)
.toLowerCase()
- .includes((inputValue ?? '').toLowerCase())
+ .includes((inputValue ?? "").toLowerCase()),
),
]
- : items ?? []
+ : (items ?? []);
const closeDropdown = () => {
- onClose()
+ onClose();
setTimeout(() => {
- setIsTouched(false)
- }, dropdownCloseAnimationDuration)
- }
+ setIsTouched(false);
+ }, dropdownCloseAnimationDuration);
+ };
useOutsideClick({
ref: dropdownRef,
handler: closeDropdown,
isEnabled: isOpen,
- })
+ });
const updateInputValue = (e: ChangeEvent) => {
- if (!isOpen) onOpen()
- if (!isTouched) setIsTouched(true)
- setInputValue(e.target.value)
- }
+ if (!isOpen) onOpen();
+ if (!isTouched) setIsTouched(true);
+ setInputValue(e.target.value);
+ };
const handleItemClick = (item: T) => () => {
- if (!isTouched) setIsTouched(true)
- setInputValue(getItemLabel(item))
- onSelect?.(getItemValue(item), item)
- setKeyboardFocusIndex(undefined)
- closeDropdown()
- }
+ if (!isTouched) setIsTouched(true);
+ setInputValue(getItemLabel(item));
+ onSelect?.(getItemValue(item), item);
+ setKeyboardFocusIndex(undefined);
+ closeDropdown();
+ };
const updateFocusedDropdownItem = (
- e: React.KeyboardEvent
+ e: React.KeyboardEvent,
) => {
- if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
- e.preventDefault()
- handleItemClick(filteredItems[keyboardFocusIndex])()
- return setKeyboardFocusIndex(undefined)
+ if (e.key === "Enter" && isDefined(keyboardFocusIndex)) {
+ e.preventDefault();
+ handleItemClick(filteredItems[keyboardFocusIndex])();
+ return setKeyboardFocusIndex(undefined);
}
- if (e.key === 'ArrowDown') {
- e.preventDefault()
- if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
+ if (e.key === "ArrowDown") {
+ e.preventDefault();
+ if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0);
if (keyboardFocusIndex === filteredItems.length - 1)
- return setKeyboardFocusIndex(0)
+ return setKeyboardFocusIndex(0);
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- })
- return setKeyboardFocusIndex(keyboardFocusIndex + 1)
+ behavior: "smooth",
+ block: "nearest",
+ });
+ return setKeyboardFocusIndex(keyboardFocusIndex + 1);
}
- if (e.key === 'ArrowUp') {
- e.preventDefault()
+ if (e.key === "ArrowUp") {
+ e.preventDefault();
if (keyboardFocusIndex === 0 || keyboardFocusIndex === undefined)
- return setKeyboardFocusIndex(filteredItems.length - 1)
+ return setKeyboardFocusIndex(filteredItems.length - 1);
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- })
- setKeyboardFocusIndex(keyboardFocusIndex - 1)
+ behavior: "smooth",
+ block: "nearest",
+ });
+ setKeyboardFocusIndex(keyboardFocusIndex - 1);
}
- }
+ };
const clearSelection = (e: React.MouseEvent) => {
- e.preventDefault()
- setInputValue('')
- onSelect?.(undefined)
- setKeyboardFocusIndex(undefined)
- closeDropdown()
- }
+ e.preventDefault();
+ setInputValue("");
+ onSelect?.(undefined);
+ setKeyboardFocusIndex(undefined);
+ closeDropdown();
+ };
return (
@@ -186,13 +187,13 @@ export const Select = ({
autoComplete="off"
ref={inputRef}
className="select-input"
- value={isTouched ? inputValue : ''}
+ value={isTouched ? inputValue : ""}
placeholder={
!items
- ? 'Loading...'
- : !isTouched && inputValue !== ''
- ? undefined
- : placeholder
+ ? "Loading..."
+ : !isTouched && inputValue !== ""
+ ? undefined
+ : placeholder
}
onChange={updateInputValue}
onFocus={onOpen}
@@ -202,7 +203,7 @@ export const Select = ({
/>
@@ -210,7 +211,7 @@ export const Select = ({
}
- aria-label={'Clear'}
+ aria-label={"Clear"}
size="sm"
variant="ghost"
pointerEvents="all"
@@ -232,54 +233,49 @@ export const Select = ({
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
- {filteredItems.length > 0 && (
- <>
- {filteredItems.map((item, idx) => {
- return (
- (itemsRef.current[idx] = el)}
- minH="40px"
- key={idx}
- onClick={handleItemClick(item)}
- fontSize="16px"
- fontWeight="normal"
- rounded="none"
- colorScheme="gray"
- role="menuitem"
- variant="ghost"
- bg={
- keyboardFocusIndex === idx
- ? focusedItemBgColor
- : selectedItem === getItemValue(item)
+ {filteredItems.length > 0 &&
+ filteredItems.map((item, idx) => {
+ return (
+ (itemsRef.current[idx] = el)}
+ minH="40px"
+ key={idx}
+ onClick={handleItemClick(item)}
+ fontSize="16px"
+ fontWeight="normal"
+ rounded="none"
+ colorScheme="gray"
+ role="menuitem"
+ variant="ghost"
+ bg={
+ keyboardFocusIndex === idx
+ ? focusedItemBgColor
+ : selectedItem === getItemValue(item)
? selectedItemBgColor
- : 'transparent'
- }
- justifyContent="flex-start"
- transition="none"
- leftIcon={
- typeof item === 'object' ? item.icon : undefined
- }
- >
- {getItemLabel(item)}
-
- )
- })}
- >
- )}
+ : "transparent"
+ }
+ justifyContent="flex-start"
+ transition="none"
+ leftIcon={typeof item === "object" ? item.icon : undefined}
+ >
+ {getItemLabel(item)}
+
+ );
+ })}
- )
-}
+ );
+};
const getItemLabel = (item?: Item) => {
- if (!item) return ''
- if (typeof item === 'object') return item.label
- return item
-}
+ if (!item) return "";
+ if (typeof item === "object") return item.label;
+ return item;
+};
const getItemValue = (item: Item) => {
- if (typeof item === 'object') return item.value
- return item
-}
+ if (typeof item === "object") return item.value;
+ return item;
+};
diff --git a/apps/builder/src/components/inputs/SwitchWithLabel.tsx b/apps/builder/src/components/inputs/SwitchWithLabel.tsx
index da6cb0c64f..551b7f6a67 100644
--- a/apps/builder/src/components/inputs/SwitchWithLabel.tsx
+++ b/apps/builder/src/components/inputs/SwitchWithLabel.tsx
@@ -1,42 +1,42 @@
import {
FormControl,
- FormControlProps,
+ type FormControlProps,
FormLabel,
HStack,
Switch,
- SwitchProps,
-} from '@chakra-ui/react'
-import React, { useEffect, useState } from 'react'
-import { MoreInfoTooltip } from '../MoreInfoTooltip'
-import { isDefined } from '@typebot.io/lib'
+ type SwitchProps,
+} from "@chakra-ui/react";
+import { isDefined } from "@typebot.io/lib/utils";
+import React, { useEffect, useState } from "react";
+import { MoreInfoTooltip } from "../MoreInfoTooltip";
export type SwitchWithLabelProps = {
- label: string
- initialValue: boolean | undefined
- moreInfoContent?: string
- onCheckChange?: (isChecked: boolean) => void
- justifyContent?: FormControlProps['justifyContent']
-} & Omit
+ label: string;
+ initialValue: boolean | undefined;
+ moreInfoContent?: string;
+ onCheckChange?: (isChecked: boolean) => void;
+ justifyContent?: FormControlProps["justifyContent"];
+} & Omit;
export const SwitchWithLabel = ({
label,
initialValue,
moreInfoContent,
onCheckChange,
- justifyContent = 'space-between',
+ justifyContent = "space-between",
...switchProps
}: SwitchWithLabelProps) => {
- const [isChecked, setIsChecked] = useState(initialValue)
+ const [isChecked, setIsChecked] = useState(initialValue);
const handleChange = () => {
- setIsChecked(!isChecked)
- if (onCheckChange) onCheckChange(!isChecked)
- }
+ setIsChecked(!isChecked);
+ if (onCheckChange) onCheckChange(!isChecked);
+ };
useEffect(() => {
if (isChecked === undefined && isDefined(initialValue))
- setIsChecked(initialValue)
- }, [initialValue, isChecked])
+ setIsChecked(initialValue);
+ }, [initialValue, isChecked]);
return (
@@ -50,5 +50,5 @@ export const SwitchWithLabel = ({
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/inputs/TextInput.tsx b/apps/builder/src/components/inputs/TextInput.tsx
index 3a3cd34e27..b6566f04bd 100644
--- a/apps/builder/src/components/inputs/TextInput.tsx
+++ b/apps/builder/src/components/inputs/TextInput.tsx
@@ -1,53 +1,54 @@
-import { VariablesButton } from '@/features/variables/components/VariablesButton'
-import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput'
-import { focusInput } from '@/helpers/focusInput'
+import { VariablesButton } from "@/features/variables/components/VariablesButton";
+import { injectVariableInText } from "@/features/variables/helpers/injectVariableInTextInput";
+import { focusInput } from "@/helpers/focusInput";
import {
+ Input as ChakraInput,
FormControl,
FormHelperText,
FormLabel,
HStack,
- Input as ChakraInput,
- InputProps,
+ type InputProps,
Stack,
-} from '@chakra-ui/react'
-import { Variable } from '@typebot.io/schemas'
-import React, {
+} from "@chakra-ui/react";
+import { env } from "@typebot.io/env";
+import type { Variable } from "@typebot.io/variables/schemas";
+import type { ReactNode } from "react";
+import type React from "react";
+import {
forwardRef,
- ReactNode,
useEffect,
useImperativeHandle,
useRef,
useState,
-} from 'react'
-import { useDebouncedCallback } from 'use-debounce'
-import { env } from '@typebot.io/env'
-import { MoreInfoTooltip } from '../MoreInfoTooltip'
+} from "react";
+import { useDebouncedCallback } from "use-debounce";
+import { MoreInfoTooltip } from "../MoreInfoTooltip";
export type TextInputProps = {
- forceDebounce?: boolean
- defaultValue?: string
- onChange?: (value: string) => void
- debounceTimeout?: number
- label?: ReactNode
- helperText?: ReactNode
- moreInfoTooltip?: string
- withVariableButton?: boolean
- isRequired?: boolean
- placeholder?: string
- isDisabled?: boolean
- direction?: 'row' | 'column'
- width?: 'full'
+ forceDebounce?: boolean;
+ defaultValue?: string;
+ onChange?: (value: string) => void;
+ debounceTimeout?: number;
+ label?: ReactNode;
+ helperText?: ReactNode;
+ moreInfoTooltip?: string;
+ withVariableButton?: boolean;
+ isRequired?: boolean;
+ placeholder?: string;
+ isDisabled?: boolean;
+ direction?: "row" | "column";
+ width?: "full";
} & Pick<
InputProps,
- | 'autoComplete'
- | 'onFocus'
- | 'onKeyUp'
- | 'type'
- | 'autoFocus'
- | 'size'
- | 'maxWidth'
- | 'flexShrink'
->
+ | "autoComplete"
+ | "onFocus"
+ | "onKeyUp"
+ | "type"
+ | "autoFocus"
+ | "size"
+ | "maxWidth"
+ | "flexShrink"
+>;
export const TextInput = forwardRef(function TextInput(
{
@@ -69,60 +70,60 @@ export const TextInput = forwardRef(function TextInput(
onKeyUp,
size,
maxWidth,
- direction = 'column',
+ direction = "column",
width,
flexShrink,
}: TextInputProps,
- ref
+ ref,
) {
- const inputRef = useRef(null)
- useImperativeHandle(ref, () => inputRef.current)
- const [isTouched, setIsTouched] = useState(false)
- const [localValue, setLocalValue] = useState(defaultValue ?? '')
+ const inputRef = useRef(null);
+ useImperativeHandle(ref, () => inputRef.current);
+ const [isTouched, setIsTouched] = useState(false);
+ const [localValue, setLocalValue] = useState(defaultValue ?? "");
const [carretPosition, setCarretPosition] = useState(
- localValue.length ?? 0
- )
+ localValue.length ?? 0,
+ );
const onChange = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function
_onChange ?? (() => {}),
- env.NEXT_PUBLIC_E2E_TEST && !forceDebounce ? 0 : debounceTimeout
- )
+ env.NEXT_PUBLIC_E2E_TEST && !forceDebounce ? 0 : debounceTimeout,
+ );
useEffect(() => {
- if (isTouched || localValue !== '' || !defaultValue || defaultValue === '')
- return
- setLocalValue(defaultValue ?? '')
- }, [defaultValue, isTouched, localValue])
+ if (isTouched || localValue !== "" || !defaultValue || defaultValue === "")
+ return;
+ setLocalValue(defaultValue ?? "");
+ }, [defaultValue, isTouched, localValue]);
useEffect(
() => () => {
- onChange.flush()
+ onChange.flush();
},
- [onChange]
- )
+ [onChange],
+ );
const changeValue = (value: string) => {
- if (!isTouched) setIsTouched(true)
- setLocalValue(value)
- onChange(value)
- }
+ if (!isTouched) setIsTouched(true);
+ setLocalValue(value);
+ onChange(value);
+ };
const handleVariableSelected = (variable?: Variable) => {
- if (!variable) return
+ if (!variable) return;
const { text, carretPosition: newCarretPosition } = injectVariableInText({
variable,
text: localValue,
at: carretPosition,
- })
- changeValue(text)
- focusInput({ at: newCarretPosition, input: inputRef.current })
- }
+ });
+ changeValue(text);
+ focusInput({ at: newCarretPosition, input: inputRef.current });
+ };
const updateCarretPosition = (e: React.FocusEvent) => {
- const carretPosition = e.target.selectionStart
- if (!carretPosition) return
- setCarretPosition(carretPosition)
- }
+ const carretPosition = e.target.selectionStart;
+ if (!carretPosition) return;
+ setCarretPosition(carretPosition);
+ };
const Input = (
- )
+ );
return (
{label && (
- {label}{' '}
+ {label}{" "}
{moreInfoTooltip && (
{moreInfoTooltip}
)}
)}
{withVariableButton ? (
-
+
{Input}
@@ -169,5 +170,5 @@ export const TextInput = forwardRef(function TextInput(
)}
{helperText && {helperText} }
- )
-})
+ );
+});
diff --git a/apps/builder/src/components/inputs/Textarea.tsx b/apps/builder/src/components/inputs/Textarea.tsx
index 666b39689d..9d3cb45fa0 100644
--- a/apps/builder/src/components/inputs/Textarea.tsx
+++ b/apps/builder/src/components/inputs/Textarea.tsx
@@ -1,34 +1,36 @@
-import { VariablesButton } from '@/features/variables/components/VariablesButton'
-import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput'
-import { focusInput } from '@/helpers/focusInput'
+import { VariablesButton } from "@/features/variables/components/VariablesButton";
+import { injectVariableInText } from "@/features/variables/helpers/injectVariableInTextInput";
+import { focusInput } from "@/helpers/focusInput";
import {
+ Textarea as ChakraTextarea,
FormControl,
+ FormHelperText,
FormLabel,
HStack,
- Textarea as ChakraTextarea,
- TextareaProps,
- FormHelperText,
Stack,
-} from '@chakra-ui/react'
-import { Variable } from '@typebot.io/schemas'
-import React, { ReactNode, useEffect, useRef, useState } from 'react'
-import { useDebouncedCallback } from 'use-debounce'
-import { env } from '@typebot.io/env'
-import { MoreInfoTooltip } from '../MoreInfoTooltip'
+ type TextareaProps,
+} from "@chakra-ui/react";
+import { env } from "@typebot.io/env";
+import type { Variable } from "@typebot.io/variables/schemas";
+import type { ReactNode } from "react";
+import type React from "react";
+import { useEffect, useRef, useState } from "react";
+import { useDebouncedCallback } from "use-debounce";
+import { MoreInfoTooltip } from "../MoreInfoTooltip";
type Props = {
- id?: string
- defaultValue?: string
- debounceTimeout?: number
- label?: string
- moreInfoTooltip?: string
- withVariableButton?: boolean
- isRequired?: boolean
- placeholder?: string
- helperText?: ReactNode
- onChange: (value: string) => void
- direction?: 'row' | 'column'
-} & Pick
+ id?: string;
+ defaultValue?: string;
+ debounceTimeout?: number;
+ label?: string;
+ moreInfoTooltip?: string;
+ withVariableButton?: boolean;
+ isRequired?: boolean;
+ placeholder?: string;
+ helperText?: ReactNode;
+ onChange: (value: string) => void;
+ direction?: "row" | "column";
+} & Pick;
export const Textarea = ({
id,
@@ -42,55 +44,55 @@ export const Textarea = ({
isRequired,
minH,
helperText,
- direction = 'column',
+ direction = "column",
width,
}: Props) => {
- const inputRef = useRef(null)
- const [isTouched, setIsTouched] = useState(false)
- const [localValue, setLocalValue] = useState(defaultValue ?? '')
+ const inputRef = useRef(null);
+ const [isTouched, setIsTouched] = useState(false);
+ const [localValue, setLocalValue] = useState(defaultValue ?? "");
const [carretPosition, setCarretPosition] = useState(
- localValue.length ?? 0
- )
+ localValue.length ?? 0,
+ );
const onChange = useDebouncedCallback(
_onChange,
- env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
- )
+ env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout,
+ );
useEffect(() => {
- if (isTouched || localValue !== '' || !defaultValue || defaultValue === '')
- return
- setLocalValue(defaultValue ?? '')
- }, [defaultValue, isTouched, localValue])
+ if (isTouched || localValue !== "" || !defaultValue || defaultValue === "")
+ return;
+ setLocalValue(defaultValue ?? "");
+ }, [defaultValue, isTouched, localValue]);
useEffect(
() => () => {
- onChange.flush()
+ onChange.flush();
},
- [onChange]
- )
+ [onChange],
+ );
const changeValue = (value: string) => {
- if (!isTouched) setIsTouched(true)
- setLocalValue(value)
- onChange(value)
- }
+ if (!isTouched) setIsTouched(true);
+ setLocalValue(value);
+ onChange(value);
+ };
const handleVariableSelected = (variable?: Variable) => {
- if (!variable) return
+ if (!variable) return;
const { text, carretPosition: newCarretPosition } = injectVariableInText({
variable,
text: localValue,
at: carretPosition,
- })
- changeValue(text)
- focusInput({ at: newCarretPosition, input: inputRef.current })
- }
+ });
+ changeValue(text);
+ focusInput({ at: newCarretPosition, input: inputRef.current });
+ };
const updateCarretPosition = (e: React.FocusEvent) => {
- const carretPosition = e.target.selectionStart
- if (!carretPosition) return
- setCarretPosition(carretPosition)
- }
+ const carretPosition = e.target.selectionStart;
+ if (!carretPosition) return;
+ setCarretPosition(carretPosition);
+ };
const Textarea = (
changeValue(e.target.value)}
placeholder={placeholder}
- minH={minH ?? '150px'}
+ minH={minH ?? "150px"}
/>
- )
+ );
return (
{label && (
- {label}{' '}
+ {label}{" "}
{moreInfoTooltip && (
{moreInfoTooltip}
)}
)}
{withVariableButton ? (
-
+
{Textarea}
@@ -130,5 +132,5 @@ export const Textarea = ({
)}
{helperText && {helperText} }
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/inputs/VariableSearchInput.tsx b/apps/builder/src/components/inputs/VariableSearchInput.tsx
index fa669bc318..71fba141a2 100644
--- a/apps/builder/src/components/inputs/VariableSearchInput.tsx
+++ b/apps/builder/src/components/inputs/VariableSearchInput.tsx
@@ -1,47 +1,49 @@
+import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons";
+import { useTypebot } from "@/features/editor/providers/TypebotProvider";
+import { useParentModal } from "@/features/graph/providers/ParentModalProvider";
+import { useOutsideClick } from "@/hooks/useOutsideClick";
import {
- useDisclosure,
- Flex,
- Popover,
- Input,
- PopoverContent,
Button,
- InputProps,
- IconButton,
+ Flex,
+ FormControl,
+ FormHelperText,
+ FormLabel,
HStack,
- useColorModeValue,
+ IconButton,
+ Input,
+ type InputProps,
+ Popover,
PopoverAnchor,
+ PopoverContent,
Portal,
+ Stack,
Tag,
Text,
- FormControl,
- FormLabel,
- FormHelperText,
- Stack,
-} from '@chakra-ui/react'
-import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
-import { useTypebot } from '@/features/editor/providers/TypebotProvider'
-import { createId } from '@paralleldrive/cuid2'
-import { Variable } from '@typebot.io/schemas'
-import React, { useState, useRef, ChangeEvent, ReactNode } from 'react'
-import { byId, isDefined, isNotDefined } from '@typebot.io/lib'
-import { useOutsideClick } from '@/hooks/useOutsideClick'
-import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
-import { MoreInfoTooltip } from '../MoreInfoTooltip'
-import { useTranslate } from '@tolgee/react'
+ useColorModeValue,
+ useDisclosure,
+} from "@chakra-ui/react";
+import { createId } from "@paralleldrive/cuid2";
+import { useTranslate } from "@tolgee/react";
+import { byId, isDefined, isNotDefined } from "@typebot.io/lib/utils";
+import type { Variable } from "@typebot.io/variables/schemas";
+import type { ChangeEvent, ReactNode } from "react";
+import type React from "react";
+import { useRef, useState } from "react";
+import { MoreInfoTooltip } from "../MoreInfoTooltip";
type Props = {
- initialVariableId: string | undefined
- autoFocus?: boolean
+ initialVariableId: string | undefined;
+ autoFocus?: boolean;
onSelectVariable: (
- variable: Pick | undefined
- ) => void
- label?: string
- placeholder?: string
- helperText?: ReactNode
- moreInfoTooltip?: string
- direction?: 'row' | 'column'
- width?: 'full'
-} & Omit
+ variable: Pick | undefined,
+ ) => void;
+ label?: string;
+ placeholder?: string;
+ helperText?: ReactNode;
+ moreInfoTooltip?: string;
+ direction?: "row" | "column";
+ width?: "full";
+} & Omit;
export const VariableSearchInput = ({
initialVariableId,
@@ -51,154 +53,156 @@ export const VariableSearchInput = ({
label,
helperText,
moreInfoTooltip,
- direction = 'column',
+ direction = "column",
isRequired,
width,
...inputProps
}: Props) => {
- const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
+ const focusedItemBgColor = useColorModeValue("gray.200", "gray.700");
const { onOpen, onClose, isOpen } = useDisclosure({
defaultIsOpen: autoFocus,
- })
+ });
const { typebot, createVariable, deleteVariable, updateVariable } =
- useTypebot()
- const variables = typebot?.variables ?? []
+ useTypebot();
+ const variables = typebot?.variables ?? [];
const [inputValue, setInputValue] = useState(
- variables.find(byId(initialVariableId))?.name ?? ''
- )
+ variables.find(byId(initialVariableId))?.name ?? "",
+ );
const [filteredItems, setFilteredItems] = useState(
- variables ?? []
- )
+ variables ?? [],
+ );
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
- >()
- const dropdownRef = useRef(null)
- const inputRef = useRef(null)
- const createVariableItemRef = useRef(null)
- const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
- const { ref: parentModalRef } = useParentModal()
- const { t } = useTranslate()
+ >();
+ const dropdownRef = useRef(null);
+ const inputRef = useRef(null);
+ const createVariableItemRef = useRef(null);
+ const itemsRef = useRef<(HTMLButtonElement | null)[]>([]);
+ const { ref: parentModalRef } = useParentModal();
+ const { t } = useTranslate();
useOutsideClick({
ref: dropdownRef,
handler: () => {
- onClose()
- setInputValue(variables.find(byId(initialVariableId))?.name ?? '')
+ onClose();
+ setInputValue(variables.find(byId(initialVariableId))?.name ?? "");
},
isEnabled: isOpen,
- })
+ });
const onInputChange = (e: ChangeEvent) => {
- setInputValue(e.target.value)
- if (e.target.value === '') {
+ setInputValue(e.target.value);
+ if (e.target.value === "") {
if (inputValue.length > 0) {
- onSelectVariable(undefined)
+ onSelectVariable(undefined);
}
- setFilteredItems([...variables.slice(0, 50)])
- return
+ setFilteredItems([...variables.slice(0, 50)]);
+ return;
}
setFilteredItems([
...variables
.filter((item) =>
- item.name.toLowerCase().includes((e.target.value ?? '').toLowerCase())
+ item.name
+ .toLowerCase()
+ .includes((e.target.value ?? "").toLowerCase()),
)
.slice(0, 50),
- ])
- }
+ ]);
+ };
const handleVariableNameClick = (variable: Variable) => () => {
- setInputValue(variable.name)
- onSelectVariable(variable)
- setKeyboardFocusIndex(undefined)
- inputRef.current?.blur()
- onClose()
- }
+ setInputValue(variable.name);
+ onSelectVariable(variable);
+ setKeyboardFocusIndex(undefined);
+ inputRef.current?.blur();
+ onClose();
+ };
const handleCreateNewVariableClick = () => {
- if (!inputValue || inputValue === '') return
- const id = 'v' + createId()
- onSelectVariable({ id, name: inputValue })
- createVariable({ id, name: inputValue, isSessionVariable: true })
- inputRef.current?.blur()
- onClose()
- }
+ if (!inputValue || inputValue === "") return;
+ const id = "v" + createId();
+ onSelectVariable({ id, name: inputValue });
+ createVariable({ id, name: inputValue, isSessionVariable: true });
+ inputRef.current?.blur();
+ onClose();
+ };
const handleDeleteVariableClick =
(variable: Variable) => (e: React.MouseEvent) => {
- e.stopPropagation()
- deleteVariable(variable.id)
- setFilteredItems(filteredItems.filter((item) => item.id !== variable.id))
+ e.stopPropagation();
+ deleteVariable(variable.id);
+ setFilteredItems(filteredItems.filter((item) => item.id !== variable.id));
if (variable.name === inputValue) {
- setInputValue('')
+ setInputValue("");
}
- }
+ };
const handleRenameVariableClick =
(variable: Variable) => (e: React.MouseEvent) => {
- e.stopPropagation()
- const name = prompt(t('variables.rename'), variable.name)
- if (!name) return
- updateVariable(variable.id, { name })
+ e.stopPropagation();
+ const name = prompt(t("variables.rename"), variable.name);
+ if (!name) return;
+ updateVariable(variable.id, { name });
setFilteredItems(
filteredItems.map((item) =>
- item.id === variable.id ? { ...item, name } : item
- )
- )
- }
+ item.id === variable.id ? { ...item, name } : item,
+ ),
+ );
+ };
const isCreateVariableButtonDisplayed =
(inputValue?.length ?? 0) > 0 &&
- isNotDefined(variables.find((v) => v.name === inputValue))
+ isNotDefined(variables.find((v) => v.name === inputValue));
const handleKeyUp = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
+ if (e.key === "Enter" && isDefined(keyboardFocusIndex)) {
if (keyboardFocusIndex === 0 && isCreateVariableButtonDisplayed)
- handleCreateNewVariableClick()
+ handleCreateNewVariableClick();
else
handleVariableNameClick(
filteredItems[
keyboardFocusIndex - (isCreateVariableButtonDisplayed ? 1 : 0)
- ]
- )()
- return setKeyboardFocusIndex(undefined)
+ ],
+ )();
+ return setKeyboardFocusIndex(undefined);
}
- if (e.key === 'ArrowDown') {
- if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
- if (keyboardFocusIndex >= filteredItems.length) return
+ if (e.key === "ArrowDown") {
+ if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0);
+ if (keyboardFocusIndex >= filteredItems.length) return;
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- })
- return setKeyboardFocusIndex(keyboardFocusIndex + 1)
+ behavior: "smooth",
+ block: "nearest",
+ });
+ return setKeyboardFocusIndex(keyboardFocusIndex + 1);
}
- if (e.key === 'ArrowUp') {
- if (keyboardFocusIndex === undefined) return
- if (keyboardFocusIndex <= 0) return setKeyboardFocusIndex(undefined)
+ if (e.key === "ArrowUp") {
+ if (keyboardFocusIndex === undefined) return;
+ if (keyboardFocusIndex <= 0) return setKeyboardFocusIndex(undefined);
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- })
- return setKeyboardFocusIndex(keyboardFocusIndex - 1)
+ behavior: "smooth",
+ block: "nearest",
+ });
+ return setKeyboardFocusIndex(keyboardFocusIndex - 1);
}
- return setKeyboardFocusIndex(undefined)
- }
+ return setKeyboardFocusIndex(undefined);
+ };
const openDropdown = () => {
- if (inputValue === '') setFilteredItems(variables)
- onOpen()
- }
+ if (inputValue === "") setFilteredItems(variables);
+ onOpen();
+ };
return (
{label && (
- {label}{' '}
+ {label}{" "}
{moreInfoTooltip && (
{moreInfoTooltip}
)}
@@ -220,7 +224,7 @@ export const VariableSearchInput = ({
onChange={onInputChange}
onFocus={openDropdown}
onKeyDown={handleKeyUp}
- placeholder={placeholder ?? t('variables.select')}
+ placeholder={placeholder ?? t("variables.select")}
autoComplete="off"
{...inputProps}
/>
@@ -254,10 +258,10 @@ export const VariableSearchInput = ({
bgColor={
keyboardFocusIndex === 0
? focusedItemBgColor
- : 'transparent'
+ : "transparent"
}
>
- {t('create')}
+ {t("create")}
{inputValue}
@@ -265,62 +269,59 @@ export const VariableSearchInput = ({
)}
- {filteredItems.length > 0 && (
- <>
- {filteredItems.map((item, idx) => {
- const indexInList = isCreateVariableButtonDisplayed
- ? idx + 1
- : idx
- return (
- (itemsRef.current[idx] = el)}
- role="menuitem"
- minH="40px"
- key={idx}
- onClick={handleVariableNameClick(item)}
- fontSize="16px"
- fontWeight="normal"
- rounded="none"
- colorScheme="gray"
- variant="ghost"
- justifyContent="space-between"
- bgColor={
- keyboardFocusIndex === indexInList
- ? focusedItemBgColor
- : 'transparent'
- }
- transition="none"
- >
-
- {item.name}
-
+ {filteredItems.length > 0 &&
+ filteredItems.map((item, idx) => {
+ const indexInList = isCreateVariableButtonDisplayed
+ ? idx + 1
+ : idx;
+ return (
+ (itemsRef.current[idx] = el)}
+ role="menuitem"
+ minH="40px"
+ key={idx}
+ onClick={handleVariableNameClick(item)}
+ fontSize="16px"
+ fontWeight="normal"
+ rounded="none"
+ colorScheme="gray"
+ variant="ghost"
+ justifyContent="space-between"
+ bgColor={
+ keyboardFocusIndex === indexInList
+ ? focusedItemBgColor
+ : "transparent"
+ }
+ transition="none"
+ >
+
+ {item.name}
+
-
- }
- aria-label={t('variables.rename')}
- size="xs"
- onClick={handleRenameVariableClick(item)}
- />
- }
- aria-label={t('variables.remove')}
- size="xs"
- onClick={handleDeleteVariableClick(item)}
- />
-
-
- )
- })}
- >
- )}
+
+ }
+ aria-label={t("variables.rename")}
+ size="xs"
+ onClick={handleRenameVariableClick(item)}
+ />
+ }
+ aria-label={t("variables.remove")}
+ size="xs"
+ onClick={handleDeleteVariableClick(item)}
+ />
+
+
+ );
+ })}
{helperText && {helperText} }
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/inputs/index.tsx b/apps/builder/src/components/inputs/index.tsx
index 9db702e325..b539242544 100644
--- a/apps/builder/src/components/inputs/index.tsx
+++ b/apps/builder/src/components/inputs/index.tsx
@@ -1,3 +1,3 @@
-export { TextInput } from './TextInput'
-export { Textarea } from './Textarea'
-export { NumberInput } from './NumberInput'
+export { TextInput } from "./TextInput";
+export { Textarea } from "./Textarea";
+export { NumberInput } from "./NumberInput";
diff --git a/apps/builder/src/components/logos/AzureAdLogo.tsx b/apps/builder/src/components/logos/AzureAdLogo.tsx
index f094427d4e..5b001c914e 100644
--- a/apps/builder/src/components/logos/AzureAdLogo.tsx
+++ b/apps/builder/src/components/logos/AzureAdLogo.tsx
@@ -1,4 +1,4 @@
-import { Icon, IconProps } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const AzureAdLogo = (props: IconProps) => {
return (
@@ -27,5 +27,5 @@ export const AzureAdLogo = (props: IconProps) => {
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/logos/FacebookLogo.tsx b/apps/builder/src/components/logos/FacebookLogo.tsx
index 5e29e1d68e..8931cc9df5 100644
--- a/apps/builder/src/components/logos/FacebookLogo.tsx
+++ b/apps/builder/src/components/logos/FacebookLogo.tsx
@@ -1,4 +1,4 @@
-import { IconProps, Icon } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const FacebookLogo = (props: IconProps) => (
@@ -8,4 +8,4 @@ export const FacebookLogo = (props: IconProps) => (
fill="#fff"
/>
-)
+);
diff --git a/apps/builder/src/components/logos/GiphyLogo.tsx b/apps/builder/src/components/logos/GiphyLogo.tsx
index 16ece7384c..c3041ccdb9 100644
--- a/apps/builder/src/components/logos/GiphyLogo.tsx
+++ b/apps/builder/src/components/logos/GiphyLogo.tsx
@@ -1,4 +1,4 @@
-import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react'
+import { Icon, type IconProps, useColorModeValue } from "@chakra-ui/react";
export const GiphyLogo = (props: IconProps) => (
@@ -15,9 +15,9 @@ export const GiphyLogo = (props: IconProps) => (
-)
+);
diff --git a/apps/builder/src/components/logos/GitlabLogo.tsx b/apps/builder/src/components/logos/GitlabLogo.tsx
index f5b0e25e03..aad593b992 100644
--- a/apps/builder/src/components/logos/GitlabLogo.tsx
+++ b/apps/builder/src/components/logos/GitlabLogo.tsx
@@ -1,4 +1,4 @@
-import { IconProps, Icon } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const GitlabLogo = (props: IconProps) => (
@@ -31,4 +31,4 @@ export const GitlabLogo = (props: IconProps) => (
fill="#E24329"
/>
-)
+);
diff --git a/apps/builder/src/components/logos/KeycloakLogo.tsx b/apps/builder/src/components/logos/KeycloakLogo.tsx
index bfa68202f0..73ee7daacc 100644
--- a/apps/builder/src/components/logos/KeycloakLogo.tsx
+++ b/apps/builder/src/components/logos/KeycloakLogo.tsx
@@ -1,4 +1,4 @@
-import { IconProps, Icon } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const KeycloackLogo = (props: IconProps) => (
@@ -8,4 +8,4 @@ export const KeycloackLogo = (props: IconProps) => (
fill="#fff"
/>
-)
+);
diff --git a/apps/builder/src/components/logos/PexelsLogo.tsx b/apps/builder/src/components/logos/PexelsLogo.tsx
index ddf50d1ea7..d77784a0b0 100644
--- a/apps/builder/src/components/logos/PexelsLogo.tsx
+++ b/apps/builder/src/components/logos/PexelsLogo.tsx
@@ -1,4 +1,4 @@
-import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react'
+import { Icon, type IconProps, useColorModeValue } from "@chakra-ui/react";
export const PexelsLogo = (props: IconProps) => (
@@ -18,8 +18,8 @@ export const PexelsLogo = (props: IconProps) => (
-)
+);
diff --git a/apps/builder/src/components/logos/StripeLogo.tsx b/apps/builder/src/components/logos/StripeLogo.tsx
index 880e69fa39..f9bb4ab9e9 100644
--- a/apps/builder/src/components/logos/StripeLogo.tsx
+++ b/apps/builder/src/components/logos/StripeLogo.tsx
@@ -1,16 +1,16 @@
-import { Icon, IconProps } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const StripeLogo = (props: IconProps) => {
return (
- )
-}
+ );
+};
diff --git a/apps/builder/src/components/logos/UnsplashLogo.tsx b/apps/builder/src/components/logos/UnsplashLogo.tsx
index 82571facda..b8fb29a3b8 100644
--- a/apps/builder/src/components/logos/UnsplashLogo.tsx
+++ b/apps/builder/src/components/logos/UnsplashLogo.tsx
@@ -1,7 +1,7 @@
-import { IconProps, Icon } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const UnsplashLogo = (props: IconProps) => (
-)
+);
diff --git a/apps/builder/src/components/logos/WhatsAppLogo.tsx b/apps/builder/src/components/logos/WhatsAppLogo.tsx
index a5b9a7d336..2bf9f2732e 100644
--- a/apps/builder/src/components/logos/WhatsAppLogo.tsx
+++ b/apps/builder/src/components/logos/WhatsAppLogo.tsx
@@ -1,6 +1,6 @@
-import { IconProps, Icon } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
-export const whatsAppBrandColor = '#25D366'
+export const whatsAppBrandColor = "#25D366";
export const WhatsAppLogo = (props: IconProps) => (
@@ -11,4 +11,4 @@ export const WhatsAppLogo = (props: IconProps) => (
fill="currentColor"
/>
-)
+);
diff --git a/apps/builder/src/features/account/UserProvider.tsx b/apps/builder/src/features/account/UserProvider.tsx
index 3e1fb1ca27..3debc6659f 100644
--- a/apps/builder/src/features/account/UserProvider.tsx
+++ b/apps/builder/src/features/account/UserProvider.tsx
@@ -1,119 +1,120 @@
-import { signOut, useSession } from 'next-auth/react'
-import { useRouter } from 'next/router'
-import { createContext, ReactNode, useEffect, useState } from 'react'
-import { isDefined, isNotDefined } from '@typebot.io/lib'
-import { User } from '@typebot.io/schemas'
-import { setUser as setSentryUser } from '@sentry/nextjs'
-import { useToast } from '@/hooks/useToast'
-import { updateUserQuery } from './queries/updateUserQuery'
-import { useDebouncedCallback } from 'use-debounce'
-import { env } from '@typebot.io/env'
-import { useColorMode } from '@chakra-ui/react'
+import { useToast } from "@/hooks/useToast";
+import { useColorMode } from "@chakra-ui/react";
+import { setUser as setSentryUser } from "@sentry/nextjs";
+import { env } from "@typebot.io/env";
+import { isDefined, isNotDefined } from "@typebot.io/lib/utils";
+import type { User } from "@typebot.io/schemas/features/user/schema";
+import { signOut, useSession } from "next-auth/react";
+import { useRouter } from "next/router";
+import type { ReactNode } from "react";
+import { createContext, useEffect, useState } from "react";
+import { useDebouncedCallback } from "use-debounce";
+import { updateUserQuery } from "./queries/updateUserQuery";
export const userContext = createContext<{
- user?: User
- isLoading: boolean
- currentWorkspaceId?: string
- logOut: () => void
- updateUser: (newUser: Partial) => void
+ user?: User;
+ isLoading: boolean;
+ currentWorkspaceId?: string;
+ logOut: () => void;
+ updateUser: (newUser: Partial) => void;
}>({
isLoading: false,
logOut: () => {},
updateUser: () => {},
-})
+});
-const debounceTimeout = 1000
+const debounceTimeout = 1000;
export const UserProvider = ({ children }: { children: ReactNode }) => {
- const router = useRouter()
- const { data: session, status } = useSession()
- const [user, setUser] = useState()
- const { showToast } = useToast()
- const [currentWorkspaceId, setCurrentWorkspaceId] = useState()
- const { setColorMode } = useColorMode()
+ const router = useRouter();
+ const { data: session, status } = useSession();
+ const [user, setUser] = useState();
+ const { showToast } = useToast();
+ const [currentWorkspaceId, setCurrentWorkspaceId] = useState();
+ const { setColorMode } = useColorMode();
useEffect(() => {
- const currentColorScheme = localStorage.getItem('chakra-ui-color-mode') as
- | 'light'
- | 'dark'
- | null
- if (!currentColorScheme) return
- const systemColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
+ const currentColorScheme = localStorage.getItem("chakra-ui-color-mode") as
+ | "light"
+ | "dark"
+ | null;
+ if (!currentColorScheme) return;
+ const systemColorScheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
- ? 'dark'
- : 'light'
+ ? "dark"
+ : "light";
const userPrefersSystemMode =
- !user?.preferredAppAppearance || user.preferredAppAppearance === 'system'
+ !user?.preferredAppAppearance || user.preferredAppAppearance === "system";
const computedColorMode = userPrefersSystemMode
? systemColorScheme
- : user?.preferredAppAppearance
- if (computedColorMode === currentColorScheme) return
- setColorMode(computedColorMode)
- }, [setColorMode, user?.preferredAppAppearance])
+ : user?.preferredAppAppearance;
+ if (computedColorMode === currentColorScheme) return;
+ setColorMode(computedColorMode);
+ }, [setColorMode, user?.preferredAppAppearance]);
useEffect(() => {
- if (isDefined(user) || isNotDefined(session)) return
+ if (isDefined(user) || isNotDefined(session)) return;
setCurrentWorkspaceId(
- localStorage.getItem('currentWorkspaceId') ?? undefined
- )
- const parsedUser = session.user as User
- setUser(parsedUser)
+ localStorage.getItem("currentWorkspaceId") ?? undefined,
+ );
+ const parsedUser = session.user as User;
+ setUser(parsedUser);
if (parsedUser?.id) {
- setSentryUser({ id: parsedUser.id })
+ setSentryUser({ id: parsedUser.id });
}
- }, [session, user])
+ }, [session, user]);
useEffect(() => {
- if (!router.isReady) return
- if (status === 'loading') return
- const isSignInPath = ['/signin', '/register'].includes(router.pathname)
+ if (!router.isReady) return;
+ if (status === "loading") return;
+ const isSignInPath = ["/signin", "/register"].includes(router.pathname);
const isPathPublicFriendly = /\/typebots\/.+\/(edit|theme|settings)/.test(
- router.pathname
- )
- if (isSignInPath || isPathPublicFriendly) return
- if (!user && status === 'unauthenticated')
+ router.pathname,
+ );
+ if (isSignInPath || isPathPublicFriendly) return;
+ if (!user && status === "unauthenticated")
router.replace({
- pathname: '/signin',
+ pathname: "/signin",
query: {
redirectPath: router.asPath,
},
- })
- }, [router, status, user])
+ });
+ }, [router, status, user]);
const updateUser = (updates: Partial) => {
- if (isNotDefined(user)) return
- const newUser = { ...user, ...updates }
- setUser(newUser)
- saveUser(updates)
- }
+ if (isNotDefined(user)) return;
+ const newUser = { ...user, ...updates };
+ setUser(newUser);
+ saveUser(updates);
+ };
const saveUser = useDebouncedCallback(
async (updates: Partial) => {
- if (isNotDefined(user)) return
- const { error } = await updateUserQuery(user.id, updates)
- if (error) showToast({ title: error.name, description: error.message })
- await refreshUser()
+ if (isNotDefined(user)) return;
+ const { error } = await updateUserQuery(user.id, updates);
+ if (error) showToast({ title: error.name, description: error.message });
+ await refreshUser();
},
- env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
- )
+ env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout,
+ );
const logOut = () => {
- signOut()
- setUser(undefined)
- }
+ signOut();
+ setUser(undefined);
+ };
useEffect(() => {
return () => {
- saveUser.flush()
- }
- }, [saveUser])
+ saveUser.flush();
+ };
+ }, [saveUser]);
return (
{
>
{children}
- )
-}
+ );
+};
export const refreshUser = async () => {
- await fetch('/api/auth/session?update')
- reloadSession()
-}
+ await fetch("/api/auth/session?update");
+ reloadSession();
+};
const reloadSession = () => {
- const event = new Event('visibilitychange')
- document.dispatchEvent(event)
-}
+ const event = new Event("visibilitychange");
+ document.dispatchEvent(event);
+};
diff --git a/apps/builder/src/features/account/account.spec.ts b/apps/builder/src/features/account/account.spec.ts
index 18adb985d4..ed4876a2e5 100644
--- a/apps/builder/src/features/account/account.spec.ts
+++ b/apps/builder/src/features/account/account.spec.ts
@@ -1,47 +1,47 @@
-import { getTestAsset } from '@/test/utils/playwright'
-import test, { expect } from '@playwright/test'
-import { env } from '@typebot.io/env'
-import { userId } from '@typebot.io/playwright/databaseSetup'
+import { getTestAsset } from "@/test/utils/playwright";
+import test, { expect } from "@playwright/test";
+import { env } from "@typebot.io/env";
+import { userId } from "@typebot.io/playwright/databaseSetup";
-test.describe.configure({ mode: 'parallel' })
+test.describe.configure({ mode: "parallel" });
-test('should display user info properly', async ({ page }) => {
- await page.goto('/typebots')
- await page.click('text=Settings & Members')
+test("should display user info properly", async ({ page }) => {
+ await page.goto("/typebots");
+ await page.click("text=Settings & Members");
expect(
- page.locator('input[type="email"]').getAttribute('disabled')
- ).toBeDefined()
- await page.getByRole('textbox', { name: 'Name:' }).fill('John Doe')
- await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg'))
- await expect(page.locator('img >> nth=1')).toHaveAttribute(
- 'src',
+ page.locator('input[type="email"]').getAttribute("disabled"),
+ ).toBeDefined();
+ await page.getByRole("textbox", { name: "Name:" }).fill("John Doe");
+ await page.setInputFiles('input[type="file"]', getTestAsset("avatar.jpg"));
+ await expect(page.locator("img >> nth=1")).toHaveAttribute(
+ "src",
new RegExp(
- `${env.S3_ENDPOINT}${env.S3_PORT ? `:${env.S3_PORT}` : ''}/${
+ `${env.S3_ENDPOINT}${env.S3_PORT ? `:${env.S3_PORT}` : ""}/${
env.S3_BUCKET
}/public/users/${userId}/avatar`,
- 'gm'
- )
- )
- await page.click('text="Preferences"')
- await expect(page.locator('text=Trackpad')).toBeVisible()
-})
+ "gm",
+ ),
+ );
+ await page.click('text="Preferences"');
+ await expect(page.locator("text=Trackpad")).toBeVisible();
+});
-test('should be able to create and delete api tokens', async ({ page }) => {
- await page.goto('/typebots')
- await page.click('text=Settings & Members')
- await expect(page.locator('text=Github')).toBeVisible()
- await page.click('text="Create"')
- await expect(page.locator('button >> text="Create token"')).toBeDisabled()
- await page.fill('[placeholder="I.e. Zapier, Github, Make.com"]', 'CLI')
- await expect(page.locator('button >> text="Create token"')).toBeEnabled()
- await page.click('button >> text="Create token"')
- await expect(page.locator('text=Please copy your token')).toBeVisible()
- await expect(page.locator('button >> text="Copy"')).toBeVisible()
- await page.click('button >> text="Done"')
- await expect(page.locator('text=CLI')).toBeVisible()
- await page.click('text="Delete" >> nth=2')
- await expect(page.locator('strong >> text="Github"')).toBeVisible()
- await page.click('button >> text="Delete" >> nth=-1')
- await expect(page.locator('button >> text="Delete" >> nth=-1')).toBeEnabled()
- await expect(page.locator('text="Github"')).toBeHidden()
-})
+test("should be able to create and delete api tokens", async ({ page }) => {
+ await page.goto("/typebots");
+ await page.click("text=Settings & Members");
+ await expect(page.locator("text=Github")).toBeVisible();
+ await page.click('text="Create"');
+ await expect(page.locator('button >> text="Create token"')).toBeDisabled();
+ await page.fill('[placeholder="I.e. Zapier, Github, Make.com"]', "CLI");
+ await expect(page.locator('button >> text="Create token"')).toBeEnabled();
+ await page.click('button >> text="Create token"');
+ await expect(page.locator("text=Please copy your token")).toBeVisible();
+ await expect(page.locator('button >> text="Copy"')).toBeVisible();
+ await page.click('button >> text="Done"');
+ await expect(page.locator("text=CLI")).toBeVisible();
+ await page.click('text="Delete" >> nth=2');
+ await expect(page.locator('strong >> text="Github"')).toBeVisible();
+ await page.click('button >> text="Delete" >> nth=-1');
+ await expect(page.locator('button >> text="Delete" >> nth=-1')).toBeEnabled();
+ await expect(page.locator('text="Github"')).toBeHidden();
+});
diff --git a/apps/builder/src/features/account/components/ApiTokensList.tsx b/apps/builder/src/features/account/components/ApiTokensList.tsx
index 2ee74794b3..791105a21c 100644
--- a/apps/builder/src/features/account/components/ApiTokensList.tsx
+++ b/apps/builder/src/features/account/components/ApiTokensList.tsx
@@ -1,67 +1,68 @@
+import { ConfirmModal } from "@/components/ConfirmModal";
+import { TimeSince } from "@/components/TimeSince";
+import { useToast } from "@/hooks/useToast";
import {
- TableContainer,
- Table,
- Thead,
- Tr,
- Th,
- Tbody,
- Td,
Button,
- Text,
- Heading,
Checkbox,
+ Flex,
+ Heading,
Skeleton,
Stack,
- Flex,
+ Table,
+ TableContainer,
+ Tbody,
+ Td,
+ Text,
+ Th,
+ Thead,
+ Tr,
useDisclosure,
-} from '@chakra-ui/react'
-import { ConfirmModal } from '@/components/ConfirmModal'
-import { useToast } from '@/hooks/useToast'
-import { User } from '@typebot.io/prisma'
-import React, { useState } from 'react'
-import { byId, isDefined } from '@typebot.io/lib'
-import { CreateTokenModal } from './CreateTokenModal'
-import { useApiTokens } from '../hooks/useApiTokens'
-import { ApiTokenFromServer } from '../types'
-import { deleteApiTokenQuery } from '../queries/deleteApiTokenQuery'
-import { T, useTranslate } from '@tolgee/react'
-import { TimeSince } from '@/components/TimeSince'
+} from "@chakra-ui/react";
+import { T, useTranslate } from "@tolgee/react";
+import { byId, isDefined } from "@typebot.io/lib/utils";
+import type { Prisma } from "@typebot.io/prisma/types";
+import React, { useState } from "react";
+import { useApiTokens } from "../hooks/useApiTokens";
+import { deleteApiTokenQuery } from "../queries/deleteApiTokenQuery";
+import type { ApiTokenFromServer } from "../types";
+import { CreateTokenModal } from "./CreateTokenModal";
-type Props = { user: User }
+type Props = { user: Prisma.User };
export const ApiTokensList = ({ user }: Props) => {
- const { t } = useTranslate()
- const { showToast } = useToast()
+ const { t } = useTranslate();
+ const { showToast } = useToast();
const { apiTokens, isLoading, mutate } = useApiTokens({
userId: user.id,
onError: (e) =>
- showToast({ title: 'Failed to fetch tokens', description: e.message }),
- })
+ showToast({ title: "Failed to fetch tokens", description: e.message }),
+ });
const {
isOpen: isCreateOpen,
onOpen: onCreateOpen,
onClose: onCreateClose,
- } = useDisclosure()
- const [deletingId, setDeletingId] = useState()
+ } = useDisclosure();
+ const [deletingId, setDeletingId] = useState();
const refreshListWithNewToken = (token: ApiTokenFromServer) => {
- if (!apiTokens) return
- mutate({ apiTokens: [token, ...apiTokens] })
- }
+ if (!apiTokens) return;
+ mutate({ apiTokens: [token, ...apiTokens] });
+ };
const deleteToken = async (tokenId?: string) => {
- if (!apiTokens || !tokenId) return
- const { error } = await deleteApiTokenQuery({ userId: user.id, tokenId })
- if (!error) mutate({ apiTokens: apiTokens.filter((t) => t.id !== tokenId) })
- }
+ if (!apiTokens || !tokenId) return;
+ const { error } = await deleteApiTokenQuery({ userId: user.id, tokenId });
+ if (!error)
+ mutate({ apiTokens: apiTokens.filter((t) => t.id !== tokenId) });
+ };
return (
- {t('account.apiTokens.heading')}
- {t('account.apiTokens.description')}
+ {t("account.apiTokens.heading")}
+ {t("account.apiTokens.description")}
- {t('account.apiTokens.createButton.label')}
+ {t("account.apiTokens.createButton.label")}
{
- {t('account.apiTokens.table.nameHeader')}
- {t('account.apiTokens.table.createdHeader')}
+ {t("account.apiTokens.table.nameHeader")}
+ {t("account.apiTokens.table.createdHeader")}
@@ -94,7 +95,7 @@ export const ApiTokensList = ({ user }: Props) => {
variant="outline"
onClick={() => setDeletingId(token.id)}
>
- {t('account.apiTokens.deleteButton.label')}
+ {t("account.apiTokens.deleteButton.label")}
@@ -132,8 +133,8 @@ export const ApiTokensList = ({ user }: Props) => {
/>
}
- confirmButtonLabel={t('account.apiTokens.deleteButton.label')}
+ confirmButtonLabel={t("account.apiTokens.deleteButton.label")}
/>
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/account/components/ApiTokensModal.tsx b/apps/builder/src/features/account/components/ApiTokensModal.tsx
index 1073d3c6a8..f117c8874d 100644
--- a/apps/builder/src/features/account/components/ApiTokensModal.tsx
+++ b/apps/builder/src/features/account/components/ApiTokensModal.tsx
@@ -5,18 +5,18 @@ import {
ModalFooter,
ModalHeader,
ModalOverlay,
-} from '@chakra-ui/react'
-import { ApiTokensList } from './ApiTokensList'
-import { useUser } from '../hooks/useUser'
+} from "@chakra-ui/react";
+import { useUser } from "../hooks/useUser";
+import { ApiTokensList } from "./ApiTokensList";
type Props = {
- isOpen: boolean
- onClose: () => void
-}
+ isOpen: boolean;
+ onClose: () => void;
+};
export const ApiTokensModal = ({ isOpen, onClose }: Props) => {
- const { user } = useUser()
+ const { user } = useUser();
- if (!user) return
+ if (!user) return;
return (
@@ -28,5 +28,5 @@ export const ApiTokensModal = ({ isOpen, onClose }: Props) => {
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx b/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx
index ea7b604876..d15f2ad9f0 100644
--- a/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx
+++ b/apps/builder/src/features/account/components/AppearanceRadioGroup.tsx
@@ -1,39 +1,39 @@
import {
- RadioGroup,
HStack,
- VStack,
- Stack,
+ Image,
Radio,
+ RadioGroup,
+ Stack,
Text,
- Image,
-} from '@chakra-ui/react'
-import { useTranslate } from '@tolgee/react'
+ VStack,
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
type Props = {
- defaultValue: string
- onChange: (value: string) => void
-}
+ defaultValue: string;
+ onChange: (value: string) => void;
+};
export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const appearanceData = [
{
- value: 'light',
- label: t('account.preferences.appearance.lightLabel'),
- image: '/images/light-mode.png',
+ value: "light",
+ label: t("account.preferences.appearance.lightLabel"),
+ image: "/images/light-mode.png",
},
{
- value: 'dark',
- label: t('account.preferences.appearance.darkLabel'),
- image: '/images/dark-mode.png',
+ value: "dark",
+ label: t("account.preferences.appearance.darkLabel"),
+ image: "/images/dark-mode.png",
},
{
- value: 'system',
- label: t('account.preferences.appearance.systemLabel'),
- image: '/images/system-mode.png',
+ value: "system",
+ label: t("account.preferences.appearance.systemLabel"),
+ image: "/images/system-mode.png",
},
- ]
+ ];
return (
@@ -54,7 +54,7 @@ export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => {
@@ -67,5 +67,5 @@ export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => {
))}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/account/components/CreateTokenModal.tsx b/apps/builder/src/features/account/components/CreateTokenModal.tsx
index 4c1c631330..118f0d0e5b 100644
--- a/apps/builder/src/features/account/components/CreateTokenModal.tsx
+++ b/apps/builder/src/features/account/components/CreateTokenModal.tsx
@@ -1,30 +1,31 @@
-import { CopyButton } from '@/components/CopyButton'
-import { useTranslate } from '@tolgee/react'
+import { CopyButton } from "@/components/CopyButton";
import {
+ Button,
+ Input,
+ InputGroup,
+ InputRightElement,
Modal,
- ModalOverlay,
+ ModalBody,
+ ModalCloseButton,
ModalContent,
+ ModalFooter,
ModalHeader,
- ModalCloseButton,
- ModalBody,
+ ModalOverlay,
Stack,
- Input,
- ModalFooter,
- Button,
Text,
- InputGroup,
- InputRightElement,
-} from '@chakra-ui/react'
-import React, { FormEvent, useRef, useState } from 'react'
-import { createApiTokenQuery } from '../queries/createApiTokenQuery'
-import { ApiTokenFromServer } from '../types'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import type { FormEvent } from "react";
+import React, { useRef, useState } from "react";
+import { createApiTokenQuery } from "../queries/createApiTokenQuery";
+import type { ApiTokenFromServer } from "../types";
type Props = {
- userId: string
- isOpen: boolean
- onNewToken: (token: ApiTokenFromServer) => void
- onClose: () => void
-}
+ userId: string;
+ isOpen: boolean;
+ onNewToken: (token: ApiTokenFromServer) => void;
+ onClose: () => void;
+};
export const CreateTokenModal = ({
userId,
@@ -32,22 +33,22 @@ export const CreateTokenModal = ({
onClose,
onNewToken,
}: Props) => {
- const inputRef = useRef(null)
- const { t } = useTranslate()
- const [name, setName] = useState('')
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [newTokenValue, setNewTokenValue] = useState()
+ const inputRef = useRef(null);
+ const { t } = useTranslate();
+ const [name, setName] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [newTokenValue, setNewTokenValue] = useState();
const createToken = async (e: FormEvent) => {
- e.preventDefault()
- setIsSubmitting(true)
- const { data } = await createApiTokenQuery(userId, { name })
+ e.preventDefault();
+ setIsSubmitting(true);
+ const { data } = await createApiTokenQuery(userId, { name });
if (data?.apiToken) {
- setNewTokenValue(data.apiToken.token)
- onNewToken(data.apiToken)
+ setNewTokenValue(data.apiToken.token);
+ onNewToken(data.apiToken);
}
- setIsSubmitting(false)
- }
+ setIsSubmitting(false);
+ };
return (
@@ -55,16 +56,16 @@ export const CreateTokenModal = ({
{newTokenValue
- ? t('account.apiTokens.createModal.createdHeading')
- : t('account.apiTokens.createModal.createHeading')}
+ ? t("account.apiTokens.createModal.createdHeading")
+ : t("account.apiTokens.createModal.createHeading")}
{newTokenValue ? (
- {t('account.apiTokens.createModal.copyInstruction')}{' '}
+ {t("account.apiTokens.createModal.copyInstruction")}{" "}
- {t('account.apiTokens.createModal.securityWarning')}
+ {t("account.apiTokens.createModal.securityWarning")}
@@ -77,12 +78,12 @@ export const CreateTokenModal = ({
) : (
- {t('account.apiTokens.createModal.nameInput.label')}
+ {t("account.apiTokens.createModal.nameInput.label")}
setName(e.target.value)}
/>
@@ -92,7 +93,7 @@ export const CreateTokenModal = ({
{newTokenValue ? (
- {t('account.apiTokens.createModal.doneButton.label')}
+ {t("account.apiTokens.createModal.doneButton.label")}
) : (
- {t('account.apiTokens.createModal.createButton.label')}
+ {t("account.apiTokens.createModal.createButton.label")}
)}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx b/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx
index e66205032b..e73246cad1 100644
--- a/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx
+++ b/apps/builder/src/features/account/components/GraphNavigationRadioGroup.tsx
@@ -1,40 +1,40 @@
-import { MouseIcon, LaptopIcon } from '@/components/icons'
-import { useTranslate } from '@tolgee/react'
+import { LaptopIcon, MouseIcon } from "@/components/icons";
import {
HStack,
Radio,
RadioGroup,
Stack,
- VStack,
Text,
-} from '@chakra-ui/react'
-import { GraphNavigation } from '@typebot.io/prisma'
+ VStack,
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { GraphNavigation } from "@typebot.io/prisma/enum";
type Props = {
- defaultValue: string
- onChange: (value: string) => void
-}
+ defaultValue: string;
+ onChange: (value: string) => void;
+};
export const GraphNavigationRadioGroup = ({
defaultValue,
onChange,
}: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const graphNavigationData = [
{
value: GraphNavigation.MOUSE,
- label: t('account.preferences.graphNavigation.mouse.label'),
- description: t('account.preferences.graphNavigation.mouse.description'),
+ label: t("account.preferences.graphNavigation.mouse.label"),
+ description: t("account.preferences.graphNavigation.mouse.description"),
icon: ,
},
{
value: GraphNavigation.TRACKPAD,
- label: t('account.preferences.graphNavigation.trackpad.label'),
+ label: t("account.preferences.graphNavigation.trackpad.label"),
description: t(
- 'account.preferences.graphNavigation.trackpad.description'
+ "account.preferences.graphNavigation.trackpad.description",
),
icon: ,
},
- ]
+ ];
return (
@@ -64,5 +64,5 @@ export const GraphNavigationRadioGroup = ({
))}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/account/components/MyAccountForm.tsx b/apps/builder/src/features/account/components/MyAccountForm.tsx
index bd86629685..792ff552c1 100644
--- a/apps/builder/src/features/account/components/MyAccountForm.tsx
+++ b/apps/builder/src/features/account/components/MyAccountForm.tsx
@@ -1,31 +1,31 @@
-import { Stack, HStack, Avatar, Text, Tooltip } from '@chakra-ui/react'
-import { UploadIcon } from '@/components/icons'
-import React, { useState } from 'react'
-import { ApiTokensList } from './ApiTokensList'
-import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
-import { useUser } from '../hooks/useUser'
-import { TextInput } from '@/components/inputs/TextInput'
-import { useTranslate } from '@tolgee/react'
+import { UploadButton } from "@/components/ImageUploadContent/UploadButton";
+import { UploadIcon } from "@/components/icons";
+import { TextInput } from "@/components/inputs/TextInput";
+import { Avatar, HStack, Stack, Text, Tooltip } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import React, { useState } from "react";
+import { useUser } from "../hooks/useUser";
+import { ApiTokensList } from "./ApiTokensList";
export const MyAccountForm = () => {
- const { t } = useTranslate()
- const { user, updateUser } = useUser()
- const [name, setName] = useState(user?.name ?? '')
- const [email, setEmail] = useState(user?.email ?? '')
+ const { t } = useTranslate();
+ const { user, updateUser } = useUser();
+ const [name, setName] = useState(user?.name ?? "");
+ const [email, setEmail] = useState(user?.email ?? "");
const handleFileUploaded = async (url: string) => {
- updateUser({ image: url })
- }
+ updateUser({ image: url });
+ };
const handleNameChange = (newName: string) => {
- setName(newName)
- updateUser({ name: newName })
- }
+ setName(newName);
+ updateUser({ name: newName });
+ };
const handleEmailChange = (newEmail: string) => {
- setEmail(newEmail)
- updateUser({ email: newEmail })
- }
+ setEmail(newEmail);
+ updateUser({ email: newEmail });
+ };
return (
@@ -42,16 +42,16 @@ export const MyAccountForm = () => {
fileType="image"
filePathProps={{
userId: user.id,
- fileName: 'avatar',
+ fileName: "avatar",
}}
leftIcon={ }
onFileUploaded={handleFileUploaded}
>
- {t('account.myAccount.changePhotoButton.label')}
+ {t("account.myAccount.changePhotoButton.label")}
)}
- {t('account.myAccount.changePhotoButton.specification')}
+ {t("account.myAccount.changePhotoButton.specification")}
@@ -59,17 +59,17 @@ export const MyAccountForm = () => {
-
+
{
{user && }
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/account/components/UserPreferencesForm.tsx b/apps/builder/src/features/account/components/UserPreferencesForm.tsx
index 61eb6f9827..2b8224591c 100644
--- a/apps/builder/src/features/account/components/UserPreferencesForm.tsx
+++ b/apps/builder/src/features/account/components/UserPreferencesForm.tsx
@@ -1,85 +1,85 @@
+import { MoreInfoTooltip } from "@/components/MoreInfoTooltip";
+import { ChevronDownIcon } from "@/components/icons";
import {
- Stack,
+ Button,
+ HStack,
Heading,
Menu,
MenuButton,
- MenuList,
MenuItem,
- Button,
- HStack,
-} from '@chakra-ui/react'
-import { GraphNavigation } from '@typebot.io/prisma'
-import React, { useEffect } from 'react'
-import { AppearanceRadioGroup } from './AppearanceRadioGroup'
-import { useUser } from '../hooks/useUser'
-import { ChevronDownIcon } from '@/components/icons'
-import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
-import { useTranslate, useTolgee } from '@tolgee/react'
-import { useRouter } from 'next/router'
-import { GraphNavigationRadioGroup } from './GraphNavigationRadioGroup'
+ MenuList,
+ Stack,
+} from "@chakra-ui/react";
+import { useTolgee, useTranslate } from "@tolgee/react";
+import { GraphNavigation } from "@typebot.io/prisma/enum";
+import { useRouter } from "next/router";
+import React, { useEffect } from "react";
+import { useUser } from "../hooks/useUser";
+import { AppearanceRadioGroup } from "./AppearanceRadioGroup";
+import { GraphNavigationRadioGroup } from "./GraphNavigationRadioGroup";
const localeHumanReadable = {
- en: 'English',
- fr: 'Français',
- de: 'Deutsch',
- pt: 'Português',
- 'pt-BR': 'Português (BR)',
- ro: 'Română',
- es: 'Español',
- it: 'Italiano',
-} as const
+ en: "English",
+ fr: "Français",
+ de: "Deutsch",
+ pt: "Português",
+ "pt-BR": "Português (BR)",
+ ro: "Română",
+ es: "Español",
+ it: "Italiano",
+} as const;
export const UserPreferencesForm = () => {
- const { getLanguage } = useTolgee()
- const router = useRouter()
- const { t } = useTranslate()
- const { user, updateUser } = useUser()
+ const { getLanguage } = useTolgee();
+ const router = useRouter();
+ const { t } = useTranslate();
+ const { user, updateUser } = useUser();
useEffect(() => {
if (!user?.graphNavigation)
- updateUser({ graphNavigation: GraphNavigation.MOUSE })
- }, [updateUser, user?.graphNavigation])
+ updateUser({ graphNavigation: GraphNavigation.MOUSE });
+ }, [updateUser, user?.graphNavigation]);
const changeAppearance = async (value: string) => {
- updateUser({ preferredAppAppearance: value })
- }
+ updateUser({ preferredAppAppearance: value });
+ };
const updateLocale = (locale: keyof typeof localeHumanReadable) => () => {
- document.cookie = `NEXT_LOCALE=${locale}; path=/; max-age=31536000`
+ document.cookie = `NEXT_LOCALE=${locale}; path=/; max-age=31536000`;
router.replace(
{
pathname: router.pathname,
query: router.query,
},
undefined,
- { locale }
- )
- }
+ { locale },
+ );
+ };
const changeGraphNavigation = async (value: string) => {
- updateUser({ graphNavigation: value as GraphNavigation })
- }
+ updateUser({ graphNavigation: value as GraphNavigation });
+ };
- const currentLanguage = getLanguage()
+ const currentLanguage = getLanguage();
return (
- {t('account.preferences.language.heading')}
+ {t("account.preferences.language.heading")}
}>
{currentLanguage
? localeHumanReadable[
currentLanguage as keyof typeof localeHumanReadable
]
- : 'Loading...'}
+ : "Loading..."}
{Object.keys(localeHumanReadable).map((locale) => (
{
@@ -91,15 +91,15 @@ export const UserPreferencesForm = () => {
))}
- {currentLanguage !== 'en' && (
+ {currentLanguage !== "en" && (
- {t('account.preferences.language.tooltip')}
+ {t("account.preferences.language.tooltip")}
)}
- {t('account.preferences.graphNavigation.heading')}
+ {t("account.preferences.graphNavigation.heading")}
{
- {t('account.preferences.appearance.heading')}
+ {t("account.preferences.appearance.heading")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/account/hooks/useApiTokens.ts b/apps/builder/src/features/account/hooks/useApiTokens.ts
index d38aafd373..69238a26ae 100644
--- a/apps/builder/src/features/account/hooks/useApiTokens.ts
+++ b/apps/builder/src/features/account/hooks/useApiTokens.ts
@@ -1,30 +1,30 @@
-import { fetcher } from '@/helpers/fetcher'
-import useSWR from 'swr'
-import { env } from '@typebot.io/env'
-import { ApiTokenFromServer } from '../types'
+import { fetcher } from "@/helpers/fetcher";
+import { env } from "@typebot.io/env";
+import useSWR from "swr";
+import type { ApiTokenFromServer } from "../types";
type ServerResponse = {
- apiTokens: ApiTokenFromServer[]
-}
+ apiTokens: ApiTokenFromServer[];
+};
export const useApiTokens = ({
userId,
onError,
}: {
- userId?: string
- onError: (error: Error) => void
+ userId?: string;
+ onError: (error: Error) => void;
}) => {
const { data, error, mutate } = useSWR(
userId ? `/api/users/${userId}/api-tokens` : null,
fetcher,
{
dedupingInterval: env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
- }
- )
- if (error) onError(error)
+ },
+ );
+ if (error) onError(error);
return {
apiTokens: data?.apiTokens,
isLoading: !error && !data,
mutate,
- }
-}
+ };
+};
diff --git a/apps/builder/src/features/account/hooks/useUser.ts b/apps/builder/src/features/account/hooks/useUser.ts
index c484569725..08f8069f1e 100644
--- a/apps/builder/src/features/account/hooks/useUser.ts
+++ b/apps/builder/src/features/account/hooks/useUser.ts
@@ -1,4 +1,4 @@
-import { useContext } from 'react'
-import { userContext } from '../UserProvider'
+import { useContext } from "react";
+import { userContext } from "../UserProvider";
-export const useUser = () => useContext(userContext)
+export const useUser = () => useContext(userContext);
diff --git a/apps/builder/src/features/account/queries/createApiTokenQuery.ts b/apps/builder/src/features/account/queries/createApiTokenQuery.ts
index fba8971fa9..1dddb07326 100644
--- a/apps/builder/src/features/account/queries/createApiTokenQuery.ts
+++ b/apps/builder/src/features/account/queries/createApiTokenQuery.ts
@@ -1,14 +1,14 @@
-import { sendRequest } from '@typebot.io/lib'
-import { ApiTokenFromServer } from '../types'
+import { sendRequest } from "@typebot.io/lib/utils";
+import type { ApiTokenFromServer } from "../types";
export const createApiTokenQuery = (
userId: string,
- { name }: { name: string }
+ { name }: { name: string },
) =>
sendRequest<{ apiToken: ApiTokenFromServer & { token: string } }>({
url: `/api/users/${userId}/api-tokens`,
- method: 'POST',
+ method: "POST",
body: {
name,
},
- })
+ });
diff --git a/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts b/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts
index 0de20aff91..34148f1be4 100644
--- a/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts
+++ b/apps/builder/src/features/account/queries/deleteApiTokenQuery.ts
@@ -1,14 +1,14 @@
-import { ApiToken } from '@typebot.io/prisma'
-import { sendRequest } from '@typebot.io/lib'
+import { sendRequest } from "@typebot.io/lib/utils";
+import type { Prisma } from "@typebot.io/prisma/types";
export const deleteApiTokenQuery = ({
userId,
tokenId,
}: {
- userId: string
- tokenId: string
+ userId: string;
+ tokenId: string;
}) =>
- sendRequest<{ apiToken: ApiToken }>({
+ sendRequest<{ apiToken: Prisma.ApiToken }>({
url: `/api/users/${userId}/api-tokens/${tokenId}`,
- method: 'DELETE',
- })
+ method: "DELETE",
+ });
diff --git a/apps/builder/src/features/account/queries/updateUserQuery.ts b/apps/builder/src/features/account/queries/updateUserQuery.ts
index 2ddb2a0927..214f02c48e 100644
--- a/apps/builder/src/features/account/queries/updateUserQuery.ts
+++ b/apps/builder/src/features/account/queries/updateUserQuery.ts
@@ -1,9 +1,9 @@
-import { sendRequest } from '@typebot.io/lib'
-import { User } from '@typebot.io/schemas'
+import { sendRequest } from "@typebot.io/lib/utils";
+import type { User } from "@typebot.io/schemas/features/user/schema";
export const updateUserQuery = async (id: string, user: Partial) =>
sendRequest({
url: `/api/users/${id}`,
- method: 'PATCH',
+ method: "PATCH",
body: user,
- })
+ });
diff --git a/apps/builder/src/features/account/types.ts b/apps/builder/src/features/account/types.ts
index 4daf9a623e..9eff39ae56 100644
--- a/apps/builder/src/features/account/types.ts
+++ b/apps/builder/src/features/account/types.ts
@@ -1 +1,5 @@
-export type ApiTokenFromServer = { id: string; name: string; createdAt: string }
+export type ApiTokenFromServer = {
+ id: string;
+ name: string;
+ createdAt: string;
+};
diff --git a/apps/builder/src/features/analytics/analytics.spec.ts b/apps/builder/src/features/analytics/analytics.spec.ts
index 7352dfe613..86109835f7 100644
--- a/apps/builder/src/features/analytics/analytics.spec.ts
+++ b/apps/builder/src/features/analytics/analytics.spec.ts
@@ -1,32 +1,32 @@
-import { getTestAsset } from '@/test/utils/playwright'
-import test, { expect } from '@playwright/test'
-import { createId } from '@paralleldrive/cuid2'
+import { getTestAsset } from "@/test/utils/playwright";
+import { createId } from "@paralleldrive/cuid2";
+import test, { expect } from "@playwright/test";
import {
importTypebotInDatabase,
injectFakeResults,
-} from '@typebot.io/playwright/databaseActions'
-import { starterWorkspaceId } from '@typebot.io/playwright/databaseSetup'
+} from "@typebot.io/playwright/databaseActions";
+import { starterWorkspaceId } from "@typebot.io/playwright/databaseSetup";
-test('analytics are not available for non-pro workspaces', async ({ page }) => {
- const typebotId = createId()
+test("analytics are not available for non-pro workspaces", async ({ page }) => {
+ const typebotId = createId();
await importTypebotInDatabase(
- getTestAsset('typebots/results/submissionHeader.json'),
+ getTestAsset("typebots/results/submissionHeader.json"),
{
id: typebotId,
workspaceId: starterWorkspaceId,
- }
- )
- await injectFakeResults({ typebotId, count: 10 })
- await page.goto(`/typebots/${typebotId}/results/analytics`)
- const firstDropoffBox = page.locator('text="%" >> nth=0')
- await firstDropoffBox.hover()
+ },
+ );
+ await injectFakeResults({ typebotId, count: 10 });
+ await page.goto(`/typebots/${typebotId}/results/analytics`);
+ const firstDropoffBox = page.locator('text="%" >> nth=0');
+ await firstDropoffBox.hover();
await expect(
- page.locator('text="Upgrade your plan to PRO to reveal drop-off rate."')
- ).toBeVisible()
- await firstDropoffBox.click()
+ page.locator('text="Upgrade your plan to PRO to reveal drop-off rate."'),
+ ).toBeVisible();
+ await firstDropoffBox.click();
await expect(
page.locator(
- 'text="You need to upgrade your plan in order to unlock in-depth analytics"'
- )
- ).toBeVisible()
-})
+ 'text="You need to upgrade your plan in order to unlock in-depth analytics"',
+ ),
+ ).toBeVisible();
+});
diff --git a/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts b/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts
index 280ffa9171..2e7e2d0713 100644
--- a/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts
+++ b/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts
@@ -1,26 +1,26 @@
-import prisma from '@typebot.io/lib/prisma'
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { TRPCError } from '@trpc/server'
-import { z } from 'zod'
-import { canReadTypebots } from '@/helpers/databaseRules'
-import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
-import { parseGroups } from '@typebot.io/schemas'
-import { isInputBlock } from '@typebot.io/schemas/helpers'
-import { defaultTimeFilter, timeFilterValues } from '../constants'
+import { canReadTypebots } from "@/helpers/databaseRules";
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { TRPCError } from "@trpc/server";
+import { isInputBlock } from "@typebot.io/blocks-core/helpers";
+import { parseGroups } from "@typebot.io/groups/schemas";
+import prisma from "@typebot.io/prisma";
+import { totalAnswersSchema } from "@typebot.io/schemas/features/analytics";
+import { z } from "@typebot.io/zod";
+import { defaultTimeFilter, timeFilterValues } from "../constants";
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
-} from '../helpers/parseDateFromTimeFilter'
+} from "../helpers/parseDateFromTimeFilter";
export const getInDepthAnalyticsData = authenticatedProcedure
.meta({
openapi: {
- method: 'GET',
- path: '/v1/typebots/{typebotId}/analytics/inDepthData',
+ method: "GET",
+ path: "/v1/typebots/{typebotId}/analytics/inDepthData",
protect: true,
summary:
- 'List total answers in blocks and off-default paths visited edges',
- tags: ['Analytics'],
+ "List total answers in blocks and off-default paths visited edges",
+ tags: ["Analytics"],
},
})
.input(
@@ -28,33 +28,33 @@ export const getInDepthAnalyticsData = authenticatedProcedure
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
- })
+ }),
)
.output(
z.object({
totalAnswers: z.array(totalAnswersSchema),
offDefaultPathVisitedEdges: z.array(
- z.object({ edgeId: z.string(), total: z.number() })
+ z.object({ edgeId: z.string(), total: z.number() }),
),
- })
+ }),
)
.query(
async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true },
- })
+ });
if (!typebot?.publishedTypebot)
throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'Published typebot not found',
- })
+ code: "NOT_FOUND",
+ message: "Published typebot not found",
+ });
- const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
- const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
+ const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone);
+ const toDate = parseToDateFromTimeFilter(timeFilter, timeZone);
const totalAnswersPerBlock = await prisma.answer.groupBy({
- by: ['blockId', 'resultId'],
+ by: ["blockId", "resultId"],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
@@ -69,14 +69,14 @@ export const getInDepthAnalyticsData = authenticatedProcedure
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
- group.blocks.filter(isInputBlock).map((block) => block.id)
+ group.blocks.filter(isInputBlock).map((block) => block.id),
),
},
},
- })
+ });
const totalAnswersV2PerBlock = await prisma.answerV2.groupBy({
- by: ['blockId', 'resultId'],
+ by: ["blockId", "resultId"],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
@@ -91,24 +91,24 @@ export const getInDepthAnalyticsData = authenticatedProcedure
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
- group.blocks.filter(isInputBlock).map((block) => block.id)
+ group.blocks.filter(isInputBlock).map((block) => block.id),
),
},
},
- })
+ });
const uniqueCounts = totalAnswersPerBlock
.concat(totalAnswersV2PerBlock)
.reduce<{
- [key: string]: Set
+ [key: string]: Set;
}>((acc, { blockId, resultId }) => {
- acc[blockId] = acc[blockId] || new Set()
- acc[blockId].add(resultId)
- return acc
- }, {})
+ acc[blockId] = acc[blockId] || new Set();
+ acc[blockId].add(resultId);
+ return acc;
+ }, {});
const offDefaultPathVisitedEdges = await prisma.visitedEdge.groupBy({
- by: ['edgeId'],
+ by: ["edgeId"],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
@@ -121,7 +121,7 @@ export const getInDepthAnalyticsData = authenticatedProcedure
},
},
_count: { resultId: true },
- })
+ });
return {
totalAnswers: Object.keys(uniqueCounts).map((blockId) => ({
@@ -132,6 +132,6 @@ export const getInDepthAnalyticsData = authenticatedProcedure
edgeId: e.edgeId,
total: e._count.resultId,
})),
- }
- }
- )
+ };
+ },
+ );
diff --git a/apps/builder/src/features/analytics/api/getStats.ts b/apps/builder/src/features/analytics/api/getStats.ts
index e264307a0c..9ba37dee19 100644
--- a/apps/builder/src/features/analytics/api/getStats.ts
+++ b/apps/builder/src/features/analytics/api/getStats.ts
@@ -1,23 +1,23 @@
-import prisma from '@typebot.io/lib/prisma'
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { TRPCError } from '@trpc/server'
-import { z } from 'zod'
-import { canReadTypebots } from '@/helpers/databaseRules'
-import { Stats, statsSchema } from '@typebot.io/schemas'
-import { defaultTimeFilter, timeFilterValues } from '../constants'
+import { canReadTypebots } from "@/helpers/databaseRules";
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { TRPCError } from "@trpc/server";
+import prisma from "@typebot.io/prisma";
+import { type Stats, statsSchema } from "@typebot.io/results/schemas/answers";
+import { z } from "@typebot.io/zod";
+import { defaultTimeFilter, timeFilterValues } from "../constants";
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
-} from '../helpers/parseDateFromTimeFilter'
+} from "../helpers/parseDateFromTimeFilter";
export const getStats = authenticatedProcedure
.meta({
openapi: {
- method: 'GET',
- path: '/v1/typebots/{typebotId}/analytics/stats',
+ method: "GET",
+ path: "/v1/typebots/{typebotId}/analytics/stats",
protect: true,
- summary: 'Get results stats',
- tags: ['Analytics'],
+ summary: "Get results stats",
+ tags: ["Analytics"],
},
})
.input(
@@ -25,7 +25,7 @@ export const getStats = authenticatedProcedure
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
- })
+ }),
)
.output(z.object({ stats: statsSchema }))
.query(
@@ -33,15 +33,15 @@ export const getStats = authenticatedProcedure
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true, id: true },
- })
+ });
if (!typebot?.publishedTypebot)
throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'Published typebot not found',
- })
+ code: "NOT_FOUND",
+ message: "Published typebot not found",
+ });
- const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
- const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
+ const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone);
+ const toDate = parseToDateFromTimeFilter(timeFilter, timeZone);
const [totalViews, totalStarts, totalCompleted] =
await prisma.$transaction([
@@ -83,16 +83,16 @@ export const getStats = authenticatedProcedure
: undefined,
},
}),
- ])
+ ]);
const stats: Stats = {
totalViews,
totalStarts,
totalCompleted,
- }
+ };
return {
stats,
- }
- }
- )
+ };
+ },
+ );
diff --git a/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts b/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts
index b083aa21ec..0953bd2dd9 100644
--- a/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts
+++ b/apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts
@@ -1,23 +1,23 @@
-import prisma from '@typebot.io/lib/prisma'
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { TRPCError } from '@trpc/server'
-import { z } from 'zod'
-import { canReadTypebots } from '@/helpers/databaseRules'
-import { totalVisitedEdgesSchema } from '@typebot.io/schemas'
-import { defaultTimeFilter, timeFilterValues } from '../constants'
+import { canReadTypebots } from "@/helpers/databaseRules";
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { TRPCError } from "@trpc/server";
+import prisma from "@typebot.io/prisma";
+import { totalVisitedEdgesSchema } from "@typebot.io/schemas/features/analytics";
+import { z } from "@typebot.io/zod";
+import { defaultTimeFilter, timeFilterValues } from "../constants";
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
-} from '../helpers/parseDateFromTimeFilter'
+} from "../helpers/parseDateFromTimeFilter";
export const getTotalVisitedEdges = authenticatedProcedure
.meta({
openapi: {
- method: 'GET',
- path: '/v1/typebots/{typebotId}/analytics/totalVisitedEdges',
+ method: "GET",
+ path: "/v1/typebots/{typebotId}/analytics/totalVisitedEdges",
protect: true,
- summary: 'List total edges used in results',
- tags: ['Analytics'],
+ summary: "List total edges used in results",
+ tags: ["Analytics"],
},
})
.input(
@@ -25,30 +25,30 @@ export const getTotalVisitedEdges = authenticatedProcedure
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
- })
+ }),
)
.output(
z.object({
totalVisitedEdges: z.array(totalVisitedEdgesSchema),
- })
+ }),
)
.query(
async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { id: true },
- })
+ });
if (!typebot?.id)
throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'Published typebot not found',
- })
+ code: "NOT_FOUND",
+ message: "Published typebot not found",
+ });
- const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
- const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
+ const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone);
+ const toDate = parseToDateFromTimeFilter(timeFilter, timeZone);
const edges = await prisma.visitedEdge.groupBy({
- by: ['edgeId'],
+ by: ["edgeId"],
where: {
result: {
typebotId: typebot.id,
@@ -61,13 +61,13 @@ export const getTotalVisitedEdges = authenticatedProcedure
},
},
_count: { resultId: true },
- })
+ });
return {
totalVisitedEdges: edges.map((e) => ({
edgeId: e.edgeId,
total: e._count.resultId,
})),
- }
- }
- )
+ };
+ },
+ );
diff --git a/apps/builder/src/features/analytics/api/router.ts b/apps/builder/src/features/analytics/api/router.ts
index 8b1252ef0b..e58241b8ba 100644
--- a/apps/builder/src/features/analytics/api/router.ts
+++ b/apps/builder/src/features/analytics/api/router.ts
@@ -1,8 +1,8 @@
-import { router } from '@/helpers/server/trpc'
-import { getStats } from './getStats'
-import { getInDepthAnalyticsData } from './getInDepthAnalyticsData'
+import { router } from "@/helpers/server/trpc";
+import { getInDepthAnalyticsData } from "./getInDepthAnalyticsData";
+import { getStats } from "./getStats";
export const analyticsRouter = router({
getInDepthAnalyticsData,
getStats,
-})
+});
diff --git a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx
index 00957e20a3..2cfba55907 100644
--- a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx
+++ b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx
@@ -1,53 +1,53 @@
+import { ChangePlanModal } from "@/features/billing/components/ChangePlanModal";
+import { useTypebot } from "@/features/editor/providers/TypebotProvider";
+import { Graph } from "@/features/graph/components/Graph";
+import { EventsCoordinatesProvider } from "@/features/graph/providers/EventsCoordinateProvider";
+import { GraphProvider } from "@/features/graph/providers/GraphProvider";
+import { trpc } from "@/lib/trpc";
import {
Flex,
Spinner,
useColorModeValue,
useDisclosure,
-} from '@chakra-ui/react'
-import { useTypebot } from '@/features/editor/providers/TypebotProvider'
-import {
- Edge,
- GroupV6,
- Stats,
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { blockHasItems, isInputBlock } from "@typebot.io/blocks-core/helpers";
+import type { GroupV6 } from "@typebot.io/groups/schemas";
+import { isDefined } from "@typebot.io/lib/utils";
+import type { Stats } from "@typebot.io/results/schemas/answers";
+import type {
TotalAnswers,
TotalVisitedEdges,
-} from '@typebot.io/schemas'
-import React, { useMemo } from 'react'
-import { StatsCards } from './StatsCards'
-import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
-import { Graph } from '@/features/graph/components/Graph'
-import { GraphProvider } from '@/features/graph/providers/GraphProvider'
-import { useTranslate } from '@tolgee/react'
-import { trpc } from '@/lib/trpc'
-import { isDefined } from '@typebot.io/lib'
-import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
-import { timeFilterValues } from '../constants'
-import { blockHasItems, isInputBlock } from '@typebot.io/schemas/helpers'
+} from "@typebot.io/schemas/features/analytics";
+import type { Edge } from "@typebot.io/typebot/schemas/edge";
+import React, { useMemo } from "react";
+import type { timeFilterValues } from "../constants";
+import { StatsCards } from "./StatsCards";
-const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
+const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
type Props = {
- timeFilter: (typeof timeFilterValues)[number]
- onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
- stats?: Stats
-}
+ timeFilter: (typeof timeFilterValues)[number];
+ onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void;
+ stats?: Stats;
+};
export const AnalyticsGraphContainer = ({
timeFilter,
onTimeFilterChange,
stats,
}: Props) => {
- const { t } = useTranslate()
- const { isOpen, onOpen, onClose } = useDisclosure()
- const { typebot, publishedTypebot } = useTypebot()
+ const { t } = useTranslate();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const { typebot, publishedTypebot } = useTypebot();
const { data } = trpc.analytics.getInDepthAnalyticsData.useQuery(
{
typebotId: typebot?.id as string,
timeFilter,
timeZone,
},
- { enabled: isDefined(publishedTypebot) }
- )
+ { enabled: isDefined(publishedTypebot) },
+ );
const totalVisitedEdges = useMemo(() => {
if (
@@ -57,9 +57,9 @@ export const AnalyticsGraphContainer = ({
!data?.totalAnswers ||
!stats?.totalViews
)
- return
- const firstEdgeId = publishedTypebot.events[0].outgoingEdgeId
- if (!firstEdgeId) return
+ return;
+ const firstEdgeId = publishedTypebot.events[0].outgoingEdgeId;
+ if (!firstEdgeId) return;
return populateEdgesWithVisitData({
edgeId: firstEdgeId,
edges: publishedTypebot.edges,
@@ -70,7 +70,7 @@ export const AnalyticsGraphContainer = ({
: [],
totalAnswers: data.totalAnswers,
edgeVisitHistory: [],
- })
+ });
}, [
data?.offDefaultPathVisitedEdges,
data?.totalAnswers,
@@ -78,16 +78,16 @@ export const AnalyticsGraphContainer = ({
publishedTypebot?.groups,
publishedTypebot?.events,
stats?.totalViews,
- ])
+ ]);
return (
- )
-}
+ );
+};
const populateEdgesWithVisitData = ({
edgeId,
@@ -141,27 +141,27 @@ const populateEdgesWithVisitData = ({
totalAnswers,
edgeVisitHistory,
}: {
- edgeId: string
- edges: Edge[]
- groups: GroupV6[]
- currentTotalUsers: number
- totalVisitedEdges: TotalVisitedEdges[]
- totalAnswers: TotalAnswers[]
- edgeVisitHistory: string[]
+ edgeId: string;
+ edges: Edge[];
+ groups: GroupV6[];
+ currentTotalUsers: number;
+ totalVisitedEdges: TotalVisitedEdges[];
+ totalAnswers: TotalAnswers[];
+ edgeVisitHistory: string[];
}): TotalVisitedEdges[] => {
- if (edgeVisitHistory.find((e) => e === edgeId)) return totalVisitedEdges
+ if (edgeVisitHistory.find((e) => e === edgeId)) return totalVisitedEdges;
totalVisitedEdges.push({
edgeId,
total: currentTotalUsers,
- })
- edgeVisitHistory.push(edgeId)
- const edge = edges.find((edge) => edge.id === edgeId)
- if (!edge) return totalVisitedEdges
- const group = groups.find((group) => edge?.to.groupId === group.id)
- if (!group) return totalVisitedEdges
+ });
+ edgeVisitHistory.push(edgeId);
+ const edge = edges.find((edge) => edge.id === edgeId);
+ if (!edge) return totalVisitedEdges;
+ const group = groups.find((group) => edge?.to.groupId === group.id);
+ if (!group) return totalVisitedEdges;
for (const block of edge.to.blockId
? group.blocks.slice(
- group.blocks.findIndex((b) => b.id === edge.to.blockId)
+ group.blocks.findIndex((b) => b.id === edge.to.blockId),
)
: group.blocks) {
if (blockHasItems(block)) {
@@ -173,19 +173,19 @@ const populateEdgesWithVisitData = ({
groups,
currentTotalUsers:
totalVisitedEdges.find(
- (tve) => tve.edgeId === item.outgoingEdgeId
+ (tve) => tve.edgeId === item.outgoingEdgeId,
)?.total ?? 0,
totalVisitedEdges,
totalAnswers,
edgeVisitHistory,
- })
+ });
}
}
}
if (block.outgoingEdgeId) {
const totalUsers = isInputBlock(block)
? totalAnswers.find((a) => a.blockId === block.id)?.total
- : currentTotalUsers
+ : currentTotalUsers;
totalVisitedEdges = populateEdgesWithVisitData({
edgeId: block.outgoingEdgeId,
edges,
@@ -194,9 +194,9 @@ const populateEdgesWithVisitData = ({
totalVisitedEdges,
totalAnswers,
edgeVisitHistory,
- })
+ });
}
}
- return totalVisitedEdges
-}
+ return totalVisitedEdges;
+};
diff --git a/apps/builder/src/features/analytics/components/StatsCards.tsx b/apps/builder/src/features/analytics/components/StatsCards.tsx
index f0bcf571c6..f4ed1c3b18 100644
--- a/apps/builder/src/features/analytics/components/StatsCards.tsx
+++ b/apps/builder/src/features/analytics/components/StatsCards.tsx
@@ -1,24 +1,24 @@
-import { useTranslate } from '@tolgee/react'
import {
- GridProps,
+ type GridProps,
SimpleGrid,
Skeleton,
Stat,
StatLabel,
StatNumber,
useColorModeValue,
-} from '@chakra-ui/react'
-import { Stats } from '@typebot.io/schemas'
-import React from 'react'
-import { timeFilterValues } from '../constants'
-import { TimeFilterDropdown } from './TimeFilterDropdown'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import type { Stats } from "@typebot.io/results/schemas/answers";
+import React from "react";
+import type { timeFilterValues } from "../constants";
+import { TimeFilterDropdown } from "./TimeFilterDropdown";
const computeCompletionRate =
(notAvailableLabel: string) =>
(totalCompleted: number, totalStarts: number): string => {
- if (totalStarts === 0) return notAvailableLabel
- return `${Math.round((totalCompleted / totalStarts) * 100)}%`
- }
+ if (totalStarts === 0) return notAvailableLabel;
+ return `${Math.round((totalCompleted / totalStarts) * 100)}%`;
+ };
export const StatsCards = ({
stats,
@@ -26,12 +26,12 @@ export const StatsCards = ({
onTimeFilterChange,
...props
}: {
- stats?: Stats
- timeFilter: (typeof timeFilterValues)[number]
- onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
+ stats?: Stats;
+ timeFilter: (typeof timeFilterValues)[number];
+ onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void;
} & GridProps) => {
- const { t } = useTranslate()
- const bg = useColorModeValue('white', 'gray.900')
+ const { t } = useTranslate();
+ const bg = useColorModeValue("white", "gray.900");
return (
- {t('analytics.viewsLabel')}
+ {t("analytics.viewsLabel")}
{stats ? (
{stats.totalViews}
) : (
@@ -49,7 +49,7 @@ export const StatsCards = ({
)}
- {t('analytics.startsLabel')}
+ {t("analytics.startsLabel")}
{stats ? (
{stats.totalStarts}
) : (
@@ -57,12 +57,12 @@ export const StatsCards = ({
)}
- {t('analytics.completionRateLabel')}
+ {t("analytics.completionRateLabel")}
{stats ? (
- {computeCompletionRate(t('analytics.notAvailableLabel'))(
+ {computeCompletionRate(t("analytics.notAvailableLabel"))(
stats.totalCompleted,
- stats.totalStarts
+ stats.totalStarts,
)}
) : (
@@ -76,5 +76,5 @@ export const StatsCards = ({
boxShadow="md"
/>
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx b/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx
index 6ba70f0359..5ead414cc6 100644
--- a/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx
+++ b/apps/builder/src/features/analytics/components/TimeFilterDropdown.tsx
@@ -1,11 +1,11 @@
-import { DropdownList } from '@/components/DropdownList'
-import { timeFilterLabels, timeFilterValues } from '../constants'
-import { ButtonProps } from '@chakra-ui/react'
+import { DropdownList } from "@/components/DropdownList";
+import type { ButtonProps } from "@chakra-ui/react";
+import { timeFilterLabels, type timeFilterValues } from "../constants";
type Props = {
- timeFilter: (typeof timeFilterValues)[number]
- onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
-} & ButtonProps
+ timeFilter: (typeof timeFilterValues)[number];
+ onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void;
+} & ButtonProps;
export const TimeFilterDropdown = ({
timeFilter,
@@ -23,4 +23,4 @@ export const TimeFilterDropdown = ({
}
{...props}
/>
-)
+);
diff --git a/apps/builder/src/features/analytics/constants.ts b/apps/builder/src/features/analytics/constants.ts
index ebba096afa..fe0b95606e 100644
--- a/apps/builder/src/features/analytics/constants.ts
+++ b/apps/builder/src/features/analytics/constants.ts
@@ -1,24 +1,24 @@
export const timeFilterValues = [
- 'today',
- 'last7Days',
- 'last30Days',
- 'monthToDate',
- 'lastMonth',
- 'yearToDate',
- 'allTime',
-] as const
+ "today",
+ "last7Days",
+ "last30Days",
+ "monthToDate",
+ "lastMonth",
+ "yearToDate",
+ "allTime",
+] as const;
export const timeFilterLabels: Record<
(typeof timeFilterValues)[number],
string
> = {
- today: 'Today',
- last7Days: 'Last 7 days',
- last30Days: 'Last 30 days',
- monthToDate: 'Month to date',
- lastMonth: 'Last month',
- yearToDate: 'Year to date',
- allTime: 'All time',
-}
+ today: "Today",
+ last7Days: "Last 7 days",
+ last30Days: "Last 30 days",
+ monthToDate: "Month to date",
+ lastMonth: "Last month",
+ yearToDate: "Year to date",
+ allTime: "All time",
+};
-export const defaultTimeFilter = 'last7Days' as const
+export const defaultTimeFilter = "last7Days" as const;
diff --git a/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts b/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts
index 65304b6b26..5fc372f3b6 100644
--- a/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts
+++ b/apps/builder/src/features/analytics/helpers/computeTotalUsersAtBlock.ts
@@ -1,10 +1,10 @@
-import { isNotDefined } from '@typebot.io/lib'
-import { PublicTypebotV6 } from '@typebot.io/schemas'
-import { isInputBlock } from '@typebot.io/schemas/helpers'
-import {
+import { isInputBlock } from "@typebot.io/blocks-core/helpers";
+import { isNotDefined } from "@typebot.io/lib/utils";
+import type {
TotalAnswers,
TotalVisitedEdges,
-} from '@typebot.io/schemas/features/analytics'
+} from "@typebot.io/schemas/features/analytics";
+import type { PublicTypebotV6 } from "@typebot.io/typebot/schemas/publicTypebot";
export const computeTotalUsersAtBlock = (
currentBlockId: string,
@@ -13,49 +13,49 @@ export const computeTotalUsersAtBlock = (
totalVisitedEdges,
totalAnswers,
}: {
- publishedTypebot: PublicTypebotV6
- totalVisitedEdges: TotalVisitedEdges[]
- totalAnswers: TotalAnswers[]
- }
+ publishedTypebot: PublicTypebotV6;
+ totalVisitedEdges: TotalVisitedEdges[];
+ totalAnswers: TotalAnswers[];
+ },
): number => {
- let totalUsers = 0
+ let totalUsers = 0;
const currentGroup = publishedTypebot.groups.find((group) =>
- group.blocks.find((block) => block.id === currentBlockId)
- )
- if (!currentGroup) return 0
+ group.blocks.find((block) => block.id === currentBlockId),
+ );
+ if (!currentGroup) return 0;
const currentBlockIndex = currentGroup.blocks.findIndex(
- (block) => block.id === currentBlockId
- )
- const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1)
+ (block) => block.id === currentBlockId,
+ );
+ const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1);
for (const block of previousBlocks.reverse()) {
if (currentBlockId !== block.id && isInputBlock(block))
- return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
+ return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0;
const incomingEdges = publishedTypebot.edges.filter(
- (edge) => edge.to.blockId === block.id
- )
- if (!incomingEdges.length) continue
+ (edge) => edge.to.blockId === block.id,
+ );
+ if (!incomingEdges.length) continue;
totalUsers += incomingEdges.reduce(
(acc, incomingEdge) =>
acc +
(totalVisitedEdges.find(
- (totalEdge) => totalEdge.edgeId === incomingEdge.id
+ (totalEdge) => totalEdge.edgeId === incomingEdge.id,
)?.total ?? 0),
- 0
- )
+ 0,
+ );
}
const edgesConnectedToGroup = publishedTypebot.edges.filter(
(edge) =>
- edge.to.groupId === currentGroup.id && isNotDefined(edge.to.blockId)
- )
+ edge.to.groupId === currentGroup.id && isNotDefined(edge.to.blockId),
+ );
totalUsers += edgesConnectedToGroup.reduce(
(acc, connectedEdge) =>
acc +
(totalVisitedEdges.find(
- (totalEdge) => totalEdge.edgeId === connectedEdge.id
+ (totalEdge) => totalEdge.edgeId === connectedEdge.id,
)?.total ?? 0),
- 0
- )
+ 0,
+ );
- return totalUsers
-}
+ return totalUsers;
+};
diff --git a/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts b/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts
index c17769e707..40cc9be18b 100644
--- a/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts
+++ b/apps/builder/src/features/analytics/helpers/getTotalAnswersAtBlock.ts
@@ -1,6 +1,6 @@
-import { byId } from '@typebot.io/lib'
-import { PublicTypebotV6 } from '@typebot.io/schemas'
-import { TotalAnswers } from '@typebot.io/schemas/features/analytics'
+import { byId } from "@typebot.io/lib/utils";
+import type { TotalAnswers } from "@typebot.io/schemas/features/analytics";
+import type { PublicTypebotV6 } from "@typebot.io/typebot/schemas/publicTypebot";
export const getTotalAnswersAtBlock = (
currentBlockId: string,
@@ -8,13 +8,13 @@ export const getTotalAnswersAtBlock = (
publishedTypebot,
totalAnswers,
}: {
- publishedTypebot: PublicTypebotV6
- totalAnswers: TotalAnswers[]
- }
+ publishedTypebot: PublicTypebotV6;
+ totalAnswers: TotalAnswers[];
+ },
): number => {
const block = publishedTypebot.groups
.flatMap((g) => g.blocks)
- .find(byId(currentBlockId))
- if (!block) throw new Error(`Block ${currentBlockId} not found`)
- return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
-}
+ .find(byId(currentBlockId));
+ if (!block) throw new Error(`Block ${currentBlockId} not found`);
+ return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0;
+};
diff --git a/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts b/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts
index 783012c421..d9fd60fd85 100644
--- a/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts
+++ b/apps/builder/src/features/analytics/helpers/parseDateFromTimeFilter.ts
@@ -1,72 +1,72 @@
-import { timeFilterValues } from '../constants'
import {
+ endOfMonth,
startOfDay,
- subDays,
- startOfYear,
startOfMonth,
- endOfMonth,
+ startOfYear,
+ subDays,
subMonths,
-} from 'date-fns'
-import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'
+} from "date-fns";
+import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
+import type { timeFilterValues } from "../constants";
export const parseFromDateFromTimeFilter = (
timeFilter: (typeof timeFilterValues)[number],
- userTimezone: string = 'UTC'
+ userTimezone = "UTC",
): Date | null => {
- const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone)
+ const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone);
switch (timeFilter) {
- case 'today': {
- return zonedTimeToUtc(startOfDay(nowInUserTimezone), userTimezone)
+ case "today": {
+ return zonedTimeToUtc(startOfDay(nowInUserTimezone), userTimezone);
}
- case 'last7Days': {
+ case "last7Days": {
return zonedTimeToUtc(
subDays(startOfDay(nowInUserTimezone), 6),
- userTimezone
- )
+ userTimezone,
+ );
}
- case 'last30Days': {
+ case "last30Days": {
return zonedTimeToUtc(
subDays(startOfDay(nowInUserTimezone), 29),
- userTimezone
- )
+ userTimezone,
+ );
}
- case 'lastMonth': {
+ case "lastMonth": {
return zonedTimeToUtc(
subMonths(startOfMonth(nowInUserTimezone), 1),
- userTimezone
- )
+ userTimezone,
+ );
}
- case 'monthToDate': {
- return zonedTimeToUtc(startOfMonth(nowInUserTimezone), userTimezone)
+ case "monthToDate": {
+ return zonedTimeToUtc(startOfMonth(nowInUserTimezone), userTimezone);
}
- case 'yearToDate': {
- return zonedTimeToUtc(startOfYear(nowInUserTimezone), userTimezone)
+ case "yearToDate": {
+ return zonedTimeToUtc(startOfYear(nowInUserTimezone), userTimezone);
}
- case 'allTime':
- return null
+ case "allTime":
+ return null;
}
-}
+};
export const parseToDateFromTimeFilter = (
timeFilter: (typeof timeFilterValues)[number],
- userTimezone: string = 'UTC'
+ userTimezone = "UTC",
): Date | null => {
- const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone)
+ const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone);
switch (timeFilter) {
- case 'lastMonth': {
+ case "lastMonth": {
return zonedTimeToUtc(
subMonths(endOfMonth(nowInUserTimezone), 1),
- userTimezone
- )
+ userTimezone,
+ );
}
- case 'last30Days':
- case 'last7Days':
- case 'today':
- case 'monthToDate':
- case 'yearToDate':
- case 'allTime':
- return null
+ case "last30Days":
+ case "last7Days":
+ case "today":
+ case "monthToDate":
+ case "yearToDate":
+ case "allTime":
+ return null;
}
-}
+};
diff --git a/apps/builder/src/features/auth/api/customAdapter.ts b/apps/builder/src/features/auth/api/customAdapter.ts
index 891de5be31..10d069b746 100644
--- a/apps/builder/src/features/auth/api/customAdapter.ts
+++ b/apps/builder/src/features/auth/api/customAdapter.ts
@@ -1,49 +1,129 @@
+import { convertInvitationsToCollaborations } from "@/features/auth/helpers/convertInvitationsToCollaborations";
+import { getNewUserInvitations } from "@/features/auth/helpers/getNewUserInvitations";
+import { joinWorkspaces } from "@/features/auth/helpers/joinWorkspaces";
+import { parseWorkspaceDefaultPlan } from "@/features/workspace/helpers/parseWorkspaceDefaultPlan";
+import { createId } from "@paralleldrive/cuid2";
+import { env } from "@typebot.io/env";
+import { generateId } from "@typebot.io/lib/utils";
+import { WorkspaceRole } from "@typebot.io/prisma/enum";
+import type { Prisma } from "@typebot.io/prisma/types";
+import type { TelemetryEvent } from "@typebot.io/telemetry/schemas";
+import { trackEvents } from "@typebot.io/telemetry/trackEvents";
+import type { Account, Awaitable, User } from "next-auth";
+
// Forked from https://github.com/nextauthjs/adapters/blob/main/packages/prisma/src/index.ts
-import {
- PrismaClient,
- Prisma,
- WorkspaceRole,
- Session,
-} from '@typebot.io/prisma'
-import type { Adapter, AdapterUser } from 'next-auth/adapters'
-import { createId } from '@paralleldrive/cuid2'
-import { generateId } from '@typebot.io/lib'
-import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
-import { convertInvitationsToCollaborations } from '@/features/auth/helpers/convertInvitationsToCollaborations'
-import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
-import { joinWorkspaces } from '@/features/auth/helpers/joinWorkspaces'
-import { parseWorkspaceDefaultPlan } from '@/features/workspace/helpers/parseWorkspaceDefaultPlan'
-import { env } from '@typebot.io/env'
-import { trackEvents } from '@typebot.io/telemetry/trackEvents'
-export function customAdapter(p: PrismaClient): Adapter {
+interface AdapterUser extends User {
+ id: string;
+ email: string;
+ emailVerified: Date | null;
+}
+
+interface AdapterAccount extends Account {
+ userId: string;
+}
+interface AdapterSession {
+ sessionToken: string;
+ userId: string;
+ expires: Date;
+}
+interface VerificationToken {
+ identifier: string;
+ expires: Date;
+ token: string;
+}
+
+type Adapter = DefaultAdapter &
+ (WithVerificationToken extends true
+ ? {
+ createVerificationToken: (
+ verificationToken: VerificationToken,
+ ) => Awaitable;
+ /**
+ * Return verification token from the database
+ * and delete it so it cannot be used again.
+ */
+ useVerificationToken: (params: {
+ identifier: string;
+ token: string;
+ }) => Awaitable;
+ }
+ : {});
+interface DefaultAdapter {
+ createUser: (user: Omit) => Awaitable;
+ getUser: (id: string) => Awaitable;
+ getUserByEmail: (email: string) => Awaitable;
+ /** Using the provider id and the id of the user for a specific account, get the user. */
+ getUserByAccount: (
+ providerAccountId: Pick,
+ ) => Awaitable;
+ updateUser: (
+ user: Partial & Pick,
+ ) => Awaitable;
+ /** @todo Implement */
+ deleteUser?: (
+ userId: string,
+ ) => Promise | Awaitable;
+ linkAccount: (
+ account: AdapterAccount,
+ ) => Promise | Awaitable;
+ /** @todo Implement */
+ unlinkAccount?: (
+ providerAccountId: Pick,
+ ) => Promise | Awaitable;
+ /** Creates a session for the user and returns it. */
+ createSession: (session: {
+ sessionToken: string;
+ userId: string;
+ expires: Date;
+ }) => Awaitable;
+ getSessionAndUser: (sessionToken: string) => Awaitable<{
+ session: AdapterSession;
+ user: AdapterUser;
+ } | null>;
+ updateSession: (
+ session: Partial & Pick,
+ ) => Awaitable;
+ deleteSession: (
+ sessionToken: string,
+ ) => Promise | Awaitable;
+ createVerificationToken?: (
+ verificationToken: VerificationToken,
+ ) => Awaitable;
+ useVerificationToken?: (params: {
+ identifier: string;
+ token: string;
+ }) => Awaitable;
+}
+
+export function customAdapter(p: Prisma.PrismaClient): Adapter {
return {
- createUser: async (data: Omit) => {
+ createUser: async (data) => {
if (!data.email)
- throw Error('Provider did not forward email but it is required')
- const user = { id: createId(), email: data.email as string }
+ throw Error("Provider did not forward email but it is required");
+ const user = { id: createId(), email: data.email as string };
const { invitations, workspaceInvitations } = await getNewUserInvitations(
p,
- user.email
- )
+ user.email,
+ );
if (
env.DISABLE_SIGNUP &&
env.ADMIN_EMAIL?.every((email) => email !== user.email) &&
invitations.length === 0 &&
workspaceInvitations.length === 0
)
- throw Error('New users are forbidden')
+ throw Error("New users are forbidden");
const newWorkspaceData = {
name: data.name ? `${data.name}'s workspace` : `My workspace`,
plan: parseWorkspaceDefaultPlan(data.email),
- }
+ };
const createdUser = await p.user.create({
data: {
...data,
id: user.id,
apiTokens: {
- create: { name: 'Default', token: generateId(24) },
+ create: { name: "Default", token: generateId(24) },
},
workspaces:
workspaceInvitations.length > 0
@@ -61,31 +141,31 @@ export function customAdapter(p: PrismaClient): Adapter {
include: {
workspaces: { select: { workspaceId: true } },
},
- })
- const newWorkspaceId = createdUser.workspaces.pop()?.workspaceId
- const events: TelemetryEvent[] = []
+ });
+ const newWorkspaceId = createdUser.workspaces.pop()?.workspaceId;
+ const events: TelemetryEvent[] = [];
if (newWorkspaceId) {
events.push({
- name: 'Workspace created',
+ name: "Workspace created",
workspaceId: newWorkspaceId,
userId: createdUser.id,
data: newWorkspaceData,
- })
+ });
}
events.push({
- name: 'User created',
+ name: "User created",
userId: createdUser.id,
data: {
email: data.email,
- name: data.name ? (data.name as string).split(' ')[0] : undefined,
+ name: data.name ? (data.name as string).split(" ")[0] : undefined,
},
- })
- await trackEvents(events)
+ });
+ await trackEvents(events);
if (invitations.length > 0)
- await convertInvitationsToCollaborations(p, user, invitations)
+ await convertInvitationsToCollaborations(p, user, invitations);
if (workspaceInvitations.length > 0)
- await joinWorkspaces(p, user, workspaceInvitations)
- return createdUser as AdapterUser
+ await joinWorkspaces(p, user, workspaceInvitations);
+ return createdUser as AdapterUser;
},
getUser: async (id) =>
(await p.user.findUnique({ where: { id } })) as AdapterUser,
@@ -95,8 +175,8 @@ export function customAdapter(p: PrismaClient): Adapter {
const account = await p.account.findUnique({
where: { provider_providerAccountId },
select: { user: true },
- })
- return (account?.user ?? null) as AdapterUser | null
+ });
+ return (account?.user ?? null) as AdapterUser | null;
},
updateUser: async (data) =>
(await p.user.update({ where: { id: data.id }, data })) as AdapterUser,
@@ -120,19 +200,22 @@ export function customAdapter(p: PrismaClient): Adapter {
oauth_token: data.oauth_token as string,
refresh_token_expires_in: data.refresh_token_expires_in as number,
},
- })
+ });
},
unlinkAccount: async (provider_providerAccountId) => {
- await p.account.delete({ where: { provider_providerAccountId } })
+ await p.account.delete({ where: { provider_providerAccountId } });
},
async getSessionAndUser(sessionToken) {
const userAndSession = await p.session.findUnique({
where: { sessionToken },
include: { user: true },
- })
- if (!userAndSession) return null
- const { user, ...session } = userAndSession
- return { user, session } as { user: AdapterUser; session: Session }
+ });
+ if (!userAndSession) return null;
+ const { user, ...session } = userAndSession;
+ return { user, session } as {
+ user: AdapterUser;
+ session: AdapterSession;
+ };
},
createSession: (data) => p.session.create({ data }),
updateSession: (data) =>
@@ -142,12 +225,17 @@ export function customAdapter(p: PrismaClient): Adapter {
createVerificationToken: (data) => p.verificationToken.create({ data }),
async useVerificationToken(identifier_token) {
try {
- return await p.verificationToken.delete({ where: { identifier_token } })
+ return await p.verificationToken.delete({
+ where: { identifier_token },
+ });
} catch (error) {
- if ((error as Prisma.PrismaClientKnownRequestError).code === 'P2025')
- return null
- throw error
+ if (
+ (error as Prisma.Prisma.PrismaClientKnownRequestError).code ===
+ "P2025"
+ )
+ return null;
+ throw error;
}
},
- }
+ };
}
diff --git a/apps/builder/src/features/auth/components/DividerWithText.tsx b/apps/builder/src/features/auth/components/DividerWithText.tsx
index c6e9c17445..7931205f0d 100644
--- a/apps/builder/src/features/auth/components/DividerWithText.tsx
+++ b/apps/builder/src/features/auth/components/DividerWithText.tsx
@@ -1,15 +1,15 @@
import {
- FlexProps,
- Flex,
Box,
Divider,
+ Flex,
+ type FlexProps,
Text,
useColorModeValue,
-} from '@chakra-ui/react'
-import React from 'react'
+} from "@chakra-ui/react";
+import React from "react";
export const DividerWithText = (props: FlexProps) => {
- const { children, ...flexProps } = props
+ const { children, ...flexProps } = props;
return (
@@ -18,7 +18,7 @@ export const DividerWithText = (props: FlexProps) => {
{children}
@@ -27,5 +27,5 @@ export const DividerWithText = (props: FlexProps) => {
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/auth/components/SignInError.tsx b/apps/builder/src/features/auth/components/SignInError.tsx
index 1f57b11495..4579814bd4 100644
--- a/apps/builder/src/features/auth/components/SignInError.tsx
+++ b/apps/builder/src/features/auth/components/SignInError.tsx
@@ -1,26 +1,26 @@
-import { useTranslate } from '@tolgee/react'
-import { Alert } from '@chakra-ui/react'
+import { Alert } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
type Props = {
- error: string
-}
+ error: string;
+};
export const SignInError = ({ error }: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const errors: Record = {
- Signin: t('auth.error.default'),
- OAuthSignin: t('auth.error.default'),
- OAuthCallback: t('auth.error.default'),
- OAuthCreateAccount: t('auth.error.email'),
- EmailCreateAccount: t('auth.error.default'),
- Callback: t('auth.error.default'),
- OAuthAccountNotLinked: t('auth.error.oauthNotLinked'),
- default: t('auth.error.unknown'),
- }
- if (!errors[error]) return null
+ Signin: t("auth.error.default"),
+ OAuthSignin: t("auth.error.default"),
+ OAuthCallback: t("auth.error.default"),
+ OAuthCreateAccount: t("auth.error.email"),
+ EmailCreateAccount: t("auth.error.default"),
+ Callback: t("auth.error.default"),
+ OAuthAccountNotLinked: t("auth.error.oauthNotLinked"),
+ default: t("auth.error.unknown"),
+ };
+ if (!errors[error]) return null;
return (
{errors[error]}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/auth/components/SignInForm.tsx b/apps/builder/src/features/auth/components/SignInForm.tsx
index 85689f16de..3b60045ea8 100644
--- a/apps/builder/src/features/auth/components/SignInForm.tsx
+++ b/apps/builder/src/features/auth/components/SignInForm.tsx
@@ -1,141 +1,141 @@
+import { TextLink } from "@/components/TextLink";
+import { useToast } from "@/hooks/useToast";
+import { sanitizeUrl } from "@braintree/sanitize-url";
import {
+ Alert,
+ AlertIcon,
Button,
- HTMLChakraProps,
+ Flex,
+ HStack,
+ type HTMLChakraProps,
Input,
+ SlideFade,
+ Spinner,
Stack,
- HStack,
Text,
- Spinner,
- Alert,
- Flex,
- AlertIcon,
- SlideFade,
-} from '@chakra-ui/react'
-import React, { ChangeEvent, FormEvent, useEffect } from 'react'
-import { useState } from 'react'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import type { BuiltInProviderType } from "next-auth/providers/index";
import {
- ClientSafeProvider,
+ type ClientSafeProvider,
+ type LiteralUnion,
getProviders,
- LiteralUnion,
signIn,
useSession,
-} from 'next-auth/react'
-import { DividerWithText } from './DividerWithText'
-import { SocialLoginButtons } from './SocialLoginButtons'
-import { useRouter } from 'next/router'
-import { BuiltInProviderType } from 'next-auth/providers'
-import { useToast } from '@/hooks/useToast'
-import { TextLink } from '@/components/TextLink'
-import { SignInError } from './SignInError'
-import { useTranslate } from '@tolgee/react'
-import { sanitizeUrl } from '@braintree/sanitize-url'
+} from "next-auth/react";
+import { useRouter } from "next/router";
+import type { ChangeEvent, FormEvent } from "react";
+import React, { useEffect, useState } from "react";
+import { DividerWithText } from "./DividerWithText";
+import { SignInError } from "./SignInError";
+import { SocialLoginButtons } from "./SocialLoginButtons";
type Props = {
- defaultEmail?: string
-}
+ defaultEmail?: string;
+};
export const SignInForm = ({
defaultEmail,
-}: Props & HTMLChakraProps<'form'>) => {
- const { t } = useTranslate()
- const router = useRouter()
- const { status } = useSession()
- const [authLoading, setAuthLoading] = useState(false)
- const [isLoadingProviders, setIsLoadingProviders] = useState(true)
+}: Props & HTMLChakraProps<"form">) => {
+ const { t } = useTranslate();
+ const router = useRouter();
+ const { status } = useSession();
+ const [authLoading, setAuthLoading] = useState(false);
+ const [isLoadingProviders, setIsLoadingProviders] = useState(true);
- const [emailValue, setEmailValue] = useState(defaultEmail ?? '')
- const [isMagicLinkSent, setIsMagicLinkSent] = useState(false)
+ const [emailValue, setEmailValue] = useState(defaultEmail ?? "");
+ const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
- const { showToast } = useToast()
+ const { showToast } = useToast();
const [providers, setProviders] =
useState<
Record, ClientSafeProvider>
- >()
+ >();
const hasNoAuthProvider =
- !isLoadingProviders && Object.keys(providers ?? {}).length === 0
+ !isLoadingProviders && Object.keys(providers ?? {}).length === 0;
useEffect(() => {
- if (status === 'authenticated') {
- const redirectPath = router.query.redirectPath?.toString()
- router.replace(redirectPath ? sanitizeUrl(redirectPath) : '/typebots')
- return
+ if (status === "authenticated") {
+ const redirectPath = router.query.redirectPath?.toString();
+ router.replace(redirectPath ? sanitizeUrl(redirectPath) : "/typebots");
+ return;
}
- ;(async () => {
- const providers = await getProviders()
- setProviders(providers ?? undefined)
- setIsLoadingProviders(false)
- })()
- }, [status, router])
+ (async () => {
+ const providers = await getProviders();
+ setProviders(providers ?? undefined);
+ setIsLoadingProviders(false);
+ })();
+ }, [status, router]);
useEffect(() => {
- if (!router.isReady) return
- if (router.query.error === 'ip-banned') {
+ if (!router.isReady) return;
+ if (router.query.error === "ip-banned") {
showToast({
- status: 'info',
+ status: "info",
description:
- 'Your account has suspicious activity and is being reviewed by our team. Feel free to contact us.',
- })
+ "Your account has suspicious activity and is being reviewed by our team. Feel free to contact us.",
+ });
}
- }, [router.isReady, router.query.error, showToast])
+ }, [router.isReady, router.query.error, showToast]);
const handleEmailChange = (e: ChangeEvent) =>
- setEmailValue(e.target.value)
+ setEmailValue(e.target.value);
const handleEmailSubmit = async (e: FormEvent) => {
- e.preventDefault()
- if (isMagicLinkSent) return
- setAuthLoading(true)
+ e.preventDefault();
+ if (isMagicLinkSent) return;
+ setAuthLoading(true);
try {
- const response = await signIn('email', {
+ const response = await signIn("email", {
email: emailValue,
redirect: false,
- })
+ });
if (response?.error) {
- if (response.error.includes('rate-limited'))
+ if (response.error.includes("rate-limited"))
showToast({
- status: 'info',
- description: t('auth.signinErrorToast.tooManyRequests'),
- })
- else if (response.error.includes('sign-up-disabled'))
+ status: "info",
+ description: t("auth.signinErrorToast.tooManyRequests"),
+ });
+ else if (response.error.includes("sign-up-disabled"))
showToast({
- title: t('auth.signinErrorToast.title'),
- description: t('auth.signinErrorToast.description'),
- })
+ title: t("auth.signinErrorToast.title"),
+ description: t("auth.signinErrorToast.description"),
+ });
else
showToast({
- status: 'info',
- description: t('errorMessage'),
+ status: "info",
+ description: t("errorMessage"),
details: {
- content: 'Check server logs to see relevent error message.',
- lang: 'json',
+ content: "Check server logs to see relevent error message.",
+ lang: "json",
},
- })
+ });
} else {
- setIsMagicLinkSent(true)
+ setIsMagicLinkSent(true);
}
} catch (e) {
showToast({
- status: 'info',
- description: 'An error occured while signing in',
- })
+ status: "info",
+ description: "An error occured while signing in",
+ });
}
- setAuthLoading(false)
- }
+ setAuthLoading(false);
+ };
- if (isLoadingProviders) return
+ if (isLoadingProviders) return ;
if (hasNoAuthProvider)
return (
- {t('auth.noProvider.preLink')}{' '}
+ {t("auth.noProvider.preLink")}{" "}
- {t('auth.noProvider.link')}
+ {t("auth.noProvider.link")}
- )
+ );
return (
{!isMagicLinkSent && (
@@ -143,7 +143,7 @@ export const SignInForm = ({
{providers?.email && (
<>
- {t('auth.orEmailLabel')}
+ {t("auth.orEmailLabel")}
- {t('auth.emailSubmitButton.label')}
+ {t("auth.emailSubmitButton.label")}
>
@@ -177,13 +177,13 @@ export const SignInForm = ({
- {t('auth.magicLink.title')}
- {t('auth.magicLink.description')}
+ {t("auth.magicLink.title")}
+ {t("auth.magicLink.description")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/auth/components/SignInPage.tsx b/apps/builder/src/features/auth/components/SignInPage.tsx
index c05f9eb8a9..7fd56c71e9 100644
--- a/apps/builder/src/features/auth/components/SignInPage.tsx
+++ b/apps/builder/src/features/auth/components/SignInPage.tsx
@@ -1,66 +1,66 @@
-import { Seo } from '@/components/Seo'
-import { TextLink } from '@/components/TextLink'
-import { T, useTranslate } from '@tolgee/react'
-import { VStack, Heading, Text } from '@chakra-ui/react'
-import { useRouter } from 'next/router'
-import { SignInForm } from './SignInForm'
+import { Seo } from "@/components/Seo";
+import { TextLink } from "@/components/TextLink";
+import { Heading, Text, VStack } from "@chakra-ui/react";
+import { T, useTranslate } from "@tolgee/react";
+import { useRouter } from "next/router";
+import { SignInForm } from "./SignInForm";
type Props = {
- type: 'signin' | 'signup'
- defaultEmail?: string
-}
+ type: "signin" | "signup";
+ defaultEmail?: string;
+};
export const SignInPage = ({ type }: Props) => {
- const { t } = useTranslate()
- const { query } = useRouter()
+ const { t } = useTranslate();
+ const { query } = useRouter();
return (
{
- throw new Error('Sentry is working')
+ throw new Error("Sentry is working");
}}
>
- {type === 'signin'
- ? t('auth.signin.heading')
- : t('auth.register.heading')}
+ {type === "signin"
+ ? t("auth.signin.heading")
+ : t("auth.register.heading")}
- {type === 'signin' ? (
+ {type === "signin" ? (
- {t('auth.signin.noAccountLabel.preLink')}{' '}
+ {t("auth.signin.noAccountLabel.preLink")}{" "}
- {t('auth.signin.noAccountLabel.link')}
+ {t("auth.signin.noAccountLabel.link")}
) : (
- {t('auth.register.alreadyHaveAccountLabel.preLink')}{' '}
+ {t("auth.register.alreadyHaveAccountLabel.preLink")}{" "}
- {t('auth.register.alreadyHaveAccountLabel.link')}
+ {t("auth.register.alreadyHaveAccountLabel.link")}
)}
- {type === 'signup' ? (
+ {type === "signup" ? (
,
+ terms: ,
privacy: (
-
+
),
}}
/>
) : null}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/auth/components/SocialLoginButtons.tsx b/apps/builder/src/features/auth/components/SocialLoginButtons.tsx
index db627552a8..e5065cb636 100644
--- a/apps/builder/src/features/auth/components/SocialLoginButtons.tsx
+++ b/apps/builder/src/features/auth/components/SocialLoginButtons.tsx
@@ -1,59 +1,59 @@
-import { Stack, Button } from '@chakra-ui/react'
-import { GithubIcon } from '@/components/icons'
+import { GoogleLogo } from "@/components/GoogleLogo";
+import { GithubIcon } from "@/components/icons";
+import { AzureAdLogo } from "@/components/logos/AzureAdLogo";
+import { FacebookLogo } from "@/components/logos/FacebookLogo";
+import { GitlabLogo } from "@/components/logos/GitlabLogo";
+import { KeycloackLogo } from "@/components/logos/KeycloakLogo";
+import { Button, Stack } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { omit } from "@typebot.io/lib/utils";
+import type { BuiltInProviderType } from "next-auth/providers/index";
import {
- ClientSafeProvider,
- LiteralUnion,
+ type ClientSafeProvider,
+ type LiteralUnion,
signIn,
useSession,
-} from 'next-auth/react'
-import { useRouter } from 'next/router'
-import React, { useState } from 'react'
-import { stringify } from 'qs'
-import { BuiltInProviderType } from 'next-auth/providers'
-import { GoogleLogo } from '@/components/GoogleLogo'
-import { omit } from '@typebot.io/lib'
-import { AzureAdLogo } from '@/components/logos/AzureAdLogo'
-import { FacebookLogo } from '@/components/logos/FacebookLogo'
-import { GitlabLogo } from '@/components/logos/GitlabLogo'
-import { useTranslate } from '@tolgee/react'
-import { KeycloackLogo } from '@/components/logos/KeycloakLogo'
+} from "next-auth/react";
+import { useRouter } from "next/router";
+import { stringify } from "qs";
+import React, { useState } from "react";
type Props = {
providers:
| Record, ClientSafeProvider>
- | undefined
-}
+ | undefined;
+};
export const SocialLoginButtons = ({ providers }: Props) => {
- const { t } = useTranslate()
- const { query } = useRouter()
- const { status } = useSession()
+ const { t } = useTranslate();
+ const { query } = useRouter();
+ const { status } = useSession();
const [authLoading, setAuthLoading] =
- useState>()
+ useState>();
const handleSignIn = async (provider: string) => {
- setAuthLoading(provider)
+ setAuthLoading(provider);
await signIn(provider, {
callbackUrl:
query.callbackUrl?.toString() ??
- `/typebots?${stringify(omit(query, 'error', 'callbackUrl'))}`,
- })
- setTimeout(() => setAuthLoading(undefined), 3000)
- }
+ `/typebots?${stringify(omit(query, "error", "callbackUrl"))}`,
+ });
+ setTimeout(() => setAuthLoading(undefined), 3000);
+ };
- const handleGitHubClick = () => handleSignIn('github')
+ const handleGitHubClick = () => handleSignIn("github");
- const handleGoogleClick = () => handleSignIn('google')
+ const handleGoogleClick = () => handleSignIn("google");
- const handleFacebookClick = () => handleSignIn('facebook')
+ const handleFacebookClick = () => handleSignIn("facebook");
- const handleGitlabClick = () => handleSignIn('gitlab')
+ const handleGitlabClick = () => handleSignIn("gitlab");
- const handleAzureAdClick = () => handleSignIn('azure-ad')
+ const handleAzureAdClick = () => handleSignIn("azure-ad");
- const handleCustomOAuthClick = () => handleSignIn('custom-oauth')
+ const handleCustomOAuthClick = () => handleSignIn("custom-oauth");
- const handleKeyCloackClick = () => handleSignIn('keycloak')
+ const handleKeyCloackClick = () => handleSignIn("keycloak");
return (
@@ -63,12 +63,12 @@ export const SocialLoginButtons = ({ providers }: Props) => {
onClick={handleGitHubClick}
data-testid="github"
isLoading={
- ['loading', 'authenticated'].includes(status) ||
- authLoading === 'github'
+ ["loading", "authenticated"].includes(status) ||
+ authLoading === "github"
}
variant="outline"
>
- {t('auth.socialLogin.githubButton.label')}
+ {t("auth.socialLogin.githubButton.label")}
)}
{providers?.google && (
@@ -77,12 +77,12 @@ export const SocialLoginButtons = ({ providers }: Props) => {
onClick={handleGoogleClick}
data-testid="google"
isLoading={
- ['loading', 'authenticated'].includes(status) ||
- authLoading === 'google'
+ ["loading", "authenticated"].includes(status) ||
+ authLoading === "google"
}
variant="outline"
>
- {t('auth.socialLogin.googleButton.label')}
+ {t("auth.socialLogin.googleButton.label")}
)}
{providers?.facebook && (
@@ -91,12 +91,12 @@ export const SocialLoginButtons = ({ providers }: Props) => {
onClick={handleFacebookClick}
data-testid="facebook"
isLoading={
- ['loading', 'authenticated'].includes(status) ||
- authLoading === 'facebook'
+ ["loading", "authenticated"].includes(status) ||
+ authLoading === "facebook"
}
variant="outline"
>
- {t('auth.socialLogin.facebookButton.label')}
+ {t("auth.socialLogin.facebookButton.label")}
)}
{providers?.gitlab && (
@@ -105,43 +105,43 @@ export const SocialLoginButtons = ({ providers }: Props) => {
onClick={handleGitlabClick}
data-testid="gitlab"
isLoading={
- ['loading', 'authenticated'].includes(status) ||
- authLoading === 'gitlab'
+ ["loading", "authenticated"].includes(status) ||
+ authLoading === "gitlab"
}
variant="outline"
>
- {t('auth.socialLogin.gitlabButton.label', {
+ {t("auth.socialLogin.gitlabButton.label", {
gitlabProviderName: providers.gitlab.name,
})}
)}
- {providers?.['azure-ad'] && (
+ {providers?.["azure-ad"] && (
}
onClick={handleAzureAdClick}
data-testid="azure-ad"
isLoading={
- ['loading', 'authenticated'].includes(status) ||
- authLoading === 'azure-ad'
+ ["loading", "authenticated"].includes(status) ||
+ authLoading === "azure-ad"
}
variant="outline"
>
- {t('auth.socialLogin.azureButton.label', {
- azureProviderName: providers['azure-ad'].name,
+ {t("auth.socialLogin.azureButton.label", {
+ azureProviderName: providers["azure-ad"].name,
})}
)}
- {providers?.['custom-oauth'] && (
+ {providers?.["custom-oauth"] && (
- {t('auth.socialLogin.customButton.label', {
- customProviderName: providers['custom-oauth'].name,
+ {t("auth.socialLogin.customButton.label", {
+ customProviderName: providers["custom-oauth"].name,
})}
)}
@@ -151,14 +151,14 @@ export const SocialLoginButtons = ({ providers }: Props) => {
onClick={handleKeyCloackClick}
data-testid="keycloak"
isLoading={
- ['loading', 'authenticated'].includes(status) ||
- authLoading === 'keycloak'
+ ["loading", "authenticated"].includes(status) ||
+ authLoading === "keycloak"
}
variant="outline"
>
- {t('auth.socialLogin.keycloakButton.label')}
+ {t("auth.socialLogin.keycloakButton.label")}
)}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts b/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts
index f90c08ab37..669a85f12d 100644
--- a/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts
+++ b/apps/builder/src/features/auth/helpers/convertInvitationsToCollaborations.ts
@@ -1,15 +1,16 @@
-import { Invitation, PrismaClient, WorkspaceRole } from '@typebot.io/prisma'
+import { WorkspaceRole } from "@typebot.io/prisma/enum";
+import type { Prisma } from "@typebot.io/prisma/types";
-export type InvitationWithWorkspaceId = Invitation & {
+export type InvitationWithWorkspaceId = Prisma.Invitation & {
typebot: {
- workspaceId: string | null
- }
-}
+ workspaceId: string | null;
+ };
+};
export const convertInvitationsToCollaborations = async (
- p: PrismaClient,
+ p: Prisma.PrismaClient,
{ id, email }: { id: string; email: string },
- invitations: InvitationWithWorkspaceId[]
+ invitations: InvitationWithWorkspaceId[],
) => {
await p.collaboratorsOnTypebots.createMany({
data: invitations.map((invitation) => ({
@@ -17,18 +18,18 @@ export const convertInvitationsToCollaborations = async (
type: invitation.type,
userId: id,
})),
- })
+ });
const workspaceInvitations = invitations.reduce(
(acc, invitation) =>
acc.some(
- (inv) => inv.typebot.workspaceId === invitation.typebot.workspaceId
+ (inv) => inv.typebot.workspaceId === invitation.typebot.workspaceId,
)
? acc
: [...acc, invitation],
- []
- )
+ [],
+ );
for (const invitation of workspaceInvitations) {
- if (!invitation.typebot.workspaceId) continue
+ if (!invitation.typebot.workspaceId) continue;
await p.memberInWorkspace.createMany({
data: [
{
@@ -38,11 +39,11 @@ export const convertInvitationsToCollaborations = async (
},
],
skipDuplicates: true,
- })
+ });
}
return p.invitation.deleteMany({
where: {
email,
},
- })
-}
+ });
+};
diff --git a/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts b/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts
index 38b456a051..a3eb82e9e5 100644
--- a/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts
+++ b/apps/builder/src/features/auth/helpers/getAuthenticatedUser.ts
@@ -1,38 +1,38 @@
-import prisma from '@typebot.io/lib/prisma'
-import { getAuthOptions } from '@/pages/api/auth/[...nextauth]'
-import * as Sentry from '@sentry/nextjs'
-import { User } from '@typebot.io/prisma'
-import { NextApiRequest, NextApiResponse } from 'next'
-import { getServerSession } from 'next-auth'
-import { env } from '@typebot.io/env'
-import { mockedUser } from '@typebot.io/lib/mockedUser'
+import { getAuthOptions } from "@/pages/api/auth/[...nextauth]";
+import * as Sentry from "@sentry/nextjs";
+import { env } from "@typebot.io/env";
+import { mockedUser } from "@typebot.io/lib/mockedUser";
+import prisma from "@typebot.io/prisma";
+import type { Prisma } from "@typebot.io/prisma/types";
+import type { NextApiRequest, NextApiResponse } from "next";
+import { getServerSession } from "next-auth";
export const getAuthenticatedUser = async (
req: NextApiRequest,
- res: NextApiResponse
-): Promise => {
- const bearerToken = extractBearerToken(req)
- if (bearerToken) return authenticateByToken(bearerToken)
+ res: NextApiResponse,
+): Promise => {
+ const bearerToken = extractBearerToken(req);
+ if (bearerToken) return authenticateByToken(bearerToken);
const user = env.NEXT_PUBLIC_E2E_TEST
? mockedUser
: ((await getServerSession(req, res, getAuthOptions({})))?.user as
- | User
- | undefined)
- if (!user || !('id' in user)) return
- Sentry.setUser({ id: user.id })
- return user
-}
+ | Prisma.User
+ | undefined);
+ if (!user || !("id" in user)) return;
+ Sentry.setUser({ id: user.id });
+ return user;
+};
const authenticateByToken = async (
- apiToken: string
-): Promise => {
- if (typeof window !== 'undefined') return
+ apiToken: string,
+): Promise => {
+ if (typeof window !== "undefined") return;
const user = (await prisma.user.findFirst({
where: { apiTokens: { some: { token: apiToken } } },
- })) as User
- Sentry.setUser({ id: user.id })
- return user
-}
+ })) as Prisma.User;
+ Sentry.setUser({ id: user.id });
+ return user;
+};
const extractBearerToken = (req: NextApiRequest) =>
- req.headers['authorization']?.slice(7)
+ req.headers["authorization"]?.slice(7);
diff --git a/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts b/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts
index 014d1c48f5..bf5885cfd4 100644
--- a/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts
+++ b/apps/builder/src/features/auth/helpers/getNewUserInvitations.ts
@@ -1,12 +1,12 @@
-import { PrismaClient, WorkspaceInvitation } from '@typebot.io/prisma'
-import { InvitationWithWorkspaceId } from './convertInvitationsToCollaborations'
+import type { Prisma } from "@typebot.io/prisma/types";
+import type { InvitationWithWorkspaceId } from "./convertInvitationsToCollaborations";
export const getNewUserInvitations = async (
- p: PrismaClient,
- email: string
+ p: Prisma.PrismaClient,
+ email: string,
): Promise<{
- invitations: InvitationWithWorkspaceId[]
- workspaceInvitations: WorkspaceInvitation[]
+ invitations: InvitationWithWorkspaceId[];
+ workspaceInvitations: Prisma.WorkspaceInvitation[];
}> => {
const [invitations, workspaceInvitations] = await p.$transaction([
p.invitation.findMany({
@@ -16,7 +16,7 @@ export const getNewUserInvitations = async (
p.workspaceInvitation.findMany({
where: { email },
}),
- ])
+ ]);
- return { invitations, workspaceInvitations }
-}
+ return { invitations, workspaceInvitations };
+};
diff --git a/apps/builder/src/features/auth/helpers/joinWorkspaces.ts b/apps/builder/src/features/auth/helpers/joinWorkspaces.ts
index 0332260bb4..064f095050 100644
--- a/apps/builder/src/features/auth/helpers/joinWorkspaces.ts
+++ b/apps/builder/src/features/auth/helpers/joinWorkspaces.ts
@@ -1,9 +1,9 @@
-import { PrismaClient, WorkspaceInvitation } from '@typebot.io/prisma'
+import type { Prisma } from "@typebot.io/prisma/types";
export const joinWorkspaces = async (
- p: PrismaClient,
+ p: Prisma.PrismaClient,
{ id, email }: { id: string; email: string },
- invitations: WorkspaceInvitation[]
+ invitations: Prisma.WorkspaceInvitation[],
) => {
await p.$transaction([
p.memberInWorkspace.createMany({
@@ -19,5 +19,5 @@ export const joinWorkspaces = async (
email,
},
}),
- ])
-}
+ ]);
+};
diff --git a/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts b/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts
index 1cf579b0ea..10f2140df8 100644
--- a/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts
+++ b/apps/builder/src/features/auth/helpers/sendVerificationRequest.ts
@@ -1,15 +1,15 @@
-import { sendMagicLinkEmail } from '@typebot.io/emails'
+import { sendMagicLinkEmail } from "@typebot.io/emails/emails/MagicLinkEmail";
type Props = {
- identifier: string
- url: string
-}
+ identifier: string;
+ url: string;
+};
export const sendVerificationRequest = async ({ identifier, url }: Props) => {
try {
- await sendMagicLinkEmail({ url, to: identifier })
+ await sendMagicLinkEmail({ url, to: identifier });
} catch (err) {
- console.error(err)
- throw new Error(`Magic link email could not be sent. See error above.`)
+ console.error(err);
+ throw new Error(`Magic link email could not be sent. See error above.`);
}
-}
+};
diff --git a/apps/builder/src/features/billing/api/createCheckoutSession.ts b/apps/builder/src/features/billing/api/createCheckoutSession.ts
index 7d15cced98..b874aebcf1 100644
--- a/apps/builder/src/features/billing/api/createCheckoutSession.ts
+++ b/apps/builder/src/features/billing/api/createCheckoutSession.ts
@@ -1,16 +1,16 @@
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { Plan } from '@typebot.io/prisma'
-import { z } from 'zod'
-import { createCheckoutSession as createCheckoutSessionHandler } from '@typebot.io/billing/api/createCheckoutSession'
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { createCheckoutSession as createCheckoutSessionHandler } from "@typebot.io/billing/api/createCheckoutSession";
+import { Plan } from "@typebot.io/prisma/enum";
+import { z } from "@typebot.io/zod";
export const createCheckoutSession = authenticatedProcedure
.meta({
openapi: {
- method: 'POST',
- path: '/v1/billing/subscription/checkout',
+ method: "POST",
+ path: "/v1/billing/subscription/checkout",
protect: true,
- summary: 'Create checkout session to create a new subscription',
- tags: ['Billing'],
+ summary: "Create checkout session to create a new subscription",
+ tags: ["Billing"],
},
})
.input(
@@ -18,7 +18,7 @@ export const createCheckoutSession = authenticatedProcedure
email: z.string(),
company: z.string(),
workspaceId: z.string(),
- currency: z.enum(['usd', 'eur']),
+ currency: z.enum(["usd", "eur"]),
plan: z.enum([Plan.STARTER, Plan.PRO]),
returnUrl: z.string(),
vat: z
@@ -27,13 +27,13 @@ export const createCheckoutSession = authenticatedProcedure
value: z.string(),
})
.optional(),
- })
+ }),
)
.output(
z.object({
checkoutUrl: z.string(),
- })
+ }),
)
.mutation(async ({ input, ctx: { user } }) =>
- createCheckoutSessionHandler({ ...input, user })
- )
+ createCheckoutSessionHandler({ ...input, user }),
+ );
diff --git a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts
index a397434f95..d9fafc7b6f 100644
--- a/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts
+++ b/apps/builder/src/features/billing/api/createCustomCheckoutSession.ts
@@ -1,16 +1,16 @@
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { z } from 'zod'
-import { createCustomCheckoutSession as createCustomCheckoutSessionHandler } from '@typebot.io/billing/api/createCustomCheckoutSession'
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { createCustomCheckoutSession as createCustomCheckoutSessionHandler } from "@typebot.io/billing/api/createCustomCheckoutSession";
+import { z } from "@typebot.io/zod";
export const createCustomCheckoutSession = authenticatedProcedure
.meta({
openapi: {
- method: 'POST',
- path: '/v1/billing/subscription/custom-checkout',
+ method: "POST",
+ path: "/v1/billing/subscription/custom-checkout",
protect: true,
summary:
- 'Create custom checkout session to make a workspace pay for a custom plan',
- tags: ['Billing'],
+ "Create custom checkout session to make a workspace pay for a custom plan",
+ tags: ["Billing"],
},
})
.input(
@@ -18,16 +18,16 @@ export const createCustomCheckoutSession = authenticatedProcedure
email: z.string(),
workspaceId: z.string(),
returnUrl: z.string(),
- })
+ }),
)
.output(
z.object({
checkoutUrl: z.string(),
- })
+ }),
)
.mutation(async ({ input, ctx: { user } }) =>
createCustomCheckoutSessionHandler({
...input,
user,
- })
- )
+ }),
+ );
diff --git a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts
index 7b1b3e8ab0..46a4d5e2fc 100644
--- a/apps/builder/src/features/billing/api/getBillingPortalUrl.ts
+++ b/apps/builder/src/features/billing/api/getBillingPortalUrl.ts
@@ -1,27 +1,27 @@
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { z } from 'zod'
-import { getBillingPortalUrl as getBillingPortalUrlHandler } from '@typebot.io/billing/api/getBillingPortalUrl'
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { getBillingPortalUrl as getBillingPortalUrlHandler } from "@typebot.io/billing/api/getBillingPortalUrl";
+import { z } from "@typebot.io/zod";
export const getBillingPortalUrl = authenticatedProcedure
.meta({
openapi: {
- method: 'GET',
- path: '/v1/billing/subscription/portal',
+ method: "GET",
+ path: "/v1/billing/subscription/portal",
protect: true,
- summary: 'Get Stripe billing portal URL',
- tags: ['Billing'],
+ summary: "Get Stripe billing portal URL",
+ tags: ["Billing"],
},
})
.input(
z.object({
workspaceId: z.string(),
- })
+ }),
)
.output(
z.object({
billingPortalUrl: z.string(),
- })
+ }),
)
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
- getBillingPortalUrlHandler({ workspaceId, user })
- )
+ getBillingPortalUrlHandler({ workspaceId, user }),
+ );
diff --git a/apps/builder/src/features/billing/api/getSubscription.ts b/apps/builder/src/features/billing/api/getSubscription.ts
index 3c17e52823..4ddb4781b2 100644
--- a/apps/builder/src/features/billing/api/getSubscription.ts
+++ b/apps/builder/src/features/billing/api/getSubscription.ts
@@ -1,28 +1,28 @@
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { z } from 'zod'
-import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
-import { getSubscription as getSubscriptionHandler } from '@typebot.io/billing/api/getSubscription'
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { getSubscription as getSubscriptionHandler } from "@typebot.io/billing/api/getSubscription";
+import { subscriptionSchema } from "@typebot.io/billing/schemas/subscription";
+import { z } from "@typebot.io/zod";
export const getSubscription = authenticatedProcedure
.meta({
openapi: {
- method: 'GET',
- path: '/v1/billing/subscription',
+ method: "GET",
+ path: "/v1/billing/subscription",
protect: true,
- summary: 'List invoices',
- tags: ['Billing'],
+ summary: "List invoices",
+ tags: ["Billing"],
},
})
.input(
z.object({
workspaceId: z.string(),
- })
+ }),
)
.output(
z.object({
- subscription: subscriptionSchema.or(z.null().openapi({ type: 'string' })),
- })
+ subscription: subscriptionSchema.or(z.null().openapi({ type: "string" })),
+ }),
)
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
- getSubscriptionHandler({ workspaceId, user })
- )
+ getSubscriptionHandler({ workspaceId, user }),
+ );
diff --git a/apps/builder/src/features/billing/api/getUsage.ts b/apps/builder/src/features/billing/api/getUsage.ts
index 2e006019a9..0a446ad96a 100644
--- a/apps/builder/src/features/billing/api/getUsage.ts
+++ b/apps/builder/src/features/billing/api/getUsage.ts
@@ -1,15 +1,15 @@
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { z } from 'zod'
-import { getUsage as getUsageHandler } from '@typebot.io/billing/api/getUsage'
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { getUsage as getUsageHandler } from "@typebot.io/billing/api/getUsage";
+import { z } from "@typebot.io/zod";
export const getUsage = authenticatedProcedure
.meta({
openapi: {
- method: 'GET',
- path: '/v1/billing/usage',
+ method: "GET",
+ path: "/v1/billing/usage",
protect: true,
- summary: 'Get current plan usage',
- tags: ['Billing'],
+ summary: "Get current plan usage",
+ tags: ["Billing"],
},
})
.input(
@@ -17,11 +17,11 @@ export const getUsage = authenticatedProcedure
workspaceId: z
.string()
.describe(
- '[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)'
+ "[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)",
),
- })
+ }),
)
.output(z.object({ totalChatsUsed: z.number(), resetsAt: z.date() }))
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
- getUsageHandler({ workspaceId, user })
- )
+ getUsageHandler({ workspaceId, user }),
+ );
diff --git a/apps/builder/src/features/billing/api/listInvoices.ts b/apps/builder/src/features/billing/api/listInvoices.ts
index 074b521e76..b4accf0c34 100644
--- a/apps/builder/src/features/billing/api/listInvoices.ts
+++ b/apps/builder/src/features/billing/api/listInvoices.ts
@@ -1,16 +1,16 @@
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { z } from 'zod'
-import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
-import { listInvoices as listInvoicesHandler } from '@typebot.io/billing/api/listInvoices'
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { listInvoices as listInvoicesHandler } from "@typebot.io/billing/api/listInvoices";
+import { invoiceSchema } from "@typebot.io/billing/schemas/invoice";
+import { z } from "@typebot.io/zod";
export const listInvoices = authenticatedProcedure
.meta({
openapi: {
- method: 'GET',
- path: '/v1/billing/invoices',
+ method: "GET",
+ path: "/v1/billing/invoices",
protect: true,
- summary: 'List invoices',
- tags: ['Billing'],
+ summary: "List invoices",
+ tags: ["Billing"],
},
})
.input(
@@ -18,15 +18,15 @@ export const listInvoices = authenticatedProcedure
workspaceId: z
.string()
.describe(
- '[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)'
+ "[Where to find my workspace ID?](../how-to#how-to-find-my-workspaceid)",
),
- })
+ }),
)
.output(
z.object({
invoices: z.array(invoiceSchema),
- })
+ }),
)
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
- listInvoicesHandler({ workspaceId, user })
- )
+ listInvoicesHandler({ workspaceId, user }),
+ );
diff --git a/apps/builder/src/features/billing/api/router.ts b/apps/builder/src/features/billing/api/router.ts
index de4cd5317c..2997e5d9e5 100644
--- a/apps/builder/src/features/billing/api/router.ts
+++ b/apps/builder/src/features/billing/api/router.ts
@@ -1,11 +1,11 @@
-import { router } from '@/helpers/server/trpc'
-import { createCheckoutSession } from './createCheckoutSession'
-import { getBillingPortalUrl } from './getBillingPortalUrl'
-import { getSubscription } from './getSubscription'
-import { getUsage } from './getUsage'
-import { listInvoices } from './listInvoices'
-import { updateSubscription } from './updateSubscription'
-import { createCustomCheckoutSession } from './createCustomCheckoutSession'
+import { router } from "@/helpers/server/trpc";
+import { createCheckoutSession } from "./createCheckoutSession";
+import { createCustomCheckoutSession } from "./createCustomCheckoutSession";
+import { getBillingPortalUrl } from "./getBillingPortalUrl";
+import { getSubscription } from "./getSubscription";
+import { getUsage } from "./getUsage";
+import { listInvoices } from "./listInvoices";
+import { updateSubscription } from "./updateSubscription";
export const billingRouter = router({
getBillingPortalUrl,
@@ -15,4 +15,4 @@ export const billingRouter = router({
getSubscription,
getUsage,
createCustomCheckoutSession,
-})
+});
diff --git a/apps/builder/src/features/billing/api/updateSubscription.ts b/apps/builder/src/features/billing/api/updateSubscription.ts
index c83b582b9d..9f7db0c057 100644
--- a/apps/builder/src/features/billing/api/updateSubscription.ts
+++ b/apps/builder/src/features/billing/api/updateSubscription.ts
@@ -1,17 +1,17 @@
-import { authenticatedProcedure } from '@/helpers/server/trpc'
-import { Plan } from '@typebot.io/prisma'
-import { workspaceSchema } from '@typebot.io/schemas'
-import { z } from 'zod'
-import { updateSubscription as updateSubscriptionHandler } from '@typebot.io/billing/api/updateSubscription'
+import { authenticatedProcedure } from "@/helpers/server/trpc";
+import { updateSubscription as updateSubscriptionHandler } from "@typebot.io/billing/api/updateSubscription";
+import { Plan } from "@typebot.io/prisma/enum";
+import { workspaceSchema } from "@typebot.io/workspaces/schemas";
+import { z } from "@typebot.io/zod";
export const updateSubscription = authenticatedProcedure
.meta({
openapi: {
- method: 'PATCH',
- path: '/v1/billing/subscription',
+ method: "PATCH",
+ path: "/v1/billing/subscription",
protect: true,
- summary: 'Update subscription',
- tags: ['Billing'],
+ summary: "Update subscription",
+ tags: ["Billing"],
},
})
.input(
@@ -19,18 +19,18 @@ export const updateSubscription = authenticatedProcedure
returnUrl: z.string(),
workspaceId: z.string(),
plan: z.enum([Plan.STARTER, Plan.PRO]),
- currency: z.enum(['usd', 'eur']),
- })
+ currency: z.enum(["usd", "eur"]),
+ }),
)
.output(
z.object({
workspace: workspaceSchema.nullish(),
checkoutUrl: z.string().nullish(),
- })
+ }),
)
.mutation(async ({ input, ctx: { user } }) =>
updateSubscriptionHandler({
...input,
user,
- })
- )
+ }),
+ );
diff --git a/apps/builder/src/features/billing/billing.spec.ts b/apps/builder/src/features/billing/billing.spec.ts
index 0391eeb69f..492477d9da 100644
--- a/apps/builder/src/features/billing/billing.spec.ts
+++ b/apps/builder/src/features/billing/billing.spec.ts
@@ -2,127 +2,127 @@ import {
addSubscriptionToWorkspace,
cancelSubscription,
createClaimableCustomPlan,
-} from '@/test/utils/databaseActions'
-import test, { expect } from '@playwright/test'
-import { createId } from '@paralleldrive/cuid2'
-import { Plan } from '@typebot.io/prisma'
+} from "@/test/utils/databaseActions";
+import { createId } from "@paralleldrive/cuid2";
+import test, { expect } from "@playwright/test";
+import { env } from "@typebot.io/env";
import {
createTypebots,
createWorkspaces,
deleteWorkspaces,
injectFakeResults,
-} from '@typebot.io/playwright/databaseActions'
-import { env } from '@typebot.io/env'
+} from "@typebot.io/playwright/databaseActions";
+import { Plan } from "@typebot.io/prisma/enum";
-const usageWorkspaceId = createId()
-const usageTypebotId = createId()
-const planChangeWorkspaceId = createId()
-const enterpriseWorkspaceId = createId()
+const usageWorkspaceId = createId();
+const usageTypebotId = createId();
+const planChangeWorkspaceId = createId();
+const enterpriseWorkspaceId = createId();
test.beforeAll(async () => {
await createWorkspaces([
{
id: usageWorkspaceId,
- name: 'Usage Workspace',
+ name: "Usage Workspace",
plan: Plan.STARTER,
},
{
id: planChangeWorkspaceId,
- name: 'Plan Change Workspace',
+ name: "Plan Change Workspace",
},
{
id: enterpriseWorkspaceId,
- name: 'Enterprise Workspace',
+ name: "Enterprise Workspace",
},
- ])
- await createTypebots([{ id: usageTypebotId, workspaceId: usageWorkspaceId }])
-})
+ ]);
+ await createTypebots([{ id: usageTypebotId, workspaceId: usageWorkspaceId }]);
+});
test.afterAll(async () => {
await deleteWorkspaces([
usageWorkspaceId,
planChangeWorkspaceId,
enterpriseWorkspaceId,
- ])
-})
-
-test('should display valid usage', async ({ page }) => {
- await page.goto('/typebots')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="/ 10,000"')).toBeVisible()
- await page.getByText('Members', { exact: true }).click()
+ ]);
+});
+
+test("should display valid usage", async ({ page }) => {
+ await page.goto("/typebots");
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="/ 10,000"')).toBeVisible();
+ await page.getByText("Members", { exact: true }).click();
await expect(
- page.getByRole('heading', { name: 'Members (1/5)' })
- ).toBeVisible()
- await page.click('text=Pro workspace', { force: true })
-
- await page.click('text=Pro workspace')
- await page.click('text="Custom workspace"')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="/ 100,000"')).toBeVisible()
- await expect(page.getByText('Upgrade to Starter')).toBeHidden()
- await expect(page.getByText('Upgrade to Pro')).toBeHidden()
- await expect(page.getByText('Need custom limits?')).toBeHidden()
- await page.getByText('Members', { exact: true }).click()
+ page.getByRole("heading", { name: "Members (1/5)" }),
+ ).toBeVisible();
+ await page.click("text=Pro workspace", { force: true });
+
+ await page.click("text=Pro workspace");
+ await page.click('text="Custom workspace"');
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="/ 100,000"')).toBeVisible();
+ await expect(page.getByText("Upgrade to Starter")).toBeHidden();
+ await expect(page.getByText("Upgrade to Pro")).toBeHidden();
+ await expect(page.getByText("Need custom limits?")).toBeHidden();
+ await page.getByText("Members", { exact: true }).click();
await expect(
- page.getByRole('heading', { name: 'Members (1/20)' })
- ).toBeVisible()
- await page.click('text=Custom workspace', { force: true })
-
- await page.click('text=Custom workspace')
- await page.click('text="Free workspace"')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="/ 200"')).toBeVisible()
- await page.getByText('Members', { exact: true }).click()
+ page.getByRole("heading", { name: "Members (1/20)" }),
+ ).toBeVisible();
+ await page.click("text=Custom workspace", { force: true });
+
+ await page.click("text=Custom workspace");
+ await page.click('text="Free workspace"');
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="/ 200"')).toBeVisible();
+ await page.getByText("Members", { exact: true }).click();
await expect(
- page.getByRole('heading', { name: 'Members (1/1)' })
- ).toBeVisible()
- await page.click('text=Free workspace', { force: true })
+ page.getByRole("heading", { name: "Members (1/1)" }),
+ ).toBeVisible();
+ await page.click("text=Free workspace", { force: true });
await injectFakeResults({
count: 10,
typebotId: usageTypebotId,
- })
- await page.click('text=Free workspace')
- await page.click('text="Usage Workspace"')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="/ 2,000"')).toBeVisible()
- await expect(page.locator('text="10" >> nth=0')).toBeVisible()
+ });
+ await page.click("text=Free workspace");
+ await page.click('text="Usage Workspace"');
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="/ 2,000"')).toBeVisible();
+ await expect(page.locator('text="10" >> nth=0')).toBeVisible();
await expect(page.locator('[role="progressbar"] >> nth=0')).toHaveAttribute(
- 'aria-valuenow',
- '1'
- )
+ "aria-valuenow",
+ "1",
+ );
await injectFakeResults({
typebotId: usageTypebotId,
count: 1090,
- })
- await page.click('text="Settings"')
- await page.click('text="Billing & Usage"')
- await expect(page.locator('text="/ 2,000"')).toBeVisible()
- await expect(page.locator('text="1,100"')).toBeVisible()
- await expect(page.locator('[aria-valuenow="55"]')).toBeVisible()
-})
-
-test('plan changes should work', async ({ page }) => {
- test.setTimeout(80000)
+ });
+ await page.click('text="Settings"');
+ await page.click('text="Billing & Usage"');
+ await expect(page.locator('text="/ 2,000"')).toBeVisible();
+ await expect(page.locator('text="1,100"')).toBeVisible();
+ await expect(page.locator('[aria-valuenow="55"]')).toBeVisible();
+});
+
+test("plan changes should work", async ({ page }) => {
+ test.setTimeout(80000);
// Upgrade to STARTER
- await page.goto('/typebots')
- await page.click('text=Pro workspace')
- await page.click('text=Plan Change Workspace')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="$39"')).toBeVisible()
- await page.click('button >> text=Upgrade >> nth=0')
- await page.getByLabel('Company name').fill('Company LLC')
- await page.getByRole('button', { name: 'Go to checkout' }).click()
- await page.waitForNavigation()
- expect(page.url()).toContain('https://checkout.stripe.com')
- await expect(page.locator('text=$39 >> nth=0')).toBeVisible()
+ await page.goto("/typebots");
+ await page.click("text=Pro workspace");
+ await page.click("text=Plan Change Workspace");
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="$39"')).toBeVisible();
+ await page.click("button >> text=Upgrade >> nth=0");
+ await page.getByLabel("Company name").fill("Company LLC");
+ await page.getByRole("button", { name: "Go to checkout" }).click();
+ await page.waitForNavigation();
+ expect(page.url()).toContain("https://checkout.stripe.com");
+ await expect(page.locator("text=$39 >> nth=0")).toBeVisible();
const stripeId = await addSubscriptionToWorkspace(
planChangeWorkspaceId,
[
@@ -134,86 +134,86 @@ test('plan changes should work', async ({ page }) => {
price: env.STRIPE_STARTER_CHATS_PRICE_ID,
},
],
- { plan: Plan.STARTER }
- )
+ { plan: Plan.STARTER },
+ );
// Update plan with additional quotas
- await page.goto('/typebots')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="/ 2,000"')).toBeVisible()
- await expect(page.getByText('/ 2,000')).toBeVisible()
+ await page.goto("/typebots");
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="/ 2,000"')).toBeVisible();
+ await expect(page.getByText("/ 2,000")).toBeVisible();
// Upgrade to PRO
- await expect(page.locator('text="$89"')).toBeVisible()
- await page.click('button >> text=Upgrade')
+ await expect(page.locator('text="$89"')).toBeVisible();
+ await page.click("button >> text=Upgrade");
await expect(
- page.locator('text="Workspace PRO plan successfully updated" >> nth=0')
- ).toBeVisible()
+ page.locator('text="Workspace PRO plan successfully updated" >> nth=0'),
+ ).toBeVisible();
// Go to customer portal
await Promise.all([
page.waitForNavigation(),
page.click('text="Billing portal"'),
- ])
- await expect(page.getByText('$39.00')).toBeVisible({
+ ]);
+ await expect(page.getByText("$39.00")).toBeVisible({
timeout: 10000,
- })
- await expect(page.getByText('$50.00')).toBeVisible({
+ });
+ await expect(page.getByText("$50.00")).toBeVisible({
timeout: 10000,
- })
- await expect(page.locator('text="Add payment method"')).toBeVisible()
- await cancelSubscription(stripeId)
+ });
+ await expect(page.locator('text="Add payment method"')).toBeVisible();
+ await cancelSubscription(stripeId);
// Cancel subscription
- await page.goto('/typebots')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
+ await page.goto("/typebots");
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
await expect(
- page.getByTestId('current-subscription').getByTestId('pro-plan-tag')
- ).toBeVisible()
- await expect(page.getByText('Will be cancelled on')).toBeVisible()
-})
-
-test('should display invoices', async ({ page }) => {
- await page.goto('/typebots')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="Invoices"')).toBeHidden()
- await page.click('text=Pro workspace', { force: true })
-
- await page.click('text=Pro workspace')
- await page.click('text=Plan Change Workspace')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.locator('text="Invoices"')).toBeVisible()
- await expect(page.locator('tr')).toHaveCount(4)
- await expect(page.getByText('$39.00')).toBeVisible()
- await expect(page.getByText('$50.00')).toBeVisible()
-})
-
-test('custom plans should work', async ({ page }) => {
- await page.goto('/typebots')
- await page.click('text=Pro workspace')
- await page.click('text=Enterprise Workspace')
- await page.click('text=Settings & Members')
- await page.click('text=Billing & Usage')
- await expect(page.getByTestId('current-subscription')).toHaveText(
- 'Current workspace subscription: Free'
- )
+ page.getByTestId("current-subscription").getByTestId("pro-plan-tag"),
+ ).toBeVisible();
+ await expect(page.getByText("Will be cancelled on")).toBeVisible();
+});
+
+test("should display invoices", async ({ page }) => {
+ await page.goto("/typebots");
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="Invoices"')).toBeHidden();
+ await page.click("text=Pro workspace", { force: true });
+
+ await page.click("text=Pro workspace");
+ await page.click("text=Plan Change Workspace");
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.locator('text="Invoices"')).toBeVisible();
+ await expect(page.locator("tr")).toHaveCount(4);
+ await expect(page.getByText("$39.00")).toBeVisible();
+ await expect(page.getByText("$50.00")).toBeVisible();
+});
+
+test("custom plans should work", async ({ page }) => {
+ await page.goto("/typebots");
+ await page.click("text=Pro workspace");
+ await page.click("text=Enterprise Workspace");
+ await page.click("text=Settings & Members");
+ await page.click("text=Billing & Usage");
+ await expect(page.getByTestId("current-subscription")).toHaveText(
+ "Current workspace subscription: Free",
+ );
await createClaimableCustomPlan({
- currency: 'usd',
+ currency: "usd",
price: 239,
workspaceId: enterpriseWorkspaceId,
chatsLimit: 100000,
storageLimit: 50,
seatsLimit: 10,
- name: 'Acme custom plan',
- description: 'Description of the deal',
- })
+ name: "Acme custom plan",
+ description: "Description of the deal",
+ });
- await page.goto('/typebots?claimCustomPlan=true')
+ await page.goto("/typebots?claimCustomPlan=true");
- await expect(page.getByRole('list').getByText('$239.00')).toBeVisible()
- await expect(page.getByText('Subscribe to Acme custom plan')).toBeVisible()
-})
+ await expect(page.getByRole("list").getByText("$239.00")).toBeVisible();
+ await expect(page.getByText("Subscribe to Acme custom plan")).toBeVisible();
+});
diff --git a/apps/builder/src/features/billing/components/BillingPortalButton.tsx b/apps/builder/src/features/billing/components/BillingPortalButton.tsx
index 747dc4e922..6519dc12d4 100644
--- a/apps/builder/src/features/billing/components/BillingPortalButton.tsx
+++ b/apps/builder/src/features/billing/components/BillingPortalButton.tsx
@@ -1,15 +1,15 @@
-import { useToast } from '@/hooks/useToast'
-import { trpc } from '@/lib/trpc'
-import { useTranslate } from '@tolgee/react'
-import { Button, ButtonProps, Link } from '@chakra-ui/react'
+import { useToast } from "@/hooks/useToast";
+import { trpc } from "@/lib/trpc";
+import { Button, type ButtonProps, Link } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
type Props = {
- workspaceId: string
-} & Pick
+ workspaceId: string;
+} & Pick;
export const BillingPortalButton = ({ workspaceId, colorScheme }: Props) => {
- const { t } = useTranslate()
- const { showToast } = useToast()
+ const { t } = useTranslate();
+ const { showToast } = useToast();
const { data } = trpc.billing.getBillingPortalUrl.useQuery(
{
workspaceId,
@@ -18,10 +18,10 @@ export const BillingPortalButton = ({ workspaceId, colorScheme }: Props) => {
onError: (error) => {
showToast({
description: error.message,
- })
+ });
},
- }
- )
+ },
+ );
return (
{
isLoading={!data}
colorScheme={colorScheme}
>
- {t('billing.billingPortalButton.label')}
+ {t("billing.billingPortalButton.label")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx b/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx
index 755950eeb2..f30e63fb73 100644
--- a/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx
+++ b/apps/builder/src/features/billing/components/BillingSettingsLayout.tsx
@@ -1,15 +1,15 @@
-import { Stack } from '@chakra-ui/react'
-import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
-import React from 'react'
-import { InvoicesList } from './InvoicesList'
-import { ChangePlanForm } from './ChangePlanForm'
-import { UsageProgressBars } from './UsageProgressBars'
-import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'
+import { useWorkspace } from "@/features/workspace/WorkspaceProvider";
+import { Stack } from "@chakra-ui/react";
+import React from "react";
+import { ChangePlanForm } from "./ChangePlanForm";
+import { CurrentSubscriptionSummary } from "./CurrentSubscriptionSummary";
+import { InvoicesList } from "./InvoicesList";
+import { UsageProgressBars } from "./UsageProgressBars";
export const BillingSettingsLayout = () => {
- const { workspace, currentRole } = useWorkspace()
+ const { workspace, currentRole } = useWorkspace();
- if (!workspace) return null
+ if (!workspace) return null;
return (
@@ -20,5 +20,5 @@ export const BillingSettingsLayout = () => {
{workspace.stripeId && }
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/ChangePlanForm.tsx b/apps/builder/src/features/billing/components/ChangePlanForm.tsx
index 5524a130ff..8bcb8af7d5 100644
--- a/apps/builder/src/features/billing/components/ChangePlanForm.tsx
+++ b/apps/builder/src/features/billing/components/ChangePlanForm.tsx
@@ -1,97 +1,98 @@
-import { Stack, HStack, Text } from '@chakra-ui/react'
-import { Plan, WorkspaceRole } from '@typebot.io/prisma'
-import { TextLink } from '@/components/TextLink'
-import { useToast } from '@/hooks/useToast'
-import { trpc } from '@/lib/trpc'
-import { PreCheckoutModal, PreCheckoutModalProps } from './PreCheckoutModal'
-import { useState } from 'react'
-import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
-import { useUser } from '@/features/account/hooks/useUser'
-import { StarterPlanPricingCard } from './StarterPlanPricingCard'
-import { ProPlanPricingCard } from './ProPlanPricingCard'
-import { useTranslate } from '@tolgee/react'
-import { StripeClimateLogo } from './StripeClimateLogo'
-import { guessIfUserIsEuropean } from '@typebot.io/billing/helpers/guessIfUserIsEuropean'
-import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider'
+import { TextLink } from "@/components/TextLink";
+import { useUser } from "@/features/account/hooks/useUser";
+import { ParentModalProvider } from "@/features/graph/providers/ParentModalProvider";
+import type { WorkspaceInApp } from "@/features/workspace/WorkspaceProvider";
+import { useToast } from "@/hooks/useToast";
+import { trpc } from "@/lib/trpc";
+import { HStack, Stack, Text } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { guessIfUserIsEuropean } from "@typebot.io/billing/helpers/guessIfUserIsEuropean";
+import { Plan, WorkspaceRole } from "@typebot.io/prisma/enum";
+import { useState } from "react";
+import type { PreCheckoutModalProps } from "./PreCheckoutModal";
+import { PreCheckoutModal } from "./PreCheckoutModal";
+import { ProPlanPricingCard } from "./ProPlanPricingCard";
+import { StarterPlanPricingCard } from "./StarterPlanPricingCard";
+import { StripeClimateLogo } from "./StripeClimateLogo";
type Props = {
- workspace: WorkspaceInApp
- currentRole?: WorkspaceRole
- excludedPlans?: ('STARTER' | 'PRO')[]
-}
+ workspace: WorkspaceInApp;
+ currentRole?: WorkspaceRole;
+ excludedPlans?: ("STARTER" | "PRO")[];
+};
export const ChangePlanForm = ({
workspace,
currentRole,
excludedPlans,
}: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
- const { user } = useUser()
- const { showToast } = useToast()
+ const { user } = useUser();
+ const { showToast } = useToast();
const [preCheckoutPlan, setPreCheckoutPlan] =
- useState()
+ useState();
- const trpcContext = trpc.useContext()
+ const trpcContext = trpc.useContext();
const { data, refetch } = trpc.billing.getSubscription.useQuery({
workspaceId: workspace.id,
- })
+ });
const { mutate: updateSubscription, isLoading: isUpdatingSubscription } =
trpc.billing.updateSubscription.useMutation({
onError: (error) => {
showToast({
description: error.message,
- })
+ });
},
onSuccess: ({ workspace, checkoutUrl }) => {
if (checkoutUrl) {
- window.location.href = checkoutUrl
- return
+ window.location.href = checkoutUrl;
+ return;
}
- refetch()
- trpcContext.workspace.getWorkspace.invalidate()
+ refetch();
+ trpcContext.workspace.getWorkspace.invalidate();
showToast({
- status: 'success',
- description: t('billing.updateSuccessToast.description', {
+ status: "success",
+ description: t("billing.updateSuccessToast.description", {
plan: workspace?.plan,
}),
- })
+ });
},
- })
+ });
- const handlePayClick = async (plan: 'STARTER' | 'PRO') => {
- if (!user) return
+ const handlePayClick = async (plan: "STARTER" | "PRO") => {
+ if (!user) return;
const newSubscription = {
plan,
workspaceId: workspace.id,
currency:
data?.subscription?.currency ??
- (guessIfUserIsEuropean() ? 'eur' : 'usd'),
- } as const
+ (guessIfUserIsEuropean() ? "eur" : "usd"),
+ } as const;
if (workspace.stripeId) {
updateSubscription({
...newSubscription,
returnUrl: window.location.href,
- })
+ });
} else {
- setPreCheckoutPlan(newSubscription)
+ setPreCheckoutPlan(newSubscription);
}
- }
+ };
if (
data?.subscription?.cancelDate ||
- data?.subscription?.status === 'past_due'
+ data?.subscription?.status === "past_due"
)
- return null
+ return null;
const isSubscribed =
(workspace.plan === Plan.STARTER || workspace.plan === Plan.PRO) &&
- workspace.stripeId
+ workspace.stripeId;
- if (workspace.plan !== Plan.FREE && !isSubscribed) return null
+ if (workspace.plan !== Plan.FREE && !isSubscribed) return null;
if (currentRole !== WorkspaceRole.ADMIN)
return (
@@ -99,16 +100,16 @@ export const ChangePlanForm = ({
Only workspace admins can change the subscription plan. Contact your
workspace admin to change the plan.
- )
+ );
return (
- {t('billing.contribution.preLink')}{' '}
+ {t("billing.contribution.preLink")}{" "}
- {t('billing.contribution.link')}
+ {t("billing.contribution.link")}
@@ -125,7 +126,7 @@ export const ChangePlanForm = ({
{data && (
- {excludedPlans?.includes('STARTER') ? null : (
+ {excludedPlans?.includes("STARTER") ? null : (
handlePayClick(Plan.STARTER)}
@@ -134,7 +135,7 @@ export const ChangePlanForm = ({
/>
)}
- {excludedPlans?.includes('PRO') ? null : (
+ {excludedPlans?.includes("PRO") ? null : (
handlePayClick(Plan.PRO)}
@@ -147,11 +148,11 @@ export const ChangePlanForm = ({
)}
- {t('billing.customLimit.preLink')}{' '}
-
- {t('billing.customLimit.link')}
+ {t("billing.customLimit.preLink")}{" "}
+
+ {t("billing.customLimit.link")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/ChangePlanModal.tsx b/apps/builder/src/features/billing/components/ChangePlanModal.tsx
index 9e53d85937..da3928c8a0 100644
--- a/apps/builder/src/features/billing/components/ChangePlanModal.tsx
+++ b/apps/builder/src/features/billing/components/ChangePlanModal.tsx
@@ -1,24 +1,24 @@
-import { AlertInfo } from '@/components/AlertInfo'
-import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
-import { useTranslate } from '@tolgee/react'
+import { AlertInfo } from "@/components/AlertInfo";
+import { useWorkspace } from "@/features/workspace/WorkspaceProvider";
import {
+ Button,
+ HStack,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalOverlay,
Stack,
- Button,
- HStack,
-} from '@chakra-ui/react'
-import { ChangePlanForm } from './ChangePlanForm'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { ChangePlanForm } from "./ChangePlanForm";
export type ChangePlanModalProps = {
- type?: string
- isOpen: boolean
- excludedPlans?: ('STARTER' | 'PRO')[]
- onClose: () => void
-}
+ type?: string;
+ isOpen: boolean;
+ excludedPlans?: ("STARTER" | "PRO")[];
+ onClose: () => void;
+};
export const ChangePlanModal = ({
onClose,
@@ -26,21 +26,21 @@ export const ChangePlanModal = ({
type,
excludedPlans,
}: ChangePlanModalProps) => {
- const { t } = useTranslate()
- const { workspace, currentRole } = useWorkspace()
+ const { t } = useTranslate();
+ const { workspace, currentRole } = useWorkspace();
return (
{type && (
- {t('billing.upgradeLimitLabel', { type: type })}
+ {t("billing.upgradeLimitLabel", { type: type })}
)}
{workspace && (
@@ -55,11 +55,11 @@ export const ChangePlanModal = ({
- {t('cancel')}
+ {t("cancel")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx b/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx
index b6206fb3bb..8bfb1865e3 100644
--- a/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx
+++ b/apps/builder/src/features/billing/components/ChatsProTiersModal.tsx
@@ -1,13 +1,13 @@
import {
+ Heading,
Modal,
- ModalOverlay,
+ ModalBody,
+ ModalCloseButton,
ModalContent,
+ ModalFooter,
ModalHeader,
- ModalCloseButton,
- ModalBody,
+ ModalOverlay,
Stack,
- ModalFooter,
- Heading,
Table,
TableContainer,
Tbody,
@@ -15,25 +15,25 @@ import {
Th,
Thead,
Tr,
-} from '@chakra-ui/react'
-import { useTranslate } from '@tolgee/react'
-import { proChatTiers } from '@typebot.io/billing/constants'
-import { formatPrice } from '@typebot.io/billing/helpers/formatPrice'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { proChatTiers } from "@typebot.io/billing/constants";
+import { formatPrice } from "@typebot.io/billing/helpers/formatPrice";
type Props = {
- isOpen: boolean
- onClose: () => void
-}
+ isOpen: boolean;
+ onClose: () => void;
+};
export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
return (
- {t('billing.tiersModal.heading')}
+ {t("billing.tiersModal.heading")}
@@ -51,33 +51,33 @@ export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => {
const pricePerMonth =
(tier.flat_amount ??
proChatTiers.at(-2)?.flat_amount ??
- 0) / 100
+ 0) / 100;
return (
- {tier.up_to === 'inf'
- ? '2,000,000+'
+ {tier.up_to === "inf"
+ ? "2,000,000+"
: tier.up_to.toLocaleString()}
- {index === 0 ? 'included' : formatPrice(pricePerMonth)}
+ {index === 0 ? "included" : formatPrice(pricePerMonth)}
{index === proChatTiers.length - 1
? formatPrice(4.42, { maxFractionDigits: 2 })
: index === 0
- ? 'included'
- : formatPrice(
- (((pricePerMonth * 100) /
- ((tier.up_to as number) -
- (proChatTiers.at(0)?.up_to as number))) *
- 1000) /
- 100,
- { maxFractionDigits: 2 }
- )}
+ ? "included"
+ : formatPrice(
+ (((pricePerMonth * 100) /
+ ((tier.up_to as number) -
+ (proChatTiers.at(0)?.up_to as number))) *
+ 1000) /
+ 100,
+ { maxFractionDigits: 2 },
+ )}
- )
+ );
})}
@@ -86,5 +86,5 @@ export const ChatsProTiersModal = ({ isOpen, onClose }: Props) => {
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx b/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx
index 13dedd0de0..9a91a86b12 100644
--- a/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx
+++ b/apps/builder/src/features/billing/components/CurrentSubscriptionSummary.tsx
@@ -1,53 +1,53 @@
+import { trpc } from "@/lib/trpc";
import {
- Text,
- HStack,
- Stack,
- Heading,
Alert,
AlertIcon,
-} from '@chakra-ui/react'
-import { Plan } from '@typebot.io/prisma'
-import React from 'react'
-import { PlanTag } from './PlanTag'
-import { BillingPortalButton } from './BillingPortalButton'
-import { trpc } from '@/lib/trpc'
-import { Workspace } from '@typebot.io/schemas'
-import { useTranslate } from '@tolgee/react'
+ HStack,
+ Heading,
+ Stack,
+ Text,
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { Plan } from "@typebot.io/prisma/enum";
+import type { Workspace } from "@typebot.io/workspaces/schemas";
+import React from "react";
+import { BillingPortalButton } from "./BillingPortalButton";
+import { PlanTag } from "./PlanTag";
type Props = {
- workspace: Pick
-}
+ workspace: Pick;
+};
export const CurrentSubscriptionSummary = ({ workspace }: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const { data } = trpc.billing.getSubscription.useQuery({
workspaceId: workspace.id,
- })
+ });
const isSubscribed =
(workspace.plan === Plan.STARTER || workspace.plan === Plan.PRO) &&
- workspace.stripeId
+ workspace.stripeId;
return (
- {t('billing.currentSubscription.heading')}
+ {t("billing.currentSubscription.heading")}
- {t('billing.currentSubscription.subheading')}
+ {t("billing.currentSubscription.subheading")}
{data?.subscription?.cancelDate && (
- ({t('billing.currentSubscription.cancelDate')}{' '}
+ ({t("billing.currentSubscription.cancelDate")}{" "}
{data.subscription.cancelDate.toDateString()})
)}
- {data?.subscription?.status === 'past_due' && (
+ {data?.subscription?.status === "past_due" && (
- {t('billing.currentSubscription.pastDueAlert')}
+ {t("billing.currentSubscription.pastDueAlert")}
)}
@@ -55,10 +55,10 @@ export const CurrentSubscriptionSummary = ({ workspace }: Props) => {
)}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/FeaturesList.tsx b/apps/builder/src/features/billing/components/FeaturesList.tsx
index 11e2015d71..da4906a9e1 100644
--- a/apps/builder/src/features/billing/components/FeaturesList.tsx
+++ b/apps/builder/src/features/billing/components/FeaturesList.tsx
@@ -1,13 +1,13 @@
+import { CheckIcon } from "@/components/icons";
import {
- ListProps,
- UnorderedList,
Flex,
- ListItem,
ListIcon,
-} from '@chakra-ui/react'
-import { CheckIcon } from '@/components/icons'
+ ListItem,
+ type ListProps,
+ UnorderedList,
+} from "@chakra-ui/react";
-type FeaturesListProps = { features: (string | JSX.Element)[] } & ListProps
+type FeaturesListProps = { features: (string | JSX.Element)[] } & ListProps;
export const FeaturesList = ({ features, ...props }: FeaturesListProps) => (
@@ -18,4 +18,4 @@ export const FeaturesList = ({ features, ...props }: FeaturesListProps) => (
))}
-)
+);
diff --git a/apps/builder/src/features/billing/components/InvoicesList.tsx b/apps/builder/src/features/billing/components/InvoicesList.tsx
index 9c1e003c15..287e76742b 100644
--- a/apps/builder/src/features/billing/components/InvoicesList.tsx
+++ b/apps/builder/src/features/billing/components/InvoicesList.tsx
@@ -1,48 +1,48 @@
+import { DownloadIcon, FileIcon } from "@/components/icons";
+import { useToast } from "@/hooks/useToast";
+import { trpc } from "@/lib/trpc";
import {
- Stack,
- Heading,
Checkbox,
+ Heading,
+ IconButton,
Skeleton,
+ Stack,
Table,
TableContainer,
Tbody,
Td,
+ Text,
Th,
Thead,
Tr,
- IconButton,
- Text,
-} from '@chakra-ui/react'
-import { DownloadIcon, FileIcon } from '@/components/icons'
-import Link from 'next/link'
-import React from 'react'
-import { trpc } from '@/lib/trpc'
-import { useToast } from '@/hooks/useToast'
-import { useTranslate } from '@tolgee/react'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import Link from "next/link";
+import React from "react";
type Props = {
- workspaceId: string
-}
+ workspaceId: string;
+};
export const InvoicesList = ({ workspaceId }: Props) => {
- const { t } = useTranslate()
- const { showToast } = useToast()
+ const { t } = useTranslate();
+ const { showToast } = useToast();
const { data, status } = trpc.billing.listInvoices.useQuery(
{
workspaceId,
},
{
onError: (error) => {
- showToast({ description: error.message })
+ showToast({ description: error.message });
},
- }
- )
+ },
+ );
return (
- {t('billing.invoices.heading')}
- {data?.invoices.length === 0 && status !== 'loading' ? (
- {t('billing.invoices.empty')}
+ {t("billing.invoices.heading")}
+ {data?.invoices.length === 0 && status !== "loading" ? (
+ {t("billing.invoices.empty")}
) : (
@@ -50,8 +50,8 @@ export const InvoicesList = ({ workspaceId }: Props) => {
#
- {t('billing.invoices.paidAt')}
- {t('billing.invoices.subtotal')}
+ {t("billing.invoices.paidAt")}
+ {t("billing.invoices.subtotal")}
@@ -65,7 +65,7 @@ export const InvoicesList = ({ workspaceId }: Props) => {
{invoice.date
? new Date(invoice.date * 1000).toDateString()
- : ''}
+ : ""}
{getFormattedPrice(invoice.amount, invoice.currency)}
@@ -77,13 +77,13 @@ export const InvoicesList = ({ workspaceId }: Props) => {
variant="outline"
href={invoice.url}
target="_blank"
- aria-label={'Download invoice'}
+ aria-label={"Download invoice"}
/>
)}
))}
- {status === 'loading' &&
+ {status === "loading" &&
Array.from({ length: 3 }).map((_, idx) => (
@@ -102,14 +102,14 @@ export const InvoicesList = ({ workspaceId }: Props) => {
)}
- )
-}
+ );
+};
const getFormattedPrice = (amount: number, currency: string) => {
- const formatter = new Intl.NumberFormat('en-US', {
- style: 'currency',
+ const formatter = new Intl.NumberFormat("en-US", {
+ style: "currency",
currency,
- })
+ });
- return formatter.format(amount / 100)
-}
+ return formatter.format(amount / 100);
+};
diff --git a/apps/builder/src/features/billing/components/LockTag.tsx b/apps/builder/src/features/billing/components/LockTag.tsx
index 8b6a1711da..39c858bb9f 100644
--- a/apps/builder/src/features/billing/components/LockTag.tsx
+++ b/apps/builder/src/features/billing/components/LockTag.tsx
@@ -1,14 +1,14 @@
-import { Tag, TagProps } from '@chakra-ui/react'
-import { LockedIcon } from '@/components/icons'
-import { Plan } from '@typebot.io/prisma'
-import { planColorSchemes } from './PlanTag'
+import { LockedIcon } from "@/components/icons";
+import { Tag, type TagProps } from "@chakra-ui/react";
+import type { Plan } from "@typebot.io/prisma/enum";
+import { planColorSchemes } from "./PlanTag";
export const LockTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => (
-)
+);
diff --git a/apps/builder/src/features/billing/components/PlanTag.tsx b/apps/builder/src/features/billing/components/PlanTag.tsx
index c4e4f171da..fbcd4fab37 100644
--- a/apps/builder/src/features/billing/components/PlanTag.tsx
+++ b/apps/builder/src/features/billing/components/PlanTag.tsx
@@ -1,15 +1,15 @@
-import { Tag, TagProps, ThemeTypings } from '@chakra-ui/react'
-import { Plan } from '@typebot.io/prisma'
+import { Tag, type TagProps, type ThemeTypings } from "@chakra-ui/react";
+import { Plan } from "@typebot.io/prisma/enum";
-export const planColorSchemes: Record = {
- [Plan.LIFETIME]: 'purple',
- [Plan.PRO]: 'blue',
- [Plan.OFFERED]: 'orange',
- [Plan.STARTER]: 'orange',
- [Plan.FREE]: 'gray',
- [Plan.CUSTOM]: 'yellow',
- [Plan.UNLIMITED]: 'yellow',
-}
+export const planColorSchemes: Record = {
+ [Plan.LIFETIME]: "purple",
+ [Plan.PRO]: "blue",
+ [Plan.OFFERED]: "orange",
+ [Plan.STARTER]: "orange",
+ [Plan.FREE]: "gray",
+ [Plan.CUSTOM]: "yellow",
+ [Plan.UNLIMITED]: "yellow",
+};
export const PlanTag = ({
plan,
@@ -25,7 +25,7 @@ export const PlanTag = ({
>
Lifetime
- )
+ );
}
case Plan.PRO: {
return (
@@ -36,7 +36,7 @@ export const PlanTag = ({
>
Pro
- )
+ );
}
case Plan.OFFERED:
case Plan.STARTER: {
@@ -48,7 +48,7 @@ export const PlanTag = ({
>
Starter
- )
+ );
}
case Plan.FREE: {
return (
@@ -59,7 +59,7 @@ export const PlanTag = ({
>
Free
- )
+ );
}
case Plan.CUSTOM: {
return (
@@ -70,7 +70,7 @@ export const PlanTag = ({
>
Custom
- )
+ );
}
case Plan.UNLIMITED: {
return (
@@ -81,7 +81,7 @@ export const PlanTag = ({
>
Unlimited
- )
+ );
}
}
-}
+};
diff --git a/apps/builder/src/features/billing/components/PreCheckoutModal.tsx b/apps/builder/src/features/billing/components/PreCheckoutModal.tsx
index 5b4c550c0b..7b974ad728 100644
--- a/apps/builder/src/features/billing/components/PreCheckoutModal.tsx
+++ b/apps/builder/src/features/billing/components/PreCheckoutModal.tsx
@@ -1,8 +1,8 @@
-import { TextInput } from '@/components/inputs'
-import { Select } from '@/components/inputs/Select'
-import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
-import { useToast } from '@/hooks/useToast'
-import { trpc } from '@/lib/trpc'
+import { TextInput } from "@/components/inputs";
+import { Select } from "@/components/inputs/Select";
+import { useParentModal } from "@/features/graph/providers/ParentModalProvider";
+import { useToast } from "@/hooks/useToast";
+import { trpc } from "@/lib/trpc";
import {
Button,
FormControl,
@@ -13,25 +13,26 @@ import {
ModalContent,
ModalOverlay,
Stack,
-} from '@chakra-ui/react'
-import { useRouter } from 'next/router'
-import React, { FormEvent, useState } from 'react'
-import { isDefined } from '@typebot.io/lib'
-import { useTranslate } from '@tolgee/react'
-import { taxIdTypes } from '@typebot.io/billing/taxIdTypes'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { taxIdTypes } from "@typebot.io/billing/taxIdTypes";
+import { isDefined } from "@typebot.io/lib/utils";
+import { useRouter } from "next/router";
+import type { FormEvent } from "react";
+import React, { useState } from "react";
export type PreCheckoutModalProps = {
selectedSubscription:
| {
- plan: 'STARTER' | 'PRO'
- workspaceId: string
- currency: 'eur' | 'usd'
+ plan: "STARTER" | "PRO";
+ workspaceId: string;
+ currency: "eur" | "usd";
}
- | undefined
- existingCompany?: string
- existingEmail?: string
- onClose: () => void
-}
+ | undefined;
+ existingCompany?: string;
+ existingEmail?: string;
+ onClose: () => void;
+};
const vatCodeLabels = taxIdTypes.map((taxIdType) => ({
label: `${taxIdType.emoji} ${taxIdType.name} (${taxIdType.code})`,
@@ -39,7 +40,7 @@ const vatCodeLabels = taxIdTypes.map((taxIdType) => ({
extras: {
placeholder: taxIdType.placeholder,
},
-}))
+}));
export const PreCheckoutModal = ({
selectedSubscription,
@@ -47,44 +48,44 @@ export const PreCheckoutModal = ({
existingEmail,
onClose,
}: PreCheckoutModalProps) => {
- const { t } = useTranslate()
- const { ref } = useParentModal()
- const vatValueInputRef = React.useRef(null)
- const router = useRouter()
- const { showToast } = useToast()
+ const { t } = useTranslate();
+ const { ref } = useParentModal();
+ const vatValueInputRef = React.useRef(null);
+ const router = useRouter();
+ const { showToast } = useToast();
const { mutate: createCheckoutSession, isLoading: isCreatingCheckout } =
trpc.billing.createCheckoutSession.useMutation({
onError: (error) => {
showToast({
description: error.message,
- })
+ });
},
onSuccess: ({ checkoutUrl }) => {
- router.push(checkoutUrl)
+ router.push(checkoutUrl);
},
- })
+ });
const [customer, setCustomer] = useState({
- company: existingCompany ?? '',
- email: existingEmail ?? '',
+ company: existingCompany ?? "",
+ email: existingEmail ?? "",
vat: {
type: undefined as string | undefined,
- value: '',
+ value: "",
},
- })
- const [vatValuePlaceholder, setVatValuePlaceholder] = useState('')
+ });
+ const [vatValuePlaceholder, setVatValuePlaceholder] = useState("");
const updateCustomerCompany = (company: string) => {
- setCustomer((customer) => ({ ...customer, company }))
- }
+ setCustomer((customer) => ({ ...customer, company }));
+ };
const updateCustomerEmail = (email: string) => {
- setCustomer((customer) => ({ ...customer, email }))
- }
+ setCustomer((customer) => ({ ...customer, email }));
+ };
const updateVatType = (
type: string | undefined,
- vatCode?: (typeof vatCodeLabels)[number]
+ vatCode?: (typeof vatCodeLabels)[number],
) => {
setCustomer((customer) => ({
...customer,
@@ -92,10 +93,10 @@ export const PreCheckoutModal = ({
...customer.vat,
type,
},
- }))
- setVatValuePlaceholder(vatCode?.extras?.placeholder ?? '')
- vatValueInputRef.current?.focus()
- }
+ }));
+ setVatValuePlaceholder(vatCode?.extras?.placeholder ?? "");
+ vatValueInputRef.current?.focus();
+ };
const updateVatValue = (value: string) => {
setCustomer((customer) => ({
@@ -104,13 +105,13 @@ export const PreCheckoutModal = ({
...customer.vat,
value,
},
- }))
- }
+ }));
+ };
const goToCheckout = (e: FormEvent) => {
- e.preventDefault()
- if (!selectedSubscription) return
- const { email, company, vat } = customer
+ e.preventDefault();
+ if (!selectedSubscription) return;
+ const { email, company, vat } = customer;
createCheckoutSession({
...selectedSubscription,
email,
@@ -120,8 +121,8 @@ export const PreCheckoutModal = ({
vat.value && vat.type
? { type: vat.type, value: vat.value }
: undefined,
- })
- }
+ });
+ };
return (
@@ -131,7 +132,7 @@ export const PreCheckoutModal = ({
- {t('billing.preCheckoutModal.taxId.label')}
+ {t("billing.preCheckoutModal.taxId.label")}
@@ -169,13 +170,13 @@ export const PreCheckoutModal = ({
type="submit"
isLoading={isCreatingCheckout}
colorScheme="blue"
- isDisabled={customer.company === '' || customer.email === ''}
+ isDisabled={customer.company === "" || customer.email === ""}
>
- {t('billing.preCheckoutModal.submitButton.label')}
+ {t("billing.preCheckoutModal.submitButton.label")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx b/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx
index aa336584b2..d50cedd2ff 100644
--- a/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx
+++ b/apps/builder/src/features/billing/components/ProPlanPricingCard.tsx
@@ -1,30 +1,30 @@
+import { MoreInfoTooltip } from "@/components/MoreInfoTooltip";
import {
- Stack,
- Heading,
- chakra,
- HStack,
Button,
- Text,
- Tooltip,
Flex,
+ HStack,
+ Heading,
+ Stack,
Tag,
+ Text,
+ Tooltip,
+ chakra,
useColorModeValue,
useDisclosure,
-} from '@chakra-ui/react'
-import { Plan } from '@typebot.io/prisma'
-import { FeaturesList } from './FeaturesList'
-import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
-import { formatPrice } from '@typebot.io/billing/helpers/formatPrice'
-import { ChatsProTiersModal } from './ChatsProTiersModal'
-import { prices } from '@typebot.io/billing/constants'
-import { T, useTranslate } from '@tolgee/react'
+} from "@chakra-ui/react";
+import { T, useTranslate } from "@tolgee/react";
+import { prices } from "@typebot.io/billing/constants";
+import { formatPrice } from "@typebot.io/billing/helpers/formatPrice";
+import { Plan } from "@typebot.io/prisma/enum";
+import { ChatsProTiersModal } from "./ChatsProTiersModal";
+import { FeaturesList } from "./FeaturesList";
type Props = {
- currentPlan: Plan
- currency?: 'usd' | 'eur'
- isLoading: boolean
- onPayClick: () => void
-}
+ currentPlan: Plan;
+ currency?: "usd" | "eur";
+ isLoading: boolean;
+ onPayClick: () => void;
+};
export const ProPlanPricingCard = ({
currentPlan,
@@ -32,18 +32,18 @@ export const ProPlanPricingCard = ({
isLoading,
onPayClick,
}: Props) => {
- const { isOpen, onOpen, onClose } = useDisclosure()
- const { t } = useTranslate()
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const { t } = useTranslate();
const getButtonLabel = () => {
if (currentPlan === Plan.PRO)
- return t('billing.pricingCard.upgradeButton.current')
- return t('upgrade')
- }
+ return t("billing.pricingCard.upgradeButton.current");
+ return t("upgrade");
+ };
return (
<>
- {' '}
+ {" "}
@@ -60,12 +60,12 @@ export const ProPlanPricingCard = ({
pos="absolute"
top="-10px"
colorScheme="blue"
- bg={useColorModeValue('blue.500', 'blue.400')}
+ bg={useColorModeValue("blue.500", "blue.400")}
variant="solid"
fontWeight="semibold"
style={{ marginTop: 0 }}
>
- {t('billing.pricingCard.pro.mostPopularLabel')}
+ {t("billing.pricingCard.pro.mostPopularLabel")}
@@ -76,7 +76,7 @@ export const ProPlanPricingCard = ({
params={{
strong: (
Pro
@@ -84,14 +84,14 @@ export const ProPlanPricingCard = ({
}}
/>
- {t('billing.pricingCard.pro.description')}
+ {t("billing.pricingCard.pro.description")}
{formatPrice(prices.PRO, { currency })}
- {t('billing.pricingCard.perMonth')}
+ {t("billing.pricingCard.perMonth")}
@@ -99,9 +99,9 @@ export const ProPlanPricingCard = ({
label={
@@ -110,36 +110,36 @@ export const ProPlanPricingCard = ({
placement="top"
>
- {t('billing.pricingCard.pro.everythingFromStarter')}
+ {t("billing.pricingCard.pro.everythingFromStarter")}
- {t('billing.pricingCard.plus')}
+ {t("billing.pricingCard.plus")}
- 10,000 {t('billing.pricingCard.chatsPerMonth')}
+ 10,000 {t("billing.pricingCard.chatsPerMonth")}
- {t('billing.pricingCard.chatsTooltip')}
+ {t("billing.pricingCard.chatsTooltip")}
- Extra chats:{' '}
+ Extra chats:{" "}
See tiers
,
- t('billing.pricingCard.pro.whatsAppIntegration'),
- t('billing.pricingCard.pro.customDomains'),
- t('billing.pricingCard.pro.analytics'),
+ t("billing.pricingCard.pro.whatsAppIntegration"),
+ t("billing.pricingCard.pro.customDomains"),
+ t("billing.pricingCard.pro.analytics"),
]}
/>
@@ -157,5 +157,5 @@ export const ProPlanPricingCard = ({
>
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx b/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx
index 2ebdabcd95..b6d3ae32aa 100644
--- a/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx
+++ b/apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx
@@ -1,25 +1,25 @@
+import { MoreInfoTooltip } from "@/components/MoreInfoTooltip";
import {
- Stack,
- Heading,
- chakra,
- HStack,
Button,
+ HStack,
+ Heading,
+ Stack,
Text,
+ chakra,
useColorModeValue,
-} from '@chakra-ui/react'
-import { Plan } from '@typebot.io/prisma'
-import { FeaturesList } from './FeaturesList'
-import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
-import { formatPrice } from '@typebot.io/billing/helpers/formatPrice'
-import { prices } from '@typebot.io/billing/constants'
-import { T, useTranslate } from '@tolgee/react'
+} from "@chakra-ui/react";
+import { T, useTranslate } from "@tolgee/react";
+import { prices } from "@typebot.io/billing/constants";
+import { formatPrice } from "@typebot.io/billing/helpers/formatPrice";
+import { Plan } from "@typebot.io/prisma/enum";
+import { FeaturesList } from "./FeaturesList";
type Props = {
- currentPlan: Plan
- currency?: 'eur' | 'usd'
- isLoading?: boolean
- onPayClick: () => void
-}
+ currentPlan: Plan;
+ currency?: "eur" | "usd";
+ isLoading?: boolean;
+ onPayClick: () => void;
+};
export const StarterPlanPricingCard = ({
currentPlan,
@@ -27,14 +27,14 @@ export const StarterPlanPricingCard = ({
currency,
onPayClick,
}: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const getButtonLabel = () => {
- if (currentPlan === Plan.PRO) return t('downgrade')
+ if (currentPlan === Plan.PRO) return t("downgrade");
if (currentPlan === Plan.STARTER)
- return t('billing.pricingCard.upgradeButton.current')
- return t('upgrade')
- }
+ return t("billing.pricingCard.upgradeButton.current");
+ return t("upgrade");
+ };
return (
- {t('billing.pricingCard.starter.description')}
+ {t("billing.pricingCard.starter.description")}
{formatPrice(prices.STARTER, { currency })}
- {t('billing.pricingCard.perMonth')}
+ {t("billing.pricingCard.perMonth")}
- 2,000 {t('billing.pricingCard.chatsPerMonth')}
+ 2,000 {t("billing.pricingCard.chatsPerMonth")}
- {t('billing.pricingCard.chatsTooltip')}
+ {t("billing.pricingCard.chatsTooltip")}
Extra chats: $10 per 500
,
- t('billing.pricingCard.starter.brandingRemoved'),
- t('billing.pricingCard.starter.fileUploadBlock'),
- t('billing.pricingCard.starter.createFolders'),
- 'Direct priority support',
+ t("billing.pricingCard.starter.brandingRemoved"),
+ t("billing.pricingCard.starter.fileUploadBlock"),
+ t("billing.pricingCard.starter.createFolders"),
+ "Direct priority support",
]}
/>
@@ -102,5 +102,5 @@ export const StarterPlanPricingCard = ({
{getButtonLabel()}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/StripeClimateLogo.tsx b/apps/builder/src/features/billing/components/StripeClimateLogo.tsx
index b51e79ddb8..932435f152 100644
--- a/apps/builder/src/features/billing/components/StripeClimateLogo.tsx
+++ b/apps/builder/src/features/billing/components/StripeClimateLogo.tsx
@@ -1,4 +1,4 @@
-import { Icon, IconProps } from '@chakra-ui/react'
+import { Icon, type IconProps } from "@chakra-ui/react";
export const StripeClimateLogo = (props: IconProps) => (
(
fill="url(#StripeClimate-gradient-c)"
/>
-)
+);
diff --git a/apps/builder/src/features/billing/components/UpgradeButton.tsx b/apps/builder/src/features/billing/components/UpgradeButton.tsx
index 9b1d345be2..3311ee1ea8 100644
--- a/apps/builder/src/features/billing/components/UpgradeButton.tsx
+++ b/apps/builder/src/features/billing/components/UpgradeButton.tsx
@@ -1,23 +1,23 @@
-import { Button, ButtonProps, useDisclosure } from '@chakra-ui/react'
-import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
-import React from 'react'
-import { isNotDefined } from '@typebot.io/lib'
-import { ChangePlanModal } from './ChangePlanModal'
-import { useTranslate } from '@tolgee/react'
+import { useWorkspace } from "@/features/workspace/WorkspaceProvider";
+import { Button, type ButtonProps, useDisclosure } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { isNotDefined } from "@typebot.io/lib/utils";
+import React from "react";
+import { ChangePlanModal } from "./ChangePlanModal";
type Props = {
- limitReachedType?: string
- excludedPlans?: ('STARTER' | 'PRO')[]
-} & ButtonProps
+ limitReachedType?: string;
+ excludedPlans?: ("STARTER" | "PRO")[];
+} & ButtonProps;
export const UpgradeButton = ({
limitReachedType,
excludedPlans,
...props
}: Props) => {
- const { t } = useTranslate()
- const { isOpen, onOpen, onClose } = useDisclosure()
- const { workspace } = useWorkspace()
+ const { t } = useTranslate();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const { workspace } = useWorkspace();
return (
- {props.children ?? t('upgrade')}
+ {props.children ?? t("upgrade")}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/components/UsageProgressBars.tsx b/apps/builder/src/features/billing/components/UsageProgressBars.tsx
index b3f454794a..49c6afd11b 100644
--- a/apps/builder/src/features/billing/components/UsageProgressBars.tsx
+++ b/apps/builder/src/features/billing/components/UsageProgressBars.tsx
@@ -1,49 +1,49 @@
+import { AlertIcon } from "@/components/icons";
+import type { WorkspaceInApp } from "@/features/workspace/WorkspaceProvider";
+import { defaultQueryOptions, trpc } from "@/lib/trpc";
import {
- Stack,
Flex,
+ HStack,
Heading,
Progress,
- Text,
Skeleton,
- HStack,
+ Stack,
+ Text,
Tooltip,
-} from '@chakra-ui/react'
-import { AlertIcon } from '@/components/icons'
-import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider'
-import { parseNumberWithCommas } from '@typebot.io/lib'
-import { defaultQueryOptions, trpc } from '@/lib/trpc'
-import { getChatsLimit } from '@typebot.io/billing/helpers/getChatsLimit'
-import { useTranslate } from '@tolgee/react'
+} from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { getChatsLimit } from "@typebot.io/billing/helpers/getChatsLimit";
+import { parseNumberWithCommas } from "@typebot.io/lib/utils";
type Props = {
- workspace: WorkspaceInApp
-}
+ workspace: WorkspaceInApp;
+};
export const UsageProgressBars = ({ workspace }: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const { data, isLoading } = trpc.billing.getUsage.useQuery(
{
workspaceId: workspace.id,
},
- defaultQueryOptions
- )
- const totalChatsUsed = data?.totalChatsUsed ?? 0
+ defaultQueryOptions,
+ );
+ const totalChatsUsed = data?.totalChatsUsed ?? 0;
- const workspaceChatsLimit = getChatsLimit(workspace)
+ const workspaceChatsLimit = getChatsLimit(workspace);
const chatsPercentage =
- workspaceChatsLimit === 'inf'
+ workspaceChatsLimit === "inf"
? 0
- : Math.round((totalChatsUsed / workspaceChatsLimit) * 100)
+ : Math.round((totalChatsUsed / workspaceChatsLimit) * 100);
return (
- {t('billing.usage.heading')}
+ {t("billing.usage.heading")}
- {t('billing.usage.chats.heading')}
+ {t("billing.usage.chats.heading")}
{chatsPercentage >= 80 && (
{
p="3"
label={
- {t('billing.usage.chats.alert.soonReach')}
+ {t("billing.usage.chats.alert.soonReach")}
- {t('billing.usage.chats.alert.updatePlan')}
+ {t("billing.usage.chats.alert.updatePlan")}
}
>
@@ -73,14 +73,14 @@ export const UsageProgressBars = ({ workspace }: Props) => {
{parseNumberWithCommas(totalChatsUsed)}
- /{' '}
- {workspaceChatsLimit === 'inf'
- ? t('billing.usage.unlimited')
+ /{" "}
+ {workspaceChatsLimit === "inf"
+ ? t("billing.usage.unlimited")
: parseNumberWithCommas(workspaceChatsLimit)}
@@ -91,9 +91,9 @@ export const UsageProgressBars = ({ workspace }: Props) => {
value={chatsPercentage}
rounded="full"
isIndeterminate={isLoading}
- colorScheme={'blue'}
+ colorScheme={"blue"}
/>
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/billing/helpers/hasProPerks.ts b/apps/builder/src/features/billing/helpers/hasProPerks.ts
index a0ed05b3d3..92a049f803 100644
--- a/apps/builder/src/features/billing/helpers/hasProPerks.ts
+++ b/apps/builder/src/features/billing/helpers/hasProPerks.ts
@@ -1,9 +1,10 @@
-import { isDefined } from '@typebot.io/lib'
-import { Workspace, Plan } from '@typebot.io/prisma'
+import { isDefined } from "@typebot.io/lib/utils";
+import { Plan } from "@typebot.io/prisma/enum";
+import type { Workspace } from "@typebot.io/workspaces/schemas";
-export const hasProPerks = (workspace?: Pick) =>
+export const hasProPerks = (workspace?: Pick) =>
isDefined(workspace) &&
(workspace.plan === Plan.PRO ||
workspace.plan === Plan.LIFETIME ||
workspace.plan === Plan.CUSTOM ||
- workspace.plan === Plan.UNLIMITED)
+ workspace.plan === Plan.UNLIMITED);
diff --git a/apps/builder/src/features/billing/helpers/isFreePlan.ts b/apps/builder/src/features/billing/helpers/isFreePlan.ts
index ba632f5584..ffa119708b 100644
--- a/apps/builder/src/features/billing/helpers/isFreePlan.ts
+++ b/apps/builder/src/features/billing/helpers/isFreePlan.ts
@@ -1,5 +1,6 @@
-import { isNotDefined } from '@typebot.io/lib'
-import { Workspace, Plan } from '@typebot.io/prisma'
+import { isNotDefined } from "@typebot.io/lib/utils";
+import { Plan } from "@typebot.io/prisma/enum";
+import type { Workspace } from "@typebot.io/workspaces/schemas";
-export const isFreePlan = (workspace?: Pick) =>
- isNotDefined(workspace) || workspace?.plan === Plan.FREE
+export const isFreePlan = (workspace?: Pick) =>
+ isNotDefined(workspace) || workspace?.plan === Plan.FREE;
diff --git a/apps/builder/src/features/billing/helpers/planToReadable.ts b/apps/builder/src/features/billing/helpers/planToReadable.ts
index b48bc18e17..d68cfb87d3 100644
--- a/apps/builder/src/features/billing/helpers/planToReadable.ts
+++ b/apps/builder/src/features/billing/helpers/planToReadable.ts
@@ -1,17 +1,17 @@
-import { Plan } from '@typebot.io/prisma'
+import { Plan } from "@typebot.io/prisma/enum";
export const planToReadable = (plan?: Plan) => {
- if (!plan) return
+ if (!plan) return;
switch (plan) {
case Plan.FREE:
- return 'Free'
+ return "Free";
case Plan.LIFETIME:
- return 'Lifetime'
+ return "Lifetime";
case Plan.OFFERED:
- return 'Offered'
+ return "Offered";
case Plan.PRO:
- return 'Pro'
+ return "Pro";
case Plan.UNLIMITED:
- return 'Unlimited'
+ return "Unlimited";
}
-}
+};
diff --git a/apps/builder/src/features/billing/helpers/storageToReadable.ts b/apps/builder/src/features/billing/helpers/storageToReadable.ts
index 6644ea5e5b..e7e0388e92 100644
--- a/apps/builder/src/features/billing/helpers/storageToReadable.ts
+++ b/apps/builder/src/features/billing/helpers/storageToReadable.ts
@@ -1,7 +1,9 @@
export const storageToReadable = (bytes: number) => {
- if (bytes == 0) {
- return '0'
+ if (bytes === 0) {
+ return "0";
}
- const e = Math.floor(Math.log(bytes) / Math.log(1024))
- return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B'
-}
+ const e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (
+ (bytes / Math.pow(1024, e)).toFixed(2) + " " + " KMGTP".charAt(e) + "B"
+ );
+};
diff --git a/apps/builder/src/features/blocks/bubbles/audio/audio.spec.ts b/apps/builder/src/features/blocks/bubbles/audio/audio.spec.ts
index 1eb59c256d..0831303364 100644
--- a/apps/builder/src/features/blocks/bubbles/audio/audio.spec.ts
+++ b/apps/builder/src/features/blocks/bubbles/audio/audio.spec.ts
@@ -1,16 +1,16 @@
-import test, { expect } from '@playwright/test'
-import { createTypebots } from '@typebot.io/playwright/databaseActions'
-import { parseDefaultGroupWithBlock } from '@typebot.io/playwright/databaseHelpers'
-import { createId } from '@paralleldrive/cuid2'
-import { getTestAsset } from '@/test/utils/playwright'
-import { proWorkspaceId } from '@typebot.io/playwright/databaseSetup'
-import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
+import { getTestAsset } from "@/test/utils/playwright";
+import { createId } from "@paralleldrive/cuid2";
+import test, { expect } from "@playwright/test";
+import { BubbleBlockType } from "@typebot.io/blocks-bubbles/constants";
+import { createTypebots } from "@typebot.io/playwright/databaseActions";
+import { parseDefaultGroupWithBlock } from "@typebot.io/playwright/databaseHelpers";
+import { proWorkspaceId } from "@typebot.io/playwright/databaseSetup";
const audioSampleUrl =
- 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
+ "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
-test('should work as expected', async ({ page }) => {
- const typebotId = createId()
+test("should work as expected", async ({ page }) => {
+ const typebotId = createId();
await createTypebots([
{
id: typebotId,
@@ -18,29 +18,29 @@ test('should work as expected', async ({ page }) => {
type: BubbleBlockType.AUDIO,
}),
},
- ])
+ ]);
- await page.goto(`/typebots/${typebotId}/edit`)
- await page.getByText('Click to edit...').click()
+ await page.goto(`/typebots/${typebotId}/edit`);
+ await page.getByText("Click to edit...").click();
await page
- .getByPlaceholder('Paste the audio file link...')
- .fill(audioSampleUrl)
- await expect(page.locator('audio')).toHaveAttribute('src', audioSampleUrl)
- await page.getByRole('button', { name: 'Upload' }).click()
- await page.setInputFiles('input[type="file"]', getTestAsset('sample.mp3'))
- await expect(page.locator('audio')).toHaveAttribute(
- 'src',
+ .getByPlaceholder("Paste the audio file link...")
+ .fill(audioSampleUrl);
+ await expect(page.locator("audio")).toHaveAttribute("src", audioSampleUrl);
+ await page.getByRole("button", { name: "Upload" }).click();
+ await page.setInputFiles('input[type="file"]', getTestAsset("sample.mp3"));
+ await expect(page.locator("audio")).toHaveAttribute(
+ "src",
RegExp(
`/public/workspaces/${proWorkspaceId}/typebots/${typebotId}/blocks`,
- 'gm'
- )
- )
- await page.getByRole('button', { name: 'Test', exact: true }).click()
- await expect(page.locator('audio')).toHaveAttribute(
- 'src',
+ "gm",
+ ),
+ );
+ await page.getByRole("button", { name: "Test", exact: true }).click();
+ await expect(page.locator("audio")).toHaveAttribute(
+ "src",
RegExp(
`/public/workspaces/${proWorkspaceId}/typebots/${typebotId}/blocks`,
- 'gm'
- )
- )
-})
+ "gm",
+ ),
+ );
+});
diff --git a/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleForm.tsx b/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleForm.tsx
index 83500d4dba..d6ca32b513 100644
--- a/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleForm.tsx
+++ b/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleForm.tsx
@@ -1,53 +1,53 @@
-import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
-import { TextInput } from '@/components/inputs'
-import { useState } from 'react'
-import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
-import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
-import { useTranslate } from '@tolgee/react'
-import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
-import { AudioBubbleBlock } from '@typebot.io/schemas'
-import { defaultAudioBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/audio/constants'
+import { UploadButton } from "@/components/ImageUploadContent/UploadButton";
+import { TextInput } from "@/components/inputs";
+import { SwitchWithLabel } from "@/components/inputs/SwitchWithLabel";
+import type { FilePathUploadProps } from "@/features/upload/api/generateUploadUrl";
+import { Button, Flex, HStack, Stack, Text } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { defaultAudioBubbleContent } from "@typebot.io/blocks-bubbles/audio/constants";
+import type { AudioBubbleBlock } from "@typebot.io/blocks-bubbles/audio/schema";
+import { useState } from "react";
type Props = {
- uploadFileProps: FilePathUploadProps
- content: AudioBubbleBlock['content']
- onContentChange: (content: AudioBubbleBlock['content']) => void
-}
+ uploadFileProps: FilePathUploadProps;
+ content: AudioBubbleBlock["content"];
+ onContentChange: (content: AudioBubbleBlock["content"]) => void;
+};
export const AudioBubbleForm = ({
uploadFileProps,
content,
onContentChange,
}: Props) => {
- const { t } = useTranslate()
- const [currentTab, setCurrentTab] = useState<'link' | 'upload'>('link')
+ const { t } = useTranslate();
+ const [currentTab, setCurrentTab] = useState<"link" | "upload">("link");
- const updateUrl = (url: string) => onContentChange({ ...content, url })
+ const updateUrl = (url: string) => onContentChange({ ...content, url });
const updateAutoPlay = (isAutoplayEnabled: boolean) =>
- onContentChange({ ...content, isAutoplayEnabled })
+ onContentChange({ ...content, isAutoplayEnabled });
return (
setCurrentTab('upload')}
+ variant={currentTab === "upload" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("upload")}
size="sm"
>
- {t('editor.blocks.bubbles.audio.settings.upload.label')}
+ {t("editor.blocks.bubbles.audio.settings.upload.label")}
setCurrentTab('link')}
+ variant={currentTab === "link" ? "solid" : "ghost"}
+ onClick={() => setCurrentTab("link")}
size="sm"
>
- {t('editor.blocks.bubbles.audio.settings.embedLink.label')}
+ {t("editor.blocks.bubbles.audio.settings.embedLink.label")}
- {currentTab === 'upload' && (
+ {currentTab === "upload" && (
- {t('editor.blocks.bubbles.audio.settings.chooseFile.label')}
+ {t("editor.blocks.bubbles.audio.settings.chooseFile.label")}
)}
- {currentTab === 'link' && (
+ {currentTab === "link" && (
<>
- {t('editor.blocks.bubbles.audio.settings.worksWith.text')}
+ {t("editor.blocks.bubbles.audio.settings.worksWith.text")}
>
)}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleIcon.tsx b/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleIcon.tsx
index bfcf98f80f..7e9d290b36 100644
--- a/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleIcon.tsx
+++ b/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleIcon.tsx
@@ -1,10 +1,10 @@
-import { featherIconsBaseProps } from '@/components/icons'
-import { Icon, IconProps } from '@chakra-ui/react'
-import React from 'react'
+import { featherIconsBaseProps } from "@/components/icons";
+import { Icon, type IconProps } from "@chakra-ui/react";
+import React from "react";
export const AudioBubbleIcon = (props: IconProps) => (
-)
+);
diff --git a/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleNode.tsx b/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleNode.tsx
index a27546a1f1..0981e8c8a8 100644
--- a/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleNode.tsx
+++ b/apps/builder/src/features/blocks/bubbles/audio/components/AudioBubbleNode.tsx
@@ -1,19 +1,19 @@
-import { chakra, Text } from '@chakra-ui/react'
-import { isDefined } from '@typebot.io/lib'
-import { useTranslate } from '@tolgee/react'
-import { AudioBubbleBlock } from '@typebot.io/schemas'
-import { findUniqueVariable } from '@typebot.io/variables/findUniqueVariableValue'
-import { useTypebot } from '@/features/editor/providers/TypebotProvider'
-import { VariableTag } from '@/features/graph/components/nodes/block/VariableTag'
+import { useTypebot } from "@/features/editor/providers/TypebotProvider";
+import { VariableTag } from "@/features/graph/components/nodes/block/VariableTag";
+import { Text, chakra } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import type { AudioBubbleBlock } from "@typebot.io/blocks-bubbles/audio/schema";
+import { isDefined } from "@typebot.io/lib/utils";
+import { findUniqueVariable } from "@typebot.io/variables/findUniqueVariableValue";
type Props = {
- url: NonNullable['url']
-}
+ url: NonNullable["url"];
+};
export const AudioBubbleNode = ({ url }: Props) => {
- const { typebot } = useTypebot()
- const { t } = useTranslate()
- const variable = typebot ? findUniqueVariable(typebot?.variables)(url) : null
+ const { typebot } = useTypebot();
+ const { t } = useTranslate();
+ const variable = typebot ? findUniqueVariable(typebot?.variables)(url) : null;
return isDefined(url) ? (
variable ? (
@@ -23,6 +23,6 @@ export const AudioBubbleNode = ({ url }: Props) => {
)
) : (
- {t('clickToEdit')}
- )
-}
+ {t("clickToEdit")}
+ );
+};
diff --git a/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleContent.tsx b/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleContent.tsx
index 6c1b288261..e3614b3b5d 100644
--- a/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleContent.tsx
+++ b/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleContent.tsx
@@ -1,21 +1,21 @@
-import { useTranslate } from '@tolgee/react'
-import { Stack, Text } from '@chakra-ui/react'
-import { EmbedBubbleBlock } from '@typebot.io/schemas'
-import { SetVariableLabel } from '@/components/SetVariableLabel'
-import { useTypebot } from '@/features/editor/providers/TypebotProvider'
+import { SetVariableLabel } from "@/components/SetVariableLabel";
+import { useTypebot } from "@/features/editor/providers/TypebotProvider";
+import { Stack, Text } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import type { EmbedBubbleBlock } from "@typebot.io/blocks-bubbles/embed/schema";
type Props = {
- block: EmbedBubbleBlock
-}
+ block: EmbedBubbleBlock;
+};
export const EmbedBubbleContent = ({ block }: Props) => {
- const { typebot } = useTypebot()
- const { t } = useTranslate()
+ const { typebot } = useTypebot();
+ const { t } = useTranslate();
if (!block.content?.url)
- return {t('clickToEdit')}
+ return {t("clickToEdit")} ;
return (
- {t('editor.blocks.bubbles.embed.node.show.text')}
+ {t("editor.blocks.bubbles.embed.node.show.text")}
{typebot &&
block.content.waitForEvent?.isEnabled &&
block.content.waitForEvent.saveDataInVariableId && (
@@ -25,5 +25,5 @@ export const EmbedBubbleContent = ({ block }: Props) => {
/>
)}
- )
-}
+ );
+};
diff --git a/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleIcon.tsx b/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleIcon.tsx
index dc341222bf..1ff2a2028e 100644
--- a/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleIcon.tsx
+++ b/apps/builder/src/features/blocks/bubbles/embed/components/EmbedBubbleIcon.tsx
@@ -1,7 +1,7 @@
-import { LayoutIcon } from '@/components/icons'
-import { IconProps } from '@chakra-ui/react'
-import React from 'react'
+import { LayoutIcon } from "@/components/icons";
+import type { IconProps } from "@chakra-ui/react";
+import React from "react";
export const EmbedBubbleIcon = (props: IconProps) => (
-)
+);
diff --git a/apps/builder/src/features/blocks/bubbles/embed/components/EmbedUploadContent.tsx b/apps/builder/src/features/blocks/bubbles/embed/components/EmbedUploadContent.tsx
index b2d9da4770..5302a226aa 100644
--- a/apps/builder/src/features/blocks/bubbles/embed/components/EmbedUploadContent.tsx
+++ b/apps/builder/src/features/blocks/bubbles/embed/components/EmbedUploadContent.tsx
@@ -1,60 +1,61 @@
-import { TextInput, NumberInput } from '@/components/inputs'
-import { Stack, Text } from '@chakra-ui/react'
-import { EmbedBubbleBlock, Variable } from '@typebot.io/schemas'
-import { sanitizeUrl } from '@typebot.io/lib'
-import { useTranslate } from '@tolgee/react'
-import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/embed/constants'
-import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
-import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
+import { SwitchWithRelatedSettings } from "@/components/SwitchWithRelatedSettings";
+import { NumberInput, TextInput } from "@/components/inputs";
+import { VariableSearchInput } from "@/components/inputs/VariableSearchInput";
+import { Stack, Text } from "@chakra-ui/react";
+import { useTranslate } from "@tolgee/react";
+import { defaultEmbedBubbleContent } from "@typebot.io/blocks-bubbles/embed/constants";
+import type { EmbedBubbleBlock } from "@typebot.io/blocks-bubbles/embed/schema";
+import { sanitizeUrl } from "@typebot.io/lib/utils";
+import type { Variable } from "@typebot.io/variables/schemas";
type Props = {
- content: EmbedBubbleBlock['content']
- onSubmit: (content: EmbedBubbleBlock['content']) => void
-}
+ content: EmbedBubbleBlock["content"];
+ onSubmit: (content: EmbedBubbleBlock["content"]) => void;
+};
export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
- const { t } = useTranslate()
+ const { t } = useTranslate();
const handleUrlChange = (url: string) => {
const iframeUrl = sanitizeUrl(
- url.trim().startsWith('