feat(editor): ✨ Team workspaces
This commit is contained in:
@@ -12,10 +12,10 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon, PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import { CredentialsType } from 'models'
|
||||
import { deleteCredentials, useCredentials } from 'services/user'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
|
||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||
type: CredentialsType
|
||||
@@ -36,13 +36,13 @@ export const CredentialsDropdown = ({
|
||||
...props
|
||||
}: Props) => {
|
||||
const router = useRouter()
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const { credentials, mutate } = useCredentials({
|
||||
userId: user?.id,
|
||||
workspaceId: workspace?.id,
|
||||
})
|
||||
const [isDeleting, setIsDeleting] = useState<string>()
|
||||
|
||||
@@ -84,9 +84,9 @@ export const CredentialsDropdown = ({
|
||||
const handleDeleteDomainClick =
|
||||
(credentialsId: string) => async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!user?.id) return
|
||||
if (!workspace?.id) return
|
||||
setIsDeleting(credentialsId)
|
||||
const { error } = await deleteCredentials(user?.id, credentialsId)
|
||||
const { error } = await deleteCredentials(workspace.id, credentialsId)
|
||||
setIsDeleting(undefined)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
onCredentialsSelect(undefined)
|
||||
|
||||
@@ -4,22 +4,31 @@ import {
|
||||
chakra,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { ImageUploadContent } from '../ImageUploadContent'
|
||||
import { TypebotIcon } from './TypebotIcon'
|
||||
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
|
||||
import { ImageUploadContent } from './ImageUploadContent'
|
||||
|
||||
type Props = { icon?: string | null; onChangeIcon: (icon: string) => void }
|
||||
type Props = {
|
||||
icon?: string | null
|
||||
onChangeIcon: (icon: string) => void
|
||||
boxSize?: string
|
||||
}
|
||||
|
||||
export const EditableTypebotIcon = ({ icon, onChangeIcon }: Props) => {
|
||||
export const EditableEmojiOrImageIcon = ({
|
||||
icon,
|
||||
onChangeIcon,
|
||||
boxSize,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Popover isLazy>
|
||||
{({ onClose }) => (
|
||||
<>
|
||||
<Tooltip label="Change icon">
|
||||
<chakra.span
|
||||
<Flex
|
||||
cursor="pointer"
|
||||
px="2"
|
||||
p="2"
|
||||
rounded="md"
|
||||
_hover={{ bgColor: 'gray.100' }}
|
||||
transition="background-color 0.2s"
|
||||
@@ -27,10 +36,14 @@ export const EditableTypebotIcon = ({ icon, onChangeIcon }: Props) => {
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<chakra.span>
|
||||
<TypebotIcon icon={icon} emojiFontSize="2xl" />
|
||||
<EmojiOrImageIcon
|
||||
icon={icon}
|
||||
emojiFontSize="2xl"
|
||||
boxSize={boxSize}
|
||||
/>
|
||||
</chakra.span>
|
||||
</PopoverTrigger>
|
||||
</chakra.span>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<PopoverContent p="2">
|
||||
<ImageUploadContent
|
||||
@@ -1,17 +1,19 @@
|
||||
import { ToolIcon } from 'assets/icons'
|
||||
import React from 'react'
|
||||
import { chakra, Image } from '@chakra-ui/react'
|
||||
import { chakra, IconProps, Image } from '@chakra-ui/react'
|
||||
|
||||
type Props = {
|
||||
icon?: string | null
|
||||
emojiFontSize?: string
|
||||
boxSize?: string
|
||||
defaultIcon?: (props: IconProps) => JSX.Element
|
||||
}
|
||||
|
||||
export const TypebotIcon = ({
|
||||
export const EmojiOrImageIcon = ({
|
||||
icon,
|
||||
boxSize = '25px',
|
||||
emojiFontSize,
|
||||
defaultIcon = ToolIcon,
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
@@ -22,7 +24,7 @@ export const TypebotIcon = ({
|
||||
boxSize={boxSize}
|
||||
objectFit={icon.endsWith('.svg') ? undefined : 'cover'}
|
||||
alt="typebot icon"
|
||||
rounded="md"
|
||||
rounded="10%"
|
||||
/>
|
||||
) : (
|
||||
<chakra.span role="img" fontSize={emojiFontSize}>
|
||||
@@ -30,7 +32,7 @@ export const TypebotIcon = ({
|
||||
</chakra.span>
|
||||
)
|
||||
) : (
|
||||
<ToolIcon boxSize={boxSize} />
|
||||
defaultIcon({ boxSize })
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VStack, Tag, Text, Tooltip } from '@chakra-ui/react'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import React, { useMemo } from 'react'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import {
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
computeSourceCoordinates,
|
||||
computeDropOffPath,
|
||||
} from 'services/graph'
|
||||
import { isFreePlan } from 'services/user'
|
||||
import { isFreePlan } from 'services/workspace'
|
||||
import { byId, isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
@@ -23,11 +23,11 @@ export const DropOffEdge = ({
|
||||
blockId,
|
||||
onUnlockProPlanClick,
|
||||
}: Props) => {
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const { sourceEndpoints, blocksCoordinates, graphPosition } = useGraph()
|
||||
const { publishedTypebot } = useTypebot()
|
||||
|
||||
const isUserOnFreePlan = isFreePlan(user)
|
||||
const isUserOnFreePlan = isFreePlan(workspace)
|
||||
|
||||
const totalAnswers = useMemo(
|
||||
() => answersCounts.find((a) => a.blockId === blockId)?.totalAnswers,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { GoogleLogo } from 'assets/logos'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { Info } from 'components/shared/Info'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import React from 'react'
|
||||
import { getGoogleSheetsConsentScreenUrl } from 'services/integrations'
|
||||
|
||||
@@ -25,6 +26,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export const GoogleSheetConnectModal = ({ stepId, isOpen, onClose }: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
<ModalOverlay />
|
||||
@@ -54,7 +56,8 @@ export const GoogleSheetConnectModal = ({ stepId, isOpen, onClose }: Props) => {
|
||||
variant="outline"
|
||||
href={getGoogleSheetsConsentScreenUrl(
|
||||
window.location.href,
|
||||
stepId
|
||||
stepId,
|
||||
workspace?.id
|
||||
)}
|
||||
mx="auto"
|
||||
>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Stack, useDisclosure, Text } from '@chakra-ui/react'
|
||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||
import { Input, Textarea } from 'components/shared/Textbox'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { CredentialsType, SendEmailOptions } from 'models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
import React, { useState } from 'react'
|
||||
import { SmtpConfigModal } from './SmtpConfigModal'
|
||||
|
||||
type Props = {
|
||||
@@ -13,16 +11,9 @@ type Props = {
|
||||
}
|
||||
|
||||
export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { owner } = useTypebot()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(options.replyTo) || !owner?.email) return
|
||||
handleReplyToChange(owner.email)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||
onOptionsChange({
|
||||
@@ -95,7 +86,7 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
<Input
|
||||
onChange={handleReplyToChange}
|
||||
defaultValue={options.replyTo}
|
||||
placeholder={owner?.email ?? 'email@gmail.com'}
|
||||
placeholder={'email@gmail.com'}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { createCredentials } from 'services/user'
|
||||
import { testSmtpConfig } from 'services/integrations'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { SmtpConfigForm } from './SmtpConfigForm'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
@@ -29,6 +30,7 @@ export const SmtpConfigModal = ({
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
@@ -40,7 +42,7 @@ export const SmtpConfigModal = ({
|
||||
})
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
if (!user?.email) return
|
||||
if (!user?.email || !workspace?.id) return
|
||||
setIsCreating(true)
|
||||
const { error: testSmtpError } = await testSmtpConfig(
|
||||
smtpConfig,
|
||||
@@ -53,10 +55,11 @@ export const SmtpConfigModal = ({
|
||||
description: "We couldn't send the test email with your configuration",
|
||||
})
|
||||
}
|
||||
const { data, error } = await createCredentials(user.id, {
|
||||
const { data, error } = await createCredentials({
|
||||
data: smtpConfig,
|
||||
name: smtpConfig.from.email as string,
|
||||
type: CredentialsType.SMTP,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
setIsCreating(false)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
|
||||
@@ -23,10 +23,13 @@ export const TypebotLinkSettingsForm = ({
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<TypebotsDropdown
|
||||
typebotId={options.typebotId}
|
||||
onSelectTypebotId={handleTypebotIdChange}
|
||||
/>
|
||||
{typebot && (
|
||||
<TypebotsDropdown
|
||||
typebotId={options.typebotId}
|
||||
onSelectTypebotId={handleTypebotIdChange}
|
||||
currentWorkspaceId={typebot.workspaceId as string}
|
||||
/>
|
||||
)}
|
||||
<BlocksDropdown
|
||||
blocks={
|
||||
typebot &&
|
||||
|
||||
@@ -9,16 +9,22 @@ import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
typebotId?: string
|
||||
currentWorkspaceId: string
|
||||
onSelectTypebotId: (typebotId: string | 'current') => void
|
||||
}
|
||||
|
||||
export const TypebotsDropdown = ({ typebotId, onSelectTypebotId }: Props) => {
|
||||
export const TypebotsDropdown = ({
|
||||
typebotId,
|
||||
onSelectTypebotId,
|
||||
currentWorkspaceId,
|
||||
}: Props) => {
|
||||
const { query } = useRouter()
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const { typebots, isLoading } = useTypebots({
|
||||
workspaceId: currentWorkspaceId,
|
||||
allFolders: true,
|
||||
onError: (e) => toast({ title: e.name, description: e.message }),
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { Plan } from 'db'
|
||||
import React from 'react'
|
||||
import { UpgradeModal } from './modals/UpgradeModal'
|
||||
import { LimitReached } from './modals/UpgradeModal/UpgradeModal'
|
||||
@@ -22,14 +23,16 @@ export const PublishFirstInfo = (props: AlertProps) => (
|
||||
<Info {...props}>You need to publish your typebot first</Info>
|
||||
)
|
||||
|
||||
export const UnlockProPlanInfo = ({
|
||||
export const UnlockPlanInfo = ({
|
||||
contentLabel,
|
||||
buttonLabel,
|
||||
buttonLabel = 'More info',
|
||||
type,
|
||||
plan = Plan.PRO,
|
||||
}: {
|
||||
contentLabel: string
|
||||
buttonLabel: string
|
||||
buttonLabel?: string
|
||||
type?: LimitReached
|
||||
plan: Plan
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
return (
|
||||
@@ -44,10 +47,10 @@ export const UnlockProPlanInfo = ({
|
||||
<AlertIcon />
|
||||
<Text>{contentLabel}</Text>
|
||||
</HStack>
|
||||
<Button colorScheme="blue" onClick={onOpen}>
|
||||
<Button colorScheme="blue" onClick={onOpen} flexShrink={0} ml="2">
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
<UpgradeModal isOpen={isOpen} onClose={onClose} type={type} />
|
||||
<UpgradeModal isOpen={isOpen} onClose={onClose} type={type} plan={plan} />
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
13
apps/builder/components/shared/MaintenancePage.tsx
Normal file
13
apps/builder/components/shared/MaintenancePage.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Heading, Text, VStack } from '@chakra-ui/react'
|
||||
import { TypebotLogo } from 'assets/logos'
|
||||
import React from 'react'
|
||||
|
||||
export const MaintenancePage = () => (
|
||||
<VStack h="100vh" justify="center">
|
||||
<TypebotLogo />
|
||||
<Heading>
|
||||
The tool is under maintenance for an exciting new feature! 🤩
|
||||
</Heading>
|
||||
<Text>Please come back again in 10 minutes.</Text>
|
||||
</VStack>
|
||||
)
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { Plan } from 'db'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isCloudProdInstance } from 'services/utils'
|
||||
import { planToReadable } from 'services/workspace'
|
||||
import { initBubble } from 'typebot-js'
|
||||
|
||||
export const SupportBubble = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const [localTypebotId, setLocalTypebotId] = useState(typebot?.id)
|
||||
const [localUserId, setLocalUserId] = useState(user?.id)
|
||||
|
||||
@@ -33,7 +36,7 @@ export const SupportBubble = () => {
|
||||
Email: user?.email ?? undefined,
|
||||
'Typebot ID': typebot?.id,
|
||||
'Avatar URL': user?.image ?? undefined,
|
||||
Plan: planToReadable(user?.plan),
|
||||
Plan: planToReadable(workspace?.plan),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -42,17 +45,3 @@ export const SupportBubble = () => {
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
const planToReadable = (plan?: Plan) => {
|
||||
if (!plan) return
|
||||
switch (plan) {
|
||||
case 'FREE':
|
||||
return 'Free'
|
||||
case 'LIFETIME':
|
||||
return 'Lifetime'
|
||||
case 'OFFERED':
|
||||
return 'Offered'
|
||||
case 'PRO':
|
||||
return 'Pro'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,15 @@ import {
|
||||
MenuList,
|
||||
SkeletonCircle,
|
||||
SkeletonText,
|
||||
Text,
|
||||
Tag,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
import { EmojiOrImageIcon } from 'components/shared/EmojiOrImageIcon'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { CollaborationType } from 'db'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { CollaborationType, WorkspaceRole } from 'db'
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import {
|
||||
deleteCollaborator,
|
||||
@@ -27,21 +31,19 @@ import {
|
||||
deleteInvitation,
|
||||
sendInvitation,
|
||||
} from 'services/typebots/invitations'
|
||||
import {
|
||||
CollaboratorIdentityContent,
|
||||
CollaboratorItem,
|
||||
} from './CollaboratorButton'
|
||||
import { CollaboratorItem } from './CollaboratorButton'
|
||||
|
||||
export const CollaborationList = () => {
|
||||
const { user } = useUser()
|
||||
const { typebot, owner } = useTypebot()
|
||||
const { currentRole, workspace } = useWorkspace()
|
||||
const { typebot } = useTypebot()
|
||||
const [invitationType, setInvitationType] = useState<CollaborationType>(
|
||||
CollaborationType.READ
|
||||
)
|
||||
const [invitationEmail, setInvitationEmail] = useState('')
|
||||
const [isSendingInvitation, setIsSendingInvitation] = useState(false)
|
||||
|
||||
const isOwner = user?.email === owner?.email
|
||||
const hasFullAccess =
|
||||
(currentRole && currentRole !== WorkspaceRole.GUEST) || false
|
||||
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
@@ -66,12 +68,12 @@ export const CollaborationList = () => {
|
||||
} = useInvitations({
|
||||
typebotId: typebot?.id,
|
||||
onError: (e) =>
|
||||
toast({ title: "Couldn't fetch collaborators", description: e.message }),
|
||||
toast({ title: "Couldn't fetch invitations", description: e.message }),
|
||||
})
|
||||
|
||||
const handleChangeInvitationCollabType =
|
||||
(email: string) => async (type: CollaborationType) => {
|
||||
if (!typebot || !isOwner) return
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await updateInvitation(typebot?.id, email, {
|
||||
email,
|
||||
typebotId: typebot.id,
|
||||
@@ -85,7 +87,7 @@ export const CollaborationList = () => {
|
||||
})
|
||||
}
|
||||
const handleDeleteInvitation = (email: string) => async () => {
|
||||
if (!typebot || !isOwner) return
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await deleteInvitation(typebot?.id, email)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutateInvitations({
|
||||
@@ -95,7 +97,7 @@ export const CollaborationList = () => {
|
||||
|
||||
const handleChangeCollaborationType =
|
||||
(userId: string) => async (type: CollaborationType) => {
|
||||
if (!typebot || !isOwner) return
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await updateCollaborator(typebot?.id, userId, {
|
||||
userId,
|
||||
type,
|
||||
@@ -109,7 +111,7 @@ export const CollaborationList = () => {
|
||||
})
|
||||
}
|
||||
const handleDeleteCollaboration = (userId: string) => async () => {
|
||||
if (!typebot || !isOwner) return
|
||||
if (!typebot || !hasFullAccess) return
|
||||
const { error } = await deleteCollaborator(typebot?.id, userId)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutateCollaborators({
|
||||
@@ -119,7 +121,7 @@ export const CollaborationList = () => {
|
||||
|
||||
const handleInvitationSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!typebot || !isOwner) return
|
||||
if (!typebot || !hasFullAccess) return
|
||||
setIsSendingInvitation(true)
|
||||
const { error } = await sendInvitation(typebot.id, {
|
||||
email: invitationEmail,
|
||||
@@ -133,60 +135,57 @@ export const CollaborationList = () => {
|
||||
setInvitationEmail('')
|
||||
}
|
||||
|
||||
const hasNobody =
|
||||
(collaborators ?? []).length > 0 ||
|
||||
((invitations ?? []).length > 0 &&
|
||||
!isInvitationsLoading &&
|
||||
!isCollaboratorsLoading)
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
{isOwner && (
|
||||
<HStack
|
||||
as="form"
|
||||
onSubmit={handleInvitationSubmit}
|
||||
pt="4"
|
||||
px="4"
|
||||
pb={hasNobody ? '0' : '4'}
|
||||
>
|
||||
<Input
|
||||
size="sm"
|
||||
placeholder="colleague@company.com"
|
||||
name="inviteEmail"
|
||||
value={invitationEmail}
|
||||
onChange={(e) => setInvitationEmail(e.target.value)}
|
||||
rounded="md"
|
||||
/>
|
||||
<Stack spacing={4} py="4">
|
||||
<HStack as="form" onSubmit={handleInvitationSubmit} px="4">
|
||||
<Input
|
||||
size="sm"
|
||||
placeholder="colleague@company.com"
|
||||
name="inviteEmail"
|
||||
value={invitationEmail}
|
||||
onChange={(e) => setInvitationEmail(e.target.value)}
|
||||
rounded="md"
|
||||
isDisabled={!hasFullAccess}
|
||||
/>
|
||||
|
||||
{hasFullAccess && (
|
||||
<CollaborationTypeMenuButton
|
||||
type={invitationType}
|
||||
onChange={setInvitationType}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
isLoading={isSendingInvitation}
|
||||
flexShrink={0}
|
||||
type="submit"
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
{owner && (collaborators ?? []).length > 0 && (
|
||||
<CollaboratorIdentityContent
|
||||
email={owner.email ?? ''}
|
||||
name={owner.name ?? undefined}
|
||||
image={owner.image ?? undefined}
|
||||
tag="Owner"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
isLoading={isSendingInvitation}
|
||||
flexShrink={0}
|
||||
type="submit"
|
||||
isDisabled={!hasFullAccess}
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
</HStack>
|
||||
{workspace && (
|
||||
<Flex py="2" px="4" justifyContent="space-between">
|
||||
<HStack minW={0}>
|
||||
<EmojiOrImageIcon icon={workspace.icon} />
|
||||
<Text fontSize="15px" noOfLines={0}>
|
||||
Everyone at {workspace.name}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Tag>
|
||||
{convertCollaborationTypeEnumToReadable(
|
||||
CollaborationType.FULL_ACCESS
|
||||
)}
|
||||
</Tag>
|
||||
</Flex>
|
||||
)}
|
||||
{invitations?.map(({ email, type }) => (
|
||||
<CollaboratorItem
|
||||
key={email}
|
||||
email={email}
|
||||
type={type}
|
||||
isOwner={isOwner}
|
||||
isOwner={hasFullAccess}
|
||||
onDeleteClick={handleDeleteInvitation(email)}
|
||||
onChangeCollaborationType={handleChangeInvitationCollabType(email)}
|
||||
isGuest
|
||||
@@ -199,7 +198,7 @@ export const CollaborationList = () => {
|
||||
image={user.image ?? undefined}
|
||||
name={user.name ?? undefined}
|
||||
type={type}
|
||||
isOwner={isOwner}
|
||||
isOwner={hasFullAccess}
|
||||
onDeleteClick={handleDeleteCollaboration(userId ?? '')}
|
||||
onChangeCollaborationType={handleChangeCollaborationType(userId)}
|
||||
/>
|
||||
@@ -253,5 +252,7 @@ export const convertCollaborationTypeEnumToReadable = (
|
||||
return 'Can view'
|
||||
case CollaborationType.WRITE:
|
||||
return 'Can edit'
|
||||
case CollaborationType.FULL_ACCESS:
|
||||
return 'Full access'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { PublishButton } from '../buttons/PublishButton'
|
||||
import { EditableEmojiOrImageIcon } from '../EditableEmojiOrImageIcon'
|
||||
import { CollaborationMenuButton } from './CollaborationMenuButton'
|
||||
import { EditableTypebotIcon } from './EditableTypebotIcons'
|
||||
import { EditableTypebotName } from './EditableTypebotName'
|
||||
|
||||
export const headerHeight = 56
|
||||
@@ -123,7 +123,7 @@ export const TypebotHeader = () => {
|
||||
}
|
||||
/>
|
||||
<HStack spacing={1}>
|
||||
<EditableTypebotIcon
|
||||
<EditableEmojiOrImageIcon
|
||||
icon={typebot?.icon}
|
||||
onChangeIcon={handleChangeIcon}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Heading,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Text,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Stack,
|
||||
ListItem,
|
||||
UnorderedList,
|
||||
ListIcon,
|
||||
chakra,
|
||||
Tooltip,
|
||||
ListProps,
|
||||
Button,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { PricingCard } from './PricingCard'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { pay } from 'services/stripe'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { Plan } from 'db'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { TypebotLogo } from 'assets/logos'
|
||||
import { CheckIcon } from 'assets/icons'
|
||||
|
||||
export enum LimitReached {
|
||||
BRAND = 'Remove branding',
|
||||
@@ -26,10 +34,16 @@ type UpgradeModalProps = {
|
||||
type?: LimitReached
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
plan?: Plan
|
||||
}
|
||||
|
||||
export const UpgradeModal = ({ type, onClose, isOpen }: UpgradeModalProps) => {
|
||||
export const UpgradeModal = ({
|
||||
onClose,
|
||||
isOpen,
|
||||
plan = Plan.PRO,
|
||||
}: UpgradeModalProps) => {
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const [payLoading, setPayLoading] = useState(false)
|
||||
const [currency, setCurrency] = useState<'usd' | 'eur'>('usd')
|
||||
|
||||
@@ -39,64 +53,133 @@ export const UpgradeModal = ({ type, onClose, isOpen }: UpgradeModalProps) => {
|
||||
)
|
||||
}, [])
|
||||
|
||||
let limitLabel
|
||||
switch (type) {
|
||||
case LimitReached.BRAND: {
|
||||
limitLabel = "You can't hide Typebot brand on the Basic plan"
|
||||
break
|
||||
}
|
||||
case LimitReached.CUSTOM_DOMAIN: {
|
||||
limitLabel = "You can't add your domain with the Basic plan"
|
||||
break
|
||||
}
|
||||
case LimitReached.FOLDER: {
|
||||
limitLabel = "You can't create folders with the basic plan"
|
||||
}
|
||||
}
|
||||
|
||||
const handlePayClick = async () => {
|
||||
if (!user) return
|
||||
if (!user || !workspace) return
|
||||
setPayLoading(true)
|
||||
await pay(user, currency)
|
||||
await pay({
|
||||
user,
|
||||
currency,
|
||||
plan: plan === Plan.TEAM ? 'team' : 'pro',
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Upgrade to Pro plan</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing={6} alignItems="center">
|
||||
{limitLabel && (
|
||||
<Alert status="warning" rounded="md">
|
||||
<AlertIcon />
|
||||
{limitLabel}
|
||||
</Alert>
|
||||
<ModalBody as={Stack} pt="10">
|
||||
{plan === Plan.PRO ? (
|
||||
<PersonalProPlanContent currency={currency} />
|
||||
) : (
|
||||
<TeamPlanContent currency={currency} />
|
||||
)}
|
||||
<PricingCard
|
||||
data={{
|
||||
price: currency === 'eur' ? '25€' : '$30',
|
||||
name: 'Pro plan',
|
||||
features: [
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<HStack>
|
||||
<Button colorScheme="gray" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePayClick}
|
||||
isLoading={payLoading}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const PersonalProPlanContent = ({ currency }: { currency: 'eur' | 'usd' }) => {
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
<TypebotLogo boxSize="30px" />
|
||||
<Heading fontSize="2xl">
|
||||
Upgrade to <chakra.span color="orange.400">Personal Pro</chakra.span>{' '}
|
||||
plan
|
||||
</Heading>
|
||||
<Text>For solo creator who want to do even more.</Text>
|
||||
<Heading>
|
||||
{currency === 'eur' ? '39€' : '$39'}
|
||||
<chakra.span fontSize="md">/ month</chakra.span>
|
||||
</Heading>
|
||||
<Text fontWeight="bold">Everything in Personal, plus:</Text>
|
||||
<FeatureList
|
||||
features={[
|
||||
'Branding removed',
|
||||
'View incomplete submissions',
|
||||
'In-depth drop off analytics',
|
||||
'Unlimited custom domains',
|
||||
'Organize typebots in folders',
|
||||
'Unlimited uploads',
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const TeamPlanContent = ({ currency }: { currency: 'eur' | 'usd' }) => {
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
<TypebotLogo boxSize="30px" />
|
||||
<Heading fontSize="2xl">
|
||||
Upgrade to <chakra.span color="purple.400">Team</chakra.span> plan
|
||||
</Heading>
|
||||
<Text>For teams to build typebots together in one spot.</Text>
|
||||
<Heading>
|
||||
{currency === 'eur' ? '99€' : '$99'}
|
||||
<chakra.span fontSize="md">/ month</chakra.span>
|
||||
</Heading>
|
||||
<Text fontWeight="bold">
|
||||
<Tooltip
|
||||
label={
|
||||
<FeatureList
|
||||
features={[
|
||||
'Branding removed',
|
||||
'View incomplete submissions',
|
||||
'In-depth drop off analytics',
|
||||
'Custom domains',
|
||||
'Organize typebots in folders',
|
||||
'Unlimited uploads',
|
||||
'Custom Google Analytics events',
|
||||
],
|
||||
}}
|
||||
button={
|
||||
<ActionButton onClick={handlePayClick} isLoading={payLoading}>
|
||||
Upgrade now
|
||||
</ActionButton>
|
||||
}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
]}
|
||||
spacing="0"
|
||||
/>
|
||||
}
|
||||
hasArrow
|
||||
placement="top"
|
||||
>
|
||||
<chakra.span textDecoration="underline" cursor="pointer">
|
||||
Everything in Pro
|
||||
</chakra.span>
|
||||
</Tooltip>
|
||||
, plus:
|
||||
</Text>
|
||||
<FeatureList
|
||||
features={[
|
||||
'Unlimited team members',
|
||||
'Collaborative workspace',
|
||||
'Custom roles',
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const FeatureList = ({
|
||||
features,
|
||||
...props
|
||||
}: { features: string[] } & ListProps) => (
|
||||
<UnorderedList listStyleType="none" spacing={2} {...props}>
|
||||
{features.map((feat) => (
|
||||
<ListItem key={feat}>
|
||||
<ListIcon as={CheckIcon} />
|
||||
{feat}
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user