feat: added dropoff visibility (#907)

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Shubham Palriwala <spalriwalau@gmail.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
Piyush Gupta
2023-10-20 18:48:16 +05:30
committed by GitHub
parent 37e83cbb81
commit 28265a7dcf
9 changed files with 379 additions and 9 deletions
@@ -2,7 +2,7 @@
import React, { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import { findTemplateByName } from "./templates";
import { TTemplate } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
interface DemoPreviewProps {
template: string;
@@ -1,4 +1,4 @@
import { TTemplate } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import TemplateList from "./TemplateList";
@@ -1,4 +1,4 @@
import { TTemplate } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { templates } from "./templates";
@@ -23,7 +23,7 @@ import {
} from "@formbricks/ui/icons";
import { createId } from "@paralleldrive/cuid2";
import { TTemplate } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
const thankYouCardDefault = {
@@ -51,6 +51,13 @@ export const customSurvey: TTemplate = {
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
};
@@ -143,6 +150,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
@@ -246,6 +260,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -319,6 +340,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -361,6 +389,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -412,6 +447,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -471,6 +513,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -533,6 +582,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -584,6 +640,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -622,6 +685,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -653,6 +723,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -675,6 +752,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -719,6 +803,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -757,6 +848,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -807,6 +905,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -856,6 +961,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -901,6 +1013,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -924,6 +1043,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -945,6 +1071,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -965,6 +1098,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -1002,6 +1142,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -1032,6 +1179,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -1062,6 +1216,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
{
@@ -1112,6 +1273,13 @@ export const templates: TTemplate[] = [
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {
enabled: false,
timeToFinish: false,
},
hiddenFields: {
enabled: false,
},
},
},
];
@@ -43,7 +43,7 @@ export default function OpenTextSummary({ questionSummary, environmentId }: Open
return (
<div
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
<Link
@@ -0,0 +1,121 @@
import { evaluateCondition } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/evaluateLogic";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { useMemo } from "react";
interface SummaryDropOffsProps {
survey: TSurvey;
responses: TResponse[];
displayCount: number;
}
export default function SummaryDropOffs({ responses, survey, displayCount }: SummaryDropOffsProps) {
const getDropoff = () => {
let dropoffArr = new Array(survey.questions.length).fill(0);
let viewsArr = new Array(survey.questions.length).fill(0);
let dropoffPercentageArr = new Array(survey.questions.length).fill(0);
responses.forEach((response) => {
let currQuesIdx = 0;
while (currQuesIdx < survey.questions.length) {
const currQues = survey.questions[currQuesIdx];
if (!currQues.required) {
if (!response.data[currQues.id]) {
viewsArr[currQuesIdx]++;
if (currQuesIdx === survey.questions.length - 1 && !response.finished) {
dropoffArr[currQuesIdx]++;
break;
}
const questionHasCustomLogic = currQues.logic;
if (questionHasCustomLogic) {
let didLogicPass = false;
for (let logic of questionHasCustomLogic) {
if (!logic.destination) continue;
if (evaluateCondition(logic, response.data[currQues.id] ?? null)) {
didLogicPass = true;
currQuesIdx = survey.questions.findIndex((q) => q.id === logic.destination);
break;
}
}
if (!didLogicPass) currQuesIdx++;
} else {
currQuesIdx++;
}
continue;
}
}
if (
(response.data[currQues.id] === undefined && !response.finished) ||
(currQues.required && !response.data[currQues.id])
) {
dropoffArr[currQuesIdx]++;
viewsArr[currQuesIdx]++;
break;
}
viewsArr[currQuesIdx]++;
let nextQuesIdx = currQuesIdx + 1;
const questionHasCustomLogic = currQues.logic;
if (questionHasCustomLogic) {
for (let logic of questionHasCustomLogic) {
if (!logic.destination) continue;
if (evaluateCondition(logic, response.data[currQues.id])) {
nextQuesIdx = survey.questions.findIndex((q) => q.id === logic.destination);
break;
}
}
}
if (!response.data[survey.questions[nextQuesIdx]?.id] && !response.finished) {
dropoffArr[nextQuesIdx]++;
viewsArr[nextQuesIdx]++;
break;
}
currQuesIdx = nextQuesIdx;
}
});
dropoffPercentageArr[0] = (dropoffArr[0] / displayCount) * 100 || 0;
for (let i = 1; i < survey.questions.length; i++) {
if (viewsArr[i - 1] !== 0) {
dropoffPercentageArr[i] = (dropoffArr[i] / viewsArr[i - 1]) * 100;
}
}
return [dropoffArr, viewsArr, dropoffPercentageArr];
};
const [dropoffCount, viewsCount, dropoffPercentage] = useMemo(() => getDropoff(), [responses]);
return (
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<div className="rounded-b-lg bg-white ">
<div className="grid h-10 grid-cols-5 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="col-span-3 pl-4 md:pl-6">Questions</div>
<div className="pl-4 text-center md:pl-6">Views</div>
<div className="px-4 text-center md:px-6">Drop-off</div>
</div>
{survey.questions.map((question, i) => (
<div
key={question.id}
className="grid grid-cols-5 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
<div className="col-span-3 pl-4 md:pl-6">{question.headline}</div>
<div className="whitespace-pre-wrap pl-6 text-center font-semibold">{viewsCount[i]}</div>
<div className="px-4 text-center md:px-6">
<span className="font-semibold">{dropoffCount[i]} </span>
<span>({Math.round(dropoffPercentage[i])}%)</span>
</div>
</div>
))}
</div>
</div>
);
}
@@ -1,10 +1,14 @@
import { timeSinceConditionally } from "@formbricks/lib/time";
import { Button } from "@formbricks/ui/Button";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid";
interface SummaryMetadataProps {
responses: TResponse[];
showDropOffs: boolean;
setShowDropOffs: React.Dispatch<React.SetStateAction<boolean>>;
survey: TSurvey;
displayCount: number;
}
@@ -30,7 +34,13 @@ const StatCard = ({ label, percentage, value, tooltipText }) => (
</TooltipProvider>
);
export default function SummaryMetadata({ responses, survey, displayCount }: SummaryMetadataProps) {
export default function SummaryMetadata({
responses,
survey,
displayCount,
setShowDropOffs,
showDropOffs,
}: SummaryMetadataProps) {
const completedResponses = responses.filter((r) => r.finished).length;
const totalResponses = responses.length;
@@ -63,10 +73,17 @@ export default function SummaryMetadata({ responses, survey, displayCount }: Sum
tooltipText="People who started but not completed the survey."
/>
</div>
<div className="flex flex-col justify-between lg:col-span-1">
<div className="flex flex-col justify-between gap-2 lg:col-span-1">
<div className="text-right text-xs text-slate-400">
Last updated: {timeSinceConditionally(survey.updatedAt.toISOString())}
</div>
<Button
variant="minimal"
className="w-max self-start"
EndIcon={showDropOffs ? ChevronDownIcon : ChevronUpIcon}
onClick={() => setShowDropOffs(!showDropOffs)}>
Analyze Drop Offs
</Button>
</div>
</div>
</div>
@@ -7,6 +7,8 @@ import SummaryMetadata from "@/app/(app)/environments/[environmentId]/surveys/[s
import CustomFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
import SummaryHeader from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SummaryHeader";
import { getFilterResponses } from "@/app/lib/surveys/surveys";
import { useEffect, useMemo, useState } from "react";
import SummaryDropOffs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TProfile } from "@formbricks/types/profile";
@@ -15,7 +17,6 @@ import { TSurvey } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import ContentWrapper from "@formbricks/ui/ContentWrapper";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo } from "react";
interface SummaryPageProps {
environment: TEnvironment;
@@ -41,6 +42,7 @@ const SummaryPage = ({
displayCount,
}: SummaryPageProps) => {
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
const searchParams = useSearchParams();
useEffect(() => {
@@ -71,7 +73,14 @@ const SummaryPage = ({
totalResponses={responses}
/>
<SurveyResultsTabs activeId="summary" environmentId={environment.id} surveyId={surveyId} />
<SummaryMetadata responses={filterResponses} survey={survey} displayCount={displayCount} />
<SummaryMetadata
responses={filterResponses}
survey={survey}
displayCount={displayCount}
showDropOffs={showDropOffs}
setShowDropOffs={setShowDropOffs}
/>
{showDropOffs && <SummaryDropOffs survey={survey} responses={responses} displayCount={displayCount} />}
<SummaryList responses={filterResponses} survey={survey} environment={environment} />
</ContentWrapper>
);
@@ -0,0 +1,55 @@
import { TSurveyLogic } from "@formbricks/types/surveys";
export function evaluateCondition(logic: TSurveyLogic, responseValue: any): boolean {
switch (logic.condition) {
case "equals":
return (
(Array.isArray(responseValue) && responseValue.length === 1 && responseValue.includes(logic.value)) ||
responseValue?.toString() === logic.value
);
case "notEquals":
return responseValue !== logic.value;
case "lessThan":
return logic.value !== undefined && responseValue < logic.value;
case "lessEqual":
return logic.value !== undefined && responseValue <= logic.value;
case "greaterThan":
return logic.value !== undefined && responseValue > logic.value;
case "greaterEqual":
return logic.value !== undefined && responseValue >= logic.value;
case "includesAll":
return (
Array.isArray(responseValue) &&
Array.isArray(logic.value) &&
logic.value.every((v) => responseValue.includes(v))
);
case "includesOne":
return (
Array.isArray(responseValue) &&
Array.isArray(logic.value) &&
logic.value.some((v) => responseValue.includes(v))
);
case "accepted":
return responseValue === "accepted";
case "clicked":
return responseValue === "clicked";
case "submitted":
if (typeof responseValue === "string") {
return responseValue !== "dismissed" && responseValue !== "" && responseValue !== null;
} else if (Array.isArray(responseValue)) {
return responseValue.length > 0;
} else if (typeof responseValue === "number") {
return responseValue !== null;
}
return false;
case "skipped":
return (
(Array.isArray(responseValue) && responseValue.length === 0) ||
responseValue === "" ||
responseValue === null ||
responseValue === "dismissed"
);
default:
return false;
}
}