From 6322402c96265b78890c8143e72c5beebfc6c528 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 3 Jan 2022 17:39:59 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=96=90=EF=B8=8F=20Analytics=20drop=20off?= =?UTF-8?q?=20rates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/analytics/StatsCards.tsx | 44 ++++++ .../analytics/graph/AnalyticsGraph.tsx | 61 +++++++++ .../analytics/graph/Edges/DropOffEdge.tsx | 18 +++ .../components/analytics/graph/Edges/Edge.tsx | 56 ++++++++ .../analytics/graph/Edges/Edges.tsx | 69 ++++++++++ .../components/analytics/graph/Edges/index.ts | 1 + .../analytics/graph/blocks/BlockNode.tsx | 62 +++++++++ .../analytics/graph/blocks/DropOffBlock.tsx | 71 ++++++++++ .../analytics/graph/blocks/StartBlockNode.tsx | 62 +++++++++ .../analytics/graph/blocks/StepsList.tsx | 27 ++++ apps/builder/components/board/Board.tsx | 5 +- .../board/graph/BlockNode/BlockNode.tsx | 8 +- .../board/graph/BlockNode/StartBlockNode.tsx | 5 +- .../BlockNode/StepNode/StepNodeOverlay.tsx | 7 +- .../board/graph/BlockNode/StepsList.tsx | 3 + apps/builder/components/board/graph/Graph.tsx | 9 +- .../components/dashboard/FolderContent.tsx | 3 +- .../dashboard/FolderContent/TypebotButton.tsx | 8 +- .../contexts/AnalyticsGraphProvider.tsx | 79 +++++++++++ apps/builder/contexts/TypebotContext.tsx | 12 +- apps/builder/cypress/tests/dashboard.ts | 4 +- .../layouts/results/AnalyticsContent.tsx | 42 +++++- .../layouts/results/ResultsContent.tsx | 39 +++--- .../layouts/results/SubmissionContent.tsx | 2 +- .../pages/api/typebots/[typebotId]/results.ts | 1 + .../[typebotId]/results/answers/count.ts | 41 ++++++ .../results/{count.ts => stats.ts} | 25 +++- apps/builder/pages/typebots/[id]/edit.tsx | 5 +- apps/builder/services/analytics.ts | 45 ++++++ apps/builder/services/graph.ts | 37 +++-- apps/builder/services/results.ts | 19 --- apps/viewer/pages/api/results/[id].ts | 2 +- apps/viewer/services/answer.ts | 4 +- package.json | 2 +- packages/bot-engine/src/models/answer.ts | 8 +- .../migration.sql | 8 ++ packages/db/prisma/schema.prisma | 1 - yarn.lock | 128 +++++++++--------- 38 files changed, 876 insertions(+), 147 deletions(-) create mode 100644 apps/builder/components/analytics/StatsCards.tsx create mode 100644 apps/builder/components/analytics/graph/AnalyticsGraph.tsx create mode 100644 apps/builder/components/analytics/graph/Edges/DropOffEdge.tsx create mode 100644 apps/builder/components/analytics/graph/Edges/Edge.tsx create mode 100644 apps/builder/components/analytics/graph/Edges/Edges.tsx create mode 100644 apps/builder/components/analytics/graph/Edges/index.ts create mode 100644 apps/builder/components/analytics/graph/blocks/BlockNode.tsx create mode 100644 apps/builder/components/analytics/graph/blocks/DropOffBlock.tsx create mode 100644 apps/builder/components/analytics/graph/blocks/StartBlockNode.tsx create mode 100644 apps/builder/components/analytics/graph/blocks/StepsList.tsx create mode 100644 apps/builder/contexts/AnalyticsGraphProvider.tsx create mode 100644 apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts rename apps/builder/pages/api/typebots/[typebotId]/results/{count.ts => stats.ts} (52%) create mode 100644 apps/builder/services/analytics.ts create mode 100644 packages/db/prisma/migrations/20220103163928_remove_updated_at_on_result/migration.sql diff --git a/apps/builder/components/analytics/StatsCards.tsx b/apps/builder/components/analytics/StatsCards.tsx new file mode 100644 index 000000000..f77a3a6c8 --- /dev/null +++ b/apps/builder/components/analytics/StatsCards.tsx @@ -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 ( + + + Views + {stats ? ( + {stats.totalViews} + ) : ( + + )} + + + Starts + {stats ? ( + {stats.totalStarts} + ) : ( + + )} + + + Completion rate + {stats ? ( + {stats.completionRate}% + ) : ( + + )} + + + ) +} diff --git a/apps/builder/components/analytics/graph/AnalyticsGraph.tsx b/apps/builder/components/analytics/graph/AnalyticsGraph.tsx new file mode 100644 index 000000000..cd7fb57af --- /dev/null +++ b/apps/builder/components/analytics/graph/AnalyticsGraph.tsx @@ -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(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 ( + + + + {typebot.startBlock && } + {(typebot.blocks ?? []).map((block) => ( + + ))} + + + ) +} + +export default AnalyticsGraph diff --git a/apps/builder/components/analytics/graph/Edges/DropOffEdge.tsx b/apps/builder/components/analytics/graph/Edges/DropOffEdge.tsx new file mode 100644 index 000000000..7bb7459fb --- /dev/null +++ b/apps/builder/components/analytics/graph/Edges/DropOffEdge.tsx @@ -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 +} diff --git a/apps/builder/components/analytics/graph/Edges/Edge.tsx b/apps/builder/components/analytics/graph/Edges/Edge.tsx new file mode 100644 index 000000000..a8e0f9867 --- /dev/null +++ b/apps/builder/components/analytics/graph/Edges/Edge.tsx @@ -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 ( + + ) +} diff --git a/apps/builder/components/analytics/graph/Edges/Edges.tsx b/apps/builder/components/analytics/graph/Edges/Edges.tsx new file mode 100644 index 000000000..fabd6ae6f --- /dev/null +++ b/apps/builder/components/analytics/graph/Edges/Edges.tsx @@ -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 ( + <> + + {stepsWithTarget.map((step) => ( + + ))} + + + + {answersCounts?.map((answersCount) => ( + + ))} + + {answersCounts?.map((answersCount) => ( + + ))} + + ) +} diff --git a/apps/builder/components/analytics/graph/Edges/index.ts b/apps/builder/components/analytics/graph/Edges/index.ts new file mode 100644 index 000000000..1f83b5a43 --- /dev/null +++ b/apps/builder/components/analytics/graph/Edges/index.ts @@ -0,0 +1 @@ +export { Edges } from './Edges' diff --git a/apps/builder/components/analytics/graph/blocks/BlockNode.tsx b/apps/builder/components/analytics/graph/blocks/BlockNode.tsx new file mode 100644 index 000000000..95057b88b --- /dev/null +++ b/apps/builder/components/analytics/graph/blocks/BlockNode.tsx @@ -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 ( + + + + + + + + ) +} diff --git a/apps/builder/components/analytics/graph/blocks/DropOffBlock.tsx b/apps/builder/components/analytics/graph/blocks/DropOffBlock.tsx new file mode 100644 index 000000000..e79109187 --- /dev/null +++ b/apps/builder/components/analytics/graph/blocks/DropOffBlock.tsx @@ -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 ( + + {dropOffRate}% + {totalDroppedUser} users + + ) +} diff --git a/apps/builder/components/analytics/graph/blocks/StartBlockNode.tsx b/apps/builder/components/analytics/graph/blocks/StartBlockNode.tsx new file mode 100644 index 000000000..c00619085 --- /dev/null +++ b/apps/builder/components/analytics/graph/blocks/StartBlockNode.tsx @@ -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 ( + + + + + + + + ) +} diff --git a/apps/builder/components/analytics/graph/blocks/StepsList.tsx b/apps/builder/components/analytics/graph/blocks/StepsList.tsx new file mode 100644 index 000000000..3a0eb180f --- /dev/null +++ b/apps/builder/components/analytics/graph/blocks/StepsList.tsx @@ -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 ( + + + {steps.map((step) => ( + + + + + ))} + + ) +} diff --git a/apps/builder/components/board/Board.tsx b/apps/builder/components/board/Board.tsx index ec63221d1..24cc10ecc 100644 --- a/apps/builder/components/board/Board.tsx +++ b/apps/builder/components/board/Board.tsx @@ -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 = () => { - + + + {rightPanel === RightPanel.PREVIEW && } diff --git a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx index 89d2e6f07..81164e738 100644 --- a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx @@ -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 } = diff --git a/apps/builder/components/board/graph/BlockNode/StartBlockNode.tsx b/apps/builder/components/board/graph/BlockNode/StartBlockNode.tsx index 4f7a026df..728da7c26 100644 --- a/apps/builder/components/board/graph/BlockNode/StartBlockNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/StartBlockNode.tsx @@ -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) diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx index d4f30cf9d..2ebcd1ed1 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx @@ -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 ( { +const Graph = ({ ...props }: FlexProps) => { const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } = useDnd() const graphContainerRef = useRef(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 ( - + { }} > + {props.children} {typebot.startBlock && } {(typebot.blocks ?? []).map((block) => ( diff --git a/apps/builder/components/dashboard/FolderContent.tsx b/apps/builder/components/dashboard/FolderContent.tsx index 927f62012..454f1ae5f 100644 --- a/apps/builder/components/dashboard/FolderContent.tsx +++ b/apps/builder/components/dashboard/FolderContent.tsx @@ -37,8 +37,7 @@ export const FolderContent = ({ folder }: Props) => { const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { - delay: 100, - tolerance: 300, + distance: 20, }, }) ) diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx index dfaae89f6..3ae72a082 100644 --- a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx +++ b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx @@ -92,7 +92,13 @@ export const TypebotButton = ({ > Duplicate - + { + e.stopPropagation() + onDeleteOpen() + }} + > Delete diff --git a/apps/builder/contexts/AnalyticsGraphProvider.tsx b/apps/builder/contexts/AnalyticsGraphProvider.tsx new file mode 100644 index 000000000..71d0db086 --- /dev/null +++ b/apps/builder/contexts/AnalyticsGraphProvider.tsx @@ -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> + // 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 ( + + {children} + + ) +} + +export const useAnalyticsGraph = () => useContext(graphContext) diff --git a/apps/builder/contexts/TypebotContext.tsx b/apps/builder/contexts/TypebotContext.tsx index 2e1923940..72377caa7 100644 --- a/apps/builder/contexts/TypebotContext.tsx +++ b/apps/builder/contexts/TypebotContext.tsx @@ -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( diff --git a/apps/builder/cypress/tests/dashboard.ts b/apps/builder/cypress/tests/dashboard.ts index a5eeb5f8a..59035bd73 100644 --- a/apps/builder/cypress/tests/dashboard.ts +++ b/apps/builder/cypress/tests/dashboard.ts @@ -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, { diff --git a/apps/builder/layouts/results/AnalyticsContent.tsx b/apps/builder/layouts/results/AnalyticsContent.tsx index fc6550a46..d56e92dab 100644 --- a/apps/builder/layouts/results/AnalyticsContent.tsx +++ b/apps/builder/layouts/results/AnalyticsContent.tsx @@ -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 ( + + {publishedTypebot && ( + + + + )} + + + ) } diff --git a/apps/builder/layouts/results/ResultsContent.tsx b/apps/builder/layouts/results/ResultsContent.tsx index 7911d6cc5..6bfa0e2dd 100644 --- a/apps/builder/layouts/results/ResultsContent.tsx +++ b/apps/builder/layouts/results/ResultsContent.tsx @@ -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 ( - - - + + + @@ -59,16 +58,18 @@ export const ResultsContent = () => { Analytics + + {typebot && (isAnalytics ? ( - + ) : ( ))} - + ) } diff --git a/apps/builder/layouts/results/SubmissionContent.tsx b/apps/builder/layouts/results/SubmissionContent.tsx index 46fa03bea..517105a81 100644 --- a/apps/builder/layouts/results/SubmissionContent.tsx +++ b/apps/builder/layouts/results/SubmissionContent.tsx @@ -43,7 +43,7 @@ export const SubmissionsContent = ({ typebotId, totalResults }: Props) => { ) return ( - + diff --git a/apps/builder/pages/api/typebots/[typebotId]/results.ts b/apps/builder/pages/api/typebots/[typebotId]/results.ts index dd3f66879..add539786 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results.ts @@ -25,6 +25,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { where: { typebotId, typebot: { ownerId: user.id }, + answers: { some: {} }, }, orderBy: { createdAt: 'desc', diff --git a/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts b/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts new file mode 100644 index 000000000..b2e0f3fed --- /dev/null +++ b/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts @@ -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 diff --git a/apps/builder/pages/api/typebots/[typebotId]/results/count.ts b/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts similarity index 52% rename from apps/builder/pages/api/typebots/[typebotId]/results/count.ts rename to apps/builder/pages/api/typebots/[typebotId]/results/stats.ts index ffb5102cc..fc724cdff 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results/count.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts @@ -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) } diff --git a/apps/builder/pages/typebots/[id]/edit.tsx b/apps/builder/pages/typebots/[id]/edit.tsx index 6ef7f2a71..3ffc16c71 100644 --- a/apps/builder/pages/typebots/[id]/edit.tsx +++ b/apps/builder/pages/typebots/[id]/edit.tsx @@ -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 = () => { - - - + diff --git a/apps/builder/services/analytics.ts b/apps/builder/services/analytics.ts new file mode 100644 index 000000000..bc074b094 --- /dev/null +++ b/apps/builder/services/analytics.ts @@ -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, + } +} diff --git a/apps/builder/services/graph.ts b/apps/builder/services/graph.ts index 7a1606814..08dd6b605 100644 --- a/apps/builder/services/graph.ts +++ b/apps/builder/services/graph.ts @@ -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 } diff --git a/apps/builder/services/results.ts b/apps/builder/services/results.ts index 99524343a..b5d8601be 100644 --- a/apps/builder/services/results.ts +++ b/apps/builder/services/results.ts @@ -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 ( diff --git a/apps/viewer/pages/api/results/[id].ts b/apps/viewer/pages/api/results/[id].ts index 0c8664e5e..a67ace73e 100644 --- a/apps/viewer/pages/api/results/[id].ts +++ b/apps/viewer/pages/api/results/[id].ts @@ -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) } diff --git a/apps/viewer/services/answer.ts b/apps/viewer/services/answer.ts index 26c1c577a..07d8b08c3 100644 --- a/apps/viewer/services/answer.ts +++ b/apps/viewer/services/answer.ts @@ -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({ url: `/api/answers`, method: 'PUT', diff --git a/package.json b/package.json index 4b5541ffc..0485a6632 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "dotenv-cli": "^4.1.1", - "turbo": "^1.0.24-canary.2" + "turbo": "^1.0.24" }, "turbo": { "baseBranch": "origin/main", diff --git a/packages/bot-engine/src/models/answer.ts b/packages/bot-engine/src/models/answer.ts index 2428990e8..e4917a6ec 100644 --- a/packages/bot-engine/src/models/answer.ts +++ b/packages/bot-engine/src/models/answer.ts @@ -1,3 +1,9 @@ import { Answer as AnswerFromPrisma } from 'db' -export type Answer = Omit +export type Answer = Omit + +export type Stats = { + totalViews: number + totalStarts: number + completionRate: number +} diff --git a/packages/db/prisma/migrations/20220103163928_remove_updated_at_on_result/migration.sql b/packages/db/prisma/migrations/20220103163928_remove_updated_at_on_result/migration.sql new file mode 100644 index 000000000..a66fc94d4 --- /dev/null +++ b/packages/db/prisma/migrations/20220103163928_remove_updated_at_on_result/migration.sql @@ -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"; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index c9af41701..ed8bb6bf9 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -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[] diff --git a/yarn.lock b/yarn.lock index d6c9b4545..cbf3ea8c5 100644 --- a/yarn.lock +++ b/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"