2
0

feat(results): 🛂 Limit incomplete submissions

This commit is contained in:
Baptiste Arnaud
2022-02-12 12:54:16 +01:00
parent 3a7b9a0c63
commit ec470b578c
11 changed files with 93 additions and 32 deletions

View File

@ -34,7 +34,9 @@ export const StatsCards = ({
<Stat bgColor="white" p="4" rounded="md" boxShadow="md">
<StatLabel>Completion rate</StatLabel>
{stats ? (
<StatNumber>{stats.completionRate}%</StatNumber>
<StatNumber>
{Math.round((stats.totalCompleted / stats.totalStarts) * 100)}%
</StatNumber>
) : (
<Skeleton w="50%" h="10px" mt="2" />
)}

View File

@ -26,14 +26,11 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
<Text>Create a folder</Text>
{isFreePlan(user) && <Tag colorScheme="orange">Pro</Tag>}
</HStack>
{user && (
<UpgradeModal
isOpen={isOpen}
onClose={onClose}
user={user}
type={LimitReached.FOLDER}
/>
)}
</Button>
)
}

View File

@ -1,5 +1,15 @@
import { Alert, AlertIcon, AlertProps } from '@chakra-ui/react'
import {
Alert,
AlertIcon,
AlertProps,
Button,
HStack,
Text,
useDisclosure,
} from '@chakra-ui/react'
import React from 'react'
import { UpgradeModal } from './modals/UpgradeModal.'
import { LimitReached } from './modals/UpgradeModal./UpgradeModal'
export const Info = (props: AlertProps) => (
<Alert status="info" bgColor={'blue.50'} rounded="md" {...props}>
@ -11,3 +21,32 @@ export const Info = (props: AlertProps) => (
export const PublishFirstInfo = (props: AlertProps) => (
<Info {...props}>You need to publish your typebot first</Info>
)
export const UnlockProPlanInfo = ({
contentLabel,
buttonLabel,
type,
}: {
contentLabel: string
buttonLabel: string
type?: LimitReached
}) => {
const { isOpen, onOpen, onClose } = useDisclosure()
return (
<Alert
status="info"
bgColor={'blue.50'}
rounded="md"
justifyContent="space-between"
>
<HStack>
<AlertIcon />
<Text>{contentLabel}</Text>
</HStack>
<Button colorScheme="blue" onClick={onOpen}>
{buttonLabel}
</Button>
<UpgradeModal isOpen={isOpen} onClose={onClose} type={type} />
</Alert>
)
}

View File

@ -13,8 +13,8 @@ import {
} from '@chakra-ui/react'
import { PricingCard } from './PricingCard'
import { ActionButton } from './ActionButton'
import { User } from 'db'
import { pay } from 'services/stripe'
import { useUser } from 'contexts/UserContext'
export enum LimitReached {
BRAND = 'Remove branding',
@ -24,18 +24,13 @@ export enum LimitReached {
}
type UpgradeModalProps = {
user: User
type: LimitReached
type?: LimitReached
isOpen: boolean
onClose: () => void
}
export const UpgradeModal = ({
type,
user,
onClose,
isOpen,
}: UpgradeModalProps) => {
export const UpgradeModal = ({ type, onClose, isOpen }: UpgradeModalProps) => {
const { user } = useUser()
const [payLoading, setPayLoading] = useState(false)
const [userLanguage, setUserLanguage] = useState<string>('en')

View File

@ -1,14 +1,17 @@
import { Button, Flex, HStack, Tag, useToast, Text } from '@chakra-ui/react'
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { useUser } from 'contexts/UserContext'
import { useRouter } from 'next/router'
import React, { useMemo } from 'react'
import { useStats } from 'services/analytics'
import { isFreePlan } from 'services/user'
import { AnalyticsContent } from './AnalyticsContent'
import { SubmissionsContent } from './SubmissionContent'
export const ResultsContent = () => {
const router = useRouter()
const { user } = useUser()
const { typebot } = useTypebot()
const isAnalytics = useMemo(
() => router.pathname.endsWith('analytics'),
@ -76,6 +79,11 @@ export const ResultsContent = () => {
typebotId={typebot.id}
onDeleteResults={handleDeletedResults}
totalResults={stats?.totalStarts ?? 0}
totalHiddenResults={
isFreePlan(user)
? (stats?.totalStarts ?? 0) - (stats?.totalCompleted ?? 0)
: undefined
}
/>
))}
</Flex>

View File

@ -10,15 +10,18 @@ import {
useResults,
} from 'services/results'
import { unparse } from 'papaparse'
import { UnlockProPlanInfo } from 'components/shared/Info'
type Props = {
typebotId: string
totalResults: number
totalHiddenResults?: number
onDeleteResults: (total: number) => void
}
export const SubmissionsContent = ({
typebotId,
totalResults,
totalHiddenResults,
onDeleteResults,
}: Props) => {
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
@ -65,13 +68,10 @@ export const SubmissionsContent = ({
setIsDeleteLoading(false)
}
const totalSelected = useMemo(
() =>
selectedIndices.length === results?.length
? totalResults
: selectedIndices.length,
[results?.length, selectedIndices.length, totalResults]
)
const totalSelected =
selectedIndices.length > 0 && selectedIndices.length === results?.length
? totalResults - (totalHiddenResults ?? 0)
: selectedIndices.length
const handleScrolledToBottom = useCallback(
() => setSize((state) => state + 1),
@ -111,6 +111,12 @@ export const SubmissionsContent = ({
return (
<Stack maxW="1200px" w="full" pb="28">
{totalHiddenResults && (
<UnlockProPlanInfo
buttonLabel={`Unlock ${totalHiddenResults} results`}
contentLabel="You are seeing complete submissions only."
/>
)}
<Flex w="full" justifyContent="flex-end">
<ResultsActionButtons
isDeleteLoading={isDeleteLoading}

View File

@ -2,6 +2,7 @@ import { User } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { isFreePlan } from 'services/user'
import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@ -27,6 +28,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
typebotId,
typebot: { ownerId: user.id },
answers: { some: {} },
isCompleted: isFreePlan(user),
},
orderBy: {
createdAt: 'desc',

View File

@ -38,7 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const stats: Stats = {
totalViews,
totalStarts,
completionRate: Math.round((totalCompleted / totalStarts) * 100),
totalCompleted,
}
return res.status(200).send({ stats })
}

View File

@ -2,6 +2,7 @@ import test, { expect, Page } from '@playwright/test'
import { readFileSync } from 'fs'
import { defaultTextInputOptions, InputStepType } from 'models'
import { parse } from 'papaparse'
import path from 'path'
import { generate } from 'short-uuid'
import {
createResults,
@ -86,6 +87,17 @@ test.describe('Results page', () => {
const { data: dataAll } = parse(fileAll)
validateExportAll(dataAll)
})
test.describe('Free user', () => {
test.use({
storageState: path.join(__dirname, '../freeUser.json'),
})
test("Incomplete results shouldn't be displayed", async ({ page }) => {
await page.goto(`/typebots/${typebotId}/results`)
await page.click('text=Unlock 200 results')
await expect(page.locator('text=Upgrade now')).toBeVisible()
})
})
})
const validateExportSelection = (data: unknown[]) => {

View File

@ -92,10 +92,10 @@ export const ChatBlock = ({
const isSingleChoiceStep =
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
if (isSingleChoiceStep) {
onBlockEnd(
currentStep.items.find((i) => i.content === answerContent)
?.outgoingEdgeId
)
const nextEdgeId = currentStep.items.find(
(i) => i.content === answerContent
)?.outgoingEdgeId
if (nextEdgeId) return onBlockEnd(nextEdgeId)
}
if (currentStep?.outgoingEdgeId || displayedSteps.length === steps.length)

View File

@ -5,5 +5,5 @@ export type Answer = Omit<AnswerFromPrisma, 'resultId' | 'createdAt'>
export type Stats = {
totalViews: number
totalStarts: number
completionRate: number
totalCompleted: number
}