diff --git a/.env.example b/.env.example
index b839bc99f..71e2da2a6 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,8 @@
DATABASE_URL=postgresql://username:password@host:5450/typebot?schema=public
+SECRET=secret
+NEXTAUTH_URL=http://localhost:3000
+
# Used for email auth and email notifications
EMAIL_SERVER_USER=username
EMAIL_SERVER_PASSWORD=password
diff --git a/README.md b/README.md
index 41244ba01..53b76bdf8 100644
--- a/README.md
+++ b/README.md
@@ -33,26 +33,10 @@
3. Copy `.env.example` to `.env`
4. Configure environment variables in the `.env` file.
-5. Setup the database
+5. Run the applications
```sh
- yarn dev:setup
- ```
-
-6. Run the applications
-
- ```sh
- yarn dev:builder
- ```
-
- ```sh
- yarn dev:viewer
- ```
-
-7. Open [Prisma Studio](https://www.prisma.io/studio) to look at or modify the database content
-
- ```sh
- yarn db:inspect
+ yarn dx
```
## Deployment
diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx
index 5a9d34e86..13ea30c81 100644
--- a/apps/builder/assets/icons.tsx
+++ b/apps/builder/assets/icons.tsx
@@ -1 +1,72 @@
import { IconProps, Icon } from '@chakra-ui/react'
+
+const featherIconsBaseProps: IconProps = {
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: '2px',
+ strokeLinecap: 'round',
+ strokeLinejoin: 'round',
+}
+export const SettingsIcon = (props: IconProps) => (
+
+
+
+
+)
+
+export const LogOutIcon = (props: IconProps) => (
+
+
+
+
+
+)
+
+export const ChevronLeftIcon = (props: IconProps) => (
+
+
+
+)
+
+export const PlusIcon = (props: IconProps) => (
+
+
+
+
+)
+
+export const FolderIcon = (props: IconProps) => (
+
+
+
+)
+
+export const MoreVerticalIcon = (props: IconProps) => (
+
+
+
+
+
+)
+
+export const GlobeIcon = (props: IconProps) => (
+
+
+
+
+
+)
+
+export const ToolIcon = (props: IconProps) => (
+
+
+
+)
+
+export const FolderPlusIcon = (props: IconProps) => (
+
+
+
+
+
+)
diff --git a/apps/builder/components/HOC/withUser.tsx b/apps/builder/components/HOC/withUser.tsx
index 7ddde1a20..5526e3484 100644
--- a/apps/builder/components/HOC/withUser.tsx
+++ b/apps/builder/components/HOC/withUser.tsx
@@ -1,17 +1,12 @@
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
-import { User } from '@typebot/prisma'
-
-export type withAuthProps = {
- user?: User
-}
const withAuth =
- (WrappedComponent: ({ user }: withAuthProps) => JSX.Element) =>
- (props: JSX.IntrinsicAttributes & withAuthProps) => {
+ (WrappedComponent: (props: any) => JSX.Element) =>
+ (props: JSX.IntrinsicAttributes) => {
const router = useRouter()
- const { data: session, status } = useSession()
+ const { status } = useSession()
useEffect(() => {
if (!router.isReady) return
@@ -19,9 +14,7 @@ const withAuth =
if (status === 'unauthenticated') router.replace('/signin')
}, [status, router])
- return (
-
- )
+ return
}
export default withAuth
diff --git a/apps/builder/components/MoreButton.tsx b/apps/builder/components/MoreButton.tsx
new file mode 100644
index 000000000..3d1ab9645
--- /dev/null
+++ b/apps/builder/components/MoreButton.tsx
@@ -0,0 +1,28 @@
+import {
+ ButtonProps,
+ IconButton,
+ Menu,
+ MenuButton,
+ MenuList,
+} from '@chakra-ui/react'
+import { MoreVerticalIcon } from 'assets/icons'
+import { ReactNode } from 'react'
+
+type Props = { children: ReactNode } & ButtonProps
+
+export const MoreButton = ({ children, ...props }: Props) => {
+ return (
+
+ )
+}
diff --git a/apps/builder/components/dashboard/DashboardHeader.tsx b/apps/builder/components/dashboard/DashboardHeader.tsx
new file mode 100644
index 000000000..b4ff8d894
--- /dev/null
+++ b/apps/builder/components/dashboard/DashboardHeader.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import {
+ Menu,
+ MenuButton,
+ MenuList,
+ MenuItem,
+ Text,
+ HStack,
+ Flex,
+ Avatar,
+ SkeletonCircle,
+ Skeleton,
+} from '@chakra-ui/react'
+import { TypebotLogo } from 'assets/logos'
+import { useUser } from 'services/user'
+import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
+import { LogOutIcon, SettingsIcon } from 'assets/icons'
+import { signOut } from 'next-auth/react'
+
+export const DashboardHeader = () => {
+ const user = useUser()
+
+ const handleLogOut = () => {
+ signOut()
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/builder/components/dashboard/FolderContent.tsx b/apps/builder/components/dashboard/FolderContent.tsx
new file mode 100644
index 000000000..ae8c233da
--- /dev/null
+++ b/apps/builder/components/dashboard/FolderContent.tsx
@@ -0,0 +1,181 @@
+import { DashboardFolder, Typebot } from '.prisma/client'
+import {
+ Button,
+ Flex,
+ Heading,
+ HStack,
+ Skeleton,
+ Stack,
+ useToast,
+ Wrap,
+} from '@chakra-ui/react'
+import {
+ DndContext,
+ DragEndEvent,
+ DragOverlay,
+ DragStartEvent,
+ MouseSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core'
+import { FolderPlusIcon } from 'assets/icons'
+import React, { useState } from 'react'
+import { createFolder, useFolders } from 'services/folders'
+import { updateTypebot, useTypebots } from 'services/typebots'
+import { BackButton } from './FolderContent/BackButton'
+import { CreateBotButton } from './FolderContent/CreateBotButton'
+import { ButtonSkeleton, FolderButton } from './FolderContent/FolderButton'
+import { TypebotButton } from './FolderContent/TypebotButton'
+import { TypebotCardOverlay } from './FolderContent/TypebotButtonOverlay'
+
+type Props = { folder: DashboardFolder | null }
+
+export const FolderContent = ({ folder }: Props) => {
+ const [isCreatingFolder, setIsCreatingFolder] = useState(false)
+ const [draggedTypebot, setDraggedTypebot] = useState()
+ const sensors = useSensors(
+ useSensor(MouseSensor, {
+ activationConstraint: {
+ delay: 100,
+ tolerance: 300,
+ },
+ })
+ )
+ const toast = useToast({
+ position: 'top-right',
+ status: 'error',
+ })
+ const {
+ folders,
+ isLoading: isFolderLoading,
+ mutate: mutateFolders,
+ } = useFolders({
+ parentId: folder?.id,
+ onError: (error) => {
+ toast({ title: "Couldn't fetch folders", description: error.message })
+ },
+ })
+
+ const {
+ typebots,
+ isLoading: isTypebotLoading,
+ mutate: mutateTypebots,
+ } = useTypebots({
+ folderId: folder?.id,
+ onError: (error) => {
+ toast({ title: "Couldn't fetch typebots", description: error.message })
+ },
+ })
+
+ const handleDragStart = (event: DragStartEvent) => {
+ if (!typebots) return
+ setDraggedTypebot(typebots.find((c) => c.id === event.active.id))
+ }
+
+ const handleDragEnd = async (event: DragEndEvent) => {
+ if (!typebots) return
+ const { over } = event
+ if (over?.id && draggedTypebot?.id)
+ await moveTypebotToFolder(draggedTypebot.id, over.id)
+ setDraggedTypebot(undefined)
+ }
+
+ const moveTypebotToFolder = async (typebotId: string, folderId: string) => {
+ if (!typebots) return
+ const { error } = await updateTypebot(typebotId, {
+ folderId: folderId === 'root' ? null : folderId,
+ })
+ if (error) toast({ description: error.message })
+ mutateTypebots({ typebots: typebots.filter((t) => t.id !== typebotId) })
+ }
+
+ const handleCreateFolder = async () => {
+ if (!folders) return
+ setIsCreatingFolder(true)
+ const { error, data: newFolder } = await createFolder({
+ parentFolderId: folder?.id ?? null,
+ })
+ setIsCreatingFolder(false)
+ if (error)
+ return toast({ title: 'An error occured', description: error.message })
+ if (newFolder) mutateFolders({ folders: [...folders, newFolder] })
+ }
+
+ const handleTypebotDeleted = (deletedId: string) => {
+ if (!typebots) return
+ mutateTypebots({ typebots: typebots.filter((t) => t.id !== deletedId) })
+ }
+
+ const handleFolderDeleted = (deletedId: string) => {
+ if (!folders) return
+ mutateFolders({ folders: folders.filter((f) => f.id !== deletedId) })
+ }
+
+ const handleFolderRenamed = (folderId: string, name: string) => {
+ if (!folders) return
+ mutateFolders({
+ folders: folders.map((f) => (f.id === folderId ? { ...f, name } : f)),
+ })
+ }
+
+ return (
+
+
+
+
+ {folder?.name}
+
+
+
+ {folder && }
+ }
+ onClick={handleCreateFolder}
+ isLoading={isCreatingFolder || isFolderLoading}
+ >
+ Create a folder
+
+
+
+
+ {isFolderLoading && }
+ {folders &&
+ folders.map((folder) => (
+ handleFolderDeleted(folder.id)}
+ onFolderRenamed={(newName: string) =>
+ handleFolderRenamed(folder.id, newName)
+ }
+ />
+ ))}
+ {isTypebotLoading && }
+ {typebots &&
+ typebots.map((typebot) => (
+ handleTypebotDeleted(typebot.id)}
+ />
+ ))}
+
+ {draggedTypebot && (
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/builder/components/dashboard/FolderContent/BackButton.tsx b/apps/builder/components/dashboard/FolderContent/BackButton.tsx
new file mode 100644
index 000000000..4f0c7e748
--- /dev/null
+++ b/apps/builder/components/dashboard/FolderContent/BackButton.tsx
@@ -0,0 +1,24 @@
+import { Button } from '@chakra-ui/react'
+import { useDroppable } from '@dnd-kit/core'
+import { ChevronLeftIcon } from 'assets/icons'
+import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
+import React from 'react'
+
+export const BackButton = ({ id }: { id: string | null }) => {
+ const { setNodeRef, isOver } = useDroppable({
+ id: id?.toString() ?? 'root',
+ })
+ return (
+ }
+ variant={'outline'}
+ colorScheme={isOver ? 'blue' : 'gray'}
+ borderWidth={isOver ? '3px' : '1px'}
+ ref={setNodeRef}
+ >
+ Back
+
+ )
+}
diff --git a/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx b/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx
new file mode 100644
index 000000000..c12a0d046
--- /dev/null
+++ b/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx
@@ -0,0 +1,41 @@
+import { Button, ButtonProps, Text, VStack } from '@chakra-ui/react'
+import { PlusIcon } from 'assets/icons'
+import { useRouter } from 'next/router'
+import React from 'react'
+
+export const CreateBotButton = ({
+ folderId,
+ ...props
+}: { folderId?: string } & ButtonProps) => {
+ const router = useRouter()
+
+ const handleClick = () =>
+ folderId
+ ? router.push(`/typebots/create?folderId=${folderId}`)
+ : router.push('/typebots/create')
+
+ return (
+
+ )
+}
diff --git a/apps/builder/components/dashboard/FolderContent/FolderButton.tsx b/apps/builder/components/dashboard/FolderContent/FolderButton.tsx
new file mode 100644
index 000000000..4038a8ec7
--- /dev/null
+++ b/apps/builder/components/dashboard/FolderContent/FolderButton.tsx
@@ -0,0 +1,158 @@
+import { DashboardFolder } from '.prisma/client'
+import {
+ Button,
+ Editable,
+ EditableInput,
+ EditablePreview,
+ MenuItem,
+ useDisclosure,
+ Text,
+ VStack,
+ IconButton,
+ Menu,
+ MenuButton,
+ MenuList,
+ useToast,
+ SkeletonText,
+ SkeletonCircle,
+ WrapItem,
+} from '@chakra-ui/react'
+import { useDroppable } from '@dnd-kit/core'
+import { FolderIcon, MoreVerticalIcon } from 'assets/icons'
+import { ConfirmModal } from 'components/modals/ConfirmModal'
+import { useRouter } from 'next/router'
+import React from 'react'
+import { deleteFolder, updateFolder } from 'services/folders'
+
+export const FolderButton = ({
+ folder,
+ onFolderDeleted,
+ onFolderRenamed,
+}: {
+ folder: DashboardFolder
+ onFolderDeleted: () => void
+ onFolderRenamed: (newName: string) => void
+}) => {
+ const router = useRouter()
+ const { setNodeRef, isOver } = useDroppable({
+ id: folder.id.toString(),
+ })
+ const { isOpen, onOpen, onClose } = useDisclosure()
+ const toast = useToast({
+ position: 'top-right',
+ status: 'error',
+ })
+
+ const onDeleteClick = async () => {
+ const { error } = await deleteFolder(folder.id)
+ return error
+ ? toast({
+ title: "Couldn't delete the folder",
+ description: error.message,
+ })
+ : onFolderDeleted()
+ }
+
+ const onRenameSubmit = async (newName: string) => {
+ if (newName === '' || newName === folder.name) return
+ const { error } = await updateFolder(folder.id, { name: newName })
+ return error
+ ? toast({ title: 'An error occured', description: error.message })
+ : onFolderRenamed(newName)
+ }
+
+ const handleClick = () => {
+ router.push(`/typebots/folders/${folder.id}`)
+ }
+
+ return (
+
+ )
+}
+
+export const ButtonSkeleton = () => (
+
+)
diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx
new file mode 100644
index 000000000..f59b7f2ba
--- /dev/null
+++ b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx
@@ -0,0 +1,133 @@
+import React from 'react'
+import {
+ Button,
+ Flex,
+ MenuItem,
+ Text,
+ useDisclosure,
+ useToast,
+ VStack,
+ WrapItem,
+} from '@chakra-ui/react'
+import { useDraggable } from '@dnd-kit/core'
+import { useRouter } from 'next/router'
+import { Typebot } from '@typebot/prisma'
+import { isMobile } from 'services/utils'
+import { MoreButton } from 'components/MoreButton'
+import { ConfirmModal } from 'components/modals/ConfirmModal'
+import { GlobeIcon, ToolIcon } from 'assets/icons'
+import { deleteTypebot, duplicateTypebot } from 'services/typebots'
+
+type ChatbotCardProps = {
+ typebot: Typebot
+ onTypebotDeleted: () => void
+}
+
+export const TypebotButton = ({
+ typebot,
+ onTypebotDeleted,
+}: ChatbotCardProps) => {
+ const router = useRouter()
+ const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
+ id: typebot.id.toString(),
+ })
+ const {
+ isOpen: isDeleteOpen,
+ onOpen: onDeleteOpen,
+ onClose: onDeleteClose,
+ } = useDisclosure()
+
+ const toast = useToast({
+ position: 'top-right',
+ status: 'error',
+ })
+
+ const handleTypebotClick = () => {
+ router.push(
+ isMobile
+ ? `/typebots/${typebot.id}/results/responses`
+ : `/typebots/${typebot.id}/edit`
+ )
+ }
+
+ const handleDeleteTypebotClick = async () => {
+ const { error } = await deleteTypebot(typebot.id)
+ if (error)
+ return toast({
+ title: "Couldn't delete typebot",
+ description: error.message,
+ })
+ onTypebotDeleted()
+ }
+
+ const handleDuplicateClick = async () => {
+ const { data: createdTypebot, error } = await duplicateTypebot(typebot)
+ if (error)
+ return toast({
+ title: "Couldn't duplicate typebot",
+ description: error.message,
+ })
+ if (createdTypebot) router.push(`/typebots/${createdTypebot?.id}`)
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx
new file mode 100644
index 000000000..f5152aad5
--- /dev/null
+++ b/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx
@@ -0,0 +1,44 @@
+import { Button, Flex, Text, VStack } from '@chakra-ui/react'
+import { Typebot } from '.prisma/client'
+import { GlobeIcon, ToolIcon } from 'assets/icons'
+
+type Props = {
+ typebot: Typebot
+}
+
+export const TypebotCardOverlay = ({ typebot }: Props) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/builder/components/modals/ConfirmModal.tsx b/apps/builder/components/modals/ConfirmModal.tsx
new file mode 100644
index 000000000..5f53385ed
--- /dev/null
+++ b/apps/builder/components/modals/ConfirmModal.tsx
@@ -0,0 +1,77 @@
+import { useRef, useState } from 'react'
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ Button,
+} from '@chakra-ui/react'
+
+type ConfirmDeleteModalProps = {
+ isOpen: boolean
+ onConfirm: () => Promise
+ onClose: () => void
+ message: JSX.Element
+ title?: string
+ confirmButtonLabel: string
+ confirmButtonColor?: 'blue' | 'red'
+}
+
+export const ConfirmModal = ({
+ title,
+ message,
+ isOpen,
+ onClose,
+ confirmButtonLabel,
+ onConfirm,
+ confirmButtonColor = 'red',
+}: ConfirmDeleteModalProps) => {
+ const [confirmLoading, setConfirmLoading] = useState(false)
+ const cancelRef = useRef(null)
+
+ const onConfirmClick = async () => {
+ setConfirmLoading(true)
+ try {
+ await onConfirm()
+ } catch (e) {
+ setConfirmLoading(false)
+ return setConfirmLoading(false)
+ }
+ setConfirmLoading(false)
+ onClose()
+ }
+
+ return (
+
+
+
+
+ {title ?? 'Are you sure?'}
+
+
+ {message}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/builder/cypress/plugins/database.ts b/apps/builder/cypress/plugins/database.ts
new file mode 100644
index 000000000..09769e919
--- /dev/null
+++ b/apps/builder/cypress/plugins/database.ts
@@ -0,0 +1,30 @@
+import { PrismaClient } from '.prisma/client'
+
+const prisma = new PrismaClient()
+
+const teardownTestData = async () => prisma.user.deleteMany()
+
+export const seedDb = async () => {
+ await teardownTestData()
+ await createUsers()
+ await createFolders()
+ return createTypebots()
+}
+
+const createUsers = () =>
+ prisma.user.createMany({
+ data: [
+ { id: 'test1', email: 'test1@gmail.com', emailVerified: new Date() },
+ { id: 'test2', email: 'test2@gmail.com', emailVerified: new Date() },
+ ],
+ })
+
+const createFolders = () =>
+ prisma.dashboardFolder.createMany({
+ data: [{ ownerId: 'test2', name: 'Folder #1', id: 'folder1' }],
+ })
+
+const createTypebots = () =>
+ prisma.typebot.createMany({
+ data: [{ name: 'Typebot #1', ownerId: 'test2' }],
+ })
diff --git a/apps/builder/cypress/plugins/index.ts b/apps/builder/cypress/plugins/index.ts
index d9f811496..0723ef984 100644
--- a/apps/builder/cypress/plugins/index.ts
+++ b/apps/builder/cypress/plugins/index.ts
@@ -3,6 +3,7 @@ import {
FacebookSocialLogin,
GoogleSocialLogin,
} from 'cypress-social-logins/src/Plugins'
+import { seedDb } from './database'
///
/**
@@ -14,6 +15,7 @@ const handler = (on: any) => {
GoogleSocialLogin: GoogleSocialLogin,
FacebookSocialLogin: FacebookSocialLogin,
GitHubSocialLogin: GitHubSocialLogin,
+ seed: seedDb,
})
}
diff --git a/apps/builder/cypress/support/commands.ts b/apps/builder/cypress/support/commands.ts
index b7c5a49d4..c1b1a848e 100644
--- a/apps/builder/cypress/support/commands.ts
+++ b/apps/builder/cypress/support/commands.ts
@@ -1,37 +1,93 @@
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add('login', (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This will overwrite an existing command --
-// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
-import '@testing-library/cypress/add-commands'
+import { signIn, signOut } from 'next-auth/react'
-Cypress.Commands.add('logOutByApi', () =>
- cy
- .request('GET', `${Cypress.env('SITE_NAME')}/api/auth/csrf/login`)
- .its('body')
- .then((result) => {
- cy.request('POST', `${Cypress.env('SITE_NAME')}/api/auth/signout`, {
- csrfToken: result.csrfToken,
+Cypress.Commands.add('signOut', () => {
+ cy.log(`🔐 Sign out`)
+ return cy.wrap(signOut({ redirect: false }), { log: false })
+})
+
+Cypress.Commands.add('signIn', (email: string) => {
+ cy.log(`🔐 Sign in as ${email}`)
+ return cy.wrap(signIn('credentials', { redirect: false, email }), {
+ log: false,
+ })
+})
+
+Cypress.Commands.add(
+ 'mouseMoveBy',
+ {
+ prevSubject: 'element',
+ },
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ (
+ subject: JQuery,
+ x: number,
+ y: number,
+ options?: { delay: number }
+ ) => {
+ cy.wrap(subject, { log: false })
+ .then((subject) => {
+ const initialRect = subject.get(0).getBoundingClientRect()
+ const windowScroll = getDocumentScroll()
+
+ return [subject, initialRect, windowScroll] as const
})
- })
+ .then(([subject, initialRect, initialWindowScroll]) => {
+ cy.wrap(subject)
+ .trigger('mousedown', { force: true })
+ .wait(options?.delay || 0, { log: Boolean(options?.delay) })
+ .trigger('mousemove', {
+ force: true,
+ clientX: Math.floor(
+ initialRect.left + initialRect.width / 2 + x / 2
+ ),
+ clientY: Math.floor(
+ initialRect.top + initialRect.height / 2 + y / 2
+ ),
+ })
+ .trigger('mousemove', {
+ force: true,
+ clientX: Math.floor(initialRect.left + initialRect.width / 2 + x),
+ clientY: Math.floor(initialRect.top + initialRect.height / 2 + y),
+ })
+ // .wait(1000)
+ .trigger('mouseup', { force: true })
+ .wait(250)
+ .then((subject: any) => {
+ const finalRect = subject.get(0).getBoundingClientRect()
+ const windowScroll = getDocumentScroll()
+ const windowScrollDelta = {
+ x: windowScroll.x - initialWindowScroll.x,
+ y: windowScroll.y - initialWindowScroll.y,
+ }
+
+ const delta = {
+ x: Math.round(
+ finalRect.left - initialRect.left - windowScrollDelta.x
+ ),
+ y: Math.round(
+ finalRect.top - initialRect.top - windowScrollDelta.y
+ ),
+ }
+
+ return [subject, { initialRect, finalRect, delta }] as const
+ })
+ })
+ }
)
+
+const getDocumentScroll = () => {
+ if (document.scrollingElement) {
+ const { scrollTop, scrollLeft } = document.scrollingElement
+
+ return {
+ x: scrollTop,
+ y: scrollLeft,
+ }
+ }
+
+ return {
+ x: 0,
+ y: 0,
+ }
+}
diff --git a/apps/builder/cypress/support/index.ts b/apps/builder/cypress/support/index.ts
index 1f4a13437..7e32111c2 100644
--- a/apps/builder/cypress/support/index.ts
+++ b/apps/builder/cypress/support/index.ts
@@ -13,20 +13,33 @@
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
-
-// Import commands.js using ES2015 syntax:
+///
declare global {
namespace Cypress {
interface Chainable {
- /**
- * Log out using the NextAuth API.
- * @example cy.logOutByApi()
- */
- logOutByApi(): Chainable>
+ signOut(): Chainable
+ signIn(email: string): Chainable
+ mouseMoveBy(
+ x: number,
+ y: number,
+ options?: { delay: number }
+ ): Chainable<
+ [
+ Element,
+ {
+ initialRect: ClientRect
+ finalRect: ClientRect
+ delta: { x: number; y: number }
+ }
+ ]
+ >
}
}
}
+
+// Import commands.js using ES2015 syntax:
+import '@testing-library/cypress/add-commands'
import './commands'
// Alternatively you can use CommonJS syntax:
diff --git a/apps/builder/cypress/tests/auth.ts b/apps/builder/cypress/tests/auth.ts
index fb85687f9..882d1c2cb 100644
--- a/apps/builder/cypress/tests/auth.ts
+++ b/apps/builder/cypress/tests/auth.ts
@@ -1,7 +1,8 @@
describe('SignIn page', () => {
beforeEach(() => {
- cy.logOutByApi()
+ cy.signOut()
})
+
it('can continue with Google', () => {
cy.visit('/signin')
const username = Cypress.env('GOOGLE_USER')
@@ -98,6 +99,6 @@ const exectueSocialLogin = (
})
}
cy.visit('/typebots')
- cy.findByText(`Hello ${username}`).should('exist')
+ cy.findByRole('button', { name: 'Create a folder' }).should('exist')
})
}
diff --git a/apps/builder/cypress/tests/dashboard.ts b/apps/builder/cypress/tests/dashboard.ts
new file mode 100644
index 000000000..f9e34eda2
--- /dev/null
+++ b/apps/builder/cypress/tests/dashboard.ts
@@ -0,0 +1,49 @@
+describe('Dashboard page', () => {
+ beforeEach(() => {
+ cy.task('seed')
+ cy.signOut()
+ })
+
+ it('should navigate correctly', () => {
+ cy.signIn('test1@gmail.com')
+ cy.visit('/typebots')
+ createFolder('My folder #1')
+ cy.findByTestId('folder-button').click()
+ cy.findByRole('heading', { name: 'My folder #1' }).should('exist')
+ createFolder('My folder #2')
+ cy.findByTestId('folder-button').click()
+ cy.findByRole('heading', { name: 'My folder #2' }).should('exist')
+ cy.findByRole('link', { name: 'Back' }).click()
+ cy.findByRole('heading', { name: 'My folder #1' }).should('exist')
+ cy.findByRole('link', { name: 'Back' }).click()
+ cy.findByRole('button', { name: 'Show folder menu' }).click()
+ cy.findByRole('menuitem', { name: 'Delete' }).click()
+ cy.findByRole('button', { name: 'Delete' }).click()
+ cy.findByDisplayValue('My folder #2').should('exist')
+ cy.findByRole('button', { name: 'Show folder menu' }).click()
+ cy.findByRole('menuitem', { name: 'Delete' }).click()
+ cy.findByRole('button', { name: 'Delete' }).click()
+ cy.findByDisplayValue('My folder #2').should('not.exist')
+ })
+
+ it('should be droppable', () => {
+ cy.signIn('test2@gmail.com')
+ cy.visit('/typebots')
+ cy.findByTestId('typebot-button').mouseMoveBy(-100, 0, {
+ delay: 120,
+ })
+ cy.visit('/typebots/folders/folder1')
+ cy.findByTestId('typebot-button').mouseMoveBy(-300, -100, {
+ delay: 120,
+ })
+ cy.visit('/typebots')
+ cy.findByDisplayValue('Folder #1').should('exist')
+ cy.findByText('Typebot #1').should('exist')
+ })
+})
+
+const createFolder = (folderName: string) => {
+ cy.findByRole('button', { name: 'Create a folder' }).click({ force: true })
+ cy.findByText('New folder').click({ force: true })
+ cy.findByDisplayValue('New folder').type(`${folderName}{enter}`)
+}
diff --git a/apps/builder/cypress/tsconfig.json b/apps/builder/cypress/tsconfig.json
index 2a76e8cce..3dbbe026f 100644
--- a/apps/builder/cypress/tsconfig.json
+++ b/apps/builder/cypress/tsconfig.json
@@ -8,6 +8,7 @@
"target": "es5",
"isolatedModules": false,
"allowJs": true,
- "noEmit": true
+ "noEmit": true,
+ "downlevelIteration": true
}
}
diff --git a/apps/builder/libs/prisma.ts b/apps/builder/libs/prisma.ts
new file mode 100644
index 000000000..ffc43a367
--- /dev/null
+++ b/apps/builder/libs/prisma.ts
@@ -0,0 +1,15 @@
+import { PrismaClient } from '@typebot/prisma'
+
+declare const global: { prisma: PrismaClient }
+let prisma: PrismaClient
+
+if (process.env.NODE_ENV === 'production') {
+ prisma = new PrismaClient()
+} else {
+ if (!global.prisma) {
+ global.prisma = new PrismaClient()
+ }
+ prisma = global.prisma
+}
+
+export default prisma
diff --git a/apps/builder/package.json b/apps/builder/package.json
index 8bb1065b7..f621ef810 100644
--- a/apps/builder/package.json
+++ b/apps/builder/package.json
@@ -9,17 +9,22 @@
"cypress": "cypress open"
},
"dependencies": {
+ "@chakra-ui/css-reset": "^1.1.1",
"@chakra-ui/react": "^1.7.2",
+ "@dnd-kit/core": "^4.0.3",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@next-auth/prisma-adapter": "next",
+ "focus-visible": "^5.2.0",
"framer-motion": "^4",
"next": "^12.0.4",
"next-auth": "beta",
"nodemailer": "^6.7.1",
"nprogress": "^0.2.0",
"react": "^17.0.2",
- "react-dom": "^17.0.2"
+ "react-dom": "^17.0.2",
+ "swr": "^1.0.1",
+ "use-debounce": "^7.0.1"
},
"devDependencies": {
"@testing-library/cypress": "^8.0.2",
diff --git a/apps/builder/pages/_app.tsx b/apps/builder/pages/_app.tsx
index b6996ddaf..02caa3369 100644
--- a/apps/builder/pages/_app.tsx
+++ b/apps/builder/pages/_app.tsx
@@ -3,8 +3,9 @@ import { AppProps } from 'next/app'
import { SessionProvider } from 'next-auth/react'
import { ChakraProvider } from '@chakra-ui/react'
import { customTheme } from 'libs/chakra'
-import 'assets/styles/routerProgressBar.css'
import { useRouterProgressBar } from 'libs/routerProgressBar'
+import 'assets/styles/routerProgressBar.css'
+import 'focus-visible/dist/focus-visible'
const App = ({ Component, pageProps }: AppProps) => {
useRouterProgressBar()
diff --git a/apps/builder/pages/api/auth/[...nextauth].ts b/apps/builder/pages/api/auth/[...nextauth].ts
index 9de52c67d..3329ffd9c 100644
--- a/apps/builder/pages/api/auth/[...nextauth].ts
+++ b/apps/builder/pages/api/auth/[...nextauth].ts
@@ -1,39 +1,87 @@
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
-import { PrismaClient } from '@typebot/prisma'
import EmailProvider from 'next-auth/providers/email'
import GitHubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import FacebookProvider from 'next-auth/providers/facebook'
+import CredentialsProvider from 'next-auth/providers/credentials'
+import prisma from 'libs/prisma'
+import { Provider } from 'next-auth/providers'
+import { User } from '@typebot/prisma'
-const prisma = new PrismaClient()
+const providers: Provider[] = [
+ EmailProvider({
+ server: {
+ host: process.env.EMAIL_SERVER_HOST,
+ port: process.env.EMAIL_SERVER_PORT,
+ auth: {
+ user: process.env.EMAIL_SERVER_USER,
+ pass: process.env.EMAIL_SERVER_PASSWORD,
+ },
+ },
+ from: process.env.EMAIL_FROM,
+ }),
+]
+
+if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)
+ providers.push(
+ GitHubProvider({
+ clientId: process.env.GITHUB_CLIENT_ID,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
+ })
+ )
+
+if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET)
+ providers.push(
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ })
+ )
+
+if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET)
+ providers.push(
+ FacebookProvider({
+ clientId: process.env.FACEBOOK_CLIENT_ID,
+ clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
+ })
+ )
+
+if (process.env.NODE_ENV !== 'production')
+ providers.push(
+ CredentialsProvider({
+ name: 'Credentials',
+ credentials: {
+ email: {
+ label: 'Email',
+ type: 'email',
+ placeholder: 'email@email.com',
+ },
+ },
+ async authorize(credentials) {
+ const user = await prisma.user.findUnique({
+ where: { email: credentials?.email },
+ })
+ return user
+ },
+ })
+ )
export default NextAuth({
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET,
- providers: [
- EmailProvider({
- server: {
- host: process.env.EMAIL_SERVER_HOST,
- port: process.env.EMAIL_SERVER_PORT,
- auth: {
- user: process.env.EMAIL_SERVER_USER,
- pass: process.env.EMAIL_SERVER_PASSWORD,
- },
- },
- from: process.env.EMAIL_FROM,
- }),
- GitHubProvider({
- clientId: process.env.GITHUB_CLIENT_ID,
- clientSecret: process.env.GITHUB_CLIENT_SECRET,
- }),
- GoogleProvider({
- clientId: process.env.GOOGLE_CLIENT_ID ?? '',
- clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
- }),
- FacebookProvider({
- clientId: process.env.FACEBOOK_CLIENT_ID ?? '',
- clientSecret: process.env.FACEBOOK_CLIENT_SECRET ?? '',
- }),
- ],
+ providers,
+ session: {
+ strategy: process.env.NODE_ENV === 'production' ? 'database' : 'jwt',
+ },
+ callbacks: {
+ jwt: async ({ token, user }) => {
+ user && (token.user = user)
+ return token
+ },
+ session: async ({ session, token }) => {
+ if (token.user) session.user = token.user as User
+ return session
+ },
+ },
})
diff --git a/apps/builder/pages/api/folders.ts b/apps/builder/pages/api/folders.ts
new file mode 100644
index 000000000..22d981516
--- /dev/null
+++ b/apps/builder/pages/api/folders.ts
@@ -0,0 +1,36 @@
+import { DashboardFolder, User } from '@typebot/prisma'
+import prisma from 'libs/prisma'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { getSession } from 'next-auth/react'
+import { methodNotAllowed } from 'services/api/utils'
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const session = await getSession({ req })
+
+ if (!session?.user)
+ return res.status(401).json({ message: 'Not authenticated' })
+
+ const user = session.user as User
+ const parentFolderId = req.query.parentId
+ ? req.query.parentId.toString()
+ : null
+ if (req.method === 'GET') {
+ const folders = await prisma.dashboardFolder.findMany({
+ where: {
+ ownerId: user.id,
+ parentFolderId,
+ },
+ })
+ return res.send({ folders })
+ }
+ if (req.method === 'POST') {
+ const data = JSON.parse(req.body) as Pick
+ const folder = await prisma.dashboardFolder.create({
+ data: { ...data, ownerId: user.id, name: 'New folder' },
+ })
+ return res.send(folder)
+ }
+ return methodNotAllowed(res)
+}
+
+export default handler
diff --git a/apps/builder/pages/api/folders/[id].ts b/apps/builder/pages/api/folders/[id].ts
new file mode 100644
index 000000000..75c11b188
--- /dev/null
+++ b/apps/builder/pages/api/folders/[id].ts
@@ -0,0 +1,37 @@
+import { DashboardFolder } from '@typebot/prisma'
+import prisma from 'libs/prisma'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { getSession } from 'next-auth/react'
+import { methodNotAllowed } from 'services/api/utils'
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const session = await getSession({ req })
+
+ if (!session?.user)
+ return res.status(401).json({ message: 'Not authenticated' })
+
+ const id = req.query.id.toString()
+ if (req.method === 'GET') {
+ const folder = await prisma.dashboardFolder.findUnique({
+ where: { id },
+ })
+ return res.send({ folder })
+ }
+ if (req.method === 'DELETE') {
+ const folders = await prisma.dashboardFolder.delete({
+ where: { id },
+ })
+ return res.send({ folders })
+ }
+ if (req.method === 'PATCH') {
+ const data = JSON.parse(req.body) as Partial
+ const folders = await prisma.dashboardFolder.update({
+ where: { id },
+ data,
+ })
+ return res.send({ typebots: folders })
+ }
+ return methodNotAllowed(res)
+}
+
+export default handler
diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts
new file mode 100644
index 000000000..d2237423e
--- /dev/null
+++ b/apps/builder/pages/api/typebots.ts
@@ -0,0 +1,34 @@
+import { Typebot, User } from '@typebot/prisma'
+import prisma from 'libs/prisma'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { getSession } from 'next-auth/react'
+import { methodNotAllowed } from 'services/api/utils'
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const session = await getSession({ req })
+
+ if (!session?.user)
+ return res.status(401).json({ message: 'Not authenticated' })
+
+ const user = session.user as User
+ if (req.method === 'GET') {
+ const folderId = req.query.folderId ? req.query.folderId.toString() : null
+ const typebots = await prisma.typebot.findMany({
+ where: {
+ ownerId: user.id,
+ folderId,
+ },
+ })
+ return res.send({ typebots })
+ }
+ if (req.method === 'POST') {
+ const data = JSON.parse(req.body) as Typebot
+ const typebot = await prisma.typebot.create({
+ data: { ...data, ownerId: user.id },
+ })
+ return res.send(typebot)
+ }
+ return methodNotAllowed(res)
+}
+
+export default handler
diff --git a/apps/builder/pages/api/typebots/[id].ts b/apps/builder/pages/api/typebots/[id].ts
new file mode 100644
index 000000000..ea364c86f
--- /dev/null
+++ b/apps/builder/pages/api/typebots/[id].ts
@@ -0,0 +1,31 @@
+import { Typebot } from '.prisma/client'
+import prisma from 'libs/prisma'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { getSession } from 'next-auth/react'
+import { methodNotAllowed } from 'services/api/utils'
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const session = await getSession({ req })
+
+ if (!session?.user)
+ return res.status(401).json({ message: 'Not authenticated' })
+
+ const id = req.query.id.toString()
+ if (req.method === 'DELETE') {
+ const typebots = await prisma.typebot.delete({
+ where: { id },
+ })
+ return res.send({ typebots })
+ }
+ if (req.method === 'PATCH') {
+ const data = JSON.parse(req.body) as Partial
+ const typebots = await prisma.typebot.update({
+ where: { id },
+ data,
+ })
+ return res.send({ typebots })
+ }
+ return methodNotAllowed(res)
+}
+
+export default handler
diff --git a/apps/builder/pages/typebots.tsx b/apps/builder/pages/typebots.tsx
new file mode 100644
index 000000000..370b784f8
--- /dev/null
+++ b/apps/builder/pages/typebots.tsx
@@ -0,0 +1,18 @@
+import withAuth from 'components/HOC/withUser'
+import React from 'react'
+import { Stack } from '@chakra-ui/layout'
+import { DashboardHeader } from 'components/dashboard/DashboardHeader'
+import { Seo } from 'components/Seo'
+import { FolderContent } from 'components/dashboard/FolderContent'
+
+const DashboardPage = () => {
+ return (
+
+
+
+
+
+ )
+}
+
+export default withAuth(DashboardPage)
diff --git a/apps/builder/pages/typebots/create.tsx b/apps/builder/pages/typebots/create.tsx
new file mode 100644
index 000000000..c666070c6
--- /dev/null
+++ b/apps/builder/pages/typebots/create.tsx
@@ -0,0 +1,43 @@
+import React, { useState } from 'react'
+import { Button, Stack, useToast } from '@chakra-ui/react'
+import { useUser } from 'services/user'
+import { useRouter } from 'next/router'
+import { Seo } from 'components/Seo'
+import { DashboardHeader } from 'components/dashboard/DashboardHeader'
+import { createTypebot } from 'services/typebots'
+
+const TemplatesPage = () => {
+ const user = useUser()
+ const router = useRouter()
+
+ const [isLoading, setIsLoading] = useState(false)
+
+ const toast = useToast({
+ position: 'top-right',
+ status: 'error',
+ title: 'An error occured',
+ })
+
+ const handleCreateSubmit = async () => {
+ if (!user) return
+ setIsLoading(true)
+ const { error, data } = await createTypebot({
+ folderId: router.query.folderId?.toString() ?? null,
+ })
+ if (error) toast({ description: error.message })
+ if (data) router.push(`/typebots/${data.id}`)
+ setIsLoading(false)
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default TemplatesPage
diff --git a/apps/builder/pages/typebots/folders/[id].tsx b/apps/builder/pages/typebots/folders/[id].tsx
new file mode 100644
index 000000000..c04416ef7
--- /dev/null
+++ b/apps/builder/pages/typebots/folders/[id].tsx
@@ -0,0 +1,44 @@
+import withAuth from 'components/HOC/withUser'
+import React from 'react'
+import { Flex, Stack } from '@chakra-ui/layout'
+import { DashboardHeader } from 'components/dashboard/DashboardHeader'
+import { Seo } from 'components/Seo'
+import { FolderContent } from 'components/dashboard/FolderContent'
+import { useRouter } from 'next/router'
+import { useFolderContent } from 'services/folders'
+import { Spinner, useToast } from '@chakra-ui/react'
+
+const FolderPage = () => {
+ const router = useRouter()
+
+ const toast = useToast({
+ position: 'top-right',
+ status: 'error',
+ })
+
+ const { folder } = useFolderContent({
+ folderId: router.query.id?.toString(),
+ onError: (error) => {
+ toast({
+ title: "Couldn't fetch folder content",
+ description: error.message,
+ })
+ },
+ })
+
+ return (
+
+
+
+ {!folder ? (
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export default withAuth(FolderPage)
diff --git a/apps/builder/pages/typebots/index.tsx b/apps/builder/pages/typebots/index.tsx
deleted file mode 100644
index ca1df0135..000000000
--- a/apps/builder/pages/typebots/index.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import withAuth, { withAuthProps } from 'components/HOC/withUser'
-import { Text } from '@chakra-ui/react'
-import React from 'react'
-
-const TypebotsPage = ({ user }: withAuthProps) => {
- return Hello {user?.email}
-}
-
-export default withAuth(TypebotsPage)
diff --git a/apps/builder/public/favicon.png b/apps/builder/public/favicon.png
new file mode 100644
index 000000000..7512abc2a
Binary files /dev/null and b/apps/builder/public/favicon.png differ
diff --git a/apps/builder/services/api/utils.ts b/apps/builder/services/api/utils.ts
new file mode 100644
index 000000000..ab872e261
--- /dev/null
+++ b/apps/builder/services/api/utils.ts
@@ -0,0 +1,4 @@
+import { NextApiResponse } from 'next'
+
+export const methodNotAllowed = (res: NextApiResponse) =>
+ res.status(405).json({ message: 'Method Not Allowed' })
diff --git a/apps/builder/services/folders.ts b/apps/builder/services/folders.ts
new file mode 100644
index 000000000..3f9135fe2
--- /dev/null
+++ b/apps/builder/services/folders.ts
@@ -0,0 +1,69 @@
+import { DashboardFolder } from '.prisma/client'
+import useSWR from 'swr'
+import { fetcher, sendRequest } from './utils'
+
+export const useFolders = ({
+ parentId,
+ onError,
+}: {
+ parentId?: string
+ onError: (error: Error) => void
+}) => {
+ const params = new URLSearchParams(
+ parentId ? { parentId: parentId.toString() } : undefined
+ )
+ const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>(
+ `/api/folders?${params}`,
+ fetcher
+ )
+ if (error) onError(error)
+ return {
+ folders: data?.folders,
+ isLoading: !error && !data,
+ mutate,
+ }
+}
+
+export const useFolderContent = ({
+ folderId,
+ onError,
+}: {
+ folderId?: string
+ onError: (error: Error) => void
+}) => {
+ const { data, error, mutate } = useSWR<{ folder: DashboardFolder }, Error>(
+ `/api/folders/${folderId}`,
+ fetcher
+ )
+ if (error) onError(error)
+ return {
+ folder: data?.folder,
+ isLoading: !error && !data,
+ mutate,
+ }
+}
+
+export const createFolder = async (
+ folder: Pick
+) =>
+ sendRequest({
+ url: `/api/folders`,
+ method: 'POST',
+ body: folder,
+ })
+
+export const deleteFolder = async (id: string) =>
+ sendRequest({
+ url: `/api/folders/${id}`,
+ method: 'DELETE',
+ })
+
+export const updateFolder = async (
+ id: string,
+ folder: Partial
+) =>
+ sendRequest({
+ url: `/api/folders/${id}`,
+ method: 'PATCH',
+ body: folder,
+ })
diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts
new file mode 100644
index 000000000..44c37e176
--- /dev/null
+++ b/apps/builder/services/typebots.ts
@@ -0,0 +1,69 @@
+import { Typebot } from '@typebot/prisma'
+import useSWR from 'swr'
+import { fetcher, sendRequest } from './utils'
+
+export const useTypebots = ({
+ folderId,
+ onError,
+}: {
+ folderId?: string
+ onError: (error: Error) => void
+}) => {
+ const params = new URLSearchParams(
+ folderId ? { folderId: folderId.toString() } : undefined
+ )
+ const { data, error, mutate } = useSWR<{ typebots: Typebot[] }, Error>(
+ `/api/typebots?${params}`,
+ fetcher
+ )
+ if (error) onError(error)
+ return {
+ typebots: data?.typebots,
+ isLoading: !error && !data,
+ mutate,
+ }
+}
+
+export const createTypebot = async ({
+ folderId,
+}: Pick) => {
+ const typebot = {
+ folderId,
+ name: 'My typebot',
+ }
+ return sendRequest({
+ url: `/api/typebots`,
+ method: 'POST',
+ body: typebot,
+ })
+}
+
+export const duplicateTypebot = async ({
+ folderId,
+ ownerId,
+ name,
+}: Typebot) => {
+ const typebot = {
+ folderId,
+ ownerId,
+ name: `${name} copy`,
+ }
+ return sendRequest({
+ url: `/api/typebots`,
+ method: 'POST',
+ body: typebot,
+ })
+}
+
+export const deleteTypebot = async (id: string) =>
+ sendRequest({
+ url: `/api/typebots/${id}`,
+ method: 'DELETE',
+ })
+
+export const updateTypebot = async (id: string, typebot: Partial) =>
+ sendRequest({
+ url: `/api/typebots/${id}`,
+ method: 'PATCH',
+ body: typebot,
+ })
diff --git a/apps/builder/services/user.ts b/apps/builder/services/user.ts
new file mode 100644
index 000000000..70169a32a
--- /dev/null
+++ b/apps/builder/services/user.ts
@@ -0,0 +1,7 @@
+import { User } from '@typebot/prisma'
+import { useSession } from 'next-auth/react'
+
+export const useUser = (): User | undefined => {
+ const { data } = useSession()
+ return data?.user as User | undefined
+}
diff --git a/apps/builder/services/utils.ts b/apps/builder/services/utils.ts
new file mode 100644
index 000000000..02626880e
--- /dev/null
+++ b/apps/builder/services/utils.ts
@@ -0,0 +1,32 @@
+export const fetcher = async (input: RequestInfo, init?: RequestInit) => {
+ const res = await fetch(input, init)
+ return res.json()
+}
+
+export const isMobile =
+ typeof window !== 'undefined' &&
+ window.matchMedia('only screen and (max-width: 760px)').matches
+
+export const sendRequest = async ({
+ url,
+ method,
+ body,
+}: {
+ url: string
+ method: string
+ body?: Record
+}): Promise<{ data?: ResponseData; error?: Error }> => {
+ try {
+ const response = await fetch(url, {
+ method,
+ mode: 'cors',
+ body: body ? JSON.stringify(body) : undefined,
+ })
+ if (!response.ok) throw new Error(response.statusText)
+ const data = await response.json()
+ return { data }
+ } catch (e) {
+ console.error(e)
+ return { error: e as Error }
+ }
+}
diff --git a/apps/builder/tsconfig.json b/apps/builder/tsconfig.json
index 7b841352e..53c073685 100644
--- a/apps/builder/tsconfig.json
+++ b/apps/builder/tsconfig.json
@@ -4,7 +4,7 @@
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
- "strict": false,
+ "strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
diff --git a/apps/viewer/package.json b/apps/viewer/package.json
index a065550fb..211c89679 100644
--- a/apps/viewer/package.json
+++ b/apps/viewer/package.json
@@ -2,7 +2,7 @@
"name": "viewer",
"packageManager": "yarn@3.1.0",
"scripts": {
- "dev": "next dev",
+ "dev": "next dev -p 3001",
"build": "next build",
"start": "next start",
"lint": "next lint"
diff --git a/docker-compose.yml b/docker-compose.yml
index 264847d10..a6b993f5c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,5 +7,8 @@ services:
restart: always
volumes:
- db_data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_PASSWORD: ""
+ POSTGRES_HOST_AUTH_METHOD: trust
volumes:
db_data:
diff --git a/package.json b/package.json
index 070188969..2fc7a9082 100644
--- a/package.json
+++ b/package.json
@@ -7,15 +7,17 @@
"apps/*"
],
"scripts": {
- "db:up": "docker-compose up -d",
- "db:setup": "yarn workspace @typebot/prisma dev",
+ "db:up": "docker-compose up -d && yarn workspace @typebot/prisma prisma db push",
"db:nuke": "docker-compose down --volumes --remove-orphans",
- "db:inspect": "dotenv -e .env yarn workspace @typebot/prisma prisma studio",
- "dev:setup": "dotenv -e .env run-s db:up db:setup",
+ "dev": "concurrently -n builder,viewer \"yarn dev:builder\" \"yarn dev:viewer\"",
"dev:builder": "yarn workspace builder dev",
- "dev:viewer": "yarn workspace viewer dev"
+ "dev:viewer": "yarn workspace viewer dev",
+ "dx": "run-s db:up dev",
+ "cypress:builder": "yarn workspace builder cypress",
+ "cypress:viewer": "yarn workspace builder cypress"
},
"devDependencies": {
+ "concurrently": "^6.4.0",
"dotenv-cli": "^4.1.0",
"npm-run-all": "^4.1.5"
}
diff --git a/packages/prisma/package.json b/packages/prisma/package.json
index bd7518289..fc9fc89b2 100644
--- a/packages/prisma/package.json
+++ b/packages/prisma/package.json
@@ -4,19 +4,14 @@
"devDependencies": {
"dotenv-cli": "^4.1.0",
"npm-run-all": "^4.1.5",
- "prisma": "^3.5.0",
+ "prisma": "latest",
"ts-node": "^10.4.0",
"typescript": "^4.5.2"
},
"dependencies": {
- "@prisma/client": "^3.5.0"
+ "@prisma/client": "latest"
},
"scripts": {
- "prisma": "dotenv -e ../../.env prisma",
- "dev": "run-s migrate generate build",
- "build": "dotenv -e ../../.env tsc --build",
- "migrate": "dotenv -e ../../.env prisma migrate dev",
- "push": "dotenv -e ../../.env prisma db push",
- "generate": "dotenv -e ../../.env prisma generate"
+ "prisma": "dotenv -e ../../.env prisma"
}
}
diff --git a/packages/prisma/prisma/migrations/20211206144727_add_barebones/migration.sql b/packages/prisma/prisma/migrations/20211206144727_add_barebones/migration.sql
new file mode 100644
index 000000000..b937a6fb5
--- /dev/null
+++ b/packages/prisma/prisma/migrations/20211206144727_add_barebones/migration.sql
@@ -0,0 +1,70 @@
+-- CreateEnum
+CREATE TYPE "Plan" AS ENUM ('FREE', 'PRO');
+
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "plan" "Plan" NOT NULL DEFAULT E'FREE';
+
+-- CreateTable
+CREATE TABLE "DashboardFolder" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT NOT NULL,
+ "ownerId" TEXT NOT NULL,
+ "parentFolderId" TEXT,
+
+ CONSTRAINT "DashboardFolder_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Typebot" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT NOT NULL,
+ "ownerId" TEXT NOT NULL,
+ "publishedTypebotId" TEXT,
+ "folderId" TEXT,
+
+ CONSTRAINT "Typebot_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PublicTypebot" (
+ "id" TEXT NOT NULL,
+ "typebotId" TEXT NOT NULL,
+ "steps" JSONB[],
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "PublicTypebot_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Result" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "typebotId" TEXT NOT NULL,
+
+ CONSTRAINT "Result_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PublicTypebot_typebotId_key" ON "PublicTypebot"("typebotId");
+
+-- AddForeignKey
+ALTER TABLE "DashboardFolder" ADD CONSTRAINT "DashboardFolder_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DashboardFolder" ADD CONSTRAINT "DashboardFolder_parentFolderId_fkey" FOREIGN KEY ("parentFolderId") REFERENCES "DashboardFolder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Typebot" ADD CONSTRAINT "Typebot_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Typebot" ADD CONSTRAINT "Typebot_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "DashboardFolder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PublicTypebot" ADD CONSTRAINT "PublicTypebot_typebotId_fkey" FOREIGN KEY ("typebotId") REFERENCES "Typebot"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Result" ADD CONSTRAINT "Result_typebotId_fkey" FOREIGN KEY ("typebotId") REFERENCES "Typebot"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/prisma/prisma/schema.draft.prisma b/packages/prisma/prisma/schema.draft.prisma
new file mode 100644
index 000000000..d95c6a640
--- /dev/null
+++ b/packages/prisma/prisma/schema.draft.prisma
@@ -0,0 +1,87 @@
+datasource db {
+ url = env("DATABASE_URL")
+ provider = "postgresql"
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+model User {
+ id String @id
+ createdAt DateTime @default(now())
+ email String @unique
+ name String?
+ avatarUrl String?
+ redeemedCoupon Boolean?
+ oAuthCredentials Json?
+ referralId String?
+ domains String[]
+ onboarding_data Json?
+ settings Json
+ typebots Typebot[] @relation("Owner")
+ sharedTypebots Typebot[] @relation("Collaborators")
+ dashboardFolders DashboardFolder[]
+}
+
+model DashboardFolder {
+ id BigInt @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ name String
+ owner User @relation(fields: [ownerId], references: [id])
+ ownerId String
+ parentFolderId BigInt
+ parentFolder DashboardFolder @relation("ParentChild", fields: [parentFolderId], references: [id])
+ childrenFolder DashboardFolder[] @relation("ParentChild")
+}
+
+model Typebot {
+ id BigInt @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now())
+ steps Json[]
+ publishedTypebotId BigInt @unique
+ publishedTypebot PublicTypebot @relation(fields: [publishedTypebotId], references: [id])
+ connectors Json[]
+ name String
+ ownerId String
+ owner User @relation("Owner", fields: [ownerId], references: [id])
+ conditions Json
+ startConditions Json
+ theme Json
+ settings Json
+ collaborators User[] @relation("Collaborators")
+ customDomains String[]
+ shareSettings Json
+ variables Json
+ checkedConversionRules String[]
+ results Result[]
+ httpRequests Json[]
+ credentials Json[]
+}
+
+model PublicTypebot {
+ id BigInt @id @default(autoincrement())
+ typebot Typebot?
+ steps Json[]
+ name String
+ conditions Json
+ startConditions Json
+ theme Json
+ settings Json
+ connectors Json
+ customDomains String[]
+ shareSettings Json
+ variables Json
+}
+
+model Result {
+ id BigInt @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now())
+ typebotId BigInt
+ typebot Typebot @relation(fields: [typebotId], references: [id])
+ variables Json[]
+ isCompleted Boolean
+ answers Json[]
+}
diff --git a/packages/prisma/prisma/schema.prisma b/packages/prisma/prisma/schema.prisma
index c1c7f97c5..403195070 100644
--- a/packages/prisma/prisma/schema.prisma
+++ b/packages/prisma/prisma/schema.prisma
@@ -4,24 +4,24 @@ datasource db {
}
generator client {
- provider = "prisma-client-js"
+ provider = "prisma-client-js"
}
model Account {
- id String @id @default(cuid())
- userId String
- type String
- provider String
- providerAccountId String
- refresh_token String?
- access_token String?
- expires_at Int?
- token_type String?
- scope String?
- id_token String?
- session_state String?
- oauth_token_secret String?
- oauth_token String?
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String?
+ access_token String?
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String?
+ session_state String?
+ oauth_token_secret String?
+ oauth_token String?
refresh_token_expires_in Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -38,13 +38,21 @@ model Session {
}
model User {
- id String @id @default(cuid())
+ id String @id @default(cuid())
name String?
- email String? @unique
+ email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
+ typebots Typebot[]
+ folders DashboardFolder[]
+ plan Plan @default(FREE)
+}
+
+enum Plan {
+ FREE
+ PRO
}
model VerificationToken {
@@ -53,4 +61,46 @@ model VerificationToken {
expires DateTime
@@unique([identifier, token])
-}
\ No newline at end of file
+}
+
+model DashboardFolder {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ name String
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ ownerId String
+ parentFolderId String?
+ parentFolder DashboardFolder? @relation("ParentChild", fields: [parentFolderId], references: [id])
+ childrenFolder DashboardFolder[] @relation("ParentChild")
+ typebots Typebot[]
+}
+
+model Typebot {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now())
+ name String
+ ownerId String
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
+ publishedTypebotId String?
+ publishedTypebot PublicTypebot?
+ results Result[]
+ folderId String?
+ folder DashboardFolder? @relation(fields: [folderId], references: [id])
+}
+
+model PublicTypebot {
+ id String @id @default(cuid())
+ typebotId String @unique
+ typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
+ steps Json[]
+ name String
+}
+
+model Result {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now())
+ typebotId String
+ typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
+}
diff --git a/yarn.lock b/yarn.lock
index 7e6b68a3e..7079e8abc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -287,7 +287,7 @@ __metadata:
languageName: node
linkType: hard
-"@chakra-ui/css-reset@npm:1.1.1":
+"@chakra-ui/css-reset@npm:1.1.1, @chakra-ui/css-reset@npm:^1.1.1":
version: 1.1.1
resolution: "@chakra-ui/css-reset@npm:1.1.1"
peerDependencies:
@@ -1010,6 +1010,42 @@ __metadata:
languageName: node
linkType: hard
+"@dnd-kit/accessibility@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "@dnd-kit/accessibility@npm:3.0.0"
+ dependencies:
+ tslib: ^2.0.0
+ peerDependencies:
+ react: ">=16.8.0"
+ checksum: feac6afea07ece1568df38973d7a1a1b994782a6df119983250c3849ca2a3818a7bcc67fe4c5d22b0a1cfde866081644d677d2188abed507b3eccc255da79320
+ languageName: node
+ linkType: hard
+
+"@dnd-kit/core@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "@dnd-kit/core@npm:4.0.3"
+ dependencies:
+ "@dnd-kit/accessibility": ^3.0.0
+ "@dnd-kit/utilities": ^3.0.1
+ tslib: ^2.0.0
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: ">=16.8.0"
+ checksum: 828609317504f32bf37238840e5d2ae545efaaa779d696fdad1606d2ae22ae5668a43466db007efde89c8c1ef04ca66b2c61b6e98465f9487ec37c8e3d11a1b2
+ languageName: node
+ linkType: hard
+
+"@dnd-kit/utilities@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "@dnd-kit/utilities@npm:3.0.1"
+ dependencies:
+ tslib: ^2.0.0
+ peerDependencies:
+ react: ">=16.8.0"
+ checksum: 01ffe77bf98707e2e0e949b4425caf6a237ad4fc07fe15f9f0e4ab7c710d81ba2190915549c5302386cb55517e7be4bea6560dfb7eba0a516fa46719f837ec57
+ languageName: node
+ linkType: hard
+
"@emotion/babel-plugin@npm:^11.3.0":
version: 11.3.0
resolution: "@emotion/babel-plugin@npm:11.3.0"
@@ -1529,31 +1565,31 @@ __metadata:
languageName: node
linkType: hard
-"@prisma/client@npm:^3.5.0":
- version: 3.5.0
- resolution: "@prisma/client@npm:3.5.0"
+"@prisma/client@npm:latest":
+ version: 3.6.0
+ resolution: "@prisma/client@npm:3.6.0"
dependencies:
- "@prisma/engines-version": 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e
+ "@prisma/engines-version": 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727
peerDependencies:
prisma: "*"
peerDependenciesMeta:
prisma:
optional: true
- checksum: 515880b22c4df43185cefb17fe34968c9ba51cf4fead39e2a9bd9da7ba58a3303194ca0d81fcee27e9a14caa017d3f92cffdcdb4e90fa912ba1ca9508991503d
+ checksum: 83835117d60d100cf0933f60933603f5b85833414ad4dae9ea6b18f99190030317de459bbb573e928f764b476733676722fd90637292defae887dd974a2914a1
languageName: node
linkType: hard
-"@prisma/engines-version@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e":
- version: 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e
- resolution: "@prisma/engines-version@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e"
- checksum: 2919f44abc369ec48c8b2c0169293934ba7e2cff6e2bf29a86731b9e1a8c6bad8a3ffc41e604187ea75cfbb04b138bad7e4fe8ec7b557b437e0e2264ef9806df
+"@prisma/engines-version@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727":
+ version: 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727
+ resolution: "@prisma/engines-version@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727"
+ checksum: 40adea282d62b7d8d03696f3f9f1c6da271671955f5305d3750d07f80eff7bcfad89430e97215183d027895487c4ab36373fc8f59df1a537eb2ab18458168993
languageName: node
linkType: hard
-"@prisma/engines@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e":
- version: 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e
- resolution: "@prisma/engines@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e"
- checksum: 2a31da94fef3c497d66ae77174c1a6627e7aae63039b01cbd4f2dbc4b44198dd4932af48a013566bb54ed173dd948d75b8d47bfb4f139551cc685f2e2cf2f0ad
+"@prisma/engines@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727":
+ version: 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727
+ resolution: "@prisma/engines@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727"
+ checksum: 3eb1e40dbd6ca8e8bd727048393b9fd993b415bcc1efca2f6bac5b0a60da8e73c2dd35602c4083b29fc31fd17a2cc9ba0522afe1b6499a68663ecdf5f833cdb3
languageName: node
linkType: hard
@@ -1689,10 +1725,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "@typebot/prisma@workspace:packages/prisma"
dependencies:
- "@prisma/client": ^3.5.0
+ "@prisma/client": latest
dotenv-cli: ^4.1.0
npm-run-all: ^4.1.5
- prisma: ^3.5.0
+ prisma: latest
ts-node: ^10.4.0
typescript: ^4.5.2
languageName: unknown
@@ -2663,7 +2699,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "builder@workspace:apps/builder"
dependencies:
+ "@chakra-ui/css-reset": ^1.1.1
"@chakra-ui/react": ^1.7.2
+ "@dnd-kit/core": ^4.0.3
"@emotion/react": ^11
"@emotion/styled": ^11
"@next-auth/prisma-adapter": next
@@ -2681,6 +2719,7 @@ __metadata:
eslint-config-prettier: ^8.3.0
eslint-plugin-cypress: ^2.12.1
eslint-plugin-prettier: ^4.0.0
+ focus-visible: ^5.2.0
framer-motion: ^4
next: ^12.0.4
next-auth: beta
@@ -2689,7 +2728,9 @@ __metadata:
prettier: ^2.4.1
react: ^17.0.2
react-dom: ^17.0.2
+ swr: ^1.0.1
typescript: ^4.5.2
+ use-debounce: ^7.0.1
languageName: unknown
linkType: soft
@@ -2899,6 +2940,17 @@ __metadata:
languageName: node
linkType: hard
+"cliui@npm:^7.0.2":
+ version: 7.0.4
+ resolution: "cliui@npm:7.0.4"
+ dependencies:
+ string-width: ^4.2.0
+ strip-ansi: ^6.0.0
+ wrap-ansi: ^7.0.0
+ checksum: ce2e8f578a4813806788ac399b9e866297740eecd4ad1823c27fd344d78b22c5f8597d548adbcc46f0573e43e21e751f39446c5a5e804a12aace402b7a315d7f
+ languageName: node
+ linkType: hard
+
"code-point-at@npm:^1.0.0":
version: 1.1.0
resolution: "code-point-at@npm:1.1.0"
@@ -3015,6 +3067,24 @@ __metadata:
languageName: node
linkType: hard
+"concurrently@npm:^6.4.0":
+ version: 6.4.0
+ resolution: "concurrently@npm:6.4.0"
+ dependencies:
+ chalk: ^4.1.0
+ date-fns: ^2.16.1
+ lodash: ^4.17.21
+ rxjs: ^6.6.3
+ spawn-command: ^0.0.2-1
+ supports-color: ^8.1.0
+ tree-kill: ^1.2.2
+ yargs: ^16.2.0
+ bin:
+ concurrently: bin/concurrently.js
+ checksum: 902864cc853176cac406246fa367a1b24ebfcbea9ba43c164f9cb4e2c9c1a5f9d8be05ce98fe7ef13329ffd5cc340a052007a9c870863e2068425d95b3bf89d7
+ languageName: node
+ linkType: hard
+
"console-control-strings@npm:^1.0.0, console-control-strings@npm:~1.1.0":
version: 1.1.0
resolution: "console-control-strings@npm:1.1.0"
@@ -3316,6 +3386,13 @@ __metadata:
languageName: node
linkType: hard
+"date-fns@npm:^2.16.1":
+ version: 2.27.0
+ resolution: "date-fns@npm:2.27.0"
+ checksum: db62036b3816eb0322c9532b353beac7f660a91e1a55dbd21c14893c621ebb8509f21c66ba287844dabd34dee0207edd54a9537bce6bb7cab9383dedc6b8bc90
+ languageName: node
+ linkType: hard
+
"dayjs@npm:^1.10.4":
version: 1.10.7
resolution: "dayjs@npm:1.10.7"
@@ -3390,6 +3467,13 @@ __metadata:
languageName: node
linkType: hard
+"dequal@npm:2.0.2":
+ version: 2.0.2
+ resolution: "dequal@npm:2.0.2"
+ checksum: 86c7a2c59f7b0797ed397c74b5fcdb744e48fc19440b70ad6ac59f57550a96b0faef3f1cfd5760ec5e6d3f7cb101f634f1f80db4e727b1dc8389bf62d977c0a0
+ languageName: node
+ linkType: hard
+
"des.js@npm:^1.0.0":
version: 1.0.1
resolution: "des.js@npm:1.0.1"
@@ -4260,6 +4344,13 @@ __metadata:
languageName: node
linkType: hard
+"focus-visible@npm:^5.2.0":
+ version: 5.2.0
+ resolution: "focus-visible@npm:5.2.0"
+ checksum: 876f646ef453680d3d34e9f9b23961527ffd5ccaed8690f423d4fbfa37ff023d98a490972bc1387850e37ec2e44958c81f6096ef95b67462e5c4b5404cf1dbb9
+ languageName: node
+ linkType: hard
+
"foreach@npm:^2.0.5":
version: 2.0.5
resolution: "foreach@npm:2.0.5"
@@ -4391,6 +4482,13 @@ __metadata:
languageName: node
linkType: hard
+"get-caller-file@npm:^2.0.5":
+ version: 2.0.5
+ resolution: "get-caller-file@npm:2.0.5"
+ checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9
+ languageName: node
+ linkType: hard
+
"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1":
version: 1.1.1
resolution: "get-intrinsic@npm:1.1.1"
@@ -6618,15 +6716,15 @@ __metadata:
languageName: node
linkType: hard
-"prisma@npm:^3.5.0":
- version: 3.5.0
- resolution: "prisma@npm:3.5.0"
+"prisma@npm:latest":
+ version: 3.6.0
+ resolution: "prisma@npm:3.6.0"
dependencies:
- "@prisma/engines": 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e
+ "@prisma/engines": 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727
bin:
prisma: build/index.js
prisma2: build/index.js
- checksum: 5ac2558dc29a8c325d991e59de2208bf6756bddb4fa2d3d97cd17aa9daa94611515d3ab284777adcda2c7b918b0624316e7081bae5d1c01b449738b8ebf81882
+ checksum: 365718b1e0ed8491aedb79bda39c500e8fddae0d62f57ed0ab3786c6709bd79d43b61c3603847b165b5a3de1e983b64388902a9ed55d4d5cb5878d67af23da8b
languageName: node
linkType: hard
@@ -7040,6 +7138,13 @@ __metadata:
languageName: node
linkType: hard
+"require-directory@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "require-directory@npm:2.1.1"
+ checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80
+ languageName: node
+ linkType: hard
+
"require-from-string@npm:^2.0.2":
version: 2.0.2
resolution: "require-from-string@npm:2.0.2"
@@ -7166,6 +7271,15 @@ __metadata:
languageName: node
linkType: hard
+"rxjs@npm:^6.6.3":
+ version: 6.6.7
+ resolution: "rxjs@npm:6.6.7"
+ dependencies:
+ tslib: ^1.9.0
+ checksum: bc334edef1bb8bbf56590b0b25734ba0deaf8825b703256a93714308ea36dff8a11d25533671adf8e104e5e8f256aa6fdfe39b2e248cdbd7a5f90c260acbbd1b
+ languageName: node
+ linkType: hard
+
"rxjs@npm:^7.4.0":
version: 7.4.0
resolution: "rxjs@npm:7.4.0"
@@ -7412,6 +7526,13 @@ __metadata:
languageName: node
linkType: hard
+"spawn-command@npm:^0.0.2-1":
+ version: 0.0.2
+ resolution: "spawn-command@npm:0.0.2"
+ checksum: e35c5d28177b4d461d33c88cc11f6f3a5079e2b132c11e1746453bbb7a0c0b8a634f07541a2a234fa4758239d88203b758def509161b651e81958894c0b4b64b
+ languageName: node
+ linkType: hard
+
"spdx-correct@npm:^3.0.0":
version: 3.1.1
resolution: "spdx-correct@npm:3.1.1"
@@ -7735,7 +7856,7 @@ __metadata:
languageName: node
linkType: hard
-"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1":
+"supports-color@npm:^8.0.0, supports-color@npm:^8.1.0, supports-color@npm:^8.1.1":
version: 8.1.1
resolution: "supports-color@npm:8.1.1"
dependencies:
@@ -7744,6 +7865,17 @@ __metadata:
languageName: node
linkType: hard
+"swr@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "swr@npm:1.0.1"
+ dependencies:
+ dequal: 2.0.2
+ peerDependencies:
+ react: ^16.11.0 || ^17.0.0
+ checksum: 8aaa10c4c65cb9b46a143a52ac2728111fc8af96e83781df1f7b7d56aa027ef720b7feb230658616e479f224f684d4cbc5d2ca3265c40f95a3140dbdba801061
+ languageName: node
+ linkType: hard
+
"table@npm:^6.0.9":
version: 6.7.3
resolution: "table@npm:6.7.3"
@@ -7873,6 +8005,15 @@ __metadata:
languageName: node
linkType: hard
+"tree-kill@npm:^1.2.2":
+ version: 1.2.2
+ resolution: "tree-kill@npm:1.2.2"
+ bin:
+ tree-kill: cli.js
+ checksum: 49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7
+ languageName: node
+ linkType: hard
+
"ts-node@npm:^10.4.0":
version: 10.4.0
resolution: "ts-node@npm:10.4.0"
@@ -7921,14 +8062,14 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:^1.0.0, tslib@npm:^1.8.1, tslib@npm:^1.9.3":
+"tslib@npm:^1.0.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd
languageName: node
linkType: hard
-"tslib@npm:^2.0.3, tslib@npm:^2.1.0":
+"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0":
version: 2.3.1
resolution: "tslib@npm:2.3.1"
checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9
@@ -8010,6 +8151,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "typebot-os@workspace:."
dependencies:
+ concurrently: ^6.4.0
dotenv-cli: ^4.1.0
npm-run-all: ^4.1.5
languageName: unknown
@@ -8125,6 +8267,15 @@ __metadata:
languageName: node
linkType: hard
+"use-debounce@npm:^7.0.1":
+ version: 7.0.1
+ resolution: "use-debounce@npm:7.0.1"
+ peerDependencies:
+ react: ">=16.8.0"
+ checksum: c5c1c34ccef6c11fe25fecef983a07f5a7260fce77b58f0f648f6ae08693a22a9077b5362b96154896e9ea756e6aaea303eb44550a293456cecb6f15cf87d73b
+ languageName: node
+ linkType: hard
+
"use-sidecar@npm:^1.0.1, use-sidecar@npm:^1.0.5":
version: 1.0.5
resolution: "use-sidecar@npm:1.0.5"
@@ -8380,6 +8531,13 @@ __metadata:
languageName: node
linkType: hard
+"y18n@npm:^5.0.5":
+ version: 5.0.8
+ resolution: "y18n@npm:5.0.8"
+ checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30
+ languageName: node
+ linkType: hard
+
"yallist@npm:^4.0.0":
version: 4.0.0
resolution: "yallist@npm:4.0.0"
@@ -8394,6 +8552,28 @@ __metadata:
languageName: node
linkType: hard
+"yargs-parser@npm:^20.2.2":
+ version: 20.2.9
+ resolution: "yargs-parser@npm:20.2.9"
+ checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3
+ languageName: node
+ linkType: hard
+
+"yargs@npm:^16.2.0":
+ version: 16.2.0
+ resolution: "yargs@npm:16.2.0"
+ dependencies:
+ cliui: ^7.0.2
+ escalade: ^3.1.1
+ get-caller-file: ^2.0.5
+ require-directory: ^2.1.1
+ string-width: ^4.2.0
+ y18n: ^5.0.5
+ yargs-parser: ^20.2.2
+ checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59
+ languageName: node
+ linkType: hard
+
"yauzl@npm:^2.10.0":
version: 2.10.0
resolution: "yauzl@npm:2.10.0"