Add authentication
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{js,json,yml}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
27
.env.example
Normal file
27
.env.example
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
DATABASE_URL=postgresql://username:password@host:5450/typebot?schema=public
|
||||||
|
|
||||||
|
# Used for email auth and email notifications
|
||||||
|
EMAIL_SERVER_USER=username
|
||||||
|
EMAIL_SERVER_PASSWORD=password
|
||||||
|
EMAIL_SERVER_HOST=smtp.example.com
|
||||||
|
EMAIL_SERVER_PORT=587
|
||||||
|
EMAIL_FROM=noreply@example.com
|
||||||
|
|
||||||
|
# AUTH
|
||||||
|
# (Optional) Used to login using GitHub
|
||||||
|
GITHUB_CLIENT_ID=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# (Optional) Used to login using Google
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# (Optional) Used to login using Facebook
|
||||||
|
FACEBOOK_CLIENT_ID=
|
||||||
|
FACEBOOK_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# Used for uploading images, videos, etc...
|
||||||
|
S3_UPLOAD_KEY=
|
||||||
|
S3_UPLOAD_SECRET=
|
||||||
|
S3_UPLOAD_REGION=
|
||||||
|
S3_UPLOAD_BUCKET=
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -24,5 +24,10 @@ node_modules
|
|||||||
|
|
||||||
.next
|
.next
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
cypress.env.json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
64
README.md
64
README.md
@ -1,6 +1,60 @@
|
|||||||
# Getting Started
|
<div align="center">
|
||||||
|
<h1>Typebot</h1>
|
||||||
|
<a href="https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" /></a>
|
||||||
|
<a href="https://github.com/prisma/prisma/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL-blue" /></a>
|
||||||
|
<br />
|
||||||
|
<a href="https://docs.typebot.io">Quickstart</a>
|
||||||
|
<span> • </span>
|
||||||
|
<a href="https://www.typebot.io/">Website</a>
|
||||||
|
<span> • </span>
|
||||||
|
<a href="https://docs.typebot.io">Docs</a>
|
||||||
|
<span> • </span>
|
||||||
|
<a href="https://www.typebot.io/blog">Blog</a>
|
||||||
|
<span> • </span>
|
||||||
|
<a href="https://twitter.com/Typebot_io">Twitter</a>
|
||||||
|
<br />
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
```sh
|
## Development (localhost)
|
||||||
yarn set version berry
|
|
||||||
yarn install
|
1. Clone the repo
|
||||||
```
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/Typebot-io/typebot.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install packages with yarn
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn set version berry
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Copy `.env.example` to `.env`
|
||||||
|
4. Configure environment variables in the `.env` file.
|
||||||
|
5. Setup the database
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
TO-DO
|
||||||
|
32
apps/builder/.eslintrc.js
Normal file
32
apps/builder/.eslintrc.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['node_modules'],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'next/core-web-vitals',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
|
||||||
|
sourceType: 'module', // Allows for the use of imports
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true, // Allows for the parsing of JSX
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: ['prettier', 'react', 'cypress', '@typescript-eslint'],
|
||||||
|
rules: {
|
||||||
|
'react/no-unescaped-entities': [0],
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'react/display-name': [0],
|
||||||
|
},
|
||||||
|
}
|
1
apps/builder/assets/icons.tsx
Normal file
1
apps/builder/assets/icons.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
import { IconProps, Icon } from '@chakra-ui/react'
|
69
apps/builder/assets/logos.tsx
Normal file
69
apps/builder/assets/logos.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { IconProps, Icon } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
export const TypebotLogo = ({
|
||||||
|
isDark,
|
||||||
|
...props
|
||||||
|
}: { isDark?: boolean } & IconProps) => (
|
||||||
|
<Icon viewBox="0 0 500 500" w="50px" h="50px" {...props}>
|
||||||
|
<rect
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
|
rx="75"
|
||||||
|
fill={isDark ? 'white' : '#0042DA'}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="438.709"
|
||||||
|
y="170.968"
|
||||||
|
width="64.5161"
|
||||||
|
height="290.323"
|
||||||
|
rx="32.2581"
|
||||||
|
transform="rotate(90 438.709 170.968)"
|
||||||
|
fill="#FF8E20"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M93.5481 235.484C111.364 235.484 125.806 221.041 125.806 203.226C125.806 185.41 111.364 170.968 93.5481 170.968C75.7325 170.968 61.29 185.41 61.29 203.226C61.29 221.041 75.7325 235.484 93.5481 235.484Z"
|
||||||
|
fill="#FF8E20"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="61.29"
|
||||||
|
y="332.259"
|
||||||
|
width="64.5161"
|
||||||
|
height="290.323"
|
||||||
|
rx="32.2581"
|
||||||
|
transform="rotate(-90 61.29 332.259)"
|
||||||
|
fill={isDark ? '#0042DA' : 'white'}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M406.451 267.742C388.635 267.742 374.193 282.184 374.193 300C374.193 317.815 388.635 332.258 406.451 332.258C424.267 332.258 438.709 317.815 438.709 300C438.709 282.184 424.267 267.742 406.451 267.742Z"
|
||||||
|
fill={isDark ? '#0042DA' : 'white'}
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const GithubLogo = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 512 512" {...props}>
|
||||||
|
<title>{'Logo Github'}</title>
|
||||||
|
<path d="M256 32C132.3 32 32 134.9 32 261.7c0 101.5 64.2 187.5 153.2 217.9a17.56 17.56 0 003.8.4c8.3 0 11.5-6.1 11.5-11.4 0-5.5-.2-19.9-.3-39.1a102.4 102.4 0 01-22.6 2.7c-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1a63 63 0 0025.6-6c2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6-2.3-5.8-10-29.2 2.2-60.8a18.64 18.64 0 015-.5c8.1 0 26.4 3.1 56.6 24.1a208.21 208.21 0 01112.2 0c30.2-21 48.5-24.1 56.6-24.1a18.64 18.64 0 015 .5c12.2 31.6 4.5 55 2.2 60.8 14.3 16.1 23 36.6 23 61.6 0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5a19.35 19.35 0 004-.4C415.9 449.2 480 363.1 480 261.7 480 134.9 379.7 32 256 32z" />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const GoogleLogo = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 512 512" {...props}>
|
||||||
|
<title>{'Logo Google'}</title>
|
||||||
|
<path d="M473.16 221.48l-2.26-9.59H262.46v88.22H387c-12.93 61.4-72.93 93.72-121.94 93.72-35.66 0-73.25-15-98.13-39.11a140.08 140.08 0 01-41.8-98.88c0-37.16 16.7-74.33 41-98.78s61-38.13 97.49-38.13c41.79 0 71.74 22.19 82.94 32.31l62.69-62.36C390.86 72.72 340.34 32 261.6 32c-60.75 0-119 23.27-161.58 65.71C58 139.5 36.25 199.93 36.25 256s20.58 113.48 61.3 155.6c43.51 44.92 105.13 68.4 168.58 68.4 57.73 0 112.45-22.62 151.45-63.66 38.34-40.4 58.17-96.3 58.17-154.9 0-24.67-2.48-39.32-2.59-39.96z" />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const FacebookLogo = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 512 512" {...props}>
|
||||||
|
<title>Logo Facebook</title>
|
||||||
|
<path
|
||||||
|
d="M480 257.35c0-123.7-100.3-224-224-224s-224 100.3-224 224c0 111.8 81.9 204.47 189 221.29V322.12h-56.89v-64.77H221V208c0-56.13 33.45-87.16 84.61-87.16 24.51 0 50.15 4.38 50.15 4.38v55.13H327.5c-27.81 0-36.51 17.26-36.51 35v42h62.12l-9.92 64.77H291v156.54c107.1-16.81 189-109.48 189-221.31z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
)
|
81
apps/builder/assets/styles/routerProgressBar.css
Normal file
81
apps/builder/assets/styles/routerProgressBar.css
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/* Make clicks pass-through */
|
||||||
|
#nprogress {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
background: #0042da;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1031;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fancy blur effect */
|
||||||
|
#nprogress .peg {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0 0 10px #0042da, 0 0 5px #0042da;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove these to get rid of the spinner */
|
||||||
|
#nprogress .spinner {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1031;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .spinner-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-top-color: #0042da;
|
||||||
|
border-left-color: #0042da;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||||
|
animation: nprogress-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nprogress-custom-parent {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nprogress-custom-parent #nprogress .spinner,
|
||||||
|
.nprogress-custom-parent #nprogress .bar {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes nprogress-spinner {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes nprogress-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
27
apps/builder/components/HOC/withUser.tsx
Normal file
27
apps/builder/components/HOC/withUser.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const { data: session, status } = useSession()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) return
|
||||||
|
if (status === 'loading') return
|
||||||
|
if (status === 'unauthenticated') router.replace('/signin')
|
||||||
|
}, [status, router])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WrappedComponent user={session?.user as User | undefined} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth
|
33
apps/builder/components/Seo.tsx
Normal file
33
apps/builder/components/Seo.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Head from 'next/head'
|
||||||
|
|
||||||
|
export const Seo = ({
|
||||||
|
title,
|
||||||
|
currentUrl = 'https://app.typebot.io',
|
||||||
|
description = 'Create and publish conversational forms that collect 4 times more answers and feel native to your product',
|
||||||
|
imagePreviewUrl = 'https://app.typebot.io/site-preview.png',
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
currentUrl?: string
|
||||||
|
imagePreviewUrl?: string
|
||||||
|
}) => (
|
||||||
|
<Head>
|
||||||
|
<title>Typebot | {title}</title>
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="twitter:title" content={title} />
|
||||||
|
|
||||||
|
<meta property="twitter:url" content={currentUrl} />
|
||||||
|
<meta property="og:url" content={currentUrl} />
|
||||||
|
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta property="twitter:description" content={description} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
|
||||||
|
<meta property="og:image" content={imagePreviewUrl} />
|
||||||
|
<meta property="twitter:image" content={imagePreviewUrl} />
|
||||||
|
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
</Head>
|
||||||
|
)
|
26
apps/builder/components/auth/AuthSwitcher.tsx
Normal file
26
apps/builder/components/auth/AuthSwitcher.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { NextChakraLink } from '../nextChakra/NextChakraLink'
|
||||||
|
import { Text } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: 'register' | 'signin'
|
||||||
|
}
|
||||||
|
export const AuthSwitcher = ({ type }: Props) => (
|
||||||
|
<>
|
||||||
|
{type === 'signin' ? (
|
||||||
|
<Text>
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<NextChakraLink href="/register" color="blue.500" textDecor="underline">
|
||||||
|
Sign up for free
|
||||||
|
</NextChakraLink>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
Already have an account?{' '}
|
||||||
|
<NextChakraLink href="/signin" color="blue.500" textDecor="underline">
|
||||||
|
Sign in
|
||||||
|
</NextChakraLink>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
25
apps/builder/components/auth/DividerWithText.tsx
Normal file
25
apps/builder/components/auth/DividerWithText.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { FlexProps, Flex, Box, Divider, Text } from '@chakra-ui/react'
|
||||||
|
import { useColorModeValue } from '@chakra-ui/system'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const DividerWithText = (props: FlexProps) => {
|
||||||
|
const { children, ...flexProps } = props
|
||||||
|
return (
|
||||||
|
<Flex align="center" color="gray.300" {...flexProps}>
|
||||||
|
<Box flex="1">
|
||||||
|
<Divider borderColor="currentcolor" />
|
||||||
|
</Box>
|
||||||
|
<Text
|
||||||
|
as="span"
|
||||||
|
px="3"
|
||||||
|
color={useColorModeValue('gray.600', 'gray.400')}
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
<Box flex="1">
|
||||||
|
<Divider borderColor="currentcolor" />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
76
apps/builder/components/auth/SignInForm.tsx
Normal file
76
apps/builder/components/auth/SignInForm.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
HTMLChakraProps,
|
||||||
|
Input,
|
||||||
|
Stack,
|
||||||
|
HStack,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import React, { ChangeEvent, FormEvent, useEffect } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { signIn, useSession } from 'next-auth/react'
|
||||||
|
import { DividerWithText } from './DividerWithText'
|
||||||
|
import { SocialLoginButtons } from './SocialLoginButtons'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultEmail?: string
|
||||||
|
}
|
||||||
|
export const SignInForm = ({
|
||||||
|
defaultEmail,
|
||||||
|
}: Props & HTMLChakraProps<'form'>) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const { status } = useSession()
|
||||||
|
const [authLoading, setAuthLoading] = useState(false)
|
||||||
|
const [emailValue, setEmailValue] = useState(defaultEmail ?? '')
|
||||||
|
const toast = useToast({
|
||||||
|
position: 'top-right',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'authenticated') router.replace('/typebots')
|
||||||
|
}, [status, router])
|
||||||
|
|
||||||
|
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setEmailValue(e.target.value)
|
||||||
|
|
||||||
|
const handleEmailSubmit = async (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setAuthLoading(true)
|
||||||
|
await signIn('email', {
|
||||||
|
email: emailValue,
|
||||||
|
redirect: false,
|
||||||
|
})
|
||||||
|
toast({
|
||||||
|
status: 'success',
|
||||||
|
title: 'Success!',
|
||||||
|
description: 'Check your inbox to sign in',
|
||||||
|
})
|
||||||
|
setAuthLoading(false)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack spacing="4">
|
||||||
|
<SocialLoginButtons />
|
||||||
|
<DividerWithText mt="6">Or with your email</DividerWithText>
|
||||||
|
<HStack as="form" onSubmit={handleEmailSubmit}>
|
||||||
|
<Input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
placeholder="email@company.com"
|
||||||
|
required
|
||||||
|
value={emailValue}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={
|
||||||
|
['loading', 'authenticated'].includes(status) || authLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
46
apps/builder/components/auth/SocialLoginButtons.tsx
Normal file
46
apps/builder/components/auth/SocialLoginButtons.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { FacebookLogo, GithubLogo, GoogleLogo } from 'assets/logos'
|
||||||
|
import { Stack, Button } from '@chakra-ui/react'
|
||||||
|
import { signIn, useSession } from 'next-auth/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const SocialLoginButtons = () => {
|
||||||
|
const { status } = useSession()
|
||||||
|
|
||||||
|
const handleGitHubClick = async () => signIn('github')
|
||||||
|
|
||||||
|
const handleGoogleClick = async () => signIn('google')
|
||||||
|
|
||||||
|
const handleFacebookClick = async () => signIn('facebook')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Button
|
||||||
|
leftIcon={<GithubLogo />}
|
||||||
|
colorScheme="gray"
|
||||||
|
onClick={handleGitHubClick}
|
||||||
|
data-testid="github"
|
||||||
|
isLoading={['loading', 'authenticated'].includes(status)}
|
||||||
|
>
|
||||||
|
Continue with GitHub
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftIcon={<GoogleLogo />}
|
||||||
|
colorScheme="gray"
|
||||||
|
onClick={handleGoogleClick}
|
||||||
|
data-testid="google"
|
||||||
|
isLoading={['loading', 'authenticated'].includes(status)}
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FacebookLogo />}
|
||||||
|
colorScheme="gray"
|
||||||
|
onClick={handleFacebookClick}
|
||||||
|
data-testid="facebook"
|
||||||
|
isLoading={['loading', 'authenticated'].includes(status)}
|
||||||
|
>
|
||||||
|
Continue with Facebook
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
47
apps/builder/components/nextChakra/NextChakraLink.tsx
Normal file
47
apps/builder/components/nextChakra/NextChakraLink.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import NextLink from 'next/link'
|
||||||
|
import { LinkProps as NextLinkProps } from 'next/dist/client/link'
|
||||||
|
import {
|
||||||
|
Link as ChakraLink,
|
||||||
|
LinkProps as ChakraLinkProps,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export type NextChakraLinkProps = PropsWithChildren<
|
||||||
|
NextLinkProps & Omit<ChakraLinkProps, 'href'>
|
||||||
|
>
|
||||||
|
|
||||||
|
export const NextChakraLink = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
NextChakraLinkProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
href,
|
||||||
|
replace,
|
||||||
|
scroll,
|
||||||
|
shallow,
|
||||||
|
prefetch,
|
||||||
|
children,
|
||||||
|
locale,
|
||||||
|
...chakraProps
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<NextLink
|
||||||
|
passHref={true}
|
||||||
|
href={href}
|
||||||
|
replace={replace}
|
||||||
|
scroll={scroll}
|
||||||
|
shallow={shallow}
|
||||||
|
prefetch={prefetch}
|
||||||
|
locale={locale}
|
||||||
|
>
|
||||||
|
<ChakraLink ref={ref} {...chakraProps}>
|
||||||
|
{children}
|
||||||
|
</ChakraLink>
|
||||||
|
</NextLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
7
apps/builder/cypress.json
Normal file
7
apps/builder/cypress.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:3000",
|
||||||
|
"chromeWebSecurity": false,
|
||||||
|
"integrationFolder": "cypress/tests",
|
||||||
|
"viewportWidth": 1400,
|
||||||
|
"viewportHeight": 800
|
||||||
|
}
|
20
apps/builder/cypress/plugins/index.ts
Normal file
20
apps/builder/cypress/plugins/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
GitHubSocialLogin,
|
||||||
|
FacebookSocialLogin,
|
||||||
|
GoogleSocialLogin,
|
||||||
|
} from 'cypress-social-logins/src/Plugins'
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const handler = (on: any) => {
|
||||||
|
on('task', {
|
||||||
|
GoogleSocialLogin: GoogleSocialLogin,
|
||||||
|
FacebookSocialLogin: FacebookSocialLogin,
|
||||||
|
GitHubSocialLogin: GitHubSocialLogin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
37
apps/builder/cypress/support/commands.ts
Normal file
37
apps/builder/cypress/support/commands.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// ***********************************************
|
||||||
|
// 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'
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
33
apps/builder/cypress/support/index.ts
Normal file
33
apps/builder/cypress/support/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// 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<Response<any>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import './commands'
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
103
apps/builder/cypress/tests/auth.ts
Normal file
103
apps/builder/cypress/tests/auth.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
describe('SignIn page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.logOutByApi()
|
||||||
|
})
|
||||||
|
it('can continue with Google', () => {
|
||||||
|
cy.visit('/signin')
|
||||||
|
const username = Cypress.env('GOOGLE_USER')
|
||||||
|
const password = Cypress.env('GOOGLE_PW')
|
||||||
|
const loginUrl = Cypress.env('SITE_NAME')
|
||||||
|
const cookieName = Cypress.env('COOKIE_NAME')
|
||||||
|
exectueSocialLogin(
|
||||||
|
'GoogleSocialLogin',
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
loginUrl,
|
||||||
|
cookieName
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can continue with GitHub', () => {
|
||||||
|
cy.visit('/signin')
|
||||||
|
const username = Cypress.env('GITHUB_USER')
|
||||||
|
const password = Cypress.env('GITHUB_PW')
|
||||||
|
const loginUrl = Cypress.env('SITE_NAME')
|
||||||
|
const cookieName = Cypress.env('COOKIE_NAME')
|
||||||
|
exectueSocialLogin(
|
||||||
|
'GitHubSocialLogin',
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
loginUrl,
|
||||||
|
cookieName
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can continue with Facebook', () => {
|
||||||
|
cy.visit('/signin')
|
||||||
|
const username = Cypress.env('FACEBOOK_USER')
|
||||||
|
const password = Cypress.env('FACEBOOK_PW')
|
||||||
|
const loginUrl = Cypress.env('SITE_NAME')
|
||||||
|
const cookieName = Cypress.env('COOKIE_NAME')
|
||||||
|
exectueSocialLogin(
|
||||||
|
'FacebookSocialLogin',
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
loginUrl,
|
||||||
|
cookieName,
|
||||||
|
[
|
||||||
|
'button[data-testid="cookie-policy-dialog-manage-button"]',
|
||||||
|
'button[data-testid="cookie-policy-manage-dialog-accept-button"]',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// We don't test email sign in because disabling email sending is not straightforward
|
||||||
|
})
|
||||||
|
|
||||||
|
const exectueSocialLogin = (
|
||||||
|
task: 'FacebookSocialLogin' | 'GoogleSocialLogin' | 'GitHubSocialLogin',
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
loginUrl: string,
|
||||||
|
cookieName: string,
|
||||||
|
trackingConsentSelectors?: string[]
|
||||||
|
) => {
|
||||||
|
const selectorId =
|
||||||
|
task === 'FacebookSocialLogin'
|
||||||
|
? 'facebook'
|
||||||
|
: task === 'GoogleSocialLogin'
|
||||||
|
? 'google'
|
||||||
|
: 'github'
|
||||||
|
const socialLoginOptions = {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
loginUrl,
|
||||||
|
headless: true,
|
||||||
|
logs: true,
|
||||||
|
isPopup: false,
|
||||||
|
loginSelector: `[data-testid="${selectorId}"]`,
|
||||||
|
postLoginSelector: `[data-testid="authenticated"]`,
|
||||||
|
trackingConsentSelectors,
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.task(task, socialLoginOptions).then(({ cookies }: any) => {
|
||||||
|
const cookie = cookies
|
||||||
|
.filter((cookie: any) => cookie.name === cookieName)
|
||||||
|
.pop()
|
||||||
|
if (cookie) {
|
||||||
|
cy.setCookie(cookie.name, cookie.value, {
|
||||||
|
domain: cookie.domain,
|
||||||
|
expiry: cookie.expires,
|
||||||
|
httpOnly: cookie.httpOnly,
|
||||||
|
path: cookie.path,
|
||||||
|
secure: cookie.secure,
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Cookies.defaults({
|
||||||
|
preserve: cookieName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cy.visit('/typebots')
|
||||||
|
cy.findByText(`Hello ${username}`).should('exist')
|
||||||
|
})
|
||||||
|
}
|
13
apps/builder/cypress/tsconfig.json
Normal file
13
apps/builder/cypress/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": [],
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["cypress", "@testing-library/cypress"],
|
||||||
|
"lib": ["es2015", "dom"],
|
||||||
|
"target": "es5",
|
||||||
|
"isolatedModules": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
11
apps/builder/layouts/LoadingPage.tsx
Normal file
11
apps/builder/layouts/LoadingPage.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { TypebotLogo } from 'assets/logos'
|
||||||
|
import { Spinner, VStack } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
export const LoadingPage = () => (
|
||||||
|
<div className="flex h-screen items-center justify-center">
|
||||||
|
<VStack spacing={6}>
|
||||||
|
<TypebotLogo boxSize="80px" />
|
||||||
|
<Spinner />
|
||||||
|
</VStack>
|
||||||
|
</div>
|
||||||
|
)
|
83
apps/builder/libs/chakra.ts
Normal file
83
apps/builder/libs/chakra.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { extendTheme } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
const fonts = {
|
||||||
|
heading: 'Outfit',
|
||||||
|
body: 'Open Sans',
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
blue: {
|
||||||
|
50: '#e0edff',
|
||||||
|
100: '#b0caff',
|
||||||
|
200: '#7ea6ff',
|
||||||
|
300: '#4b83ff',
|
||||||
|
400: '#1a5fff',
|
||||||
|
500: '#0042da',
|
||||||
|
600: '#0036b4',
|
||||||
|
700: '#002782',
|
||||||
|
800: '#001751',
|
||||||
|
900: '#1a202c',
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
50: '#fff1da',
|
||||||
|
100: '#ffd7ae',
|
||||||
|
200: '#ffbf7d',
|
||||||
|
300: '#ffa54c',
|
||||||
|
400: '#ff8b1a',
|
||||||
|
500: '#e67200',
|
||||||
|
600: '#b45800',
|
||||||
|
700: '#813e00',
|
||||||
|
800: '#4f2500',
|
||||||
|
900: '#200b00',
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
50: '#fff9da',
|
||||||
|
100: '#ffedad',
|
||||||
|
200: '#ffe17d',
|
||||||
|
300: '#ffd54b',
|
||||||
|
400: '#ffc91a',
|
||||||
|
500: '#e6b000',
|
||||||
|
600: '#b38800',
|
||||||
|
700: '#806200',
|
||||||
|
800: '#4e3a00',
|
||||||
|
900: '#1d1400',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
Spinner: {
|
||||||
|
defaultProps: {
|
||||||
|
colorScheme: 'blue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
defaultProps: {
|
||||||
|
colorScheme: 'blue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NumberInput: {
|
||||||
|
defaultProps: {
|
||||||
|
focusBorderColor: 'blue.200',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
defaultProps: {
|
||||||
|
focusBorderColor: 'blue.200',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Popover: {
|
||||||
|
baseStyle: {
|
||||||
|
popper: {
|
||||||
|
width: 'fit-content',
|
||||||
|
maxWidth: 'fit-content',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Link: {
|
||||||
|
baseStyle: {
|
||||||
|
_hover: { textDecoration: 'none' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customTheme = extendTheme({ colors, fonts, components })
|
22
apps/builder/libs/routerProgressBar.tsx
Normal file
22
apps/builder/libs/routerProgressBar.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Router from 'next/router'
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export const useRouterProgressBar = () =>
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
|
Router.events.on('routeChangeStart', () => {
|
||||||
|
NProgress.start()
|
||||||
|
})
|
||||||
|
|
||||||
|
Router.events.on('routeChangeComplete', () => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
|
|
||||||
|
Router.events.on('routeChangeError', () => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
42
apps/builder/package.json
Normal file
42
apps/builder/package.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "builder",
|
||||||
|
"packageManager": "yarn@3.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "dotenv -e ../../.env next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"cypress": "cypress open"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@chakra-ui/react": "^1.7.2",
|
||||||
|
"@emotion/react": "^11",
|
||||||
|
"@emotion/styled": "^11",
|
||||||
|
"@next-auth/prisma-adapter": "next",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/cypress": "^8.0.2",
|
||||||
|
"@types/node": "^16.11.9",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@types/react": "^17.0.35",
|
||||||
|
"@types/testing-library__cypress": "^5.0.9",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
|
"cypress": "^9.1.0",
|
||||||
|
"cypress-social-logins": "^1.12.0",
|
||||||
|
"dotenv-cli": "^4.1.0",
|
||||||
|
"eslint": "<8.0.0",
|
||||||
|
"eslint-config-next": "12.0.4",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
|
}
|
||||||
|
}
|
21
apps/builder/pages/_app.tsx
Normal file
21
apps/builder/pages/_app.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
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'
|
||||||
|
|
||||||
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
|
useRouterProgressBar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraProvider theme={customTheme}>
|
||||||
|
<SessionProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</SessionProvider>
|
||||||
|
</ChakraProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
36
apps/builder/pages/_document.tsx
Normal file
36
apps/builder/pages/_document.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Document, {
|
||||||
|
Html,
|
||||||
|
Head,
|
||||||
|
Main,
|
||||||
|
NextScript,
|
||||||
|
DocumentContext,
|
||||||
|
} from 'next/document'
|
||||||
|
|
||||||
|
class MyDocument extends Document {
|
||||||
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
|
const initialProps = await Document.getInitialProps(ctx)
|
||||||
|
return { ...initialProps }
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyDocument
|
39
apps/builder/pages/api/auth/[...nextauth].ts
Normal file
39
apps/builder/pages/api/auth/[...nextauth].ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
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 ?? '',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
18
apps/builder/pages/index.tsx
Normal file
18
apps/builder/pages/index.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { GetServerSidePropsContext } from 'next'
|
||||||
|
import { getSession } from 'next-auth/react'
|
||||||
|
|
||||||
|
function RedirectPage() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async (
|
||||||
|
context: GetServerSidePropsContext
|
||||||
|
) => {
|
||||||
|
const session = await getSession(context)
|
||||||
|
if (!session?.user) {
|
||||||
|
return { redirect: { permanent: false, destination: '/signin' } }
|
||||||
|
}
|
||||||
|
return { redirect: { permanent: false, destination: '/typebots' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RedirectPage
|
19
apps/builder/pages/register.tsx
Normal file
19
apps/builder/pages/register.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { AuthSwitcher } from 'components/auth/AuthSwitcher'
|
||||||
|
import { SignInForm } from 'components/auth/SignInForm'
|
||||||
|
import { Heading, VStack } from '@chakra-ui/react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const RegisterPage = () => {
|
||||||
|
const { query } = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={4} h="100vh" justifyContent="center">
|
||||||
|
<Heading>Create an account</Heading>
|
||||||
|
<AuthSwitcher type="register" />
|
||||||
|
<SignInForm defaultEmail={query.g?.toString()} />
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterPage
|
16
apps/builder/pages/signin.tsx
Normal file
16
apps/builder/pages/signin.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { AuthSwitcher } from 'components/auth/AuthSwitcher'
|
||||||
|
import { SignInForm } from 'components/auth/SignInForm'
|
||||||
|
import { Heading, VStack } from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const SignInPage = () => {
|
||||||
|
return (
|
||||||
|
<VStack spacing={4} h="100vh" justifyContent="center">
|
||||||
|
<Heading>Sign in</Heading>
|
||||||
|
<AuthSwitcher type="signin" />
|
||||||
|
<SignInForm />
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignInPage
|
9
apps/builder/pages/typebots/index.tsx
Normal file
9
apps/builder/pages/typebots/index.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import withAuth, { withAuthProps } from 'components/HOC/withUser'
|
||||||
|
import { Text } from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const TypebotsPage = ({ user }: withAuthProps) => {
|
||||||
|
return <Text data-testid="authenticated">Hello {user?.email}</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(TypebotsPage)
|
22
apps/builder/tsconfig.json
Normal file
22
apps/builder/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules", "cypress"]
|
||||||
|
}
|
6
apps/viewer/next-env.d.ts
vendored
Normal file
6
apps/viewer/next-env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/types/global" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
27
apps/viewer/package.json
Normal file
27
apps/viewer/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "viewer",
|
||||||
|
"packageManager": "yarn@3.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^12.0.4",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.11.9",
|
||||||
|
"@types/react": "^17.0.35",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
|
"eslint": "<8.0.0",
|
||||||
|
"eslint-config-next": "12.0.4",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return <div>Welcome to "Next.js"!</div>
|
return <div>Welcome to "Viewer"!</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage
|
@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
@ -17,14 +13,9 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve"
|
"jsx": "preserve",
|
||||||
|
"composite": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
version: '3.6'
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:13
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
db_data:
|
34
package.json
34
package.json
@ -1,26 +1,22 @@
|
|||||||
{
|
{
|
||||||
|
"name": "typebot-os",
|
||||||
"packageManager": "yarn@3.1.0",
|
"packageManager": "yarn@3.1.0",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*",
|
||||||
|
"apps/*"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"db:up": "docker-compose up -d",
|
||||||
"build": "next build",
|
"db:setup": "yarn workspace @typebot/prisma dev",
|
||||||
"start": "next start",
|
"db:nuke": "docker-compose down --volumes --remove-orphans",
|
||||||
"lint": "next lint"
|
"db:inspect": "dotenv -e .env yarn workspace @typebot/prisma prisma studio",
|
||||||
},
|
"dev:setup": "dotenv -e .env run-s db:up db:setup",
|
||||||
"dependencies": {
|
"dev:builder": "yarn workspace builder dev",
|
||||||
"next": "^12.0.4",
|
"dev:viewer": "yarn workspace viewer dev"
|
||||||
"react": "^17.0.2",
|
|
||||||
"react-dom": "^17.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^16.11.9",
|
"dotenv-cli": "^4.1.0",
|
||||||
"@types/react": "^17.0.35",
|
"npm-run-all": "^4.1.5"
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
|
||||||
"eslint": "<8.0.0",
|
|
||||||
"eslint-config-next": "12.0.4",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"prettier": "^2.4.1",
|
|
||||||
"typescript": "^4.5.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
packages/prisma/.gitignore
vendored
Normal file
5
packages/prisma/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
||||||
|
|
||||||
|
build
|
1
packages/prisma/README.md
Normal file
1
packages/prisma/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schemas
|
1
packages/prisma/index.ts
Normal file
1
packages/prisma/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@prisma/client'
|
22
packages/prisma/package.json
Normal file
22
packages/prisma/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@typebot/prisma",
|
||||||
|
"packageManager": "yarn@3.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"dotenv-cli": "^4.1.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prisma": "^3.5.0",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^3.5.0"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Account" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"provider" TEXT NOT NULL,
|
||||||
|
"providerAccountId" TEXT NOT NULL,
|
||||||
|
"refresh_token" TEXT,
|
||||||
|
"access_token" TEXT,
|
||||||
|
"expires_at" INTEGER,
|
||||||
|
"token_type" TEXT,
|
||||||
|
"scope" TEXT,
|
||||||
|
"id_token" TEXT,
|
||||||
|
"session_state" TEXT,
|
||||||
|
"oauth_token_secret" TEXT,
|
||||||
|
"oauth_token" TEXT,
|
||||||
|
"refresh_token_expires_in" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"sessionToken" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"expires" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT,
|
||||||
|
"email" TEXT,
|
||||||
|
"emailVerified" TIMESTAMP(3),
|
||||||
|
"image" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "VerificationToken" (
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expires" TIMESTAMP(3) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
3
packages/prisma/prisma/migrations/migration_lock.toml
Normal file
3
packages/prisma/prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
56
packages/prisma/prisma/schema.prisma
Normal file
56
packages/prisma/prisma/schema.prisma
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
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?
|
||||||
|
refresh_token_expires_in Int?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([provider, providerAccountId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionToken String @unique
|
||||||
|
userId String
|
||||||
|
expires DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String?
|
||||||
|
email String? @unique
|
||||||
|
emailVerified DateTime?
|
||||||
|
image String?
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String @unique
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@unique([identifier, token])
|
||||||
|
}
|
15
packages/prisma/tsconfig.json
Normal file
15
packages/prisma/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"isolatedModules": false
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules", "build"]
|
||||||
|
}
|
60
workspace.code-workspace
Normal file
60
workspace.code-workspace
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".",
|
||||||
|
"name": "root"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "apps/builder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "apps/viewer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/bot-engine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/prisma"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"restoreTerminals.terminals": [
|
||||||
|
{
|
||||||
|
"splitTerminals": [
|
||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"commands": ["cd ~/Dev/typebot-os"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"splitTerminals": [
|
||||||
|
{
|
||||||
|
"name": "builder",
|
||||||
|
"commands": ["cd ~/Dev/typebot-os/apps/builder"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cypress",
|
||||||
|
"commands": ["cd ~/Dev/typebot-os/apps/builder"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"splitTerminals": [
|
||||||
|
{
|
||||||
|
"name": "bot-engine",
|
||||||
|
"commands": ["cd ~/Dev/typebot-os/packages/bot-engine"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"splitTerminals": [
|
||||||
|
{
|
||||||
|
"name": "viewer",
|
||||||
|
"commands": ["cd ~/Dev/typebot-os/apps/viewer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user