mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 13:39:39 -05:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
+111
@@ -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">
|
||||
|
||||
-1
@@ -36,7 +36,6 @@ export default function ResponseTimeline({ environmentId, surveyId }) {
|
||||
}
|
||||
return { ...response, responses: updatedResponse };
|
||||
});
|
||||
|
||||
return updatedResponses;
|
||||
}
|
||||
return [];
|
||||
|
||||
+10
-8
@@ -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>
|
||||
);
|
||||
|
||||
+16
-3
@@ -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
|
||||
|
||||
+4
-1
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+199
-45
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user