Files
bot/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx

210 lines
6.4 KiB
TypeScript
Raw Normal View History

import {
VStack,
Tag,
Text,
Tooltip,
useColorModeValue,
theme,
} from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
2022-02-11 18:06:59 +01:00
import React, { useMemo } from 'react'
import { useEndpoints } from '../../providers/EndpointsProvider'
import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider'
import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
import { computeDropOffPath } from '../../helpers/computeDropOffPath'
import { computeSourceCoordinates } from '../../helpers/computeSourceCoordinates'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
import { computePreviousTotalAnswers } from '@/features/analytics/helpers/computePreviousTotalAnswers'
import { blockHasItems } from '@typebot.io/lib'
export const dropOffBoxDimensions = {
width: 100,
height: 55,
}
export const dropOffSegmentLength = 80
const dropOffSegmentMinWidth = 2
const dropOffSegmentMaxWidth = 20
export const dropOffStubLength = 30
2022-02-11 18:06:59 +01:00
type Props = {
totalAnswersInBlocks: TotalAnswersInBlock[]
blockId: string
2022-02-13 06:53:48 +01:00
onUnlockProPlanClick?: () => void
2022-02-11 18:06:59 +01:00
}
2022-02-13 06:53:48 +01:00
export const DropOffEdge = ({
totalAnswersInBlocks,
blockId,
2022-02-13 06:53:48 +01:00
onUnlockProPlanClick,
}: Props) => {
const dropOffColor = useColorModeValue(
theme.colors.red[500],
2023-01-27 09:50:36 +01:00
theme.colors.red[400]
)
2022-05-13 15:22:44 -07:00
const { workspace } = useWorkspace()
const { groupsCoordinates } = useGroupsCoordinates()
const { sourceEndpointYOffsets: sourceEndpoints } = useEndpoints()
2022-02-11 18:06:59 +01:00
const { publishedTypebot } = useTypebot()
const currentBlock = useMemo(
() =>
totalAnswersInBlocks.reduce<TotalAnswersInBlock | undefined>(
(block, totalAnswersInBlock) => {
if (totalAnswersInBlock.blockId === blockId) {
return block
? { ...block, total: block.total + totalAnswersInBlock.total }
: totalAnswersInBlock
}
return block
},
undefined
),
[blockId, totalAnswersInBlocks]
)
2022-02-11 18:06:59 +01:00
const isWorkspaceProPlan = hasProPerks(workspace)
2022-02-13 06:53:48 +01:00
2022-02-11 18:06:59 +01:00
const { totalDroppedUser, dropOffRate } = useMemo(() => {
if (!publishedTypebot || currentBlock?.total === undefined)
2022-02-11 18:06:59 +01:00
return { previousTotal: undefined, dropOffRate: undefined }
const totalAnswers = currentBlock.total
const previousTotal = computePreviousTotalAnswers(
publishedTypebot,
currentBlock.blockId,
totalAnswersInBlocks
)
2022-02-11 18:06:59 +01:00
if (previousTotal === 0)
return { previousTotal: undefined, dropOffRate: undefined }
const totalDroppedUser = previousTotal - totalAnswers
return {
totalDroppedUser,
dropOffRate: Math.round((totalDroppedUser / previousTotal) * 100),
}
}, [
currentBlock?.blockId,
currentBlock?.total,
publishedTypebot,
totalAnswersInBlocks,
])
const sourceTop = useMemo(() => {
const blockTop = currentBlock?.blockId
? sourceEndpoints.get(currentBlock.blockId)?.y
: undefined
if (blockTop) return blockTop
const block = publishedTypebot?.groups
.flatMap((group) => group.blocks)
.find((block) => block.id === currentBlock?.blockId)
if (!block || !blockHasItems(block)) return 0
const itemId = block.items.at(-1)?.id
if (!itemId) return 0
return sourceEndpoints.get(itemId)?.y
}, [currentBlock?.blockId, publishedTypebot?.groups, sourceEndpoints])
const endpointCoordinates = useMemo(() => {
const groupId = publishedTypebot?.groups.find((group) =>
group.blocks.some((block) => block.id === currentBlock?.blockId)
)?.id
if (!groupId) return undefined
const coordinates = groupsCoordinates[groupId]
if (!coordinates) return undefined
return computeSourceCoordinates(coordinates, sourceTop ?? 0)
}, [
publishedTypebot?.groups,
groupsCoordinates,
sourceTop,
currentBlock?.blockId,
])
const isLastBlock = useMemo(() => {
if (!publishedTypebot) return false
const lastBlock = publishedTypebot.groups
.find((group) =>
group.blocks.some((block) => block.id === currentBlock?.blockId)
)
?.blocks.at(-1)
return lastBlock?.id === currentBlock?.blockId
}, [publishedTypebot, currentBlock?.blockId])
2022-02-11 18:06:59 +01:00
if (!endpointCoordinates) return null
2022-02-11 18:06:59 +01:00
return (
<>
<path
d={computeDropOffPath(
{
x: endpointCoordinates.x,
y: endpointCoordinates.y,
},
isLastBlock
2022-02-11 18:06:59 +01:00
)}
stroke={dropOffColor}
strokeWidth={
dropOffSegmentMinWidth * (1 - (dropOffRate ?? 0) / 100) +
dropOffSegmentMaxWidth * ((dropOffRate ?? 0) / 100)
}
2022-02-11 18:06:59 +01:00
fill="none"
/>
<foreignObject
width={dropOffBoxDimensions.width}
height={dropOffBoxDimensions.height}
x={endpointCoordinates.x + dropOffStubLength}
y={
endpointCoordinates.y +
(isLastBlock
? dropOffSegmentLength
: -(dropOffBoxDimensions.height / 2))
}
2022-02-11 18:06:59 +01:00
>
2022-02-13 06:53:48 +01:00
<Tooltip
label={
isWorkspaceProPlan
? `At this input, ${totalDroppedUser} user${
(totalDroppedUser ?? 2) > 1 ? 's' : ''
} left. This represents ${dropOffRate}% of the users who saw this input.`
: 'Upgrade your plan to PRO to reveal drop-off rate.'
}
placement="top"
2022-02-11 18:06:59 +01:00
>
2022-02-13 06:53:48 +01:00
<VStack
bgColor={dropOffColor}
2022-02-13 06:53:48 +01:00
color="white"
rounded="md"
p="2"
justifyContent="center"
w="full"
h="full"
onClick={isWorkspaceProPlan ? undefined : onUnlockProPlanClick}
cursor={isWorkspaceProPlan ? 'auto' : 'pointer'}
spacing={0.5}
2022-02-13 06:53:48 +01:00
>
<Text filter={isWorkspaceProPlan ? '' : 'blur(2px)'} fontSize="sm">
{isWorkspaceProPlan ? (
dropOffRate
) : (
<Text as="span" filter="blur(2px)">
X
</Text>
)}
%
</Text>
<Tag colorScheme="red" size="sm">
{isWorkspaceProPlan ? (
totalDroppedUser
) : (
<Text as="span" filter="blur(3px)" mr="1">
NN
</Text>
)}{' '}
user{(totalDroppedUser ?? 2) > 1 ? 's' : ''}
2022-02-13 06:53:48 +01:00
</Tag>
</VStack>
</Tooltip>
2022-02-11 18:06:59 +01:00
</foreignObject>
</>
)
}