🖐️ 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 { StepTypesList } from './StepTypesList'
|
||||||
import { PreviewDrawer } from './preview/PreviewDrawer'
|
import { PreviewDrawer } from './preview/PreviewDrawer'
|
||||||
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||||
|
import { GraphProvider } from 'contexts/GraphContext'
|
||||||
|
|
||||||
export const Board = () => {
|
export const Board = () => {
|
||||||
const { rightPanel } = useEditor()
|
const { rightPanel } = useEditor()
|
||||||
@ -12,7 +13,9 @@ export const Board = () => {
|
|||||||
<Flex flex="1" pos="relative" bgColor="gray.50" h="full">
|
<Flex flex="1" pos="relative" bgColor="gray.50" h="full">
|
||||||
<DndContext>
|
<DndContext>
|
||||||
<StepTypesList />
|
<StepTypesList />
|
||||||
<Graph />
|
<GraphProvider>
|
||||||
|
<Graph flex="1" />
|
||||||
|
</GraphProvider>
|
||||||
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
|
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
useEventListener,
|
useEventListener,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import React, { useEffect, useMemo, useState } from '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 { useGraph } from 'contexts/GraphContext'
|
||||||
import { useDnd } from 'contexts/DndContext'
|
import { useDnd } from 'contexts/DndContext'
|
||||||
import { StepsList } from './StepsList'
|
import { StepsList } from './StepsList'
|
||||||
@ -15,7 +15,11 @@ import { useTypebot } from 'contexts/TypebotContext'
|
|||||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||||
import { BlockNodeContextMenu } from './BlockNodeContextMenu'
|
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 { connectingIds, setConnectingIds, previewingIds } = useGraph()
|
||||||
const { updateBlockPosition, addStepToBlock } = useTypebot()
|
const { updateBlockPosition, addStepToBlock } = useTypebot()
|
||||||
const { draggedStep, draggedStepType, setDraggedStepType, setDraggedStep } =
|
const { draggedStep, draggedStepType, setDraggedStepType, setDraggedStep } =
|
||||||
|
@ -11,7 +11,10 @@ import { StepNode } from './StepNode'
|
|||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { useGraph } from 'contexts/GraphContext'
|
import { useGraph } from 'contexts/GraphContext'
|
||||||
|
|
||||||
export const StartBlockNode = ({ block }: { block: StartBlock }) => {
|
type Props = {
|
||||||
|
block: StartBlock
|
||||||
|
}
|
||||||
|
export const StartBlockNode = ({ block }: Props) => {
|
||||||
const { previewingIds } = useGraph()
|
const { previewingIds } = useGraph()
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||||
const [titleValue, setTitleValue] = useState(block.title)
|
const [titleValue, setTitleValue] = useState(block.title)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { StackProps, HStack } from '@chakra-ui/react'
|
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 { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||||
import { StepContent } from './StepContent'
|
import { StepContent } from './StepContent'
|
||||||
|
|
||||||
export const StepNodeOverlay = ({
|
export const StepNodeOverlay = ({
|
||||||
step,
|
step,
|
||||||
...props
|
...props
|
||||||
}: { step: Step } & StackProps) => {
|
}: { step: Step | StartStep } & StackProps) => {
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
p="3"
|
p="3"
|
||||||
@ -14,9 +14,6 @@ export const StepNodeOverlay = ({
|
|||||||
rounded="lg"
|
rounded="lg"
|
||||||
bgColor="white"
|
bgColor="white"
|
||||||
cursor={'grab'}
|
cursor={'grab'}
|
||||||
pos="fixed"
|
|
||||||
top="0"
|
|
||||||
left="0"
|
|
||||||
w="264px"
|
w="264px"
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -118,6 +118,9 @@ export const StepsList = ({
|
|||||||
<StepNodeOverlay
|
<StepNodeOverlay
|
||||||
step={draggedStep}
|
step={draggedStep}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
|
pos="fixed"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
|
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 React, { useRef, useMemo } from 'react'
|
||||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
||||||
import { BlockNode } from './BlockNode/BlockNode'
|
import { BlockNode } from './BlockNode/BlockNode'
|
||||||
@ -8,11 +8,11 @@ import { useTypebot } from 'contexts/TypebotContext'
|
|||||||
import { StartBlockNode } from './BlockNode/StartBlockNode'
|
import { StartBlockNode } from './BlockNode/StartBlockNode'
|
||||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||||
|
|
||||||
const Graph = () => {
|
const Graph = ({ ...props }: FlexProps) => {
|
||||||
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
||||||
useDnd()
|
useDnd()
|
||||||
const graphContainerRef = useRef<HTMLDivElement | null>(null)
|
const graphContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
const { typebot, addNewBlock } = useTypebot()
|
const { addNewBlock, typebot } = useTypebot()
|
||||||
const { graphPosition, setGraphPosition } = useGraph()
|
const { graphPosition, setGraphPosition } = useGraph()
|
||||||
const transform = useMemo(
|
const transform = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -60,7 +60,7 @@ const Graph = () => {
|
|||||||
|
|
||||||
if (!typebot) return <></>
|
if (!typebot) return <></>
|
||||||
return (
|
return (
|
||||||
<Flex ref={graphContainerRef} flex="1">
|
<Flex ref={graphContainerRef} {...props}>
|
||||||
<Flex
|
<Flex
|
||||||
flex="1"
|
flex="1"
|
||||||
boxSize={'200px'}
|
boxSize={'200px'}
|
||||||
@ -70,6 +70,7 @@ const Graph = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Edges />
|
<Edges />
|
||||||
|
{props.children}
|
||||||
{typebot.startBlock && <StartBlockNode block={typebot.startBlock} />}
|
{typebot.startBlock && <StartBlockNode block={typebot.startBlock} />}
|
||||||
{(typebot.blocks ?? []).map((block) => (
|
{(typebot.blocks ?? []).map((block) => (
|
||||||
<BlockNode block={block} key={block.id} />
|
<BlockNode block={block} key={block.id} />
|
||||||
|
@ -37,8 +37,7 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(MouseSensor, {
|
useSensor(MouseSensor, {
|
||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
delay: 100,
|
distance: 20,
|
||||||
tolerance: 300,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -92,7 +92,13 @@ export const TypebotButton = ({
|
|||||||
>
|
>
|
||||||
<MoreButton pos="absolute" top="10px" right="10px">
|
<MoreButton pos="absolute" top="10px" right="10px">
|
||||||
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
|
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
|
||||||
<MenuItem color="red" onClick={onDeleteOpen}>
|
<MenuItem
|
||||||
|
color="red"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onDeleteOpen()
|
||||||
|
}}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MoreButton>
|
</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 () => {
|
const publishTypebot = async () => {
|
||||||
if (!localTypebot) return
|
if (!localTypebot) return
|
||||||
if (!localPublishedTypebot)
|
if (!localPublishedTypebot) {
|
||||||
updatePublicId(parseDefaultPublicId(localTypebot.name, localTypebot.id))
|
const newPublicId = parseDefaultPublicId(
|
||||||
if (hasUnsavedChanges) await saveTypebot()
|
localTypebot.name,
|
||||||
|
localTypebot.id
|
||||||
|
)
|
||||||
|
updatePublicId(newPublicId)
|
||||||
|
localTypebot.publicId = newPublicId
|
||||||
|
}
|
||||||
|
if (hasUnsavedChanges || !localPublishedTypebot) await saveTypebot()
|
||||||
setIsPublishing(true)
|
setIsPublishing(true)
|
||||||
if (localPublishedTypebot) {
|
if (localPublishedTypebot) {
|
||||||
const { error } = await updatePublishedTypebot(
|
const { error } = await updatePublishedTypebot(
|
||||||
|
@ -4,7 +4,7 @@ describe('Dashboard page', () => {
|
|||||||
cy.signOut()
|
cy.signOut()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should navigate correctly', () => {
|
test('folders navigation should work', () => {
|
||||||
cy.signIn('test1@gmail.com')
|
cy.signIn('test1@gmail.com')
|
||||||
cy.visit('/typebots')
|
cy.visit('/typebots')
|
||||||
createFolder('My folder #1')
|
createFolder('My folder #1')
|
||||||
@ -26,7 +26,7 @@ describe('Dashboard page', () => {
|
|||||||
cy.findByDisplayValue('My folder #2').should('not.exist')
|
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.signIn('test2@gmail.com')
|
||||||
cy.visit('/typebots')
|
cy.visit('/typebots')
|
||||||
cy.findByTestId('typebot-button-typebot1').mouseMoveBy(-100, 0, {
|
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 React from 'react'
|
||||||
|
import { useAnswersCount } from 'services/analytics'
|
||||||
|
|
||||||
export const AnalyticsContent = () => {
|
export const AnalyticsContent = ({ stats }: { stats?: Stats }) => {
|
||||||
return <>Hi</>
|
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 {
|
import { Button, Flex, HStack, Tag, useToast, Text } from '@chakra-ui/react'
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
HStack,
|
|
||||||
Stack,
|
|
||||||
Tag,
|
|
||||||
useToast,
|
|
||||||
Text,
|
|
||||||
} from '@chakra-ui/react'
|
|
||||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { useResultsCount } from 'services/results'
|
import { useStats } from 'services/analytics'
|
||||||
import { AnalyticsContent } from './AnalyticsContent'
|
import { AnalyticsContent } from './AnalyticsContent'
|
||||||
import { SubmissionsContent } from './SubmissionContent'
|
import { SubmissionsContent } from './SubmissionContent'
|
||||||
|
|
||||||
@ -27,14 +19,21 @@ export const ResultsContent = () => {
|
|||||||
status: 'error',
|
status: 'error',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { totalResults } = useResultsCount({
|
const { stats } = useStats({
|
||||||
typebotId: typebot?.id,
|
typebotId: typebot?.id,
|
||||||
onError: (err) => toast({ title: err.name, description: err.message }),
|
onError: (err) => toast({ title: err.name, description: err.message }),
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
<Flex h="full" w="full">
|
||||||
<Stack maxW="1200px" w="full" pt="4" spacing={6}>
|
<Flex
|
||||||
<HStack>
|
pos="absolute"
|
||||||
|
zIndex={2}
|
||||||
|
bgColor="white"
|
||||||
|
w="full"
|
||||||
|
justifyContent="center"
|
||||||
|
h="60px"
|
||||||
|
>
|
||||||
|
<HStack maxW="1200px" w="full">
|
||||||
<Button
|
<Button
|
||||||
as={NextChakraLink}
|
as={NextChakraLink}
|
||||||
colorScheme={!isAnalytics ? 'blue' : 'gray'}
|
colorScheme={!isAnalytics ? 'blue' : 'gray'}
|
||||||
@ -43,9 +42,9 @@ export const ResultsContent = () => {
|
|||||||
href={`/typebots/${typebot?.id}/results`}
|
href={`/typebots/${typebot?.id}/results`}
|
||||||
>
|
>
|
||||||
<Text>Submissions</Text>
|
<Text>Submissions</Text>
|
||||||
{totalResults && (
|
{(stats?.totalStarts ?? 0) > 0 && (
|
||||||
<Tag size="sm" colorScheme="blue" ml="1">
|
<Tag size="sm" colorScheme="blue" ml="1">
|
||||||
{totalResults}
|
{stats?.totalStarts}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@ -59,16 +58,18 @@ export const ResultsContent = () => {
|
|||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
<Flex pt="60px" w="full" justify="center">
|
||||||
{typebot &&
|
{typebot &&
|
||||||
(isAnalytics ? (
|
(isAnalytics ? (
|
||||||
<AnalyticsContent />
|
<AnalyticsContent stats={stats} />
|
||||||
) : (
|
) : (
|
||||||
<SubmissionsContent
|
<SubmissionsContent
|
||||||
typebotId={typebot.id}
|
typebotId={typebot.id}
|
||||||
totalResults={totalResults ?? 0}
|
totalResults={stats?.totalStarts ?? 0}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export const SubmissionsContent = ({ typebotId, totalResults }: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack maxW="1200px" w="full">
|
||||||
<Flex w="full" justifyContent="flex-end">
|
<Flex w="full" justifyContent="flex-end">
|
||||||
<HStack>
|
<HStack>
|
||||||
<HStack as={Button} colorScheme="blue">
|
<HStack as={Button} colorScheme="blue">
|
||||||
|
@ -25,6 +25,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
where: {
|
where: {
|
||||||
typebotId,
|
typebotId,
|
||||||
typebot: { ownerId: user.id },
|
typebot: { ownerId: user.id },
|
||||||
|
answers: { some: {} },
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
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 { User } from 'db'
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
@ -13,13 +14,33 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
const user = session.user as User
|
const user = session.user as User
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const typebotId = req.query.typebotId.toString()
|
const typebotId = req.query.typebotId.toString()
|
||||||
const totalResults = await prisma.result.count({
|
|
||||||
|
const totalViews = await prisma.result.count({
|
||||||
where: {
|
where: {
|
||||||
typebotId,
|
typebotId,
|
||||||
typebot: { ownerId: user.id },
|
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)
|
return methodNotAllowed(res)
|
||||||
}
|
}
|
@ -3,7 +3,6 @@ import { Board } from 'components/board/Board'
|
|||||||
import { Seo } from 'components/Seo'
|
import { Seo } from 'components/Seo'
|
||||||
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
||||||
import { EditorContext } from 'contexts/EditorContext'
|
import { EditorContext } from 'contexts/EditorContext'
|
||||||
import { GraphProvider } from 'contexts/GraphContext'
|
|
||||||
import { TypebotContext } from 'contexts/TypebotContext'
|
import { TypebotContext } from 'contexts/TypebotContext'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { KBarProvider } from 'kbar'
|
import { KBarProvider } from 'kbar'
|
||||||
@ -23,9 +22,7 @@ const TypebotEditPage = () => {
|
|||||||
<KBar />
|
<KBar />
|
||||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
<TypebotHeader />
|
<TypebotHeader />
|
||||||
<GraphProvider>
|
<Board />
|
||||||
<Board />
|
|
||||||
</GraphProvider>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</KBarProvider>
|
</KBarProvider>
|
||||||
</EditorContext>
|
</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 { roundCorners } from 'svg-round-corners'
|
||||||
import { isDefined } from 'utils'
|
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 = ({
|
export const computeFlowChartConnectorPath = ({
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
@ -125,20 +148,16 @@ export const getAnchorsPosition = (
|
|||||||
sourceStepIndex: number,
|
sourceStepIndex: number,
|
||||||
targetStepIndex?: number
|
targetStepIndex?: number
|
||||||
): AnchorsPositionProps => {
|
): AnchorsPositionProps => {
|
||||||
const sourceOffsetY =
|
|
||||||
(sourceBlock.graphCoordinates.y ?? 0) +
|
|
||||||
firstStepOffsetY +
|
|
||||||
spaceBetweenSteps * sourceStepIndex
|
|
||||||
const targetOffsetY = isDefined(targetStepIndex)
|
const targetOffsetY = isDefined(targetStepIndex)
|
||||||
? (targetBlock.graphCoordinates.y ?? 0) +
|
? (targetBlock.graphCoordinates.y ?? 0) +
|
||||||
firstStepOffsetY +
|
firstStepOffsetY +
|
||||||
spaceBetweenSteps * targetStepIndex
|
spaceBetweenSteps * targetStepIndex
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const sourcePosition = {
|
const sourcePosition = computeSourceCoordinates(
|
||||||
x: (sourceBlock.graphCoordinates.x ?? 0) + blockWidth,
|
sourceBlock.graphCoordinates,
|
||||||
y: sourceOffsetY,
|
sourceStepIndex
|
||||||
}
|
)
|
||||||
let sourceType: 'right' | 'left' = 'right'
|
let sourceType: 'right' | 'left' = 'right'
|
||||||
if (sourceBlock.graphCoordinates.x > targetBlock.graphCoordinates.x) {
|
if (sourceBlock.graphCoordinates.x > targetBlock.graphCoordinates.x) {
|
||||||
sourcePosition.x = sourceBlock.graphCoordinates.x
|
sourcePosition.x = sourceBlock.graphCoordinates.x
|
||||||
@ -148,7 +167,7 @@ export const getAnchorsPosition = (
|
|||||||
const { targetPosition, totalSegments } = computeBlockTargetPosition(
|
const { targetPosition, totalSegments } = computeBlockTargetPosition(
|
||||||
sourceBlock.graphCoordinates,
|
sourceBlock.graphCoordinates,
|
||||||
targetBlock.graphCoordinates,
|
targetBlock.graphCoordinates,
|
||||||
sourceOffsetY,
|
sourcePosition.y,
|
||||||
targetOffsetY
|
targetOffsetY
|
||||||
)
|
)
|
||||||
return { sourcePosition, targetPosition, sourceType, totalSegments }
|
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 => {
|
export const parseDateToReadable = (dateStr: string): string => {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return (
|
return (
|
||||||
|
@ -8,7 +8,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
const id = req.query.id.toString()
|
const id = req.query.id.toString()
|
||||||
const result = await prisma.result.update({
|
const result = await prisma.result.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { ...data, updatedAt: new Date() },
|
data,
|
||||||
})
|
})
|
||||||
return res.send(result)
|
return res.send(result)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Answer } from 'db'
|
import { Answer } from 'bot-engine'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
export const upsertAnswer = async (answer: Answer) => {
|
export const upsertAnswer = async (answer: Answer & { resultId: string }) => {
|
||||||
return sendRequest<Answer>({
|
return sendRequest<Answer>({
|
||||||
url: `/api/answers`,
|
url: `/api/answers`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv-cli": "^4.1.1",
|
"dotenv-cli": "^4.1.1",
|
||||||
"turbo": "^1.0.24-canary.2"
|
"turbo": "^1.0.24"
|
||||||
},
|
},
|
||||||
"turbo": {
|
"turbo": {
|
||||||
"baseBranch": "origin/main",
|
"baseBranch": "origin/main",
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
import { Answer as AnswerFromPrisma } from 'db'
|
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 {
|
model Result {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now())
|
|
||||||
typebotId String
|
typebotId String
|
||||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||||
answers Answer[]
|
answers Answer[]
|
||||||
|
128
yarn.lock
128
yarn.lock
@ -7325,83 +7325,83 @@ tunnel-agent@^0.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
turbo-darwin-64@1.0.24-canary.2:
|
turbo-darwin-64@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24-canary.2.tgz#5d64c95d14fb06f45b2c0c44d1657b1884e009d7"
|
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24.tgz#f135baff0e44f9160c9b027e8c4dd2d5c8bb10a7"
|
||||||
integrity sha512-MTY6Eea5QQc+ZqC2EqZWtIRaIygenWY7i0f+PVyCyfaXp5TqZiIUljd4s1vfp4tygVg66EVkI2Pu34FsXNVB3g==
|
integrity sha512-A65Wxp+jBMfI3QX2uObX6DKvk+TxNXTf7ufQTHvRSLeAreB8QiVzJdYE0nC6YdrRwfPgFY3L72dhYd2v8ouXDg==
|
||||||
|
|
||||||
turbo-darwin-arm64@1.0.24-canary.2:
|
turbo-darwin-arm64@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24-canary.2.tgz#2169cebc1be5d8fbcf8334a50d411432c7480c10"
|
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24.tgz#c360d7cc6a7403855733e3aebb841b1227fbbb2e"
|
||||||
integrity sha512-FaaAJ25smSeKjnkyUYxJhth0L59VmGjM9MrHXg+KhjU9jPFzyXTr9Bnb0nV6TAFRcL+IzZ3TXHykRCqfY4FjYA==
|
integrity sha512-31zfexqUhvk/CIfAUk2mwjlpEjIURXu4QG8hoWlGxpcpAhlnkIX6CXle+LoQSnU3+4EbNe2SE92fYXsT/SnHAg==
|
||||||
|
|
||||||
turbo-freebsd-64@1.0.24-canary.2:
|
turbo-freebsd-64@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24-canary.2.tgz#91528c2a75f621d7e7861e94b73c9a70443206ef"
|
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24.tgz#9ef8914e7d1aaa995a8001a0ad81f7cc4520d332"
|
||||||
integrity sha512-alNrWyWvpIdKoQARB85PJchwr6VqKbWdayq2zlzGnSnTmH0EQb0bSDL9zNsdiKu4ISGE2E7YUN616lfvqx3wHA==
|
integrity sha512-vZYbDkOHH5eeQrxsAYldrh2nDY884irtmgJdGbpjryJgnJx+xzriZfoFalm/d1ZfG3ArENRJqGU+k6BriefZzw==
|
||||||
|
|
||||||
turbo-freebsd-arm64@1.0.24-canary.2:
|
turbo-freebsd-arm64@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24-canary.2.tgz#8cea1bf6335e284beea3136eb7cc811da8501c72"
|
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24.tgz#12644e8f1b077f9d7afb367f2b8c2a2e0592ca72"
|
||||||
integrity sha512-vfS+L5NqRLN2w4R0QgmBFl9Z9pXnIYwoS3IWcfx2SRqTUdcqfrM3Mq3jHUdfQE2DufMYcGt1u28aakYb5LEpdA==
|
integrity sha512-TDIu1PlyusY8AB69KGM4wGrCjtfbzmVF4Hlgf9mVeSWVKzqkRASorOEq1k8KvfZ+sBTS2GBMpqwpa1KVkYpVhw==
|
||||||
|
|
||||||
turbo-linux-32@1.0.24-canary.2:
|
turbo-linux-32@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24-canary.2.tgz#41d68a780b805111a05a2e4b692b3f10c944bb51"
|
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24.tgz#6129f7560f5c48214c1724ae7e8196dedc56de21"
|
||||||
integrity sha512-kjhHH3IQv83MrxpW5ScWlC08XzoWgoI2IbrZsZeZYXVdzDgIk5Uf8PfiUe0fJXpdUL8bIFq3nQ+yX8U/FuChAw==
|
integrity sha512-lhhK7914sUtuWYcDO8LV7NQkvTIwpAZlYH0XEOC/OTiYRQJvtKbEySLvefvtwuGjx7cGNI6OYraUsY3WWoK3FA==
|
||||||
|
|
||||||
turbo-linux-64@1.0.24-canary.2:
|
turbo-linux-64@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24-canary.2.tgz#18331dfc7b1d0f89a5f82cc17383571186779e85"
|
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24.tgz#221e3e14037e8fc3108e12a62de209d8a47f0348"
|
||||||
integrity sha512-v8+u+gV01UsCl7psvptL+mr863lrVr3L1j0CFKXhjyZQJlEgXoRbkxU4VUWaXwf9ABaadsPsmv9B2oQnudypeA==
|
integrity sha512-EbfdrkwVsHDG7AIVQ1enWHoD6riAApx4VRAuFcQHTvJU9e+BuOQBMjb7e9jO4mUrpumtN3n20tP+86odRwsk5g==
|
||||||
|
|
||||||
turbo-linux-arm64@1.0.24-canary.2:
|
turbo-linux-arm64@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24-canary.2.tgz#8873aa5b812f668934d16b69d59f5030b3d9fc8e"
|
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24.tgz#95891e7d4375ccbf2478677568557948be33717a"
|
||||||
integrity sha512-v3/XDIQ9Cyz1dfPXnuJyH2V4xIDGPw9SaQS3FPiGH6TvUULkFYphg+cLttk7DPo3573GDxUQug3c5TvlIZFO8Q==
|
integrity sha512-H4rqlgP2L7G3iAB/un/7DclExzLUkQ1NoZ0p/1Oa7Wb8H1YUlc8GkwUmpIFd5AOFSPL75DjYvlS8T5Tm23i+1A==
|
||||||
|
|
||||||
turbo-linux-arm@1.0.24-canary.2:
|
turbo-linux-arm@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24-canary.2.tgz#22126953b881f952a1b5f480a3053c514cf314aa"
|
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24.tgz#f5acb74170a8b5a787915e799e7b52840c7c6982"
|
||||||
integrity sha512-pkfSuuOjN8DDJDOAdx0jqwTL4jeEsZbjmgNnhgrkb1HwVjQCjmqHbh15Wa2E51G12/Ctr6h2aKit2kbLXPfGWw==
|
integrity sha512-lCNDVEkwxcn0acyPFVJgV5N5vKAP4LfXb+8uW/JpGHVoPHSONKtzYQG05J1KbHXpIjUT+DNgFtshtsdZYOewZQ==
|
||||||
|
|
||||||
turbo-linux-mips64le@1.0.24-canary.2:
|
turbo-linux-mips64le@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24-canary.2.tgz#11254292c40ac8acef8452a379bb74bad39bf858"
|
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24.tgz#f2cc99570222ac42fdcc0d0638f13bc0176859f9"
|
||||||
integrity sha512-35WJMoIxfIG8ruoMJVdOCxuQlJjNeLbxbqigDizJh2zkjlJITzEK+WwmF9KrqzMaN7Ks4LPq+xd/YvBEULx4Zw==
|
integrity sha512-AmrgQUDIe9AdNyh5YrI6pfMTUHD/gYfbylNmedLuN5Al3xINdZObcISzd/7VWd+V8wNW/1b9lUnt70Rv/KExfA==
|
||||||
|
|
||||||
turbo-linux-ppc64le@1.0.24-canary.2:
|
turbo-linux-ppc64le@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24-canary.2.tgz#abc93ede90deca66f20c23fedc0f2b032e43a48a"
|
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24.tgz#4d9508290d24cfdbaca24e57d8bcd0127281e2ed"
|
||||||
integrity sha512-ZCipAVam+ZKyiQ5KYrL8pPgQ06JJ09H6mqTxEAP0OP2kN6Se+TuycFjAsedE0Gn/G+6MBmStjR99ha+I2fRnDA==
|
integrity sha512-+6ESjsfrvRUr1AsurNcRTrqYr+XHG8g763+hXLog1MP9mn1cufZqWlAyE4G8/MLXDHsEKgK+tXqPLIyLBRjLEw==
|
||||||
|
|
||||||
turbo-windows-32@1.0.24-canary.2:
|
turbo-windows-32@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24-canary.2.tgz#c24ae090c84c4ad83833deaeb0dba68186fe8e7d"
|
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24.tgz#2bf906c0cc9d675afc4693221fc339ade29e6c13"
|
||||||
integrity sha512-b9FBMoxhunGQNN/DwT9KK7g4FwDG3/cc4DEV5TdD809I66LmyVk1ThvpnUdZIRZRymcWq1c6kjT+0LfJFEBxGw==
|
integrity sha512-pqRys+FfHxuLVmW/AariITL5qpItp4WPAsYnWLx4u7VpCOO/qmTAI/SL7/jnTm4gxjBv3uf//lisu0AvEZd+TA==
|
||||||
|
|
||||||
turbo-windows-64@1.0.24-canary.2:
|
turbo-windows-64@1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24-canary.2.tgz#f783fcb3661634cc3c1f578fdc79441ed3de5f8e"
|
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24.tgz#5dd30b10110f2bb69caa479ddd72b4c471fb0dea"
|
||||||
integrity sha512-HqXW3k+5h4nP8eCg5i/0NhUlR4+3vxTSyI3UvoACVz/IkiUIwVKF9ZhlAfFpkRCYU+1KKOMs7iDWlYiTyW4Inw==
|
integrity sha512-YHAWha5XkW0Ate1HtwhzFD32kZFXtC8KB4ReEvHc9GM2inQob1ZinvktS0xi5MC5Sxl9+bObOWmsxeZPOgNCFA==
|
||||||
|
|
||||||
turbo@^1.0.24-canary.2:
|
turbo@^1.0.24:
|
||||||
version "1.0.24-canary.2"
|
version "1.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24-canary.2.tgz#7b7c134d4e6dce0cf6e9e7b09393dde9767c397e"
|
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24.tgz#5efdeb44aab2f5e97b24a3e0ed4a159bfcd0a877"
|
||||||
integrity sha512-Ve/KYeSxPBGKB6qmHc7BBZhZ7AaFH6WeUjd97IB38zfEtrcLlQ0xziY8EX7qiACarbrV9TKkYG5QctI5OIbD9A==
|
integrity sha512-bfOr7iW48+chDl+yKiZ5FIWzXOF6xOIyrAGPaWI+I5CdD27IZCEGvqvTV/weaHvjLbV7otybHQ56XCybBlVjoA==
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
turbo-darwin-64 "1.0.24-canary.2"
|
turbo-darwin-64 "1.0.24"
|
||||||
turbo-darwin-arm64 "1.0.24-canary.2"
|
turbo-darwin-arm64 "1.0.24"
|
||||||
turbo-freebsd-64 "1.0.24-canary.2"
|
turbo-freebsd-64 "1.0.24"
|
||||||
turbo-freebsd-arm64 "1.0.24-canary.2"
|
turbo-freebsd-arm64 "1.0.24"
|
||||||
turbo-linux-32 "1.0.24-canary.2"
|
turbo-linux-32 "1.0.24"
|
||||||
turbo-linux-64 "1.0.24-canary.2"
|
turbo-linux-64 "1.0.24"
|
||||||
turbo-linux-arm "1.0.24-canary.2"
|
turbo-linux-arm "1.0.24"
|
||||||
turbo-linux-arm64 "1.0.24-canary.2"
|
turbo-linux-arm64 "1.0.24"
|
||||||
turbo-linux-mips64le "1.0.24-canary.2"
|
turbo-linux-mips64le "1.0.24"
|
||||||
turbo-linux-ppc64le "1.0.24-canary.2"
|
turbo-linux-ppc64le "1.0.24"
|
||||||
turbo-windows-32 "1.0.24-canary.2"
|
turbo-windows-32 "1.0.24"
|
||||||
turbo-windows-64 "1.0.24-canary.2"
|
turbo-windows-64 "1.0.24"
|
||||||
|
|
||||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||||
version "0.14.5"
|
version "0.14.5"
|
||||||
|
Reference in New Issue
Block a user