Add MultipleChoice Multi-Select Question Type (#238)

Add MultipleChoice Multi-Select Question Type

---------

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Moritz Rengert
2023-04-19 13:38:23 +02:00
committed by GitHub
parent 72c6ea6f39
commit d15d062581
24 changed files with 730 additions and 166 deletions
@@ -52,7 +52,9 @@ export default function ResponseFeed({ person, sortByDate, environmentId }) {
<div key={question.id}>
<p className="text-sm text-slate-500">{question.headline}</p>
<p className="my-1 text-lg font-semibold text-slate-700">
{response.data[question.id]}
{response.data[question.id] instanceof Array
? response.data[question.id].join(", ")
: response.data[question.id]}
</p>
</div>
))}
@@ -24,19 +24,18 @@ export default function PreviewSurvey({
brandColor,
}: PreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
const [progress, setProgress] = useState(0); // [0, 1]
useEffect(() => {
if (currentQuestion && localSurvey) {
setProgress(calculateProgress(currentQuestion, localSurvey));
if (activeQuestionId && localSurvey) {
setProgress(calculateProgress(localSurvey));
}
function calculateProgress(currentQuestion, survey) {
const elementIdx = survey.questions.findIndex((e) => e.id === currentQuestion.id);
function calculateProgress(survey) {
const elementIdx = survey.questions.findIndex((e) => e.id === activeQuestionId);
return elementIdx / survey.questions.length;
}
}, [currentQuestion, localSurvey]);
}, [activeQuestionId, localSurvey]);
useEffect(() => {
// close modal if there are no questions left
@@ -44,7 +43,6 @@ export default function PreviewSurvey({
if (activeQuestionId === "thank-you-card") {
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
@@ -52,43 +50,25 @@ export default function PreviewSurvey({
}
}, [activeQuestionId, localSurvey, questions, setActiveQuestionId]);
useEffect(() => {
const currentIndex = questions.findIndex((q) => q.id === currentQuestion?.id);
if (currentIndex < questions.length && currentIndex >= 0 && !localSurvey) return;
if (activeQuestionId) {
if (currentQuestion && currentQuestion.id === activeQuestionId) {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
return;
}
if (activeQuestionId === "thank-you-card") return;
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
setIsModalOpen(true);
}, 300);
} else {
if (questions && questions.length > 0) {
setCurrentQuestion(questions[0]);
}
}
}, [activeQuestionId, currentQuestion, localSurvey, questions]);
const gotoNextQuestion = () => {
if (currentQuestion) {
const currentIndex = questions.findIndex((q) => q.id === currentQuestion.id);
if (currentIndex < questions.length - 1) {
setCurrentQuestion(questions[currentIndex + 1]);
setActiveQuestionId(questions[currentIndex + 1].id);
const currentIndex = questions.findIndex((q) => q.id === activeQuestionId);
if (currentIndex < questions.length - 1) {
setActiveQuestionId(questions[currentIndex + 1].id);
} else {
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
setProgress(1);
} else {
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
@@ -100,18 +80,15 @@ export default function PreviewSurvey({
const resetPreview = () => {
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
};
if (!currentQuestion) {
if (!activeQuestionId) {
return null;
}
const lastQuestion = questions.length > 0 && currentQuestion.id === questions[questions.length - 1].id;
return (
<>
{localSurvey?.type === "link" ? (
@@ -131,12 +108,18 @@ export default function PreviewSurvey({
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : (
<QuestionConditional
currentQuestion={currentQuestion}
brandColor={brandColor}
lastQuestion={lastQuestion}
onSubmit={gotoNextQuestion}
/>
questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
)
)
)}
</ContentWrapper>
</div>
@@ -155,12 +138,18 @@ export default function PreviewSurvey({
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : (
<QuestionConditional
currentQuestion={currentQuestion}
brandColor={brandColor}
lastQuestion={lastQuestion}
onSubmit={gotoNextQuestion}
/>
questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
)
)
)}
</Modal>
)}
@@ -0,0 +1,111 @@
import { Button } from "@formbricks/ui";
import { Input } from "@formbricks/ui";
import { Label } from "@formbricks/ui";
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import { createId } from "@paralleldrive/cuid2";
import { TrashIcon } from "@heroicons/react/24/solid";
interface OpenQuestionFormProps {
question: MultipleChoiceMultiQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
lastQuestion: boolean;
}
export default function MultipleChoiceMultiForm({
question,
questionIdx,
updateQuestion,
lastQuestion,
}: OpenQuestionFormProps) {
const updateChoice = (choiceIdx: number, updatedAttributes: any) => {
const newChoices = !question.choices
? []
: question.choices.map((choice, idx) => {
if (idx === choiceIdx) {
return { ...choice, ...updatedAttributes };
}
return choice;
});
updateQuestion(questionIdx, { choices: newChoices });
};
const addChoice = () => {
const newChoices = !question.choices ? [] : question.choices;
newChoices.push({ id: createId(), label: "" });
updateQuestion(questionIdx, { choices: newChoices });
};
const deleteChoice = (choiceIdx: number) => {
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
updateQuestion(questionIdx, { choices: newChoices });
};
return (
<form>
<div className="mt-3">
<Label htmlFor="headline">Question</Label>
<div className="mt-2">
<Input
id="headline"
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
/>
</div>
</div>
<div className="mt-3">
<Label htmlFor="subheader">Description</Label>
<div className="mt-2">
<Input
id="subheader"
name="subheader"
value={question.subheader}
onChange={(e) => updateQuestion(questionIdx, { subheader: e.target.value })}
/>
</div>
</div>
<div className="mt-3">
<Label htmlFor="choices">Choices</Label>
<div className="mt-2 space-y-2" id="choices">
{question.choices &&
question.choices.map((choice, choiceIdx) => (
<div key={choiceIdx} className="inline-flex w-full items-center">
<Input
id={choice.id}
name={choice.id}
value={choice.label}
placeholder={`Choice ${choiceIdx + 1}`}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
/>
{question.choices && question.choices.length > 2 && (
<TrashIcon
className="ml-2 h-4 w-4 text-slate-400"
onClick={() => deleteChoice(choiceIdx)}
/>
)}
</div>
))}
<Button variant="secondary" type="button" onClick={() => addChoice()}>
Add Choice
</Button>
</div>
</div>
<div className="mt-3">
<Label htmlFor="buttonLabel">Button Label</Label>
<div className="mt-2">
<Input
id="buttonLabel"
name="buttonLabel"
value={question.buttonLabel}
placeholder={lastQuestion ? "Finish" : "Next"}
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
/>
</div>
</div>
</form>
);
}
@@ -10,6 +10,7 @@ import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { Draggable } from "react-beautiful-dnd";
import MultipleChoiceSingleForm from "./MultipleChoiceSingleForm";
import MultipleChoiceMultiForm from "./MultipleChoiceMultiForm";
import OpenQuestionForm from "./OpenQuestionForm";
import QuestionDropdown from "./QuestionDropdown";
import UpdateQuestionId from "./UpdateQuestionId";
@@ -112,6 +113,13 @@ export default function QuestionCard({
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
/>
) : question.type === "multipleChoiceMulti" ? (
<MultipleChoiceMultiForm
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
/>
) : null}
<div className="mt-4 border-t border-slate-200">
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-3">
@@ -36,7 +36,6 @@ export default function ResponseTimeline({ environmentId, surveyId }) {
}
return { ...response, responses: updatedResponse };
});
return updatedResponses;
}
return [];
@@ -13,7 +13,7 @@ interface OpenTextSummaryProps {
responses: {
id: string;
question: string;
answer: string;
answer: string | any[];
}[];
};
environmentId: string;
@@ -53,14 +53,16 @@ export default function SingleResponse({ data, environmentId }: OpenTextSummaryP
</div>
</div>
<div className="space-y-6 rounded-b-lg bg-white p-6">
{data.responses.map((response) => {
return (
<div key={response.id}>
<p className="text-sm text-slate-500">{response.question}</p>
{data.responses.map((response, idx) => (
<div key={`${response.id}-${idx}`}>
<p className="text-sm text-slate-500">{response.question}</p>
{typeof response.answer === "string" ? (
<p className="my-1 font-semibold text-slate-700">{response.answer}</p>
</div>
);
})}
) : (
<p className="my-1 font-semibold text-slate-700">{response.answer.join(", ")}</p>
)}
</div>
))}
</div>
</div>
);
@@ -14,6 +14,8 @@ interface ChoiceResult {
}
export default function MultipleChoiceSummary({ questionSummary }: MultipleChoiceSummaryProps) {
const isSingleChoice = questionSummary.question.type === "multipleChoiceSingle";
const results: ChoiceResult[] = useMemo(() => {
if (!("choices" in questionSummary.question)) return [];
// build a dictionary of choices
@@ -27,9 +29,16 @@ export default function MultipleChoiceSummary({ questionSummary }: MultipleChoic
}
// count the responses
for (const response of questionSummary.responses) {
// only add responses that are in the choices
if (response.value in resultsDict) {
// if single choice, only add responses that are in the choices
if (isSingleChoice && response.value in resultsDict) {
resultsDict[response.value].count += 1;
} else {
// if multi choice add all responses
for (const choice of response.value) {
if (choice in resultsDict) {
resultsDict[choice].count += 1;
}
}
}
}
// add the percentage
@@ -59,7 +68,11 @@ export default function MultipleChoiceSummary({ questionSummary }: MultipleChoic
<h3 className="pb-1 text-xl font-semibold text-slate-900">{questionSummary.question.headline}</h3>
</div>
<div className="flex space-x-2 font-semibold text-slate-600">
<div className="rounded-lg bg-slate-100 p-2 text-sm">Multiple-Choice Single Select Question</div>
<div className="rounded-lg bg-slate-100 p-2 text-sm">
{isSingleChoice
? "Multiple-Choice Single Select Question"
: "Multiple-Choice Multi Select Question"}
</div>
<div className=" flex items-center rounded-lg bg-slate-100 p-2 text-sm">
<InboxStackIcon className="mr-2 h-4 w-4 " />
{totalResponses} responses
@@ -61,7 +61,10 @@ export default function SummaryList({ environmentId, surveyId }) {
/>
);
}
if (questionSummary.question.type === "multipleChoiceSingle") {
if (
questionSummary.question.type === "multipleChoiceSingle" ||
questionSummary.question.type === "multipleChoiceMulti"
) {
return (
<MultipleChoiceSummary
key={questionSummary.question.id}
+5 -13
View File
@@ -1,7 +1,5 @@
"use client";
import MultipleChoiceSingleQuestion from "@/components/preview/MultipleChoiceSingleQuestion";
import OpenTextQuestion from "@/components/preview/OpenTextQuestion";
import Progress from "@/components/preview/Progress";
import ThankYouCard from "@/components/preview/ThankYouCard";
import ContentWrapper from "@/components/shared/ContentWrapper";
@@ -12,6 +10,7 @@ import type { Question } from "@formbricks/types/questions";
import type { Survey } from "@formbricks/types/surveys";
import { Confetti } from "@formbricks/ui";
import { useEffect, useState } from "react";
import QuestionConditional from "@/components/preview/QuestionConditional";
import { createDisplay, markDisplayResponded } from "@formbricks/lib/clientDisplay/display";
type EnhancedSurvey = Survey & {
@@ -124,21 +123,14 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
brandColor={survey.brandColor}
/>
</div>
) : currentQuestion.type === "openText" ? (
<OpenTextQuestion
) : (
<QuestionConditional
question={currentQuestion}
onSubmit={submitResponse}
lastQuestion={lastQuestion}
brandColor={survey.brandColor}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
onSubmit={submitResponse}
lastQuestion={lastQuestion}
brandColor={survey.brandColor}
onSubmit={submitResponse}
/>
) : null}
)}
</ContentWrapper>
</div>
<div className="top-0 z-10 w-full border-b bg-white">
@@ -0,0 +1,105 @@
import { useState, useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceMultiProps {
question: MultipleChoiceMultiQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceMultiQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: MultipleChoiceMultiProps) {
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
const [isAtLeastOneChecked, setIsAtLeastOneChecked] = useState(false);
useEffect(() => {
setIsAtLeastOneChecked(selectedChoices.length > 0);
}, [selectedChoices]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (question.required && selectedChoices.length <= 0) {
return;
}
const data = {
[question.id]: selectedChoices,
};
onSubmit(data);
setSelectedChoices([]); // reset value
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="mt-4">
<fieldset>
<legend className="sr-only">Choices</legend>
<div className="relative space-y-2 rounded-md bg-white">
{question.choices &&
question.choices.map((choice) => (
<label
key={choice.id}
className={cn(
selectedChoices.includes(choice.label)
? "z-10 border-slate-400 bg-slate-50"
: "border-gray-200",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
type="checkbox"
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${choice.id}-label`}
checked={selectedChoices.includes(choice.label)}
onChange={(e) => {
if (e.currentTarget.checked) {
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
} else {
setSelectedChoices(
selectedChoices.filter((label) => label !== e.currentTarget.value)
);
}
}}
style={{ borderColor: brandColor, color: brandColor }}
/>
<span id={`${choice.id}-label`} className="ml-3 font-medium">
{choice.label}
</span>
</span>
</label>
))}
</div>
</fieldset>
</div>
<input
type="text"
className="clip-[rect(0,0,0,0)] absolute m-[-1px] h-1 w-1 overflow-hidden whitespace-nowrap border-0 p-0 text-transparent caret-transparent focus:border-transparent focus:ring-0"
required={question.required}
value={isAtLeastOneChecked ? "checked" : ""}
onChange={() => {}}
/>
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</form>
);
}
@@ -26,9 +26,8 @@ export default function MultipleChoiceSingleQuestion({
[question.id]: e.currentTarget[question.id].value,
};
e.currentTarget[question.id].value = "";
onSubmit(data);
// reset form
setSelectedChoice(null); // reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
@@ -55,6 +54,7 @@ export default function MultipleChoiceSingleQuestion({
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);
}}
checked={selectedChoice === choice.label}
style={{ borderColor: brandColor, color: brandColor }}
required={question.required && idx === 0}
/>
@@ -26,9 +26,8 @@ export default function OpenTextQuestion({
const data = {
[question.id]: value,
};
setValue("");
setValue(""); // reset value
onSubmit(data);
// reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
@@ -1,30 +1,38 @@
import type { Question } from "@formbricks/types/questions";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
interface QuestionConditionalProps {
currentQuestion: Question;
question: Question;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function QuestionConditional({
currentQuestion,
question,
onSubmit,
lastQuestion,
brandColor,
}: QuestionConditionalProps) {
return currentQuestion.type === "openText" ? (
return question.type === "openText" ? (
<OpenTextQuestion
question={currentQuestion}
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
) : question.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "multipleChoiceMulti" ? (
<MultipleChoiceMultiQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
@@ -25,12 +25,14 @@ export default function ThankYouCard({ headline, subheader, brandColor }: ThankY
/>
</svg>
</div>
<span className="mb-[10px] inline-block h-1 w-16 rounded-[100%] bg-slate-300"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" />
<Subheader subheader={subheader} questionId="thankYouCard" />
</div>
{/* <span
className="mb-[10px] mt-[35px] inline-block h-[2px] w-4/5 rounded-full opacity-25"
style={{ backgroundColor: brandColor }}></span>
+12
View File
@@ -31,6 +31,18 @@ export const questionTypes: QuestionType[] = [
],
},
},
{
id: "multipleChoiceMulti",
label: "Multiple Choice Multi-Select",
description: "Number of choices from a list of options (checkboxes)",
icon: ListBulletIcon,
defaults: {
choices: [
{ id: createId(), label: "" },
{ id: createId(), label: "" },
],
},
},
];
export const universalQuestionDefaults = {
+1 -1
View File
@@ -27,7 +27,7 @@
"eslint-config-next": "^13.3.0",
"jsonwebtoken": "^9.0.0",
"lucide-react": "^0.161.0",
"next": "13.3.0",
"next": "13.2.4",
"next-auth": "^4.22.0",
"nodemailer": "^6.9.1",
"platform": "^1.3.6",
@@ -0,0 +1,102 @@
import { h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { cn } from "../lib/utils";
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/js";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceMultiProps {
question: MultipleChoiceMultiQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceMultiQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: MultipleChoiceMultiProps) {
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
const isAtLeastOneChecked = () => {
return selectedChoices.length > 0;
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (!isAtLeastOneChecked() && question.required) return;
const data = {
[question.id]: selectedChoices,
};
onSubmit(data);
setSelectedChoices([]); // reset value
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="fb-mt-4">
<fieldset>
<legend className="fb-sr-only">Choices</legend>
<div className="fb-relative fb-space-y-2 fb-rounded-md fb-bg-white">
{question.choices &&
question.choices.map((choice) => (
<label
key={choice.id}
className={cn(
selectedChoices.includes(choice.label)
? "fb-z-10 fb-border-slate-400 fb-bg-slate-50"
: "fb-border-gray-200",
"fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-rounded-md fb-border fb-p-4 hover:fb-bg-slate-50 focus:fb-outline-none"
)}>
<span className="fb-flex fb-items-center fb-text-sm">
<input
type="checkbox"
id={choice.id}
name={question.id}
value={choice.label}
className="fb-h-4 fb-w-4 fb-border fb-border-slate-300 focus:fb-ring-0 focus:fb-ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
if (e.currentTarget.checked) {
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
} else {
setSelectedChoices(
selectedChoices.filter((label) => label !== e.currentTarget.value)
);
}
}}
checked={selectedChoices.includes(choice.label)}
style={{ borderColor: brandColor, color: brandColor }}
/>
<span id={`${choice.id}-label`} className="fb-ml-3 fb-font-medium">
{choice.label}
</span>
</span>
</label>
))}
</div>
</fieldset>
</div>
<input
type="text"
className="clip-[rect(0,0,0,0)] fb-absolute fb-m-[-1px] fb-h-1 fb-w-1 fb-overflow-hidden fb-whitespace-nowrap fb-border-0 fb-p-0 fb-text-transparent fb-caret-transparent focus:fb-border-transparent focus:fb-ring-0"
required={question.required}
value={isAtLeastOneChecked() ? "checked" : ""}
/>
<div className="fb-mt-4 fb-flex fb-w-full fb-justify-between">
<div></div>
<button
type="submit"
className="fb-flex fb-items-center fb-rounded-md fb-border fb-border-transparent fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 fb-text-white fb-shadow-sm hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 focus:ring-slate-500"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</form>
);
}
@@ -26,9 +26,8 @@ export default function MultipleChoiceSingleQuestion({
const data = {
[question.id]: e.currentTarget[question.id].value,
};
e.currentTarget[question.id].value = "";
onSubmit(data);
// reset form
setSelectedChoice(null); // reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
@@ -57,6 +56,7 @@ export default function MultipleChoiceSingleQuestion({
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);
}}
checked={selectedChoice === choice.label}
style={{ borderColor: brandColor, color: brandColor }}
required={question.required && idx === 0}
/>
@@ -23,7 +23,7 @@ export default function OpenTextQuestion({
const data = {
[question.id]: e.currentTarget[question.id].value,
};
e.currentTarget[question.id].value = "";
e.currentTarget[question.id].value = ""; // reset value
onSubmit(data);
// reset form
}}>
@@ -0,0 +1,42 @@
import { h } from "preact";
import type { Question } from "@formbricks/types/js";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
interface QuestionConditionalProps {
question: Question;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function QuestionConditional({
question,
onSubmit,
lastQuestion,
brandColor,
}: QuestionConditionalProps) {
return question.type === "openText" ? (
<OpenTextQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "multipleChoiceMulti" ? (
<MultipleChoiceMultiQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : null;
}
+21 -26
View File
@@ -4,10 +4,9 @@ import { createDisplay, markDisplayResponded } from "../lib/display";
import { createResponse, updateResponse } from "../lib/response";
import { cn } from "../lib/utils";
import { JsConfig, Survey } from "@formbricks/types/js";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import Progress from "./Progress";
import ThankYouCard from "./ThankYouCard";
import QuestionConditional from "./QuestionConditional";
interface SurveyViewProps {
config: JsConfig;
@@ -17,7 +16,7 @@ interface SurveyViewProps {
}
export default function SurveyView({ config, survey, close, brandColor }: SurveyViewProps) {
const [currentQuestion, setCurrentQuestion] = useState(survey.questions[0]);
const [activeQuestionId, setActiveQuestionId] = useState(survey.questions[0].id);
const [progress, setProgress] = useState(0); // [0, 1]
const [responseId, setResponseId] = useState(null);
const [displayId, setDisplayId] = useState(null);
@@ -29,20 +28,21 @@ export default function SurveyView({ config, survey, close, brandColor }: Survey
const displayId = await createDisplay({ surveyId: survey.id, personId: config.person.id }, config);
setDisplayId(displayId.id);
}
console.log(survey);
}, [config, survey]);
useEffect(() => {
setProgress(calculateProgress());
function calculateProgress() {
const elementIdx = survey.questions.findIndex((e) => e.id === currentQuestion.id);
const elementIdx = survey.questions.findIndex((e) => e.id === activeQuestionId);
return elementIdx / survey.questions.length;
}
}, [currentQuestion, survey]);
}, [activeQuestionId, survey]);
const submitResponse = async (data: { [x: string]: any }) => {
setLoadingElement(true);
const questionIdx = survey.questions.findIndex((e) => e.id === currentQuestion.id);
const questionIdx = survey.questions.findIndex((e) => e.id === activeQuestionId);
const finished = questionIdx === survey.questions.length - 1;
// build response
const responseRequest = {
@@ -61,7 +61,7 @@ export default function SurveyView({ config, survey, close, brandColor }: Survey
}
setLoadingElement(false);
if (!finished) {
setCurrentQuestion(survey.questions[questionIdx + 1]);
setActiveQuestionId(survey.questions[questionIdx + 1].id);
} else {
setProgress(100);
@@ -88,25 +88,20 @@ export default function SurveyView({ config, survey, close, brandColor }: Survey
subheader={survey.thankYouCard.subheader}
brandColor={config.settings?.brandColor}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
onSubmit={submitResponse}
lastQuestion={
survey.questions.findIndex((e) => e.id === currentQuestion.id) === survey.questions.length - 1
}
brandColor={brandColor}
/>
) : currentQuestion.type === "openText" ? (
<OpenTextQuestion
question={currentQuestion}
onSubmit={submitResponse}
lastQuestion={
survey.questions.findIndex((e) => e.id === currentQuestion.id) === survey.questions.length - 1
}
brandColor={brandColor}
/>
) : null}
) : (
survey.questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
brandColor={brandColor}
lastQuestion={idx === survey.questions.length - 1}
onSubmit={submitResponse}
question={question}
/>
)
)
)}
</div>
<Progress progress={progress} brandColor={brandColor} />
</div>
+17 -1
View File
@@ -79,7 +79,13 @@ export interface ThankYouCard {
subheader?: string;
}
export type Question = OpenTextQuestion | MultipleChoiceSingleQuestion;
export interface ThankYouCard {
enabled: boolean;
headline?: string;
subheader?: string;
}
export type Question = OpenTextQuestion | MultipleChoiceSingleQuestion | MultipleChoiceMultiQuestion;
export interface OpenTextQuestion {
id: string;
@@ -101,6 +107,16 @@ export interface MultipleChoiceSingleQuestion {
choices?: Choice[];
}
export interface MultipleChoiceMultiQuestion {
id: string;
type: "multipleChoiceMulti";
headline: string;
subheader?: string;
required: boolean;
buttonLabel?: string;
choices?: Choice[];
}
export interface Choice {
id: string;
label: string;
+11 -1
View File
@@ -1,4 +1,4 @@
export type Question = OpenTextQuestion | MultipleChoiceSingleQuestion;
export type Question = OpenTextQuestion | MultipleChoiceSingleQuestion | MultipleChoiceMultiQuestion;
export interface OpenTextQuestion {
id: string;
@@ -20,6 +20,16 @@ export interface MultipleChoiceSingleQuestion {
choices: Choice[];
}
export interface MultipleChoiceMultiQuestion {
id: string;
type: "multipleChoiceMulti";
headline: string;
subheader?: string;
required: boolean;
buttonLabel?: string;
choices: Choice[];
}
export interface Choice {
id: string;
label: string;
+199 -45
View File
@@ -15,7 +15,7 @@ importers:
version: 3.12.6
turbo:
specifier: latest
version: 1.8.8
version: 1.9.1
apps/demo:
dependencies:
@@ -229,11 +229,11 @@ importers:
specifier: ^0.161.0
version: 0.161.0(react@18.2.0)
next:
specifier: 13.3.0
version: 13.3.0(react-dom@18.2.0)(react@18.2.0)
specifier: 13.2.4
version: 13.2.4(react-dom@18.2.0)(react@18.2.0)
next-auth:
specifier: ^4.22.0
version: 4.22.0(next@13.3.0)(nodemailer@6.9.1)(react-dom@18.2.0)(react@18.2.0)
version: 4.22.0(next@13.2.4)(nodemailer@6.9.1)(react-dom@18.2.0)(react@18.2.0)
nodemailer:
specifier: ^6.9.1
version: 6.9.1
@@ -346,7 +346,7 @@ importers:
version: 4.4.1
tsup:
specifier: ^6.7.0
version: 6.7.0(postcss@8.4.21)(typescript@5.0.4)
version: 6.7.0(typescript@5.0.3)
tsx:
specifier: ^3.12.6
version: 3.12.6
@@ -404,7 +404,7 @@ importers:
version: 8.8.0(eslint@8.37.0)
eslint-config-turbo:
specifier: latest
version: 1.8.8(eslint@8.37.0)
version: 1.9.1(eslint@8.37.0)
eslint-plugin-react:
specifier: 7.32.2
version: 7.32.2(eslint@8.37.0)
@@ -6551,7 +6551,7 @@ packages:
minipass-pipeline: 1.2.4
mkdirp: 1.0.4
p-map: 4.0.0
promise-inflight: 1.0.1(bluebird@3.7.2)
promise-inflight: 1.0.1
rimraf: 3.0.2
ssri: 8.0.1
tar: 6.1.12
@@ -8754,7 +8754,7 @@ packages:
eslint: 8.37.0
eslint-import-resolver-node: 0.3.6
eslint-import-resolver-typescript: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.37.0)
eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.55.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.37.0)
eslint-plugin-import: 2.26.0(eslint@8.37.0)
eslint-plugin-jsx-a11y: 6.6.1(eslint@8.37.0)
eslint-plugin-react: 7.32.2(eslint@8.37.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.37.0)
@@ -8779,7 +8779,7 @@ packages:
eslint: 8.38.0
eslint-import-resolver-node: 0.3.6
eslint-import-resolver-typescript: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.38.0)
eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.55.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.37.0)
eslint-plugin-import: 2.26.0(eslint@8.38.0)
eslint-plugin-jsx-a11y: 6.6.1(eslint@8.38.0)
eslint-plugin-react: 7.32.2(eslint@8.38.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.38.0)
@@ -8820,13 +8820,13 @@ packages:
eslint: 8.37.0
dev: false
/eslint-config-turbo@1.8.8(eslint@8.37.0):
resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==}
/eslint-config-turbo@1.9.1(eslint@8.37.0):
resolution: {integrity: sha512-tUqm5TxI5bpbDEgClbw+UygVPAwYB20FIpAiQsZI8imJNDz30E40TZkp6uWpAKmxykU8T0+t3jwkYokvXmXc0Q==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
eslint: 8.37.0
eslint-plugin-turbo: 1.8.8(eslint@8.37.0)
eslint-plugin-turbo: 1.9.1(eslint@8.37.0)
dev: false
/eslint-import-resolver-node@0.3.6:
@@ -8848,7 +8848,7 @@ packages:
debug: 4.3.4
enhanced-resolve: 5.12.0
eslint: 8.37.0
eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.55.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.37.0)
eslint-plugin-import: 2.26.0(eslint@8.37.0)
get-tsconfig: 4.4.0
globby: 13.1.2
is-core-module: 2.11.0
@@ -8868,7 +8868,7 @@ packages:
debug: 4.3.4
enhanced-resolve: 5.12.0
eslint: 8.38.0
eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.55.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.37.0)
eslint-plugin-import: 2.26.0(eslint@8.38.0)
get-tsconfig: 4.4.0
globby: 13.1.2
is-core-module: 2.11.0
@@ -8878,7 +8878,7 @@ packages:
- supports-color
dev: false
/eslint-module-utils@2.7.4(@typescript-eslint/parser@5.55.0)(eslint-import-resolver-node@0.3.6)(eslint-import-resolver-typescript@3.5.2)(eslint@8.37.0):
/eslint-module-utils@2.7.4(eslint-import-resolver-node@0.3.6)(eslint@8.37.0):
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
engines: {node: '>=4'}
peerDependencies:
@@ -8899,11 +8899,37 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 5.55.0(eslint@8.37.0)(typescript@5.0.3)
debug: 3.2.7
eslint: 8.37.0
eslint-import-resolver-node: 0.3.6
eslint-import-resolver-typescript: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.37.0)
transitivePeerDependencies:
- supports-color
dev: false
/eslint-module-utils@2.7.4(eslint-import-resolver-node@0.3.6)(eslint@8.38.0):
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: '*'
eslint-import-resolver-node: '*'
eslint-import-resolver-typescript: '*'
eslint-import-resolver-webpack: '*'
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
eslint:
optional: true
eslint-import-resolver-node:
optional: true
eslint-import-resolver-typescript:
optional: true
eslint-import-resolver-webpack:
optional: true
dependencies:
debug: 3.2.7
eslint: 8.38.0
eslint-import-resolver-node: 0.3.6
transitivePeerDependencies:
- supports-color
dev: false
@@ -8924,7 +8950,7 @@ packages:
semver: 7.3.8
dev: true
/eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.55.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.37.0):
/eslint-plugin-import@2.26.0(eslint@8.37.0):
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
engines: {node: '>=4'}
peerDependencies:
@@ -8934,14 +8960,43 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 5.55.0(eslint@8.37.0)(typescript@5.0.3)
array-includes: 3.1.6
array.prototype.flat: 1.3.1
debug: 2.6.9
doctrine: 2.1.0
eslint: 8.37.0
eslint-import-resolver-node: 0.3.6
eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.55.0)(eslint-import-resolver-node@0.3.6)(eslint-import-resolver-typescript@3.5.2)(eslint@8.37.0)
eslint-module-utils: 2.7.4(eslint-import-resolver-node@0.3.6)(eslint@8.37.0)
has: 1.0.3
is-core-module: 2.11.0
is-glob: 4.0.3
minimatch: 3.1.2
object.values: 1.1.6
resolve: 1.22.1
tsconfig-paths: 3.14.1
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
dev: false
/eslint-plugin-import@2.26.0(eslint@8.38.0):
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
dependencies:
array-includes: 3.1.6
array.prototype.flat: 1.3.1
debug: 2.6.9
doctrine: 2.1.0
eslint: 8.38.0
eslint-import-resolver-node: 0.3.6
eslint-module-utils: 2.7.4(eslint-import-resolver-node@0.3.6)(eslint@8.38.0)
has: 1.0.3
is-core-module: 2.11.0
is-glob: 4.0.3
@@ -9085,8 +9140,8 @@ packages:
string.prototype.matchall: 4.0.8
dev: false
/eslint-plugin-turbo@1.8.8(eslint@8.37.0):
resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==}
/eslint-plugin-turbo@1.9.1(eslint@8.37.0):
resolution: {integrity: sha512-QPd0EG0xkoDkXJLwPQKULxHjkR27VmvJtILW4C9aIrqauLZ+Yc/V7R+A9yVwAi6nkMHxUlCSUsBxmiQP9TIlPw==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
@@ -11474,7 +11529,7 @@ packages:
exit: 0.1.2
graceful-fs: 4.2.10
import-local: 3.1.0
jest-config: 29.5.0(@types/node@18.15.11)
jest-config: 29.5.0
jest-util: 29.5.0
jest-validate: 29.5.0
prompts: 2.4.2
@@ -11485,6 +11540,44 @@ packages:
- ts-node
dev: true
/jest-config@29.5.0:
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@types/node': '*'
ts-node: '>=9.0.0'
peerDependenciesMeta:
'@types/node':
optional: true
ts-node:
optional: true
dependencies:
'@babel/core': 7.20.12
'@jest/test-sequencer': 29.5.0
'@jest/types': 29.5.0
babel-jest: 29.5.0(@babel/core@7.20.12)
chalk: 4.1.2
ci-info: 3.7.0
deepmerge: 4.2.2
glob: 7.2.3
graceful-fs: 4.2.10
jest-circus: 29.5.0
jest-environment-node: 29.5.0
jest-get-type: 29.4.3
jest-regex-util: 29.4.3
jest-resolve: 29.5.0
jest-runner: 29.5.0
jest-util: 29.5.0
jest-validate: 29.5.0
micromatch: 4.0.5
parse-json: 5.2.0
pretty-format: 29.5.0
slash: 3.0.0
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
dev: true
/jest-config@29.5.0(@types/node@18.15.11):
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -13665,7 +13758,7 @@ packages:
engines: {node: '>=10'}
dev: true
/next-auth@4.22.0(next@13.3.0)(nodemailer@6.9.1)(react-dom@18.2.0)(react@18.2.0):
/next-auth@4.22.0(next@13.2.4)(nodemailer@6.9.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-08+kjnDoE7aQ52O996x6cwA3ffc2CbHIkrCgLYhbE+aDIJBKI0oA9UbIEIe19/+ODYJgpAHHOtJx4izmsgaVag==}
peerDependencies:
next: ^12.2.5 || ^13
@@ -13680,7 +13773,7 @@ packages:
'@panva/hkdf': 1.0.2
cookie: 0.5.0
jose: 4.13.1
next: 13.3.0(react-dom@18.2.0)(react@18.2.0)
next: 13.2.4(react-dom@18.2.0)(react@18.2.0)
nodemailer: 6.9.1
oauth: 0.9.15
openid-client: 5.4.0
@@ -14758,6 +14851,22 @@ packages:
postcss: 8.4.22
dev: true
/postcss-load-config@3.1.4:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
dependencies:
lilconfig: 2.0.6
yaml: 1.10.2
dev: true
/postcss-load-config@3.1.4(postcss@8.4.21):
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
@@ -15721,6 +15830,15 @@ packages:
engines: {node: '>=0.4.0'}
dev: true
/promise-inflight@1.0.1:
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
bluebird: '*'
peerDependenciesMeta:
bluebird:
optional: true
dev: true
/promise-inflight@1.0.1(bluebird@3.7.2):
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
@@ -18374,6 +18492,42 @@ packages:
- ts-node
dev: true
/tsup@6.7.0(typescript@5.0.3):
resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==}
engines: {node: '>=14.18'}
hasBin: true
peerDependencies:
'@swc/core': ^1
postcss: ^8.4.12
typescript: '>=4.1.0'
peerDependenciesMeta:
'@swc/core':
optional: true
postcss:
optional: true
typescript:
optional: true
dependencies:
bundle-require: 4.0.1(esbuild@0.17.11)
cac: 6.7.14
chokidar: 3.5.3
debug: 4.3.4
esbuild: 0.17.11
execa: 5.1.1
globby: 11.1.0
joycon: 3.1.1
postcss-load-config: 3.1.4
resolve-from: 5.0.0
rollup: 3.5.1
source-map: 0.8.0-beta.0
sucrase: 3.29.0
tree-kill: 1.2.2
typescript: 5.0.3
transitivePeerDependencies:
- supports-color
- ts-node
dev: true
/tsutils@3.21.0(typescript@5.0.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
@@ -18427,65 +18581,65 @@ packages:
dependencies:
safe-buffer: 5.2.1
/turbo-darwin-64@1.8.8:
resolution: {integrity: sha512-18cSeIm7aeEvIxGyq7PVoFyEnPpWDM/0CpZvXKHpQ6qMTkfNt517qVqUTAwsIYqNS8xazcKAqkNbvU1V49n65Q==}
/turbo-darwin-64@1.9.1:
resolution: {integrity: sha512-IX/Ph4CO80lFKd9pPx3BWpN2dynt6mcUFifyuHUNVkOP1Usza/G9YuZnKQFG6wUwKJbx40morFLjk1TTeLe04w==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-darwin-arm64@1.8.8:
resolution: {integrity: sha512-ruGRI9nHxojIGLQv1TPgN7ud4HO4V8mFBwSgO6oDoZTNuk5ybWybItGR+yu6fni5vJoyMHXOYA2srnxvOc7hjQ==}
/turbo-darwin-arm64@1.9.1:
resolution: {integrity: sha512-6tCbmIboy9dTbhIZ/x9KIpje73nvxbiyVnHbr9xKnsxLJavD0xqjHZzbL5U2tHp8chqmYf0E4WYOXd+XCNg+OQ==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-linux-64@1.8.8:
resolution: {integrity: sha512-N/GkHTHeIQogXB1/6ZWfxHx+ubYeb8Jlq3b/3jnU4zLucpZzTQ8XkXIAfJG/TL3Q7ON7xQ8yGOyGLhHL7MpFRg==}
/turbo-linux-64@1.9.1:
resolution: {integrity: sha512-ti8XofnJFO1XaadL92lYJXgxb0VBl03Yu9VfhxkOTywFe7USTLBkJcdvQ4EpFk/KZwLiTdCmT2NQVxsG4AxBiQ==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-linux-arm64@1.8.8:
resolution: {integrity: sha512-hKqLbBHgUkYf2Ww8uBL9UYdBFQ5677a7QXdsFhONXoACbDUPvpK4BKlz3NN7G4NZ+g9dGju+OJJjQP0VXRHb5w==}
/turbo-linux-arm64@1.9.1:
resolution: {integrity: sha512-XYvIbeiCCCr+ENujd2Jtck/lJPTKWb8T2MSL/AEBx21Zy3Sa7HgrQX6LX0a0pNHjaleHz00XXt1D0W5hLeP+tA==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-windows-64@1.8.8:
resolution: {integrity: sha512-2ndjDJyzkNslXxLt+PQuU21AHJWc8f6MnLypXy3KsN4EyX/uKKGZS0QJWz27PeHg0JS75PVvhfFV+L9t9i+Yyg==}
/turbo-windows-64@1.9.1:
resolution: {integrity: sha512-x7lWAspe4/v3XQ0gaFRWDX/X9uyWdhwFBPEfb8BA0YKtnsrPOHkV0mRHCRrXzvzjA7pcDCl2agGzb7o863O+Jg==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo-windows-arm64@1.8.8:
resolution: {integrity: sha512-xCA3oxgmW9OMqpI34AAmKfOVsfDljhD5YBwgs0ZDsn5h3kCHhC4x9W5dDk1oyQ4F5EXSH3xVym5/xl1J6WRpUg==}
/turbo-windows-arm64@1.9.1:
resolution: {integrity: sha512-QSLNz8dRBLDqXOUv/KnoesBomSbIz2Huef/a3l2+Pat5wkQVgMfzFxDOnkK5VWujPYXz+/prYz+/7cdaC78/kw==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo@1.8.8:
resolution: {integrity: sha512-qYJ5NjoTX+591/x09KgsDOPVDUJfU9GoS+6jszQQlLp1AHrf1wRFA3Yps8U+/HTG03q0M4qouOfOLtRQP4QypA==}
/turbo@1.9.1:
resolution: {integrity: sha512-Rqe8SP96e53y4Pk29kk2aZbA8EF11UtHJ3vzXJseadrc1T3V6UhzvAWwiKJL//x/jojyOoX1axnoxmX3UHbZ0g==}
hasBin: true
requiresBuild: true
optionalDependencies:
turbo-darwin-64: 1.8.8
turbo-darwin-arm64: 1.8.8
turbo-linux-64: 1.8.8
turbo-linux-arm64: 1.8.8
turbo-windows-64: 1.8.8
turbo-windows-arm64: 1.8.8
turbo-darwin-64: 1.9.1
turbo-darwin-arm64: 1.9.1
turbo-linux-64: 1.9.1
turbo-linux-arm64: 1.9.1
turbo-windows-64: 1.9.1
turbo-windows-arm64: 1.9.1
dev: true
/tween-functions@1.2.0: