🖐️ Analytics drop off rates
This commit is contained in:
44
apps/builder/components/analytics/StatsCards.tsx
Normal file
44
apps/builder/components/analytics/StatsCards.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
GridProps,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
} from '@chakra-ui/react'
|
||||
import { Stats } from 'bot-engine'
|
||||
import React from 'react'
|
||||
|
||||
export const StatsCards = ({
|
||||
stats,
|
||||
...props
|
||||
}: { stats?: Stats } & GridProps) => {
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing="6" {...props}>
|
||||
<Stat bgColor="white" p="4" rounded="md" boxShadow="md">
|
||||
<StatLabel>Views</StatLabel>
|
||||
{stats ? (
|
||||
<StatNumber>{stats.totalViews}</StatNumber>
|
||||
) : (
|
||||
<Skeleton w="50%" h="10px" mt="2" />
|
||||
)}
|
||||
</Stat>
|
||||
<Stat bgColor="white" p="4" rounded="md" boxShadow="md">
|
||||
<StatLabel>Starts</StatLabel>
|
||||
{stats ? (
|
||||
<StatNumber>{stats.totalStarts}</StatNumber>
|
||||
) : (
|
||||
<Skeleton w="50%" h="10px" mt="2" />
|
||||
)}
|
||||
</Stat>
|
||||
<Stat bgColor="white" p="4" rounded="md" boxShadow="md">
|
||||
<StatLabel>Completion rate</StatLabel>
|
||||
{stats ? (
|
||||
<StatNumber>{stats.completionRate}%</StatNumber>
|
||||
) : (
|
||||
<Skeleton w="50%" h="10px" mt="2" />
|
||||
)}
|
||||
</Stat>
|
||||
</SimpleGrid>
|
||||
)
|
||||
}
|
61
apps/builder/components/analytics/graph/AnalyticsGraph.tsx
Normal file
61
apps/builder/components/analytics/graph/AnalyticsGraph.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
|
||||
import React, { useMemo, useRef } from 'react'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import { BlockNode } from './blocks/BlockNode'
|
||||
import { StartBlockNode } from './blocks/StartBlockNode'
|
||||
import { Edges } from './Edges'
|
||||
|
||||
const AnalyticsGraph = ({
|
||||
answersCounts,
|
||||
...props
|
||||
}: { answersCounts?: AnswersCount[] } & FlexProps) => {
|
||||
const { typebot, graphPosition, setGraphPosition } = useAnalyticsGraph()
|
||||
const graphContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const transform = useMemo(
|
||||
() =>
|
||||
`translate(${graphPosition.x}px, ${graphPosition.y}px) scale(${graphPosition.scale})`,
|
||||
[graphPosition]
|
||||
)
|
||||
|
||||
const handleMouseWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
const isPinchingTrackpad = e.ctrlKey
|
||||
if (isPinchingTrackpad) {
|
||||
const scale = graphPosition.scale - e.deltaY * 0.01
|
||||
if (scale <= 0.2 || scale >= 1) return
|
||||
setGraphPosition({
|
||||
...graphPosition,
|
||||
scale,
|
||||
})
|
||||
} else
|
||||
setGraphPosition({
|
||||
...graphPosition,
|
||||
x: graphPosition.x - e.deltaX,
|
||||
y: graphPosition.y - e.deltaY,
|
||||
})
|
||||
}
|
||||
useEventListener('wheel', handleMouseWheel, graphContainerRef.current)
|
||||
|
||||
if (!typebot) return <></>
|
||||
return (
|
||||
<Flex ref={graphContainerRef} {...props}>
|
||||
<Flex
|
||||
flex="1"
|
||||
boxSize={'200px'}
|
||||
maxW={'200px'}
|
||||
style={{
|
||||
transform,
|
||||
}}
|
||||
>
|
||||
<Edges answersCounts={answersCounts} />
|
||||
{typebot.startBlock && <StartBlockNode block={typebot.startBlock} />}
|
||||
{(typebot.blocks ?? []).map((block) => (
|
||||
<BlockNode block={block} key={block.id} />
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnalyticsGraph
|
@ -0,0 +1,18 @@
|
||||
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
|
||||
import React, { useMemo } from 'react'
|
||||
import { computeDropOffPath } from 'services/graph'
|
||||
|
||||
type Props = {
|
||||
blockId: string
|
||||
}
|
||||
export const DropOffEdge = ({ blockId }: Props) => {
|
||||
const { typebot } = useAnalyticsGraph()
|
||||
const path = useMemo(() => {
|
||||
if (!typebot) return
|
||||
const block = (typebot?.blocks ?? []).find((b) => b.id === blockId)
|
||||
if (!block) return ''
|
||||
return computeDropOffPath(block.graphCoordinates, block.steps.length - 1)
|
||||
}, [blockId, typebot])
|
||||
|
||||
return <path d={path} stroke={'#E53E3E'} strokeWidth="2px" fill="none" />
|
||||
}
|
56
apps/builder/components/analytics/graph/Edges/Edge.tsx
Normal file
56
apps/builder/components/analytics/graph/Edges/Edge.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Block } from 'bot-engine'
|
||||
import { StepWithTarget } from 'components/board/graph/Edges/Edge'
|
||||
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
getAnchorsPosition,
|
||||
computeFlowChartConnectorPath,
|
||||
} from 'services/graph'
|
||||
|
||||
export const Edge = ({ step }: { step: StepWithTarget }) => {
|
||||
const { typebot } = useAnalyticsGraph()
|
||||
const { blocks, startBlock } = typebot ?? {}
|
||||
|
||||
const { sourceBlock, targetBlock, targetStepIndex } = useMemo(() => {
|
||||
const targetBlock = blocks?.find(
|
||||
(b) => b?.id === step.target.blockId
|
||||
) as Block
|
||||
const targetStepIndex = step.target.stepId
|
||||
? targetBlock.steps.findIndex((s) => s.id === step.target.stepId)
|
||||
: undefined
|
||||
return {
|
||||
sourceBlock: [startBlock, ...(blocks ?? [])].find(
|
||||
(b) => b?.id === step.blockId
|
||||
),
|
||||
targetBlock,
|
||||
targetStepIndex,
|
||||
}
|
||||
}, [
|
||||
blocks,
|
||||
startBlock,
|
||||
step.blockId,
|
||||
step.target.blockId,
|
||||
step.target.stepId,
|
||||
])
|
||||
|
||||
const path = useMemo(() => {
|
||||
if (!sourceBlock || !targetBlock) return ``
|
||||
const anchorsPosition = getAnchorsPosition(
|
||||
sourceBlock,
|
||||
targetBlock,
|
||||
sourceBlock.steps.findIndex((s) => s.id === step.id),
|
||||
targetStepIndex
|
||||
)
|
||||
return computeFlowChartConnectorPath(anchorsPosition)
|
||||
}, [sourceBlock, step.id, targetBlock, targetStepIndex])
|
||||
|
||||
return (
|
||||
<path
|
||||
d={path}
|
||||
stroke={'#718096'}
|
||||
strokeWidth="2px"
|
||||
markerEnd="url(#arrow)"
|
||||
fill="none"
|
||||
/>
|
||||
)
|
||||
}
|
69
apps/builder/components/analytics/graph/Edges/Edges.tsx
Normal file
69
apps/builder/components/analytics/graph/Edges/Edges.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { chakra } from '@chakra-ui/system'
|
||||
import { StepWithTarget } from 'components/board/graph/Edges/Edge'
|
||||
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
|
||||
import React, { useMemo } from 'react'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import { DropOffBlock } from '../blocks/DropOffBlock'
|
||||
import { DropOffEdge } from './DropOffEdge'
|
||||
import { Edge } from './Edge'
|
||||
|
||||
type Props = { answersCounts?: AnswersCount[] }
|
||||
|
||||
export const Edges = ({ answersCounts }: Props) => {
|
||||
const { typebot } = useAnalyticsGraph()
|
||||
const { blocks, startBlock } = typebot ?? {}
|
||||
const stepsWithTarget: StepWithTarget[] = useMemo(() => {
|
||||
if (!startBlock) return []
|
||||
return [
|
||||
...(startBlock.steps.filter((s) => s.target) as StepWithTarget[]),
|
||||
...((blocks ?? [])
|
||||
.flatMap((b) => b.steps)
|
||||
.filter((s) => s.target) as StepWithTarget[]),
|
||||
]
|
||||
}, [blocks, startBlock])
|
||||
|
||||
return (
|
||||
<>
|
||||
<chakra.svg
|
||||
width="full"
|
||||
height="full"
|
||||
overflow="visible"
|
||||
pos="absolute"
|
||||
left="0"
|
||||
top="0"
|
||||
>
|
||||
{stepsWithTarget.map((step) => (
|
||||
<Edge key={step.id} step={step} />
|
||||
))}
|
||||
<marker
|
||||
id={'arrow'}
|
||||
refX="8"
|
||||
refY="4"
|
||||
orient="auto"
|
||||
viewBox="0 0 20 20"
|
||||
markerUnits="userSpaceOnUse"
|
||||
markerWidth="20"
|
||||
markerHeight="20"
|
||||
>
|
||||
<path
|
||||
d="M7.07138888,5.50174526 L2.43017246,7.82235347 C1.60067988,8.23709976 0.592024983,7.90088146 0.177278692,7.07138888 C0.0606951226,6.83822174 0,6.58111307 0,6.32042429 L0,1.67920787 C0,0.751806973 0.751806973,0 1.67920787,0 C1.93989666,0 2.19700532,0.0606951226 2.43017246,0.177278692 L7,3 C7.82949258,3.41474629 8.23709976,3.92128809 7.82235347,4.75078067 C7.6598671,5.07575341 7.39636161,5.33925889 7.07138888,5.50174526 Z"
|
||||
fill="#718096"
|
||||
/>
|
||||
</marker>
|
||||
{answersCounts?.map((answersCount) => (
|
||||
<DropOffEdge
|
||||
key={answersCount.blockId}
|
||||
blockId={answersCount.blockId}
|
||||
/>
|
||||
))}
|
||||
</chakra.svg>
|
||||
{answersCounts?.map((answersCount) => (
|
||||
<DropOffBlock
|
||||
key={answersCount.blockId}
|
||||
answersCounts={answersCounts}
|
||||
blockId={answersCount.blockId}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
1
apps/builder/components/analytics/graph/Edges/index.ts
Normal file
1
apps/builder/components/analytics/graph/Edges/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Edges } from './Edges'
|
62
apps/builder/components/analytics/graph/blocks/BlockNode.tsx
Normal file
62
apps/builder/components/analytics/graph/blocks/BlockNode.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import {
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Stack,
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useState } from 'react'
|
||||
import { Block } from 'bot-engine'
|
||||
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
|
||||
import { StepsList } from './StepsList'
|
||||
|
||||
type Props = {
|
||||
block: Block
|
||||
}
|
||||
|
||||
export const BlockNode = ({ block }: Props) => {
|
||||
const { updateBlockPosition } = useAnalyticsGraph()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
const handleMouseDown = () => {
|
||||
setIsMouseDown(true)
|
||||
}
|
||||
const handleMouseUp = () => {
|
||||
setIsMouseDown(false)
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!isMouseDown) return
|
||||
const { movementX, movementY } = event
|
||||
|
||||
updateBlockPosition(block.id, {
|
||||
x: block.graphCoordinates.x + movementX,
|
||||
y: block.graphCoordinates.y + movementY,
|
||||
})
|
||||
}
|
||||
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return (
|
||||
<Stack
|
||||
p="4"
|
||||
rounded="lg"
|
||||
bgColor="blue.50"
|
||||
borderWidth="2px"
|
||||
minW="300px"
|
||||
transition="border 300ms"
|
||||
pos="absolute"
|
||||
style={{
|
||||
transform: `translate(${block.graphCoordinates.x}px, ${block.graphCoordinates.y}px)`,
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<Editable defaultValue={block.title}>
|
||||
<EditablePreview px="1" userSelect={'none'} />
|
||||
<EditableInput minW="0" px="1" />
|
||||
</Editable>
|
||||
<StepsList blockId={block.id} steps={block.steps} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import { Tag, Text, VStack } from '@chakra-ui/react'
|
||||
import { Block } from 'bot-engine'
|
||||
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
|
||||
import React, { useMemo } from 'react'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import { computeSourceCoordinates } from 'services/graph'
|
||||
|
||||
type Props = {
|
||||
answersCounts: AnswersCount[]
|
||||
blockId: string
|
||||
}
|
||||
|
||||
export const DropOffBlock = ({ answersCounts, blockId }: Props) => {
|
||||
const { typebot } = useAnalyticsGraph()
|
||||
|
||||
const totalAnswers = useMemo(
|
||||
() => answersCounts.find((a) => a.blockId === blockId)?.totalAnswers,
|
||||
[answersCounts, blockId]
|
||||
)
|
||||
|
||||
const { totalDroppedUser, dropOffRate } = useMemo(() => {
|
||||
if (!typebot || totalAnswers === undefined)
|
||||
return { previousTotal: undefined, dropOffRate: undefined }
|
||||
const previousTotal = answersCounts
|
||||
.filter(
|
||||
(a) =>
|
||||
[typebot.startBlock, ...typebot.blocks].find((b) =>
|
||||
(b as Block).steps.find((s) => s.target?.blockId === blockId)
|
||||
)?.id === a.blockId
|
||||
)
|
||||
.reduce((prev, acc) => acc.totalAnswers + prev, 0)
|
||||
if (previousTotal === 0)
|
||||
return { previousTotal: undefined, dropOffRate: undefined }
|
||||
const totalDroppedUser = previousTotal - totalAnswers
|
||||
|
||||
return {
|
||||
totalDroppedUser,
|
||||
dropOffRate: Math.round((totalDroppedUser / previousTotal) * 100),
|
||||
}
|
||||
}, [answersCounts, blockId, totalAnswers, typebot])
|
||||
|
||||
const labelCoordinates = useMemo(() => {
|
||||
if (!typebot) return { x: 0, y: 0 }
|
||||
const sourceBlock = typebot?.blocks.find((b) => b.id === blockId)
|
||||
if (!sourceBlock) return
|
||||
return computeSourceCoordinates(
|
||||
sourceBlock?.graphCoordinates,
|
||||
sourceBlock?.steps.length - 1
|
||||
)
|
||||
}, [blockId, typebot])
|
||||
|
||||
if (!labelCoordinates) return <></>
|
||||
return (
|
||||
<VStack
|
||||
bgColor={'red.500'}
|
||||
color="white"
|
||||
rounded="md"
|
||||
p="2"
|
||||
justifyContent="center"
|
||||
style={{
|
||||
transform: `translate(${labelCoordinates.x - 20}px, ${
|
||||
labelCoordinates.y + 80
|
||||
}px)`,
|
||||
}}
|
||||
pos="absolute"
|
||||
>
|
||||
<Text>{dropOffRate}%</Text>
|
||||
<Tag colorScheme="red">{totalDroppedUser} users</Tag>
|
||||
</VStack>
|
||||
)
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import {
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Stack,
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useState } from 'react'
|
||||
import { StartBlock } from 'bot-engine'
|
||||
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
|
||||
import { StepsList } from './StepsList'
|
||||
|
||||
type Props = {
|
||||
block: StartBlock
|
||||
}
|
||||
|
||||
export const StartBlockNode = ({ block }: Props) => {
|
||||
const { updateBlockPosition } = useAnalyticsGraph()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
const handleMouseDown = () => {
|
||||
setIsMouseDown(true)
|
||||
}
|
||||
const handleMouseUp = () => {
|
||||
setIsMouseDown(false)
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!isMouseDown) return
|
||||
const { movementX, movementY } = event
|
||||
|
||||
updateBlockPosition(block.id, {
|
||||
x: block.graphCoordinates.x + movementX,
|
||||
y: block.graphCoordinates.y + movementY,
|
||||
})
|
||||
}
|
||||
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return (
|
||||
<Stack
|
||||
p="4"
|
||||
rounded="lg"
|
||||
bgColor="blue.50"
|
||||
borderWidth="2px"
|
||||
minW="300px"
|
||||
transition="border 300ms"
|
||||
pos="absolute"
|
||||
style={{
|
||||
transform: `translate(${block.graphCoordinates.x}px, ${block.graphCoordinates.y}px)`,
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<Editable value={block.title} isDisabled>
|
||||
<EditablePreview px="1" userSelect={'none'} />
|
||||
<EditableInput minW="0" px="1" />
|
||||
</Editable>
|
||||
<StepsList blockId={block.id} steps={block.steps} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
27
apps/builder/components/analytics/graph/blocks/StepsList.tsx
Normal file
27
apps/builder/components/analytics/graph/blocks/StepsList.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Flex, Stack } from '@chakra-ui/react'
|
||||
import { StartStep, Step } from 'bot-engine'
|
||||
import { StepNodeOverlay } from 'components/board/graph/BlockNode/StepNode'
|
||||
|
||||
export const StepsList = ({
|
||||
steps,
|
||||
}: {
|
||||
blockId: string
|
||||
steps: Step[] | [StartStep]
|
||||
}) => {
|
||||
return (
|
||||
<Stack spacing={1} transition="none">
|
||||
<Flex h={'2px'} bgColor={'gray.400'} visibility={'hidden'} rounded="lg" />
|
||||
{steps.map((step) => (
|
||||
<Stack key={step.id} spacing={1}>
|
||||
<StepNodeOverlay key={step.id} step={step} />
|
||||
<Flex
|
||||
h={'2px'}
|
||||
bgColor={'gray.400'}
|
||||
visibility={'hidden'}
|
||||
rounded="lg"
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -5,6 +5,7 @@ import { DndContext } from 'contexts/DndContext'
|
||||
import { StepTypesList } from './StepTypesList'
|
||||
import { PreviewDrawer } from './preview/PreviewDrawer'
|
||||
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||
import { GraphProvider } from 'contexts/GraphContext'
|
||||
|
||||
export const Board = () => {
|
||||
const { rightPanel } = useEditor()
|
||||
@ -12,7 +13,9 @@ export const Board = () => {
|
||||
<Flex flex="1" pos="relative" bgColor="gray.50" h="full">
|
||||
<DndContext>
|
||||
<StepTypesList />
|
||||
<Graph />
|
||||
<GraphProvider>
|
||||
<Graph flex="1" />
|
||||
</GraphProvider>
|
||||
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
|
||||
</DndContext>
|
||||
</Flex>
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Block, StartBlock } from 'bot-engine'
|
||||
import { Block } from 'bot-engine'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { StepsList } from './StepsList'
|
||||
@ -15,7 +15,11 @@ import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||
import { BlockNodeContextMenu } from './BlockNodeContextMenu'
|
||||
|
||||
export const BlockNode = ({ block }: { block: Block | StartBlock }) => {
|
||||
type Props = {
|
||||
block: Block
|
||||
}
|
||||
|
||||
export const BlockNode = ({ block }: Props) => {
|
||||
const { connectingIds, setConnectingIds, previewingIds } = useGraph()
|
||||
const { updateBlockPosition, addStepToBlock } = useTypebot()
|
||||
const { draggedStep, draggedStepType, setDraggedStepType, setDraggedStep } =
|
||||
|
@ -11,7 +11,10 @@ import { StepNode } from './StepNode'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
|
||||
export const StartBlockNode = ({ block }: { block: StartBlock }) => {
|
||||
type Props = {
|
||||
block: StartBlock
|
||||
}
|
||||
export const StartBlockNode = ({ block }: Props) => {
|
||||
const { previewingIds } = useGraph()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
const [titleValue, setTitleValue] = useState(block.title)
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { StackProps, HStack } from '@chakra-ui/react'
|
||||
import { Step } from 'bot-engine'
|
||||
import { StartStep, Step } from 'bot-engine'
|
||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||
import { StepContent } from './StepContent'
|
||||
|
||||
export const StepNodeOverlay = ({
|
||||
step,
|
||||
...props
|
||||
}: { step: Step } & StackProps) => {
|
||||
}: { step: Step | StartStep } & StackProps) => {
|
||||
return (
|
||||
<HStack
|
||||
p="3"
|
||||
@ -14,9 +14,6 @@ export const StepNodeOverlay = ({
|
||||
rounded="lg"
|
||||
bgColor="white"
|
||||
cursor={'grab'}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
w="264px"
|
||||
pointerEvents="none"
|
||||
{...props}
|
||||
|
@ -118,6 +118,9 @@ export const StepsList = ({
|
||||
<StepNodeOverlay
|
||||
step={draggedStep}
|
||||
onMouseUp={handleMouseUp}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
|
||||
}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex, useEventListener } from '@chakra-ui/react'
|
||||
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||
import React, { useRef, useMemo } from 'react'
|
||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
||||
import { BlockNode } from './BlockNode/BlockNode'
|
||||
@ -8,11 +8,11 @@ import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { StartBlockNode } from './BlockNode/StartBlockNode'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||
|
||||
const Graph = () => {
|
||||
const Graph = ({ ...props }: FlexProps) => {
|
||||
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
||||
useDnd()
|
||||
const graphContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const { typebot, addNewBlock } = useTypebot()
|
||||
const { addNewBlock, typebot } = useTypebot()
|
||||
const { graphPosition, setGraphPosition } = useGraph()
|
||||
const transform = useMemo(
|
||||
() =>
|
||||
@ -60,7 +60,7 @@ const Graph = () => {
|
||||
|
||||
if (!typebot) return <></>
|
||||
return (
|
||||
<Flex ref={graphContainerRef} flex="1">
|
||||
<Flex ref={graphContainerRef} {...props}>
|
||||
<Flex
|
||||
flex="1"
|
||||
boxSize={'200px'}
|
||||
@ -70,6 +70,7 @@ const Graph = () => {
|
||||
}}
|
||||
>
|
||||
<Edges />
|
||||
{props.children}
|
||||
{typebot.startBlock && <StartBlockNode block={typebot.startBlock} />}
|
||||
{(typebot.blocks ?? []).map((block) => (
|
||||
<BlockNode block={block} key={block.id} />
|
||||
|
@ -37,8 +37,7 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 300,
|
||||
distance: 20,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
@ -92,7 +92,13 @@ export const TypebotButton = ({
|
||||
>
|
||||
<MoreButton pos="absolute" top="10px" right="10px">
|
||||
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
|
||||
<MenuItem color="red" onClick={onDeleteOpen}>
|
||||
<MenuItem
|
||||
color="red"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDeleteOpen()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MoreButton>
|
||||
|
79
apps/builder/contexts/AnalyticsGraphProvider.tsx
Normal file
79
apps/builder/contexts/AnalyticsGraphProvider.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { Block, PublicTypebot } from 'bot-engine'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { Coordinates } from './GraphContext'
|
||||
|
||||
type Position = Coordinates & { scale: number }
|
||||
|
||||
const graphPositionDefaultValue = { x: 400, y: 100, scale: 1 }
|
||||
|
||||
const graphContext = createContext<{
|
||||
typebot?: PublicTypebot
|
||||
updateBlockPosition: (blockId: string, newPositon: Coordinates) => void
|
||||
graphPosition: Position
|
||||
setGraphPosition: Dispatch<SetStateAction<Position>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({
|
||||
graphPosition: graphPositionDefaultValue,
|
||||
})
|
||||
|
||||
export const AnalyticsGraphProvider = ({
|
||||
children,
|
||||
initialTypebot,
|
||||
}: {
|
||||
children: ReactNode
|
||||
initialTypebot: PublicTypebot
|
||||
}) => {
|
||||
const [typebot, setTypebot] = useState(initialTypebot)
|
||||
|
||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||
|
||||
const updateBlocks = (blocks: Block[]) => {
|
||||
if (!typebot) return
|
||||
setTypebot({
|
||||
...typebot,
|
||||
blocks: [...blocks],
|
||||
})
|
||||
}
|
||||
|
||||
const updateBlockPosition = (blockId: string, newPosition: Coordinates) => {
|
||||
if (!typebot) return
|
||||
blockId === 'start-block'
|
||||
? setTypebot({
|
||||
...typebot,
|
||||
startBlock: {
|
||||
...typebot.startBlock,
|
||||
graphCoordinates: newPosition,
|
||||
},
|
||||
})
|
||||
: updateBlocks(
|
||||
typebot.blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? { ...block, graphCoordinates: newPosition }
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<graphContext.Provider
|
||||
value={{
|
||||
typebot,
|
||||
graphPosition,
|
||||
setGraphPosition,
|
||||
updateBlockPosition,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</graphContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAnalyticsGraph = () => useContext(graphContext)
|
@ -323,9 +323,15 @@ export const TypebotContext = ({
|
||||
|
||||
const publishTypebot = async () => {
|
||||
if (!localTypebot) return
|
||||
if (!localPublishedTypebot)
|
||||
updatePublicId(parseDefaultPublicId(localTypebot.name, localTypebot.id))
|
||||
if (hasUnsavedChanges) await saveTypebot()
|
||||
if (!localPublishedTypebot) {
|
||||
const newPublicId = parseDefaultPublicId(
|
||||
localTypebot.name,
|
||||
localTypebot.id
|
||||
)
|
||||
updatePublicId(newPublicId)
|
||||
localTypebot.publicId = newPublicId
|
||||
}
|
||||
if (hasUnsavedChanges || !localPublishedTypebot) await saveTypebot()
|
||||
setIsPublishing(true)
|
||||
if (localPublishedTypebot) {
|
||||
const { error } = await updatePublishedTypebot(
|
||||
|
@ -4,7 +4,7 @@ describe('Dashboard page', () => {
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
it('should navigate correctly', () => {
|
||||
test('folders navigation should work', () => {
|
||||
cy.signIn('test1@gmail.com')
|
||||
cy.visit('/typebots')
|
||||
createFolder('My folder #1')
|
||||
@ -26,7 +26,7 @@ describe('Dashboard page', () => {
|
||||
cy.findByDisplayValue('My folder #2').should('not.exist')
|
||||
})
|
||||
|
||||
it('should be droppable', () => {
|
||||
test('folders should be draggable and droppable', () => {
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots')
|
||||
cy.findByTestId('typebot-button-typebot1').mouseMoveBy(-100, 0, {
|
||||
|
@ -1,5 +1,43 @@
|
||||
import { Flex, useToast } from '@chakra-ui/react'
|
||||
import { Stats } from 'bot-engine'
|
||||
import AnalyticsGraph from 'components/analytics/graph/AnalyticsGraph'
|
||||
import { StatsCards } from 'components/analytics/StatsCards'
|
||||
import { AnalyticsGraphProvider } from 'contexts/AnalyticsGraphProvider'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import React from 'react'
|
||||
import { useAnswersCount } from 'services/analytics'
|
||||
|
||||
export const AnalyticsContent = () => {
|
||||
return <>Hi</>
|
||||
export const AnalyticsContent = ({ stats }: { stats?: Stats }) => {
|
||||
const { typebot, publishedTypebot } = useTypebot()
|
||||
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const { answersCounts } = useAnswersCount({
|
||||
typebotId: typebot?.id,
|
||||
onError: (err) => toast({ title: err.name, description: err.message }),
|
||||
})
|
||||
return (
|
||||
<Flex
|
||||
w="full"
|
||||
pos="relative"
|
||||
bgColor="gray.50"
|
||||
h="full"
|
||||
justifyContent="center"
|
||||
>
|
||||
{publishedTypebot && (
|
||||
<AnalyticsGraphProvider initialTypebot={publishedTypebot}>
|
||||
<AnalyticsGraph
|
||||
flex="1"
|
||||
answersCounts={[
|
||||
{ blockId: 'start-block', totalAnswers: stats?.totalViews ?? 0 },
|
||||
...(answersCounts ?? []),
|
||||
]}
|
||||
/>
|
||||
</AnalyticsGraphProvider>
|
||||
)}
|
||||
<StatsCards stats={stats} pos="absolute" top={10} />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
@ -1,17 +1,9 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Stack,
|
||||
Tag,
|
||||
useToast,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { Button, Flex, HStack, Tag, useToast, Text } from '@chakra-ui/react'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useResultsCount } from 'services/results'
|
||||
import { useStats } from 'services/analytics'
|
||||
import { AnalyticsContent } from './AnalyticsContent'
|
||||
import { SubmissionsContent } from './SubmissionContent'
|
||||
|
||||
@ -27,14 +19,21 @@ export const ResultsContent = () => {
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
const { totalResults } = useResultsCount({
|
||||
const { stats } = useStats({
|
||||
typebotId: typebot?.id,
|
||||
onError: (err) => toast({ title: err.name, description: err.message }),
|
||||
})
|
||||
return (
|
||||
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||
<Stack maxW="1200px" w="full" pt="4" spacing={6}>
|
||||
<HStack>
|
||||
<Flex h="full" w="full">
|
||||
<Flex
|
||||
pos="absolute"
|
||||
zIndex={2}
|
||||
bgColor="white"
|
||||
w="full"
|
||||
justifyContent="center"
|
||||
h="60px"
|
||||
>
|
||||
<HStack maxW="1200px" w="full">
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
colorScheme={!isAnalytics ? 'blue' : 'gray'}
|
||||
@ -43,9 +42,9 @@ export const ResultsContent = () => {
|
||||
href={`/typebots/${typebot?.id}/results`}
|
||||
>
|
||||
<Text>Submissions</Text>
|
||||
{totalResults && (
|
||||
{(stats?.totalStarts ?? 0) > 0 && (
|
||||
<Tag size="sm" colorScheme="blue" ml="1">
|
||||
{totalResults}
|
||||
{stats?.totalStarts}
|
||||
</Tag>
|
||||
)}
|
||||
</Button>
|
||||
@ -59,16 +58,18 @@ export const ResultsContent = () => {
|
||||
Analytics
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Flex pt="60px" w="full" justify="center">
|
||||
{typebot &&
|
||||
(isAnalytics ? (
|
||||
<AnalyticsContent />
|
||||
<AnalyticsContent stats={stats} />
|
||||
) : (
|
||||
<SubmissionsContent
|
||||
typebotId={typebot.id}
|
||||
totalResults={totalResults ?? 0}
|
||||
totalResults={stats?.totalStarts ?? 0}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ export const SubmissionsContent = ({ typebotId, totalResults }: Props) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack maxW="1200px" w="full">
|
||||
<Flex w="full" justifyContent="flex-end">
|
||||
<HStack>
|
||||
<HStack as={Button} colorScheme="blue">
|
||||
|
@ -25,6 +25,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
where: {
|
||||
typebotId,
|
||||
typebot: { ownerId: user.id },
|
||||
answers: { some: {} },
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { PublicTypebot } from 'bot-engine'
|
||||
import { User } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getSession } from 'next-auth/react'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (!session?.user)
|
||||
return res.status(401).send({ message: 'Not authenticated' })
|
||||
|
||||
const user = session.user as User
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId.toString()
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: { id: typebotId },
|
||||
include: { publishedTypebot: true },
|
||||
})
|
||||
if (!typebot) return res.status(404).send({ answersCounts: [] })
|
||||
if (typebot?.ownerId !== user.id)
|
||||
return res.status(403).send({ message: 'Forbidden' })
|
||||
|
||||
const answersCounts: { blockId: string; totalAnswers: number }[] =
|
||||
await Promise.all(
|
||||
(typebot.publishedTypebot as PublicTypebot).blocks.map(
|
||||
async (block) => {
|
||||
const totalAnswers = await prisma.answer.count({
|
||||
where: { blockId: block.id },
|
||||
})
|
||||
return { blockId: block.id, totalAnswers }
|
||||
}
|
||||
)
|
||||
)
|
||||
return res.status(200).send({ answersCounts })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
@ -1,3 +1,4 @@
|
||||
import { Stats } from 'bot-engine'
|
||||
import { User } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
@ -13,13 +14,33 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = session.user as User
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId.toString()
|
||||
const totalResults = await prisma.result.count({
|
||||
|
||||
const totalViews = await prisma.result.count({
|
||||
where: {
|
||||
typebotId,
|
||||
typebot: { ownerId: user.id },
|
||||
},
|
||||
})
|
||||
return res.status(200).send({ totalResults })
|
||||
const totalStarts = await prisma.result.count({
|
||||
where: {
|
||||
typebotId,
|
||||
typebot: { ownerId: user.id },
|
||||
answers: { some: {} },
|
||||
},
|
||||
})
|
||||
const totalCompleted = await prisma.result.count({
|
||||
where: {
|
||||
typebotId,
|
||||
typebot: { ownerId: user.id },
|
||||
isCompleted: true,
|
||||
},
|
||||
})
|
||||
const stats: Stats = {
|
||||
totalViews,
|
||||
totalStarts,
|
||||
completionRate: Math.round((totalCompleted / totalStarts) * 100),
|
||||
}
|
||||
return res.status(200).send({ stats })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
@ -3,7 +3,6 @@ import { Board } from 'components/board/Board'
|
||||
import { Seo } from 'components/Seo'
|
||||
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
||||
import { EditorContext } from 'contexts/EditorContext'
|
||||
import { GraphProvider } from 'contexts/GraphContext'
|
||||
import { TypebotContext } from 'contexts/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import { KBarProvider } from 'kbar'
|
||||
@ -23,9 +22,7 @@ const TypebotEditPage = () => {
|
||||
<KBar />
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<TypebotHeader />
|
||||
<GraphProvider>
|
||||
<Board />
|
||||
</GraphProvider>
|
||||
</Flex>
|
||||
</KBarProvider>
|
||||
</EditorContext>
|
||||
|
45
apps/builder/services/analytics.ts
Normal file
45
apps/builder/services/analytics.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Stats } from 'bot-engine'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from './utils'
|
||||
|
||||
export const useStats = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ stats: Stats }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}/results/stats` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
stats: data?.stats,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export type AnswersCount = { blockId: string; totalAnswers: number }
|
||||
export const useAnswersCount = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ answersCounts: AnswersCount[] },
|
||||
Error
|
||||
>(
|
||||
typebotId ? `/api/typebots/${typebotId}/results/answers/count` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
answersCounts: data?.answersCounts,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
@ -11,6 +11,29 @@ import {
|
||||
import { roundCorners } from 'svg-round-corners'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
export const computeDropOffPath = (
|
||||
sourcePosition: Coordinates,
|
||||
sourceStepIndex: number
|
||||
) => {
|
||||
const sourceCoord = computeSourceCoordinates(sourcePosition, sourceStepIndex)
|
||||
const segments = computeTwoSegments(sourceCoord, {
|
||||
x: sourceCoord.x + 20,
|
||||
y: sourceCoord.y + 80,
|
||||
})
|
||||
return roundCorners(`M${sourceCoord.x},${sourceCoord.y} ${segments}`, 10).path
|
||||
}
|
||||
|
||||
export const computeSourceCoordinates = (
|
||||
sourcePosition: Coordinates,
|
||||
sourceStepIndex: number
|
||||
) => ({
|
||||
x: sourcePosition.x + blockWidth,
|
||||
y:
|
||||
(sourcePosition.y ?? 0) +
|
||||
firstStepOffsetY +
|
||||
spaceBetweenSteps * sourceStepIndex,
|
||||
})
|
||||
|
||||
export const computeFlowChartConnectorPath = ({
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
@ -125,20 +148,16 @@ export const getAnchorsPosition = (
|
||||
sourceStepIndex: number,
|
||||
targetStepIndex?: number
|
||||
): AnchorsPositionProps => {
|
||||
const sourceOffsetY =
|
||||
(sourceBlock.graphCoordinates.y ?? 0) +
|
||||
firstStepOffsetY +
|
||||
spaceBetweenSteps * sourceStepIndex
|
||||
const targetOffsetY = isDefined(targetStepIndex)
|
||||
? (targetBlock.graphCoordinates.y ?? 0) +
|
||||
firstStepOffsetY +
|
||||
spaceBetweenSteps * targetStepIndex
|
||||
: undefined
|
||||
|
||||
const sourcePosition = {
|
||||
x: (sourceBlock.graphCoordinates.x ?? 0) + blockWidth,
|
||||
y: sourceOffsetY,
|
||||
}
|
||||
const sourcePosition = computeSourceCoordinates(
|
||||
sourceBlock.graphCoordinates,
|
||||
sourceStepIndex
|
||||
)
|
||||
let sourceType: 'right' | 'left' = 'right'
|
||||
if (sourceBlock.graphCoordinates.x > targetBlock.graphCoordinates.x) {
|
||||
sourcePosition.x = sourceBlock.graphCoordinates.x
|
||||
@ -148,7 +167,7 @@ export const getAnchorsPosition = (
|
||||
const { targetPosition, totalSegments } = computeBlockTargetPosition(
|
||||
sourceBlock.graphCoordinates,
|
||||
targetBlock.graphCoordinates,
|
||||
sourceOffsetY,
|
||||
sourcePosition.y,
|
||||
targetOffsetY
|
||||
)
|
||||
return { sourcePosition, targetPosition, sourceType, totalSegments }
|
||||
|
@ -28,25 +28,6 @@ export const useResults = ({
|
||||
}
|
||||
}
|
||||
|
||||
export const useResultsCount = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ totalResults: number }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}/results/count` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
totalResults: data?.totalResults,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseDateToReadable = (dateStr: string): string => {
|
||||
const date = new Date(dateStr)
|
||||
return (
|
||||
|
@ -8,7 +8,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const id = req.query.id.toString()
|
||||
const result = await prisma.result.update({
|
||||
where: { id },
|
||||
data: { ...data, updatedAt: new Date() },
|
||||
data,
|
||||
})
|
||||
return res.send(result)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Answer } from 'db'
|
||||
import { Answer } from 'bot-engine'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const upsertAnswer = async (answer: Answer) => {
|
||||
export const upsertAnswer = async (answer: Answer & { resultId: string }) => {
|
||||
return sendRequest<Answer>({
|
||||
url: `/api/answers`,
|
||||
method: 'PUT',
|
||||
|
@ -16,7 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv-cli": "^4.1.1",
|
||||
"turbo": "^1.0.24-canary.2"
|
||||
"turbo": "^1.0.24"
|
||||
},
|
||||
"turbo": {
|
||||
"baseBranch": "origin/main",
|
||||
|
@ -1,3 +1,9 @@
|
||||
import { Answer as AnswerFromPrisma } from 'db'
|
||||
|
||||
export type Answer = Omit<AnswerFromPrisma, 'resultId'>
|
||||
export type Answer = Omit<AnswerFromPrisma, 'resultId' | 'createdAt'>
|
||||
|
||||
export type Stats = {
|
||||
totalViews: number
|
||||
totalStarts: number
|
||||
completionRate: number
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `updatedAt` on the `Result` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Result" DROP COLUMN "updatedAt";
|
@ -112,7 +112,6 @@ model PublicTypebot {
|
||||
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)
|
||||
answers Answer[]
|
||||
|
128
yarn.lock
128
yarn.lock
@ -7325,83 +7325,83 @@ tunnel-agent@^0.6.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
turbo-darwin-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24-canary.2.tgz#5d64c95d14fb06f45b2c0c44d1657b1884e009d7"
|
||||
integrity sha512-MTY6Eea5QQc+ZqC2EqZWtIRaIygenWY7i0f+PVyCyfaXp5TqZiIUljd4s1vfp4tygVg66EVkI2Pu34FsXNVB3g==
|
||||
turbo-darwin-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24.tgz#f135baff0e44f9160c9b027e8c4dd2d5c8bb10a7"
|
||||
integrity sha512-A65Wxp+jBMfI3QX2uObX6DKvk+TxNXTf7ufQTHvRSLeAreB8QiVzJdYE0nC6YdrRwfPgFY3L72dhYd2v8ouXDg==
|
||||
|
||||
turbo-darwin-arm64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24-canary.2.tgz#2169cebc1be5d8fbcf8334a50d411432c7480c10"
|
||||
integrity sha512-FaaAJ25smSeKjnkyUYxJhth0L59VmGjM9MrHXg+KhjU9jPFzyXTr9Bnb0nV6TAFRcL+IzZ3TXHykRCqfY4FjYA==
|
||||
turbo-darwin-arm64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24.tgz#c360d7cc6a7403855733e3aebb841b1227fbbb2e"
|
||||
integrity sha512-31zfexqUhvk/CIfAUk2mwjlpEjIURXu4QG8hoWlGxpcpAhlnkIX6CXle+LoQSnU3+4EbNe2SE92fYXsT/SnHAg==
|
||||
|
||||
turbo-freebsd-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24-canary.2.tgz#91528c2a75f621d7e7861e94b73c9a70443206ef"
|
||||
integrity sha512-alNrWyWvpIdKoQARB85PJchwr6VqKbWdayq2zlzGnSnTmH0EQb0bSDL9zNsdiKu4ISGE2E7YUN616lfvqx3wHA==
|
||||
turbo-freebsd-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24.tgz#9ef8914e7d1aaa995a8001a0ad81f7cc4520d332"
|
||||
integrity sha512-vZYbDkOHH5eeQrxsAYldrh2nDY884irtmgJdGbpjryJgnJx+xzriZfoFalm/d1ZfG3ArENRJqGU+k6BriefZzw==
|
||||
|
||||
turbo-freebsd-arm64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24-canary.2.tgz#8cea1bf6335e284beea3136eb7cc811da8501c72"
|
||||
integrity sha512-vfS+L5NqRLN2w4R0QgmBFl9Z9pXnIYwoS3IWcfx2SRqTUdcqfrM3Mq3jHUdfQE2DufMYcGt1u28aakYb5LEpdA==
|
||||
turbo-freebsd-arm64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24.tgz#12644e8f1b077f9d7afb367f2b8c2a2e0592ca72"
|
||||
integrity sha512-TDIu1PlyusY8AB69KGM4wGrCjtfbzmVF4Hlgf9mVeSWVKzqkRASorOEq1k8KvfZ+sBTS2GBMpqwpa1KVkYpVhw==
|
||||
|
||||
turbo-linux-32@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24-canary.2.tgz#41d68a780b805111a05a2e4b692b3f10c944bb51"
|
||||
integrity sha512-kjhHH3IQv83MrxpW5ScWlC08XzoWgoI2IbrZsZeZYXVdzDgIk5Uf8PfiUe0fJXpdUL8bIFq3nQ+yX8U/FuChAw==
|
||||
turbo-linux-32@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24.tgz#6129f7560f5c48214c1724ae7e8196dedc56de21"
|
||||
integrity sha512-lhhK7914sUtuWYcDO8LV7NQkvTIwpAZlYH0XEOC/OTiYRQJvtKbEySLvefvtwuGjx7cGNI6OYraUsY3WWoK3FA==
|
||||
|
||||
turbo-linux-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24-canary.2.tgz#18331dfc7b1d0f89a5f82cc17383571186779e85"
|
||||
integrity sha512-v8+u+gV01UsCl7psvptL+mr863lrVr3L1j0CFKXhjyZQJlEgXoRbkxU4VUWaXwf9ABaadsPsmv9B2oQnudypeA==
|
||||
turbo-linux-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24.tgz#221e3e14037e8fc3108e12a62de209d8a47f0348"
|
||||
integrity sha512-EbfdrkwVsHDG7AIVQ1enWHoD6riAApx4VRAuFcQHTvJU9e+BuOQBMjb7e9jO4mUrpumtN3n20tP+86odRwsk5g==
|
||||
|
||||
turbo-linux-arm64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24-canary.2.tgz#8873aa5b812f668934d16b69d59f5030b3d9fc8e"
|
||||
integrity sha512-v3/XDIQ9Cyz1dfPXnuJyH2V4xIDGPw9SaQS3FPiGH6TvUULkFYphg+cLttk7DPo3573GDxUQug3c5TvlIZFO8Q==
|
||||
turbo-linux-arm64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24.tgz#95891e7d4375ccbf2478677568557948be33717a"
|
||||
integrity sha512-H4rqlgP2L7G3iAB/un/7DclExzLUkQ1NoZ0p/1Oa7Wb8H1YUlc8GkwUmpIFd5AOFSPL75DjYvlS8T5Tm23i+1A==
|
||||
|
||||
turbo-linux-arm@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24-canary.2.tgz#22126953b881f952a1b5f480a3053c514cf314aa"
|
||||
integrity sha512-pkfSuuOjN8DDJDOAdx0jqwTL4jeEsZbjmgNnhgrkb1HwVjQCjmqHbh15Wa2E51G12/Ctr6h2aKit2kbLXPfGWw==
|
||||
turbo-linux-arm@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24.tgz#f5acb74170a8b5a787915e799e7b52840c7c6982"
|
||||
integrity sha512-lCNDVEkwxcn0acyPFVJgV5N5vKAP4LfXb+8uW/JpGHVoPHSONKtzYQG05J1KbHXpIjUT+DNgFtshtsdZYOewZQ==
|
||||
|
||||
turbo-linux-mips64le@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24-canary.2.tgz#11254292c40ac8acef8452a379bb74bad39bf858"
|
||||
integrity sha512-35WJMoIxfIG8ruoMJVdOCxuQlJjNeLbxbqigDizJh2zkjlJITzEK+WwmF9KrqzMaN7Ks4LPq+xd/YvBEULx4Zw==
|
||||
turbo-linux-mips64le@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24.tgz#f2cc99570222ac42fdcc0d0638f13bc0176859f9"
|
||||
integrity sha512-AmrgQUDIe9AdNyh5YrI6pfMTUHD/gYfbylNmedLuN5Al3xINdZObcISzd/7VWd+V8wNW/1b9lUnt70Rv/KExfA==
|
||||
|
||||
turbo-linux-ppc64le@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24-canary.2.tgz#abc93ede90deca66f20c23fedc0f2b032e43a48a"
|
||||
integrity sha512-ZCipAVam+ZKyiQ5KYrL8pPgQ06JJ09H6mqTxEAP0OP2kN6Se+TuycFjAsedE0Gn/G+6MBmStjR99ha+I2fRnDA==
|
||||
turbo-linux-ppc64le@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24.tgz#4d9508290d24cfdbaca24e57d8bcd0127281e2ed"
|
||||
integrity sha512-+6ESjsfrvRUr1AsurNcRTrqYr+XHG8g763+hXLog1MP9mn1cufZqWlAyE4G8/MLXDHsEKgK+tXqPLIyLBRjLEw==
|
||||
|
||||
turbo-windows-32@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24-canary.2.tgz#c24ae090c84c4ad83833deaeb0dba68186fe8e7d"
|
||||
integrity sha512-b9FBMoxhunGQNN/DwT9KK7g4FwDG3/cc4DEV5TdD809I66LmyVk1ThvpnUdZIRZRymcWq1c6kjT+0LfJFEBxGw==
|
||||
turbo-windows-32@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24.tgz#2bf906c0cc9d675afc4693221fc339ade29e6c13"
|
||||
integrity sha512-pqRys+FfHxuLVmW/AariITL5qpItp4WPAsYnWLx4u7VpCOO/qmTAI/SL7/jnTm4gxjBv3uf//lisu0AvEZd+TA==
|
||||
|
||||
turbo-windows-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24-canary.2.tgz#f783fcb3661634cc3c1f578fdc79441ed3de5f8e"
|
||||
integrity sha512-HqXW3k+5h4nP8eCg5i/0NhUlR4+3vxTSyI3UvoACVz/IkiUIwVKF9ZhlAfFpkRCYU+1KKOMs7iDWlYiTyW4Inw==
|
||||
turbo-windows-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24.tgz#5dd30b10110f2bb69caa479ddd72b4c471fb0dea"
|
||||
integrity sha512-YHAWha5XkW0Ate1HtwhzFD32kZFXtC8KB4ReEvHc9GM2inQob1ZinvktS0xi5MC5Sxl9+bObOWmsxeZPOgNCFA==
|
||||
|
||||
turbo@^1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24-canary.2.tgz#7b7c134d4e6dce0cf6e9e7b09393dde9767c397e"
|
||||
integrity sha512-Ve/KYeSxPBGKB6qmHc7BBZhZ7AaFH6WeUjd97IB38zfEtrcLlQ0xziY8EX7qiACarbrV9TKkYG5QctI5OIbD9A==
|
||||
turbo@^1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24.tgz#5efdeb44aab2f5e97b24a3e0ed4a159bfcd0a877"
|
||||
integrity sha512-bfOr7iW48+chDl+yKiZ5FIWzXOF6xOIyrAGPaWI+I5CdD27IZCEGvqvTV/weaHvjLbV7otybHQ56XCybBlVjoA==
|
||||
optionalDependencies:
|
||||
turbo-darwin-64 "1.0.24-canary.2"
|
||||
turbo-darwin-arm64 "1.0.24-canary.2"
|
||||
turbo-freebsd-64 "1.0.24-canary.2"
|
||||
turbo-freebsd-arm64 "1.0.24-canary.2"
|
||||
turbo-linux-32 "1.0.24-canary.2"
|
||||
turbo-linux-64 "1.0.24-canary.2"
|
||||
turbo-linux-arm "1.0.24-canary.2"
|
||||
turbo-linux-arm64 "1.0.24-canary.2"
|
||||
turbo-linux-mips64le "1.0.24-canary.2"
|
||||
turbo-linux-ppc64le "1.0.24-canary.2"
|
||||
turbo-windows-32 "1.0.24-canary.2"
|
||||
turbo-windows-64 "1.0.24-canary.2"
|
||||
turbo-darwin-64 "1.0.24"
|
||||
turbo-darwin-arm64 "1.0.24"
|
||||
turbo-freebsd-64 "1.0.24"
|
||||
turbo-freebsd-arm64 "1.0.24"
|
||||
turbo-linux-32 "1.0.24"
|
||||
turbo-linux-64 "1.0.24"
|
||||
turbo-linux-arm "1.0.24"
|
||||
turbo-linux-arm64 "1.0.24"
|
||||
turbo-linux-mips64le "1.0.24"
|
||||
turbo-linux-ppc64le "1.0.24"
|
||||
turbo-windows-32 "1.0.24"
|
||||
turbo-windows-64 "1.0.24"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
|
Reference in New Issue
Block a user