feat: Time to complete Metadata (#1416)

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Dhruwang Jariwala
2023-11-30 19:52:33 +05:30
committed by GitHub
parent 05884ead56
commit 2118f881f6
29 changed files with 478 additions and 87 deletions
@@ -78,7 +78,7 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
{Array.isArray(response.value) &&
(response.value.length > 0 ? (
response.value.map((fileUrl, index) => (
<div className="relative m-2 rounded-lg bg-slate-200">
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
<a href={fileUrl as string} key={index} download target="_blank">
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
@@ -1,7 +1,9 @@
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";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
import { TimerIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
interface SummaryDropOffsProps {
survey: TSurvey;
@@ -10,12 +12,45 @@ interface SummaryDropOffsProps {
}
export default function SummaryDropOffs({ responses, survey, displayCount }: SummaryDropOffsProps) {
const getDropoff = () => {
const initialAvgTtc = useMemo(
() =>
survey.questions.reduce((acc, question) => {
acc[question.id] = 0;
return acc;
}, {}),
[survey.questions]
);
const [avgTtc, setAvgTtc] = useState(initialAvgTtc);
interface DropoffMetricsType {
dropoffCount: number[];
viewsCount: number[];
dropoffPercentage: number[];
}
const [dropoffMetrics, setDropoffMetrics] = useState<DropoffMetricsType>({
dropoffCount: [],
viewsCount: [],
dropoffPercentage: [],
});
const calculateMetrics = useCallback(() => {
let totalTtc = { ...initialAvgTtc };
let responseCounts = { ...initialAvgTtc };
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) => {
// Calculate total time-to-completion
Object.keys(avgTtc).forEach((questionId) => {
if (response.ttc && response.ttc[questionId]) {
totalTtc[questionId] += response.ttc[questionId];
responseCounts[questionId]++;
}
});
let currQuesIdx = 0;
while (currQuesIdx < survey.questions.length) {
@@ -84,6 +119,13 @@ export default function SummaryDropOffs({ responses, survey, displayCount }: Sum
}
});
// Calculate the average time for each question
Object.keys(totalTtc).forEach((questionId) => {
totalTtc[questionId] =
responseCounts[questionId] > 0 ? totalTtc[questionId] / responseCounts[questionId] : 0;
});
// Calculate drop-off percentages
dropoffPercentageArr[0] = (dropoffArr[0] / displayCount) * 100 || 0;
for (let i = 1; i < survey.questions.length; i++) {
if (viewsArr[i - 1] !== 0) {
@@ -91,28 +133,54 @@ export default function SummaryDropOffs({ responses, survey, displayCount }: Sum
}
}
return [dropoffArr, viewsArr, dropoffPercentageArr];
};
return {
newAvgTtc: totalTtc,
dropoffCount: dropoffArr,
viewsCount: viewsArr,
dropoffPercentage: dropoffPercentageArr,
};
}, [responses, survey.questions, displayCount, initialAvgTtc, avgTtc]);
const [dropoffCount, viewsCount, dropoffPercentage] = useMemo(() => getDropoff(), [responses]);
useEffect(() => {
const { newAvgTtc, dropoffCount, viewsCount, dropoffPercentage } = calculateMetrics();
setAvgTtc(newAvgTtc);
setDropoffMetrics({ dropoffCount, viewsCount, dropoffPercentage });
}, [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="grid h-10 grid-cols-6 items-center border-y border-slate-200 bg-slate-100 text-sm font-semibold 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 className="flex justify-center">
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<TimerIcon className="h-5 w-5" />
</TooltipTrigger>
<TooltipContent side={"top"}>
<p className="text-center font-normal">Average time to complete each question.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="px-4 text-center md:px-6">Views</div>
<div className="pr-6 text-center md:pl-6">Drop Offs</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">
className="grid grid-cols-6 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 className="whitespace-pre-wrap text-center font-semibold">
{avgTtc[question.id] !== undefined ? (avgTtc[question.id] / 1000).toFixed(2) + "s" : "N/A"}
</div>
<div className="whitespace-pre-wrap text-center font-semibold">
{dropoffMetrics.viewsCount[i]}
</div>
<div className=" pl-6 text-center md:px-6">
<span className="font-semibold">{dropoffMetrics.dropoffCount[i]} </span>
<span>({Math.round(dropoffMetrics.dropoffPercentage[i])}%)</span>
</div>
</div>
))}
@@ -1,9 +1,10 @@
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 { Button } from "@formbricks/ui/Button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid";
import { useMemo, useState } from "react";
interface SummaryMetadataProps {
responses: TResponse[];
@@ -34,6 +35,21 @@ const StatCard = ({ label, percentage, value, tooltipText }) => (
</TooltipProvider>
);
function formatTime(ttc, totalResponses) {
const seconds = ttc / (1000 * totalResponses);
let formattedValue;
if (seconds >= 60) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
formattedValue = `${minutes}m ${remainingSeconds.toFixed(2)}s`;
} else {
formattedValue = `${seconds.toFixed(2)}s`;
}
return formattedValue;
}
export default function SummaryMetadata({
responses,
survey,
@@ -41,13 +57,30 @@ export default function SummaryMetadata({
setShowDropOffs,
showDropOffs,
}: SummaryMetadataProps) {
const completedResponses = responses.filter((r) => r.finished).length;
const completedResponsesCount = useMemo(() => responses.filter((r) => r.finished).length, [responses]);
const [validTtcResponsesCount, setValidResponsesCount] = useState(0);
const ttc = useMemo(() => {
let validTtcResponsesCountAcc = 0; //stores the count of responses that contains a _total value
const ttc = responses.reduce((acc, response) => {
if (response.ttc._total) {
validTtcResponsesCountAcc++;
return acc + response.ttc._total;
}
return acc;
}, 0);
setValidResponsesCount(validTtcResponsesCountAcc);
return ttc;
}, [responses]);
console.log(ttc);
const totalResponses = responses.length;
return (
<div className="mb-4">
<div className="flex flex-col-reverse gap-y-2 lg:grid lg:grid-cols-2 lg:gap-x-2">
<div className="grid grid-cols-2 gap-4 md:grid md:grid-cols-4 md:gap-x-2">
<div className="flex flex-col-reverse gap-y-2 lg:grid lg:grid-cols-3 lg:gap-x-2">
<div className="grid grid-cols-2 gap-4 md:grid-cols-5 md:gap-x-2 lg:col-span-2">
<div className="flex flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<p className="text-sm text-slate-600">Displays</p>
<p className="text-2xl font-bold text-slate-800">
@@ -62,16 +95,24 @@ export default function SummaryMetadata({
/>
<StatCard
label="Responses"
percentage={`${Math.round((completedResponses / displayCount) * 100)}%`}
value={responses.length === 0 ? <span>-</span> : completedResponses}
percentage={`${Math.round((completedResponsesCount / displayCount) * 100)}%`}
value={responses.length === 0 ? <span>-</span> : completedResponsesCount}
tooltipText="People who completed the survey."
/>
<StatCard
label="Drop Offs"
percentage={`${Math.round(((totalResponses - completedResponses) / totalResponses) * 100)}%`}
value={responses.length === 0 ? <span>-</span> : totalResponses - completedResponses}
percentage={`${Math.round(((totalResponses - completedResponsesCount) / totalResponses) * 100)}%`}
value={responses.length === 0 ? <span>-</span> : totalResponses - completedResponsesCount}
tooltipText="People who started but not completed the survey."
/>
<StatCard
label="Time to Complete"
percentage={null}
value={
validTtcResponsesCount === 0 ? <span>-</span> : `${formatTime(ttc, validTtcResponsesCount)}`
}
tooltipText="Average time to complete the survey."
/>
</div>
<div className="flex flex-col justify-between gap-2 lg:col-span-1">
<div className="text-right text-xs text-slate-400">
+3 -1
View File
@@ -3,6 +3,7 @@ import { env } from "@formbricks/lib/env.mjs";
export const formbricksEnabled =
typeof env.NEXT_PUBLIC_FORMBRICKS_API_HOST && env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID;
const ttc = { onboarding: 0 };
export const createResponse = async (
surveyId: string,
@@ -11,12 +12,12 @@ export const createResponse = async (
finished: boolean = false
): Promise<any> => {
const api = formbricks.getApi();
return await api.client.response.create({
surveyId,
userId,
finished,
data,
ttc,
});
};
@@ -30,6 +31,7 @@ export const updateResponse = async (
responseId,
finished,
data,
ttc,
});
};
@@ -161,6 +161,7 @@ export default function LinkSurvey({
...responseUpdate.data,
...hiddenFieldsRecord,
},
ttc: responseUpdate.ttc,
finished: responseUpdate.finished,
meta: {
url: window.location.href,
@@ -63,6 +63,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
surveyId: true,
finished: true,
data: true,
ttc: true,
meta: true,
personAttributes: true,
singleUseId: true,
@@ -107,6 +107,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
surveyId: true,
finished: true,
data: true,
ttc: true,
meta: true,
personAttributes: true,
singleUseId: true,
+2
View File
@@ -24,10 +24,12 @@ export class ResponseAPI {
responseId,
finished,
data,
ttc,
}: TResponseUpdateInputWithResponseId): Promise<Result<{}, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", {
finished,
data,
ttc,
});
}
}
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Response" ADD COLUMN "ttc" JSONB NOT NULL DEFAULT '{}';
+3
View File
@@ -118,6 +118,9 @@ model Response {
/// @zod.custom(imports.ZResponseData)
/// [ResponseData]
data Json @default("{}")
/// @zod.custom(imports.ZResponseTtc)
/// [ResponseTtc]
ttc Json @default("{}")
/// @zod.custom(imports.ZResponseMeta)
/// [ResponseMeta]
meta Json @default("{}")
+6 -1
View File
@@ -4,7 +4,12 @@ export const ZActionProperties = z.record(z.string());
export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
export { ZIntegrationConfig } from "@formbricks/types/integration";
export { ZResponseData, ZResponsePersonAttributes, ZResponseMeta } from "@formbricks/types/responses";
export {
ZResponseData,
ZResponsePersonAttributes,
ZResponseMeta,
ZResponseTtc,
} from "@formbricks/types/responses";
export {
ZSurveyWelcomeCard,
+1
View File
@@ -128,6 +128,7 @@ export const renderWidget = (survey: TSurvey) => {
responseQueue.updateSurveyState(surveyState);
responseQueue.add({
data: responseUpdate.data,
ttc: responseUpdate.ttc,
finished: responseUpdate.finished,
});
},
+14 -2
View File
@@ -20,7 +20,7 @@ import { unstable_cache } from "next/cache";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { deleteDisplayByResponseId } from "../display/service";
import { createPerson, getPerson, getPersonByUserId, transformPrismaPerson } from "../person/service";
import { formatResponseDateFields } from "../response/util";
import { calculateTtcTotal, formatResponseDateFields } from "../response/util";
import { responseNoteCache } from "../responseNote/cache";
import { getResponseNotes } from "../responseNote/service";
import { captureTelemetry } from "../telemetry";
@@ -35,6 +35,7 @@ const responseSelection = {
finished: true,
data: true,
meta: true,
ttc: true,
personAttributes: true,
singleUseId: true,
person: {
@@ -269,7 +270,14 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
if (responseInput.personId) {
person = await getPerson(responseInput.personId);
}
const ttcTemp = responseInput.ttc;
const questionId = Object.keys(ttcTemp)[0];
const ttc = responseInput.finished
? {
...ttcTemp,
_total: ttcTemp[questionId], // Add _total property with the same value
}
: ttcTemp;
const responsePrisma = await prisma.response.create({
data: {
survey: {
@@ -279,6 +287,7 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
},
finished: responseInput.finished,
data: responseInput.data,
ttc,
...(responseInput.personId && {
person: {
connect: {
@@ -287,6 +296,7 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
},
personAttributes: person?.attributes,
}),
...(responseInput.meta && ({ meta: responseInput?.meta } as Prisma.JsonObject)),
singleUseId: responseInput.singleUseId,
},
@@ -502,6 +512,7 @@ export const updateResponse = async (
...currentResponse.data,
...responseInput.data,
};
const ttc = responseInput.finished ? calculateTtcTotal(responseInput.ttc) : responseInput.ttc;
const responsePrisma = await prisma.response.update({
where: {
@@ -510,6 +521,7 @@ export const updateResponse = async (
data: {
finished: responseInput.finished,
data,
ttc,
},
select: responseSelection,
});
+8 -1
View File
@@ -1,6 +1,6 @@
import "server-only";
import { TResponseDates } from "@formbricks/types/responses";
import { TResponseDates, TResponseTtc } from "@formbricks/types/responses";
export const formatResponseDateFields = (response: TResponseDates): TResponseDates => {
if (typeof response.createdAt === "string") {
@@ -24,3 +24,10 @@ export const formatResponseDateFields = (response: TResponseDates): TResponseDat
return response;
};
export function calculateTtcTotal(ttc: TResponseTtc) {
const result = { ...ttc };
result._total = Object.values(result).reduce((acc: number, val: number) => acc + val, 0);
return result;
}
+3 -2
View File
@@ -5,7 +5,7 @@ export class SurveyState {
displayId: string | null = null;
userId: string | null = null;
surveyId: string;
responseAcc: TResponseUpdate = { finished: false, data: {} };
responseAcc: TResponseUpdate = { finished: false, data: {}, ttc: {} };
singleUseId: string | null;
constructor(
@@ -74,6 +74,7 @@ export class SurveyState {
accumulateResponse(responseUpdate: TResponseUpdate) {
this.responseAcc = {
finished: responseUpdate.finished,
ttc: responseUpdate.ttc,
data: { ...this.responseAcc.data, ...responseUpdate.data },
};
}
@@ -90,7 +91,7 @@ export class SurveyState {
*/
clear() {
this.responseId = null;
this.responseAcc = { finished: false, data: {} };
this.responseAcc = { finished: false, data: {}, ttc: {} };
}
}
@@ -7,7 +7,7 @@ import NPSQuestion from "@/components/questions/NPSQuestion";
import OpenTextQuestion from "@/components/questions/OpenTextQuestion";
import PictureSelectionQuestion from "@/components/questions/PictureSelectionQuestion";
import RatingQuestion from "@/components/questions/RatingQuestion";
import { TResponseData } from "@formbricks/types/responses";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
@@ -15,12 +15,14 @@ interface QuestionConditionalProps {
question: TSurveyQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
isFirstQuestion: boolean;
isLastQuestion: boolean;
autoFocus?: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
surveyId: string;
}
@@ -33,6 +35,8 @@ export default function QuestionConditional({
isFirstQuestion,
isLastQuestion,
autoFocus = true,
ttc,
setTtc,
surveyId,
onFileUpload,
}: QuestionConditionalProps) {
@@ -46,6 +50,8 @@ export default function QuestionConditional({
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
autoFocus={autoFocus}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
<MultipleChoiceSingleQuestion
@@ -56,6 +62,8 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
<MultipleChoiceMultiQuestion
@@ -66,6 +74,8 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.NPS ? (
<NPSQuestion
@@ -76,6 +86,8 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.CTA ? (
<CTAQuestion
@@ -86,6 +98,8 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.Rating ? (
<RatingQuestion
@@ -96,6 +110,8 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.Consent ? (
<ConsentQuestion
@@ -106,6 +122,8 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.PictureSelection ? (
<PictureSelectionQuestion
@@ -116,6 +134,8 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.FileUpload ? (
<FileUploadQuestion
@@ -128,6 +148,8 @@ export default function QuestionConditional({
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
onFileUpload={onFileUpload}
ttc={ttc}
setTtc={setTtc}
/>
) : null;
}
@@ -3,7 +3,7 @@ import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper";
import { evaluateCondition } from "@/lib/logicEvaluator";
import { cn } from "@/lib/utils";
import { SurveyBaseProps } from "@/types/props";
import type { TResponseData } from "@formbricks/types/responses";
import type { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import { useEffect, useRef, useState } from "preact/hooks";
import ProgressBar from "./ProgressBar";
import QuestionConditional from "./QuestionConditional";
@@ -32,6 +32,7 @@ export function Survey({
const currentQuestionIndex = survey.questions.findIndex((q) => q.id === questionId);
const currentQuestion = survey.questions[currentQuestionIndex];
const contentRef = useRef<HTMLDivElement | null>(null);
const [ttc, setTtc] = useState<TResponseTtc>({});
useEffect(() => {
if (activeQuestionId === "hidden") return;
@@ -53,7 +54,7 @@ export function Survey({
// call onDisplay when component is mounted
onDisplay();
if (prefillResponseData) {
onSubmit(prefillResponseData, true);
onSubmit(prefillResponseData, {}, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -90,11 +91,12 @@ export function Survey({
setResponseData(updatedResponseData);
};
const onSubmit = (responseData: TResponseData, isFromPrefilling: Boolean = false) => {
const onSubmit = (responseData: TResponseData, ttc: TResponseTtc, isFromPrefilling: Boolean = false) => {
const questionId = Object.keys(responseData)[0];
setLoadingElement(true);
const nextQuestionId = getNextQuestionId(responseData, isFromPrefilling);
const finished = nextQuestionId === "end";
onResponse({ data: responseData, finished });
onResponse({ data: responseData, ttc, finished });
if (finished) {
onFinished();
}
@@ -154,6 +156,8 @@ export function Survey({
onChange={onChange}
onSubmit={onSubmit}
onBack={onBack}
ttc={ttc}
setTtc={setTtc}
onFileUpload={onFileUpload}
isFirstQuestion={
history && prefillResponseData
@@ -1,5 +1,6 @@
import SubmitButton from "@/components/buttons/SubmitButton";
import { calculateElementIdx } from "@/lib/utils";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
@@ -10,7 +11,7 @@ interface WelcomeCardProps {
fileUrl?: string;
buttonLabel?: string;
timeToFinish?: boolean;
onSubmit: (data: { [x: string]: any }) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
survey: TSurvey;
}
@@ -85,7 +86,7 @@ export default function WelcomeCard({
isLastQuestion={false}
focus={true}
onClick={() => {
onSubmit({ ["welcomeCard"]: "clicked" });
onSubmit({ ["welcomeCard"]: "clicked" }, {});
}}
type="button"
/>
@@ -5,15 +5,19 @@ import Headline from "@/components/general/Headline";
import HtmlBody from "@/components/general/HtmlBody";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyCTAQuestion } from "@formbricks/types/surveys";
import { useState } from "react";
import { TResponseTtc } from "@formbricks/types/responses";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface CTAQuestionProps {
question: TSurveyCTAQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function CTAQuestion({
@@ -22,7 +26,13 @@ export default function CTAQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
ttc,
setTtc,
}: CTAQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
return (
<div>
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
@@ -31,7 +41,15 @@ export default function CTAQuestion({
<div className="mt-4 flex w-full justify-between">
{!isFirstQuestion && (
<BackButton backButtonLabel={question.backButtonLabel} onClick={() => onBack()} />
<BackButton
backButtonLabel={question.backButtonLabel}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: "" }, updatedTtcObj);
onBack();
}}
/>
)}
<div className="flex w-full justify-end">
{!question.required && (
@@ -39,7 +57,9 @@ export default function CTAQuestion({
tabIndex={0}
type="button"
onClick={() => {
onSubmit({ [question.id]: "dismissed" });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: "dismissed" }, updatedTtcObj);
}}
className="text-heading focus:ring-focus mr-4 flex items-center rounded-md px-3 py-3 text-base font-medium leading-4 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2">
{question.dismissButtonLabel || "Skip"}
@@ -53,7 +73,9 @@ export default function CTAQuestion({
if (question.buttonExternal && question.buttonUrl) {
window?.open(question.buttonUrl, "_blank")?.focus();
}
onSubmit({ [question.id]: "clicked" });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: "clicked" }, updatedTtcObj);
}}
type="button"
/>
@@ -1,19 +1,23 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import QuestionImage from "@/components/general/QuestionImage";
import Headline from "@/components/general/Headline";
import HtmlBody from "@/components/general/HtmlBody";
import { TResponseData } from "@formbricks/types/responses";
import QuestionImage from "@/components/general/QuestionImage";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyConsentQuestion } from "@formbricks/types/surveys";
import { useState } from "preact/hooks";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface ConsentQuestionProps {
question: TSurveyConsentQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function ConsentQuestion({
@@ -24,7 +28,13 @@ export default function ConsentQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
ttc,
setTtc,
}: ConsentQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
return (
<div>
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
@@ -34,7 +44,9 @@ export default function ConsentQuestion({
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit({ [question.id]: value });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}>
<label
tabIndex={1}
@@ -68,7 +80,16 @@ export default function ConsentQuestion({
<div className="mt-4 flex w-full justify-between">
{!isFirstQuestion && (
<BackButton tabIndex={3} backButtonLabel={question.backButtonLabel} onClick={() => onBack()} />
<BackButton
tabIndex={3}
backButtonLabel={question.backButtonLabel}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
onBack();
}}
/>
)}
<div />
<SubmitButton
@@ -1,4 +1,4 @@
import { TResponseData } from "@formbricks/types/responses";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
import { BackButton } from "../buttons/BackButton";
import SubmitButton from "../buttons/SubmitButton";
@@ -6,17 +6,21 @@ import FileInput from "../general/FileInput";
import Headline from "../general/Headline";
import Subheader from "../general/Subheader";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { useState } from "preact/hooks";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface FileUploadQuestionProps {
question: TSurveyFileUploadQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
isFirstQuestion: boolean;
isLastQuestion: boolean;
surveyId: string;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function FileUploadQuestion({
@@ -29,22 +33,30 @@ export default function FileUploadQuestion({
isLastQuestion,
surveyId,
onFileUpload,
ttc,
setTtc,
}: FileUploadQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
return (
<form
onSubmit={(e) => {
e.preventDefault();
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
if (question.required) {
if (value && (typeof value === "string" || Array.isArray(value)) && value.length > 0) {
onSubmit({ [question.id]: typeof value === "string" ? [value] : value });
onSubmit({ [question.id]: typeof value === "string" ? [value] : value }, updatedTtcObj);
} else {
alert("Please upload a file");
}
} else {
if (value) {
onSubmit({ [question.id]: typeof value === "string" ? [value] : value });
onSubmit({ [question.id]: typeof value === "string" ? [value] : value }, updatedTtcObj);
} else {
onSubmit({ [question.id]: "skipped" });
onSubmit({ [question.id]: "skipped" }, updatedTtcObj);
}
}
}}
@@ -7,15 +7,18 @@ import { cn, shuffleQuestions } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { TResponseTtc } from "@formbricks/types/responses";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface MultipleChoiceMultiProps {
question: TSurveyMultipleChoiceMultiQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function MultipleChoiceMultiQuestion({
@@ -26,7 +29,13 @@ export default function MultipleChoiceMultiQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
ttc,
setTtc,
}: MultipleChoiceMultiProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const getChoicesWithoutOtherLabels = useCallback(
() => question.choices.filter((choice) => choice.id !== "other").map((item) => item.label),
[question]
@@ -89,7 +98,9 @@ export default function MultipleChoiceMultiQuestion({
return getChoicesWithoutOtherLabels().includes(item) || item === otherValue;
}); // filter out all those values which are either in getChoicesWithoutOtherLabels() (i.e. selected by checkbox) or the latest entered otherValue
onChange({ [question.id]: newValue });
onSubmit({ [question.id]: newValue });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
@@ -196,8 +207,10 @@ export default function MultipleChoiceMultiQuestion({
}}
onKeyDown={(e) => {
if (e.key == "Enter") {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
setTimeout(() => {
onSubmit({ [question.id]: value });
onSubmit({ [question.id]: value }, updatedTtcObj);
}, 100);
}
}}
@@ -217,7 +230,11 @@ export default function MultipleChoiceMultiQuestion({
<BackButton
tabIndex={questionChoices.length + 3}
backButtonLabel={question.backButtonLabel}
onClick={onBack}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onBack();
}}
/>
)}
<div></div>
@@ -7,15 +7,18 @@ import { cn, shuffleQuestions } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { TResponseTtc } from "@formbricks/types/responses";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface MultipleChoiceSingleProps {
question: TSurveyMultipleChoiceSingleQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function MultipleChoiceSingleQuestion({
@@ -26,7 +29,13 @@ export default function MultipleChoiceSingleQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
ttc,
setTtc,
}: MultipleChoiceSingleProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const [otherSelected, setOtherSelected] = useState(
!!value && !question.choices.find((c) => c.label === value)
); // initially set to true if value is not in choices
@@ -58,7 +67,9 @@ export default function MultipleChoiceSingleQuestion({
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit({ [question.id]: value });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
@@ -78,8 +89,10 @@ export default function MultipleChoiceSingleQuestion({
onKeyDown={(e) => {
if (e.key == "Enter") {
onChange({ [question.id]: choice.label });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
setTimeout(() => {
onSubmit({ [question.id]: choice.label });
onSubmit({ [question.id]: choice.label }, updatedTtcObj);
}, 350);
}
}}
@@ -157,8 +170,10 @@ export default function MultipleChoiceSingleQuestion({
}}
onKeyDown={(e) => {
if (e.key == "Enter") {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
setTimeout(() => {
onSubmit({ [question.id]: value });
onSubmit({ [question.id]: value }, updatedTtcObj);
}, 100);
}
}}
@@ -178,7 +193,11 @@ export default function MultipleChoiceSingleQuestion({
<BackButton
backButtonLabel={question.backButtonLabel}
tabIndex={questionChoices.length + 3}
onClick={onBack}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onBack();
}}
/>
)}
<div></div>
@@ -6,15 +6,19 @@ import Subheader from "@/components/general/Subheader";
import { cn } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyNPSQuestion } from "@formbricks/types/surveys";
import { useState } from "preact/hooks";
import { TResponseTtc } from "@formbricks/types/responses";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface NPSQuestionProps {
question: TSurveyNPSQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function NPSQuestion({
@@ -25,12 +29,20 @@ export default function NPSQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
ttc,
setTtc,
}: NPSQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
return (
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit({ [question.id]: value });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}>
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
<Headline headline={question.headline} questionId={question.id} required={question.required} />
@@ -45,7 +57,9 @@ export default function NPSQuestion({
tabIndex={idx + 1}
onKeyDown={(e) => {
if (e.key == "Enter") {
onSubmit({ [question.id]: number });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: number }, updatedTtcObj);
}
}}
className={cn(
@@ -60,9 +74,14 @@ export default function NPSQuestion({
className="absolute h-full w-full cursor-pointer opacity-0"
onClick={() => {
if (question.required) {
onSubmit({
[question.id]: number,
});
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit(
{
[question.id]: number,
},
updatedTtcObj
);
}
onChange({ [question.id]: number });
}}
@@ -85,6 +104,8 @@ export default function NPSQuestion({
tabIndex={isLastQuestion ? 12 : 13}
backButtonLabel={question.backButtonLabel}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onBack();
}}
/>
@@ -6,16 +6,21 @@ import Subheader from "@/components/general/Subheader";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyOpenTextQuestion } from "@formbricks/types/surveys";
import { useCallback } from "react";
import { useState } from "preact/hooks";
import { TResponseTtc } from "@formbricks/types/responses";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface OpenTextQuestionProps {
question: TSurveyOpenTextQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
autoFocus?: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function OpenTextQuestion({
@@ -27,7 +32,13 @@ export default function OpenTextQuestion({
isFirstQuestion,
isLastQuestion,
autoFocus = true,
ttc,
setTtc,
}: OpenTextQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const handleInputChange = (inputValue: string) => {
// const isValidInput = validateInput(inputValue, question.inputType, question.required);
// setIsValid(isValidInput);
@@ -50,7 +61,9 @@ export default function OpenTextQuestion({
onSubmit={(e) => {
e.preventDefault();
// if ( validateInput(value as string, question.inputType, question.required)) {
onSubmit({ [question.id]: value, inputType: question.inputType });
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedttc);
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
// }
}}
className="w-full">
@@ -75,7 +88,9 @@ export default function OpenTextQuestion({
if (e.key === "Enter" && isInputEmpty(value as string)) {
e.preventDefault(); // Prevent form submission
} else if (e.key === "Enter") {
onSubmit({ [question.id]: value });
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedttc);
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
}
}}
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
@@ -106,6 +121,8 @@ export default function OpenTextQuestion({
<BackButton
backButtonLabel={question.backButtonLabel}
onClick={() => {
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedttc);
onBack();
}}
/>
@@ -1,21 +1,23 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import QuestionImage from "@/components/general/QuestionImage";
import Headline from "@/components/general/Headline";
import QuestionImage from "@/components/general/QuestionImage";
import Subheader from "@/components/general/Subheader";
import { cn } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys";
import { useEffect } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
interface PictureSelectionProps {
question: TSurveyPictureSelectionQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function PictureSelectionQuestion({
@@ -26,7 +28,13 @@ export default function PictureSelectionQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
ttc,
setTtc,
}: PictureSelectionProps) {
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const addItem = (item: string) => {
let values: string[] = [];
@@ -80,7 +88,9 @@ export default function PictureSelectionQuestion({
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit({ [question.id]: value });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
@@ -155,7 +165,11 @@ export default function PictureSelectionQuestion({
<BackButton
tabIndex={questionChoices.length + 3}
backButtonLabel={question.backButtonLabel}
onClick={onBack}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onBack();
}}
/>
)}
<div></div>
@@ -1,11 +1,12 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import QuestionImage from "@/components/general/QuestionImage";
import Headline from "@/components/general/Headline";
import QuestionImage from "@/components/general/QuestionImage";
import { cn } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyRatingQuestion } from "@formbricks/types/surveys";
import { useState } from "preact/hooks";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
import {
ConfusedFace,
FrowningFace,
@@ -24,10 +25,12 @@ interface RatingQuestionProps {
question: TSurveyRatingQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function RatingQuestion({
@@ -38,15 +41,25 @@ export default function RatingQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
ttc,
setTtc,
}: RatingQuestionProps) {
const [hoveredNumber, setHoveredNumber] = useState(0);
const [startTime, setStartTime] = useState(performance.now());
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const handleSelect = (number: number) => {
onChange({ [question.id]: number });
if (question.required) {
onSubmit({
[question.id]: number,
});
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit(
{
[question.id]: number,
},
updatedTtcObj
);
}
};
@@ -66,7 +79,9 @@ export default function RatingQuestion({
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit({ [question.id]: value });
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
@@ -184,6 +199,8 @@ export default function RatingQuestion({
tabIndex={!question.required || value ? question.range + 2 : question.range + 1}
backButtonLabel={question.backButtonLabel}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onBack();
}}
/>
+49
View File
@@ -0,0 +1,49 @@
import { TResponseTtc } from "@formbricks/types/responses";
import { useEffect } from "react";
export const getUpdatedTtc = (ttc: TResponseTtc, questionId: string, time: number) => {
// Check if the question ID already exists
if (ttc.hasOwnProperty(questionId)) {
return {
...ttc,
[questionId]: ttc[questionId] + time,
};
} else {
// If the question ID does not exist, add it to the object
return {
...ttc,
[questionId]: time,
};
}
};
export const useTtc = (
questionId: string,
ttc: TResponseTtc,
setTtc: (ttc: TResponseTtc) => void,
startTime: number,
setStartTime: (time: number) => void
) => {
useEffect(() => {
setStartTime(performance.now());
}, [questionId]);
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") {
// Restart the timer when the tab becomes visible again
setStartTime(performance.now());
} else {
const updatedTtc = getUpdatedTtc(ttc, questionId, performance.now() - startTime);
setTtc(updatedTtc);
}
};
// Attach the event listener
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
// Clean up the event listener when the component is unmounted
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);
};
+8
View File
@@ -7,6 +7,10 @@ export const ZResponseData = z.record(z.union([z.string(), z.number(), z.array(z
export type TResponseData = z.infer<typeof ZResponseData>;
export const ZResponseTtc = z.record(z.number());
export type TResponseTtc = z.infer<typeof ZResponseTtc>;
export const ZResponsePersonAttributes = ZPersonAttributes.nullable();
export type TResponsePersonAttributes = z.infer<typeof ZResponsePersonAttributes>;
@@ -51,6 +55,7 @@ export const ZResponse = z.object({
personAttributes: ZResponsePersonAttributes,
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
notes: z.array(ZResponseNote),
tags: z.array(ZTag),
meta: ZResponseMeta.nullable(),
@@ -72,6 +77,7 @@ export const ZResponseInput = z.object({
singleUseId: z.string().nullable().optional(),
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
meta: z
.object({
source: z.string().optional(),
@@ -98,6 +104,7 @@ export type TResponseLegacyInput = z.infer<typeof ZResponseLegacyInput>;
export const ZResponseUpdateInput = z.object({
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
});
export type TResponseUpdateInput = z.infer<typeof ZResponseUpdateInput>;
@@ -111,6 +118,7 @@ export type TResponseWithSurvey = z.infer<typeof ZResponseWithSurvey>;
export const ZResponseUpdate = z.object({
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
meta: z
.object({
url: z.string().optional(),